From 0f08e6c55ad88c93499f71f2af4a2e7ae0185cdf Mon Sep 17 00:00:00 2001 From: Michael Riabzev Date: Mon, 9 Nov 2020 10:25:52 +0200 Subject: [PATCH] Key derivation and asset id computation. --- crypto/starkware/crypto/signature/asset.js | 146 ++++++++++++++++++ .../crypto/signature/assets_precomputed.json | 58 +++++++ .../crypto/signature/key_derivation.js | 125 +++++++++++++++ .../crypto/signature/test/asset_test.js | 30 ++++ .../signature/test/key_derivation_test.js | 46 ++++++ 5 files changed, 405 insertions(+) create mode 100644 crypto/starkware/crypto/signature/asset.js create mode 100644 crypto/starkware/crypto/signature/assets_precomputed.json create mode 100644 crypto/starkware/crypto/signature/key_derivation.js create mode 100644 crypto/starkware/crypto/signature/test/asset_test.js create mode 100644 crypto/starkware/crypto/signature/test/key_derivation_test.js diff --git a/crypto/starkware/crypto/signature/asset.js b/crypto/starkware/crypto/signature/asset.js new file mode 100644 index 0000000..459be5c --- /dev/null +++ b/crypto/starkware/crypto/signature/asset.js @@ -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. +}; diff --git a/crypto/starkware/crypto/signature/assets_precomputed.json b/crypto/starkware/crypto/signature/assets_precomputed.json new file mode 100644 index 0000000..0224637 --- /dev/null +++ b/crypto/starkware/crypto/signature/assets_precomputed.json @@ -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" + } + } + } +} diff --git a/crypto/starkware/crypto/signature/key_derivation.js b/crypto/starkware/crypto/signature/key_derivation.js new file mode 100644 index 0000000..6acb91b --- /dev/null +++ b/crypto/starkware/crypto/signature/key_derivation.js @@ -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. +}; diff --git a/crypto/starkware/crypto/signature/test/asset_test.js b/crypto/starkware/crypto/signature/test/asset_test.js new file mode 100644 index 0000000..2ea3a1b --- /dev/null +++ b/crypto/starkware/crypto/signature/test/asset_test.js @@ -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); + } + } + }); +}); diff --git a/crypto/starkware/crypto/signature/test/key_derivation_test.js b/crypto/starkware/crypto/signature/test/key_derivation_test.js new file mode 100644 index 0000000..c31a331 --- /dev/null +++ b/crypto/starkware/crypto/signature/test/key_derivation_test.js @@ -0,0 +1,46 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai'); +const { StarkExEc, getKeyPairFromPath, getAccountPath, grindKey } = + require('.././key_derivation.js'); +const { expect } = chai; + +const layer = 'starkex'; +const application = 'starkdeployement'; + +const mnemonic = 'range mountain blast problem vibrant void vivid doctor cluster enough melody ' + + 'salt layer language laptop boat major space monkey unit glimpse pause change vibrant'; +const ethAddress = '0xa4864d977b944315389d1765ffa7e66F74ee8cd7'; + +describe('Key derivation', () => { + it('should derive key from mnemonic and eth-address correctly', () => { + let index = 0; + let path = getAccountPath(layer, application, ethAddress, index); + let keyPair = getKeyPairFromPath(mnemonic, path); + expect(keyPair.getPrivate('hex')).to.equal( + '06cf0a8bf113352eb863157a45c5e5567abb34f8d32cddafd2c22aa803f4892c' + ); + + index = 7; + path = getAccountPath(layer, application, ethAddress, index); + keyPair = getKeyPairFromPath(mnemonic, path); + expect(keyPair.getPrivate('hex')).to.equal( + '0341751bdc42841da35ab74d13a1372c1f0250617e8a2ef96034d9f46e6847af' + ); + + index = 598; + path = getAccountPath(layer, application, ethAddress, index); + keyPair = getKeyPairFromPath(mnemonic, path); + expect(keyPair.getPrivate('hex')).to.equal( + '041a4d591a868353d28b7947eb132aa4d00c4a022743689ffd20a3628d6ca28c' + ); + }); +}); + +describe('Key grinding', () => { + it('should produce the correct ground key', () => { + const privateKey = '86F3E7293141F20A8BAFF320E8EE4ACCB9D4A4BF2B4D295E8CEE784DB46E0519'; + expect(grindKey(privateKey, StarkExEc)).to.equal( + '5c8c8683596c732541a59e03007b2d30dbbbb873556fe65b5fb63c16688f941' + ); + }); +});