-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Key derivation and asset id computation.
- Loading branch information
1 parent
09340d6
commit 0f08e6c
Showing
5 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
///////////////////////////////////////////////////////////////////////////////// | ||
// Copyright 2019 StarkWare Industries Ltd. // | ||
// // | ||
// Licensed under the Apache License, Version 2.0 (the "License"). // | ||
// You may not use this file except in compliance with the License. // | ||
// You may obtain a copy of the License at // | ||
// // | ||
// https://www.starkware.co/open-source-license/ // | ||
// // | ||
// Unless required by applicable law or agreed to in writing, // | ||
// software distributed under the License is distributed on an "AS IS" BASIS, // | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // | ||
// See the License for the specific language governing permissions // | ||
// and limitations under the License. // | ||
///////////////////////////////////////////////////////////////////////////////// | ||
|
||
const BN = require('bn.js'); | ||
const encUtils = require('enc-utils'); | ||
const sha3 = require('js-sha3'); | ||
const assert = require('assert'); | ||
|
||
|
||
// Generate BN of 1. | ||
const oneBn = new BN('1', 16); | ||
|
||
// This number is used to shift the packed encoded asset information by 256 bits. | ||
const shiftBN = new BN('10000000000000000000000000000000000000000000000000000000000000000', 16); | ||
|
||
// Used to mask the 251 least signifcant bits given by Keccack256 to produce the final asset ID. | ||
const mask = new BN('3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16); | ||
|
||
|
||
/* | ||
Computes the hash representing the asset ID for a given asset. | ||
asset is a dictionary containing the type and data of the asset to parse. the asset type is | ||
represented by a string describing the associated asset while the data is a dictionary | ||
containing further infomartion to distinguish between assets of a given type (such as the | ||
address of the smart contract of an ERC20 asset). | ||
The function returns the computed asset ID as a hex-string. | ||
For example: | ||
assetDict = { | ||
type: 'ERC20', | ||
data: { quantum: '10000', tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' } | ||
} | ||
Will produce an the following asset ID: | ||
'0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653'. | ||
*/ | ||
function getAssetType(assetDict) { | ||
const assetSelector = getAssetSelector(assetDict.type); | ||
|
||
// Expected length is maintained to fix the length of the resulting asset info string in case of | ||
// leading zeroes (which might be omitted by the BN object). | ||
let expectedLen = encUtils.removeHexPrefix(assetSelector).length; | ||
|
||
// The asset info hex string is a packed message containing the hexadecimal representation of | ||
// the asset data. | ||
let assetInfo = new BN(encUtils.removeHexPrefix(assetSelector), 16); | ||
|
||
if (assetDict.data.tokenAddress !== undefined) { | ||
// In the case there is a valid tokenAddress in the data, we append that to the asset info | ||
// (before the quantum). | ||
const tokenAddress = new BN(encUtils.removeHexPrefix(assetDict.data.tokenAddress), 16); | ||
assetInfo = assetInfo.mul(shiftBN); | ||
expectedLen += 64; | ||
assetInfo = assetInfo.add(tokenAddress); | ||
} | ||
|
||
// Default quantum is 1 (for assets which don't specify quantum explicitly). | ||
const quantInfo = assetDict.data.quantum; | ||
const quantum = (quantInfo === undefined) ? oneBn : new BN(quantInfo, 10); | ||
assetInfo = assetInfo.mul(shiftBN); | ||
expectedLen += 64; | ||
assetInfo = assetInfo.add(quantum); | ||
|
||
let assetType = sha3.keccak_256( | ||
encUtils.hexToBuffer(addLeadingZeroes(assetInfo.toJSON(), expectedLen)) | ||
); | ||
assetType = new BN(assetType, 16); | ||
assetType = assetType.and(mask); | ||
|
||
return '0x' + assetType.toJSON(); | ||
} | ||
|
||
function getAssetId(assetDict) { | ||
const assetType = new BN(encUtils.removeHexPrefix(getAssetType(assetDict)), 16); | ||
// For ETH and ERC20, the asset ID is simply the asset type. | ||
let assetId = assetType; | ||
if (assetDict.type === 'ERC721') { | ||
// ERC721 assets require a slightly different construction for asset info. | ||
let assetInfo = new BN(encUtils.utf8ToBuffer('NFT:'), 16); | ||
assetInfo = assetInfo.mul(shiftBN); | ||
assetInfo = assetInfo.add(assetType); | ||
assetInfo = assetInfo.mul(shiftBN); | ||
assetInfo = assetInfo.add(new BN(parseInt(assetDict.data.tokenId), 16)); | ||
const expectedLen = 136; | ||
assetId = sha3.keccak_256( | ||
encUtils.hexToBuffer(addLeadingZeroes(assetInfo.toJSON(), expectedLen)) | ||
); | ||
assetId = new BN(assetId, 16); | ||
assetId = assetId.and(mask); | ||
} | ||
|
||
return '0x' + assetId.toJSON(); | ||
} | ||
|
||
/* | ||
Computes the given asset's unique selector based on its type. | ||
*/ | ||
function getAssetSelector(assetDictType) { | ||
let seed = ''; | ||
switch (assetDictType.toUpperCase()) { | ||
case 'ETH': | ||
seed = 'ETH()'; | ||
break; | ||
case 'ERC20': | ||
seed = 'ERC20Token(address)'; | ||
break; | ||
case 'ERC721': | ||
seed = 'ERC721Token(address,uint256)'; | ||
break; | ||
default: | ||
throw new Error(`Unknown token type: ${assetDictType}`); | ||
} | ||
return encUtils.sanitizeHex(sha3.keccak_256(seed).slice(0, 8)); | ||
} | ||
|
||
/* | ||
Adds leading zeroes to the input hex-string to complement the expected length. | ||
*/ | ||
function addLeadingZeroes(hexStr, expectedLen) { | ||
let res = hexStr; | ||
assert(res.length <= expectedLen); | ||
while (res.length < expectedLen) { | ||
res = '0' + res; | ||
} | ||
return res; | ||
} | ||
|
||
module.exports = { | ||
getAssetType, | ||
getAssetId // Function. | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
{ | ||
"assetId":{ | ||
"0x1142460171646987f20c714eda4b92812b22b811f56f27130937c267e29bd9e": { | ||
"type": "ETH", | ||
"data": { | ||
"quantum": "1" | ||
} | ||
}, | ||
"0xd5b742d29ab21fdb06ac5c7c460550131c0b30cbc4c911985174c0ea4a92ec": { | ||
"type": "ETH", | ||
"data": { | ||
"quantum": "10000000" | ||
} | ||
}, | ||
"0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653": { | ||
"type": "ERC20", | ||
"data": { | ||
"quantum": "10000", | ||
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7" | ||
} | ||
}, | ||
"0x2b0ff0c09505bc40f9d1659becf16855a7b2298b010f8a54f4b05325885b40c": { | ||
"type": "ERC721", | ||
"data": { | ||
"tokenId": "4100", | ||
"tokenAddress": "0xB18ed4768F87b0fFAb83408014f1caF066b91380" | ||
} | ||
} | ||
}, | ||
"assetType":{ | ||
"0x1142460171646987f20c714eda4b92812b22b811f56f27130937c267e29bd9e": { | ||
"type": "ETH", | ||
"data": { | ||
"quantum": "1" | ||
} | ||
}, | ||
"0xd5b742d29ab21fdb06ac5c7c460550131c0b30cbc4c911985174c0ea4a92ec": { | ||
"type": "ETH", | ||
"data": { | ||
"quantum": "10000000" | ||
} | ||
}, | ||
"0x352386d5b7c781d47ecd404765307d74edc4d43b0490b8e03c71ac7a7429653": { | ||
"type": "ERC20", | ||
"data": { | ||
"quantum": "10000", | ||
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7" | ||
} | ||
}, | ||
"0x20c0e279ea2e027258d3056f34eca6e47ad9aaa995b896cafcb68d5a65b115b": { | ||
"type": "ERC721", | ||
"data": { | ||
"tokenId": "4100", | ||
"tokenAddress": "0xB18ed4768F87b0fFAb83408014f1caF066b91380" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
///////////////////////////////////////////////////////////////////////////////// | ||
// Copyright 2019 StarkWare Industries Ltd. // | ||
// // | ||
// Licensed under the Apache License, Version 2.0 (the "License"). // | ||
// You may not use this file except in compliance with the License. // | ||
// You may obtain a copy of the License at // | ||
// // | ||
// https://www.starkware.co/open-source-license/ // | ||
// // | ||
// Unless required by applicable law or agreed to in writing, // | ||
// software distributed under the License is distributed on an "AS IS" BASIS, // | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // | ||
// See the License for the specific language governing permissions // | ||
// and limitations under the License. // | ||
///////////////////////////////////////////////////////////////////////////////// | ||
|
||
const { hdkey } = require('ethereumjs-wallet'); | ||
const bip39 = require('bip39'); | ||
const encUtils = require('enc-utils'); | ||
const BN = require('bn.js'); | ||
const hash = require('hash.js'); | ||
const { ec } = require('./signature.js'); | ||
|
||
/* | ||
Returns an integer from a given section of bits out of a hex string. | ||
hex is the target hex string to slice. | ||
start represents the index of the first bit to cut from the hex string (binary) in LSB order. | ||
end represents the index of the last bit to cut from the hex string. | ||
*/ | ||
function getIntFromBits(hex, start, end = undefined) { | ||
const bin = encUtils.hexToBinary(hex); | ||
const bits = bin.slice(start, end); | ||
const int = encUtils.binaryToNumber(bits); | ||
return int; | ||
} | ||
|
||
/* | ||
Derives key-pair from given mnemonic string and path. | ||
mnemonic should be a sentence comprised of 12 words with single spaces between them. | ||
path is a formatted string describing the stark key path based on the layer, application and eth | ||
address. | ||
*/ | ||
function getKeyPairFromPath(mnemonic, path) { | ||
const seed = bip39.mnemonicToSeedSync(mnemonic); | ||
const keySeed = hdkey | ||
.fromMasterSeed(seed, 'hex') | ||
.derivePath(path) | ||
.getWallet() | ||
.getPrivateKeyString(); | ||
const starkEcOrder = ec.n; | ||
return ec.keyFromPrivate(grindKey(keySeed, starkEcOrder), 'hex'); | ||
} | ||
|
||
/* | ||
Calculates the stark path based on the layer, application, eth address and a given index. | ||
layer is a string representing the operating layer (usually 'starkex'). | ||
application is a string representing the relevant application (For a list of valid applications, | ||
refer to https://starkware.co/starkex/docs/requirementsApplicationParameters.html). | ||
ethereumAddress is a string representing the ethereum public key from which we derive the stark | ||
key. | ||
index represents an index of the possible associated wallets derived from the seed. | ||
*/ | ||
function getAccountPath(layer, application, ethereumAddress, index) { | ||
const layerHash = hash | ||
.sha256() | ||
.update(layer) | ||
.digest('hex'); | ||
const applicationHash = hash | ||
.sha256() | ||
.update(application) | ||
.digest('hex'); | ||
const layerInt = getIntFromBits(layerHash, -31); | ||
const applicationInt = getIntFromBits(applicationHash, -31); | ||
// Draws the 31 LSBs of the eth address. | ||
const ethAddressInt1 = getIntFromBits(ethereumAddress, -31); | ||
// Draws the following 31 LSBs of the eth address. | ||
const ethAddressInt2 = getIntFromBits(ethereumAddress, -62, -31); | ||
return `m/2645'/${layerInt}'/${applicationInt}'/${ethAddressInt1}'/${ethAddressInt2}'/${index}`; | ||
} | ||
|
||
/* | ||
This function receives a key seed and produces an appropriate StarkEx key from a uniform | ||
distribution. | ||
Although it is possible to define a StarkEx key as a residue between the StarkEx EC order and a | ||
random 256bit digest value, the result would be a biased key. In order to prevent this bias, we | ||
deterministically search (by applying more hashes, AKA grinding) for a value lower than the largest | ||
256bit multiple of StarkEx EC order. | ||
*/ | ||
function grindKey(keySeed, keyValLimit) { | ||
const sha256EcMaxDigest = new BN( | ||
'1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000', | ||
16 | ||
); | ||
const maxAllowedVal = sha256EcMaxDigest.sub(sha256EcMaxDigest.mod(keyValLimit)); | ||
let i = 0; | ||
let key = hashKeyWithIndex(keySeed, i); | ||
i++; | ||
// Make sure the produced key is devided by the Stark EC order, and falls within the range | ||
// [0, maxAllowedVal). | ||
while (!(key.lt(maxAllowedVal))) { | ||
key = hashKeyWithIndex(keySeed.toString('hex'), i); | ||
i++; | ||
} | ||
return key.umod(keyValLimit).toString('hex'); | ||
} | ||
|
||
function hashKeyWithIndex(key, index) { | ||
return new BN( | ||
hash | ||
.sha256() | ||
.update( | ||
encUtils.hexToBuffer( | ||
encUtils.removeHexPrefix(key) + | ||
encUtils.sanitizeBytes(encUtils.numberToHex(index), 2) | ||
) | ||
) | ||
.digest('hex'), | ||
16 | ||
); | ||
} | ||
|
||
module.exports = { | ||
StarkExEc: ec.n, // Data. | ||
getKeyPairFromPath, getAccountPath, grindKey // Function. | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
const chai = require('chai'); | ||
const { getAssetId, getAssetType } = require('.././asset.js'); | ||
const { expect } = chai; | ||
|
||
describe('Asset Type computation', () => { | ||
it('should compute asset type correctly', () => { | ||
const precomputedAssets = require('../assets_precomputed.json'); | ||
const precompytedAssetTypes = precomputedAssets.assetType; | ||
for (const expectedAssetType in precompytedAssetTypes) { | ||
if ({}.hasOwnProperty.call(precompytedAssetTypes, expectedAssetType)) { | ||
const asset = precompytedAssetTypes[expectedAssetType]; | ||
expect(getAssetType(asset)).to.equal(expectedAssetType); | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
describe('Asset ID computation', () => { | ||
it('should compute asset ID correctly', () => { | ||
const precomputedAssets = require('../assets_precomputed.json'); | ||
const precompytedAssetIds = precomputedAssets.assetId; | ||
for (const expectedAssetId in precompytedAssetIds) { | ||
if ({}.hasOwnProperty.call(precompytedAssetIds, expectedAssetId)) { | ||
const asset = precompytedAssetIds[expectedAssetId]; | ||
expect(getAssetId(asset)).to.equal(expectedAssetId); | ||
} | ||
} | ||
}); | ||
}); |
Oops, something went wrong.
0f08e6c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
w