Skip to content

Commit 0f08e6c

Browse files
Key derivation and asset id computation.
1 parent 09340d6 commit 0f08e6c

File tree

5 files changed

+405
-0
lines changed

5 files changed

+405
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/////////////////////////////////////////////////////////////////////////////////
2+
// Copyright 2019 StarkWare Industries Ltd. //
3+
// //
4+
// Licensed under the Apache License, Version 2.0 (the "License"). //
5+
// You may not use this file except in compliance with the License. //
6+
// You may obtain a copy of the License at //
7+
// //
8+
// https://www.starkware.co/open-source-license/ //
9+
// //
10+
// Unless required by applicable law or agreed to in writing, //
11+
// software distributed under the License is distributed on an "AS IS" BASIS, //
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
13+
// See the License for the specific language governing permissions //
14+
// and limitations under the License. //
15+
/////////////////////////////////////////////////////////////////////////////////
16+
17+
const BN = require('bn.js');
18+
const encUtils = require('enc-utils');
19+
const sha3 = require('js-sha3');
20+
const assert = require('assert');
21+
22+
23+
// Generate BN of 1.
24+
const oneBn = new BN('1', 16);
25+
26+
// This number is used to shift the packed encoded asset information by 256 bits.
27+
const shiftBN = new BN('10000000000000000000000000000000000000000000000000000000000000000', 16);
28+
29+
// Used to mask the 251 least signifcant bits given by Keccack256 to produce the final asset ID.
30+
const mask = new BN('3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16);
31+
32+
33+
/*
34+
Computes the hash representing the asset ID for a given asset.
35+
asset is a dictionary containing the type and data of the asset to parse. the asset type is
36+
represented by a string describing the associated asset while the data is a dictionary
37+
containing further infomartion to distinguish between assets of a given type (such as the
38+
address of the smart contract of an ERC20 asset).
39+
The function returns the computed asset ID as a hex-string.
40+
41+
For example:
42+
43+
assetDict = {
44+
type: 'ERC20',
45+
data: { quantum: '10000', tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' }
46+
}
47+
48+
Will produce an the following asset ID:
49+
50+
'0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653'.
51+
*/
52+
function getAssetType(assetDict) {
53+
const assetSelector = getAssetSelector(assetDict.type);
54+
55+
// Expected length is maintained to fix the length of the resulting asset info string in case of
56+
// leading zeroes (which might be omitted by the BN object).
57+
let expectedLen = encUtils.removeHexPrefix(assetSelector).length;
58+
59+
// The asset info hex string is a packed message containing the hexadecimal representation of
60+
// the asset data.
61+
let assetInfo = new BN(encUtils.removeHexPrefix(assetSelector), 16);
62+
63+
if (assetDict.data.tokenAddress !== undefined) {
64+
// In the case there is a valid tokenAddress in the data, we append that to the asset info
65+
// (before the quantum).
66+
const tokenAddress = new BN(encUtils.removeHexPrefix(assetDict.data.tokenAddress), 16);
67+
assetInfo = assetInfo.mul(shiftBN);
68+
expectedLen += 64;
69+
assetInfo = assetInfo.add(tokenAddress);
70+
}
71+
72+
// Default quantum is 1 (for assets which don't specify quantum explicitly).
73+
const quantInfo = assetDict.data.quantum;
74+
const quantum = (quantInfo === undefined) ? oneBn : new BN(quantInfo, 10);
75+
assetInfo = assetInfo.mul(shiftBN);
76+
expectedLen += 64;
77+
assetInfo = assetInfo.add(quantum);
78+
79+
let assetType = sha3.keccak_256(
80+
encUtils.hexToBuffer(addLeadingZeroes(assetInfo.toJSON(), expectedLen))
81+
);
82+
assetType = new BN(assetType, 16);
83+
assetType = assetType.and(mask);
84+
85+
return '0x' + assetType.toJSON();
86+
}
87+
88+
function getAssetId(assetDict) {
89+
const assetType = new BN(encUtils.removeHexPrefix(getAssetType(assetDict)), 16);
90+
// For ETH and ERC20, the asset ID is simply the asset type.
91+
let assetId = assetType;
92+
if (assetDict.type === 'ERC721') {
93+
// ERC721 assets require a slightly different construction for asset info.
94+
let assetInfo = new BN(encUtils.utf8ToBuffer('NFT:'), 16);
95+
assetInfo = assetInfo.mul(shiftBN);
96+
assetInfo = assetInfo.add(assetType);
97+
assetInfo = assetInfo.mul(shiftBN);
98+
assetInfo = assetInfo.add(new BN(parseInt(assetDict.data.tokenId), 16));
99+
const expectedLen = 136;
100+
assetId = sha3.keccak_256(
101+
encUtils.hexToBuffer(addLeadingZeroes(assetInfo.toJSON(), expectedLen))
102+
);
103+
assetId = new BN(assetId, 16);
104+
assetId = assetId.and(mask);
105+
}
106+
107+
return '0x' + assetId.toJSON();
108+
}
109+
110+
/*
111+
Computes the given asset's unique selector based on its type.
112+
*/
113+
function getAssetSelector(assetDictType) {
114+
let seed = '';
115+
switch (assetDictType.toUpperCase()) {
116+
case 'ETH':
117+
seed = 'ETH()';
118+
break;
119+
case 'ERC20':
120+
seed = 'ERC20Token(address)';
121+
break;
122+
case 'ERC721':
123+
seed = 'ERC721Token(address,uint256)';
124+
break;
125+
default:
126+
throw new Error(`Unknown token type: ${assetDictType}`);
127+
}
128+
return encUtils.sanitizeHex(sha3.keccak_256(seed).slice(0, 8));
129+
}
130+
131+
/*
132+
Adds leading zeroes to the input hex-string to complement the expected length.
133+
*/
134+
function addLeadingZeroes(hexStr, expectedLen) {
135+
let res = hexStr;
136+
assert(res.length <= expectedLen);
137+
while (res.length < expectedLen) {
138+
res = '0' + res;
139+
}
140+
return res;
141+
}
142+
143+
module.exports = {
144+
getAssetType,
145+
getAssetId // Function.
146+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"assetId":{
3+
"0x1142460171646987f20c714eda4b92812b22b811f56f27130937c267e29bd9e": {
4+
"type": "ETH",
5+
"data": {
6+
"quantum": "1"
7+
}
8+
},
9+
"0xd5b742d29ab21fdb06ac5c7c460550131c0b30cbc4c911985174c0ea4a92ec": {
10+
"type": "ETH",
11+
"data": {
12+
"quantum": "10000000"
13+
}
14+
},
15+
"0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653": {
16+
"type": "ERC20",
17+
"data": {
18+
"quantum": "10000",
19+
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
20+
}
21+
},
22+
"0x2b0ff0c09505bc40f9d1659becf16855a7b2298b010f8a54f4b05325885b40c": {
23+
"type": "ERC721",
24+
"data": {
25+
"tokenId": "4100",
26+
"tokenAddress": "0xB18ed4768F87b0fFAb83408014f1caF066b91380"
27+
}
28+
}
29+
},
30+
"assetType":{
31+
"0x1142460171646987f20c714eda4b92812b22b811f56f27130937c267e29bd9e": {
32+
"type": "ETH",
33+
"data": {
34+
"quantum": "1"
35+
}
36+
},
37+
"0xd5b742d29ab21fdb06ac5c7c460550131c0b30cbc4c911985174c0ea4a92ec": {
38+
"type": "ETH",
39+
"data": {
40+
"quantum": "10000000"
41+
}
42+
},
43+
"0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653": {
44+
"type": "ERC20",
45+
"data": {
46+
"quantum": "10000",
47+
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
48+
}
49+
},
50+
"0x20c0e279ea2e027258d3056f34eca6e47ad9aaa995b896cafcb68d5a65b115b": {
51+
"type": "ERC721",
52+
"data": {
53+
"tokenId": "4100",
54+
"tokenAddress": "0xB18ed4768F87b0fFAb83408014f1caF066b91380"
55+
}
56+
}
57+
}
58+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/////////////////////////////////////////////////////////////////////////////////
2+
// Copyright 2019 StarkWare Industries Ltd. //
3+
// //
4+
// Licensed under the Apache License, Version 2.0 (the "License"). //
5+
// You may not use this file except in compliance with the License. //
6+
// You may obtain a copy of the License at //
7+
// //
8+
// https://www.starkware.co/open-source-license/ //
9+
// //
10+
// Unless required by applicable law or agreed to in writing, //
11+
// software distributed under the License is distributed on an "AS IS" BASIS, //
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
13+
// See the License for the specific language governing permissions //
14+
// and limitations under the License. //
15+
/////////////////////////////////////////////////////////////////////////////////
16+
17+
const { hdkey } = require('ethereumjs-wallet');
18+
const bip39 = require('bip39');
19+
const encUtils = require('enc-utils');
20+
const BN = require('bn.js');
21+
const hash = require('hash.js');
22+
const { ec } = require('./signature.js');
23+
24+
/*
25+
Returns an integer from a given section of bits out of a hex string.
26+
hex is the target hex string to slice.
27+
start represents the index of the first bit to cut from the hex string (binary) in LSB order.
28+
end represents the index of the last bit to cut from the hex string.
29+
*/
30+
function getIntFromBits(hex, start, end = undefined) {
31+
const bin = encUtils.hexToBinary(hex);
32+
const bits = bin.slice(start, end);
33+
const int = encUtils.binaryToNumber(bits);
34+
return int;
35+
}
36+
37+
/*
38+
Derives key-pair from given mnemonic string and path.
39+
mnemonic should be a sentence comprised of 12 words with single spaces between them.
40+
path is a formatted string describing the stark key path based on the layer, application and eth
41+
address.
42+
*/
43+
function getKeyPairFromPath(mnemonic, path) {
44+
const seed = bip39.mnemonicToSeedSync(mnemonic);
45+
const keySeed = hdkey
46+
.fromMasterSeed(seed, 'hex')
47+
.derivePath(path)
48+
.getWallet()
49+
.getPrivateKeyString();
50+
const starkEcOrder = ec.n;
51+
return ec.keyFromPrivate(grindKey(keySeed, starkEcOrder), 'hex');
52+
}
53+
54+
/*
55+
Calculates the stark path based on the layer, application, eth address and a given index.
56+
layer is a string representing the operating layer (usually 'starkex').
57+
application is a string representing the relevant application (For a list of valid applications,
58+
refer to https://starkware.co/starkex/docs/requirementsApplicationParameters.html).
59+
ethereumAddress is a string representing the ethereum public key from which we derive the stark
60+
key.
61+
index represents an index of the possible associated wallets derived from the seed.
62+
*/
63+
function getAccountPath(layer, application, ethereumAddress, index) {
64+
const layerHash = hash
65+
.sha256()
66+
.update(layer)
67+
.digest('hex');
68+
const applicationHash = hash
69+
.sha256()
70+
.update(application)
71+
.digest('hex');
72+
const layerInt = getIntFromBits(layerHash, -31);
73+
const applicationInt = getIntFromBits(applicationHash, -31);
74+
// Draws the 31 LSBs of the eth address.
75+
const ethAddressInt1 = getIntFromBits(ethereumAddress, -31);
76+
// Draws the following 31 LSBs of the eth address.
77+
const ethAddressInt2 = getIntFromBits(ethereumAddress, -62, -31);
78+
return `m/2645'/${layerInt}'/${applicationInt}'/${ethAddressInt1}'/${ethAddressInt2}'/${index}`;
79+
}
80+
81+
/*
82+
This function receives a key seed and produces an appropriate StarkEx key from a uniform
83+
distribution.
84+
Although it is possible to define a StarkEx key as a residue between the StarkEx EC order and a
85+
random 256bit digest value, the result would be a biased key. In order to prevent this bias, we
86+
deterministically search (by applying more hashes, AKA grinding) for a value lower than the largest
87+
256bit multiple of StarkEx EC order.
88+
*/
89+
function grindKey(keySeed, keyValLimit) {
90+
const sha256EcMaxDigest = new BN(
91+
'1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000',
92+
16
93+
);
94+
const maxAllowedVal = sha256EcMaxDigest.sub(sha256EcMaxDigest.mod(keyValLimit));
95+
let i = 0;
96+
let key = hashKeyWithIndex(keySeed, i);
97+
i++;
98+
// Make sure the produced key is devided by the Stark EC order, and falls within the range
99+
// [0, maxAllowedVal).
100+
while (!(key.lt(maxAllowedVal))) {
101+
key = hashKeyWithIndex(keySeed.toString('hex'), i);
102+
i++;
103+
}
104+
return key.umod(keyValLimit).toString('hex');
105+
}
106+
107+
function hashKeyWithIndex(key, index) {
108+
return new BN(
109+
hash
110+
.sha256()
111+
.update(
112+
encUtils.hexToBuffer(
113+
encUtils.removeHexPrefix(key) +
114+
encUtils.sanitizeBytes(encUtils.numberToHex(index), 2)
115+
)
116+
)
117+
.digest('hex'),
118+
16
119+
);
120+
}
121+
122+
module.exports = {
123+
StarkExEc: ec.n, // Data.
124+
getKeyPairFromPath, getAccountPath, grindKey // Function.
125+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable no-unused-expressions */
2+
const chai = require('chai');
3+
const { getAssetId, getAssetType } = require('.././asset.js');
4+
const { expect } = chai;
5+
6+
describe('Asset Type computation', () => {
7+
it('should compute asset type correctly', () => {
8+
const precomputedAssets = require('../assets_precomputed.json');
9+
const precompytedAssetTypes = precomputedAssets.assetType;
10+
for (const expectedAssetType in precompytedAssetTypes) {
11+
if ({}.hasOwnProperty.call(precompytedAssetTypes, expectedAssetType)) {
12+
const asset = precompytedAssetTypes[expectedAssetType];
13+
expect(getAssetType(asset)).to.equal(expectedAssetType);
14+
}
15+
}
16+
});
17+
});
18+
19+
describe('Asset ID computation', () => {
20+
it('should compute asset ID correctly', () => {
21+
const precomputedAssets = require('../assets_precomputed.json');
22+
const precompytedAssetIds = precomputedAssets.assetId;
23+
for (const expectedAssetId in precompytedAssetIds) {
24+
if ({}.hasOwnProperty.call(precompytedAssetIds, expectedAssetId)) {
25+
const asset = precompytedAssetIds[expectedAssetId];
26+
expect(getAssetId(asset)).to.equal(expectedAssetId);
27+
}
28+
}
29+
});
30+
});

0 commit comments

Comments
 (0)