├── .gitignore ├── README.md ├── compiler.json ├── contract_templates ├── contract.handlebars └── partials │ ├── call.handlebars │ ├── callAsync.handlebars │ ├── event.handlebars │ ├── params.handlebars │ ├── return_type.handlebars │ ├── tx.handlebars │ └── typed_params.handlebars ├── contracts └── MerkleTreeVerifier.sol ├── package.json ├── src └── index.ts ├── test ├── helpers.ts ├── solidity.spec.ts └── typescript.spec.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /lib -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | typescript-solidity-merkle-tree 2 | =============================== 3 | 4 | [![npm version](https://badge.fury.io/js/typescript-solidity-merkle-tree.svg)](https://badge.fury.io/js/typescript-solidity-merkle-tree) 5 | 6 | TypeScript Merkle tree that comes with Solidity proof verifier. Unit tested for your pleasure! 7 | 8 | * handles all edge cases: odd numbers of items, empty/duplicates, lists that aren't powers of two 9 | * simple API based on Buffer's - use whatever data type you want, just convert it 10 | * works well with Ethereum smart contracts, with its accompanying Solidity verifier and root generator 11 | * fixed array allocations (perf++) 12 | * secure against [second preimage attacks](https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack/) 13 | 14 | Developed as part of our [cross-chain communications protocol, 0dex](https://github.com/liamzebedee/0dex). 15 | 16 | ## Usage 17 | ### TypeScript 18 | ```ts 19 | import { MerkleTree } from 'typescript-solidity-merkle-tree'; 20 | import { keccak256 } from 'ethereumjs-util'; 21 | 22 | let items = [ 23 | Buffer.from('12', 'hex'), 24 | Buffer.from('15', 'hex'), 25 | Buffer.from('20', 'hex'), 26 | Buffer.from('25', 'hex') 27 | ]; 28 | 29 | let tree = new MerkleTree(items, keccak256); 30 | 31 | let itemToProveIdx = 2; 32 | let proof = tree.generateProof(itemToProveIdx); 33 | tree.verifyProof(proof); // true 34 | 35 | tree.root() 36 | // 37 | 38 | // Print tree for debugging (it shows the unhashed leaves on the right) 39 | console.log(tree.toString()) 40 | 41 | /* 42 | Layer 0 - 43 | 677034980f47f6cb0a55e7d8674ba838c39165afe34da2fc538f695d4950b38e 12 44 | 33ae796f786efa387ecd29accca89b44e9194cc6689994e4fe5870f88e84e1e8 15 45 | 123aaed9e19ab45afd57abf57eb48ebcdd3940c31f412aceb059602b877e61bf 25 46 | 123aaed9e19ab45afd57abf57eb48ebcdd3940c31f412aceb059602b877e61bf 25 47 | Layer 1 - 48 | 7e42e1f215cc6f44fa77b0d8e51285787e962eeab58733a1d6ffe703829e50d5 49 | 8ed73d335daaa310ddda00cd4bafa7047fe759d86058c4aef727d02147d9e5cb 50 | Layer 2 - 51 | 8c9fef65d6b7e773d356b6efcd62af26ef512422f0f61678b7c89b4a376ff3d3 52 | */ 53 | ``` 54 | 55 | ### Solidity 56 | ```sol 57 | pragma solidity ^0.5.0; 58 | 59 | import "typescript-solidity-merkle-tree/contracts/MerkleTreeVerifier.sol"; 60 | 61 | contract YourContract { 62 | bytes root = 0x00; 63 | bytes32[] events; 64 | 65 | function computeState() public { 66 | root = MerkleTreeVerifier._computeMerkleRoot(events); 67 | } 68 | 69 | function updateState(bytes32[] proof, bool[] proofPaths, bytes32 leaf) public { 70 | require(MerkleTreeVerifier._verify(proof, proofPaths, root, leaf) == true, "INVALID_PROOF"); 71 | // allow withdraw, etc. 72 | } 73 | } 74 | ``` 75 | 76 | Note that if you are using 0x's compiler, you should be aware of naming conflicts if there are other contract files named 'MerkleTreeVerifier.sol'. 77 | 78 | ## Develop 79 | ``` 80 | # tests 81 | yarn test:watch 82 | yarn test 83 | 84 | # build 85 | yarn build 86 | ``` -------------------------------------------------------------------------------- /compiler.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractsDir": "contracts", 3 | "artifactsDir": "build/artifacts", 4 | "contracts": "*", 5 | "compilerSettings": { 6 | "optimizer": { "enabled": false }, 7 | "outputSelection": { 8 | "*": { 9 | "*": ["abi", "evm.bytecode.object"] 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /contract_templates/contract.handlebars: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-consecutive-blank-lines ordered-imports align trailing-comma whitespace class-name 2 | // tslint:disable:no-unused-variable 3 | // tslint:disable:no-unbound-method 4 | import { BaseContract } from '@0x/base-contract'; 5 | import { BlockParam, BlockParamLiteral, CallData, ContractAbi, ContractArtifact, DecodedLogArgs, MethodAbi, Provider, TxData, TxDataPayable } from 'ethereum-types'; 6 | import { BigNumber, classUtils, logUtils } from '@0x/utils'; 7 | import { SimpleContractArtifact } from '@0x/types'; 8 | import { Web3Wrapper } from '@0x/web3-wrapper'; 9 | import * as ethers from 'ethers'; 10 | import * as _ from 'lodash'; 11 | // tslint:enable:no-unused-variable 12 | 13 | {{#if events}} 14 | export type {{contractName}}EventArgs = 15 | {{#each events}} 16 | | {{@root.contractName}}{{name}}EventArgs{{#if @last}};{{/if}} 17 | {{/each}} 18 | 19 | export enum {{contractName}}Events { 20 | {{#each events}} 21 | {{name}} = '{{name}}', 22 | {{/each}} 23 | } 24 | 25 | {{#each events}} 26 | {{> event}} 27 | 28 | {{/each}} 29 | {{/if}} 30 | 31 | /* istanbul ignore next */ 32 | // tslint:disable:no-parameter-reassignment 33 | // tslint:disable-next-line:class-name 34 | export class {{contractName}}Contract extends BaseContract { 35 | {{#each methods}} 36 | {{#this.constant}} 37 | {{> call contractName=../contractName}} 38 | {{/this.constant}} 39 | {{^this.constant}} 40 | {{> tx contractName=../contractName}} 41 | {{/this.constant}} 42 | {{/each}} 43 | public static async deployFrom0xArtifactAsync( 44 | artifact: ContractArtifact | SimpleContractArtifact, 45 | provider: Provider, 46 | txDefaults: Partial, 47 | {{> typed_params inputs=ctor.inputs}} 48 | ): Promise<{{contractName}}Contract> { 49 | if (_.isUndefined(artifact.compilerOutput)) { 50 | throw new Error('Compiler output not found in the artifact file'); 51 | } 52 | const bytecode = artifact.compilerOutput.evm.bytecode.object; 53 | const abi = artifact.compilerOutput.abi; 54 | return {{contractName}}Contract.deployAsync(bytecode, abi, provider, txDefaults, {{> params inputs=ctor.inputs}}); 55 | } 56 | public static async deployAsync( 57 | bytecode: string, 58 | abi: ContractAbi, 59 | provider: Provider, 60 | txDefaults: Partial, 61 | {{> typed_params inputs=ctor.inputs}} 62 | ): Promise<{{contractName}}Contract> { 63 | const constructorAbi = BaseContract._lookupConstructorAbi(abi); 64 | [{{> params inputs=ctor.inputs}}] = BaseContract._formatABIDataItemList( 65 | constructorAbi.inputs, 66 | [{{> params inputs=ctor.inputs}}], 67 | BaseContract._bigNumberToString, 68 | ); 69 | const iface = new ethers.utils.Interface(abi); 70 | const deployInfo = iface.deployFunction; 71 | const txData = deployInfo.encode(bytecode, [{{> params inputs=ctor.inputs}}]); 72 | const web3Wrapper = new Web3Wrapper(provider); 73 | const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync( 74 | {data: txData}, 75 | txDefaults, 76 | web3Wrapper.estimateGasAsync.bind(web3Wrapper), 77 | ); 78 | const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); 79 | logUtils.log(`transactionHash: ${txHash}`); 80 | const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); 81 | logUtils.log(`{{contractName}} successfully deployed at ${txReceipt.contractAddress}`); 82 | const contractInstance = new {{contractName}}Contract(abi, txReceipt.contractAddress as string, provider, txDefaults); 83 | contractInstance.constructorArgs = [{{> params inputs=ctor.inputs}}]; 84 | return contractInstance; 85 | } 86 | constructor(abi: ContractAbi, address: string, provider: Provider, txDefaults?: Partial) { 87 | super('{{contractName}}', abi, address, provider, txDefaults); 88 | classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', 'abi', '_web3Wrapper']); 89 | } 90 | } // tslint:disable:max-file-line-count 91 | // tslint:enable:no-unbound-method 92 | -------------------------------------------------------------------------------- /contract_templates/partials/call.handlebars: -------------------------------------------------------------------------------- 1 | public {{this.tsName}} = { 2 | {{> callAsync}} 3 | }; 4 | -------------------------------------------------------------------------------- /contract_templates/partials/callAsync.handlebars: -------------------------------------------------------------------------------- 1 | async callAsync( 2 | {{> typed_params inputs=inputs}} 3 | callData: Partial = {}, 4 | defaultBlock?: BlockParam, 5 | ): Promise<{{> return_type outputs=outputs}}> { 6 | const self = this as any as {{contractName}}Contract; 7 | const encodedData = self._strictEncodeArguments('{{this.functionSignature}}', [{{> params inputs=inputs}}]); 8 | const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync( 9 | { 10 | to: self.address, 11 | ...callData, 12 | data: encodedData, 13 | }, 14 | self._web3Wrapper.getContractDefaults(), 15 | ); 16 | const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock); 17 | BaseContract._throwIfRevertWithReasonCallResult(rawCallResult); 18 | const abiEncoder = self._lookupAbiEncoder('{{this.functionSignature}}'); 19 | // tslint:disable boolean-naming 20 | const result = abiEncoder.strictDecodeReturnValue<{{> return_type outputs=outputs}}>(rawCallResult); 21 | // tslint:enable boolean-naming 22 | return result; 23 | }, 24 | -------------------------------------------------------------------------------- /contract_templates/partials/event.handlebars: -------------------------------------------------------------------------------- 1 | export interface {{@root.contractName}}{{name}}EventArgs extends DecodedLogArgs { 2 | {{#each inputs}} 3 | {{name}}: {{#returnType type components}}{{/returnType}}; 4 | {{/each}} 5 | } 6 | -------------------------------------------------------------------------------- /contract_templates/partials/params.handlebars: -------------------------------------------------------------------------------- 1 | {{#each inputs}} 2 | {{name}}{{#if @last}}{{else}},{{/if}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /contract_templates/partials/return_type.handlebars: -------------------------------------------------------------------------------- 1 | {{#if outputs.length}} 2 | {{#singleReturnValue}} 3 | {{#returnType outputs.0.type outputs.0.components}}{{/returnType}} 4 | {{/singleReturnValue}} 5 | {{^singleReturnValue}} 6 | [{{#each outputs}}{{#returnType type components}}{{/returnType}}{{#unless @last}}, {{/unless}}{{/each}}] 7 | {{/singleReturnValue}} 8 | {{else}} 9 | void 10 | {{/if}} 11 | -------------------------------------------------------------------------------- /contract_templates/partials/tx.handlebars: -------------------------------------------------------------------------------- 1 | public {{this.tsName}} = { 2 | async sendTransactionAsync( 3 | {{> typed_params inputs=inputs}} 4 | {{#this.payable}} 5 | txData: Partial = {}, 6 | {{/this.payable}} 7 | {{^this.payable}} 8 | txData: Partial = {}, 9 | {{/this.payable}} 10 | ): Promise { 11 | const self = this as any as {{contractName}}Contract; 12 | const encodedData = self._strictEncodeArguments('{{this.functionSignature}}', [{{> params inputs=inputs}}]); 13 | const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync( 14 | { 15 | to: self.address, 16 | ...txData, 17 | data: encodedData, 18 | }, 19 | self._web3Wrapper.getContractDefaults(), 20 | self.{{this.tsName}}.estimateGasAsync.bind( 21 | self, 22 | {{> params inputs=inputs}} 23 | ), 24 | ); 25 | const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); 26 | return txHash; 27 | }, 28 | async estimateGasAsync( 29 | {{> typed_params inputs=inputs}} 30 | txData: Partial = {}, 31 | ): Promise { 32 | const self = this as any as {{contractName}}Contract; 33 | const encodedData = self._strictEncodeArguments('{{this.functionSignature}}', [{{> params inputs=inputs}}]); 34 | const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync( 35 | { 36 | to: self.address, 37 | ...txData, 38 | data: encodedData, 39 | }, 40 | self._web3Wrapper.getContractDefaults(), 41 | ); 42 | const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults); 43 | return gas; 44 | }, 45 | getABIEncodedTransactionData( 46 | {{> typed_params inputs=inputs}} 47 | ): string { 48 | const self = this as any as {{contractName}}Contract; 49 | const abiEncodedTransactionData = self._strictEncodeArguments('{{this.functionSignature}}', [{{> params inputs=inputs}}]); 50 | return abiEncodedTransactionData; 51 | }, 52 | {{> callAsync}} 53 | }; 54 | -------------------------------------------------------------------------------- /contract_templates/partials/typed_params.handlebars: -------------------------------------------------------------------------------- 1 | {{#each inputs}} 2 | {{name}}: {{#parameterType type components}}{{/parameterType}}, 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /contracts/MerkleTreeVerifier.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | library MerkleTreeVerifier { 4 | function math_log2(uint x) public pure returns (uint y){ 5 | assembly { 6 | let arg := x 7 | x := sub(x,1) 8 | x := or(x, div(x, 0x02)) 9 | x := or(x, div(x, 0x04)) 10 | x := or(x, div(x, 0x10)) 11 | x := or(x, div(x, 0x100)) 12 | x := or(x, div(x, 0x10000)) 13 | x := or(x, div(x, 0x100000000)) 14 | x := or(x, div(x, 0x10000000000000000)) 15 | x := or(x, div(x, 0x100000000000000000000000000000000)) 16 | x := add(x, 1) 17 | let m := mload(0x40) 18 | mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd) 19 | mstore(add(m,0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe) 20 | mstore(add(m,0x40), 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616) 21 | mstore(add(m,0x60), 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff) 22 | mstore(add(m,0x80), 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e) 23 | mstore(add(m,0xa0), 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707) 24 | mstore(add(m,0xc0), 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606) 25 | mstore(add(m,0xe0), 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100) 26 | mstore(0x40, add(m, 0x100)) 27 | let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff 28 | let shift := 0x100000000000000000000000000000000000000000000000000000000000000 29 | let a := div(mul(x, magic), shift) 30 | y := div(mload(add(m,sub(255,a))), shift) 31 | y := add(y, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000))) 32 | } 33 | } 34 | 35 | function _computeMerkleRoot(bytes32[] memory items) public pure returns (bytes32) { 36 | for(uint i = 0; i < items.length; i++) { 37 | items[i] = _hashLeaf(items[i]); 38 | } 39 | 40 | // extend layer to be a power of 2 41 | // this simplifies logic later 42 | bytes32[] memory layer = _getBalancedLayer(items); 43 | 44 | while(layer.length > 1) { 45 | layer = _computeLayer(layer); 46 | } 47 | 48 | return layer[0]; 49 | } 50 | 51 | function _getBalancedLayer(bytes32[] memory items) public pure returns (bytes32[] memory) { 52 | uint powerOf2Size = 2 ** math_log2(items.length); 53 | if(items.length == 1) { 54 | powerOf2Size = 2; 55 | } 56 | bytes32[] memory layer = new bytes32[](powerOf2Size); 57 | for(uint i = 0; i < layer.length; i++) { 58 | if(i < items.length) { 59 | layer[i] = items[i]; 60 | } else { 61 | // duplicate last leaf 62 | layer[i] = items[items.length - 1]; 63 | } 64 | } 65 | return layer; 66 | } 67 | 68 | function _computeLayer(bytes32[] memory layer) public pure returns (bytes32[] memory) { 69 | // uint nLayers = math_log2(layer.length); 70 | // bytes32[] memory nextLayer = new bytes32[](2**(nLayers-1)); 71 | require(layer.length == 2 ** math_log2(layer.length), "NOT_PERFECT_POWEROF2"); 72 | 73 | bytes32[] memory nextLayer = new bytes32[](layer.length / 2); 74 | 75 | for(uint i = 0; i < nextLayer.length; i++) { 76 | uint left = i * 2; 77 | uint right = left + 1; 78 | 79 | // if(layer.length % 2 == 1) { 80 | // if(right == layer.length) { 81 | // right = left; 82 | // } 83 | // } 84 | 85 | nextLayer[i] = _hashBranch(layer[left], layer[right]); 86 | } 87 | 88 | return nextLayer; 89 | } 90 | 91 | 92 | /** 93 | * @dev Verifies a Merkle proof proving the existence of a leaf in a Merkle tree. Assumes that each pair of leaves 94 | * and each pair of pre-images are sorted. 95 | * @param proof Merkle proof containing sibling hashes on the branch from the leaf to the root of the Merkle tree 96 | * @param root Merkle root 97 | * @param leaf Leaf of Merkle tree 98 | */ 99 | function _verify(bytes32[] memory proof, bool[] memory paths, bytes32 root, bytes32 leaf) public pure returns (bool) { 100 | // Check if the computed hash (root) is equal to the provided root 101 | return _computeRoot(proof, paths, leaf) == root; 102 | } 103 | 104 | function _computeRoot(bytes32[] memory proof, bool[] memory paths, bytes32 leaf) public pure returns (bytes32) { 105 | bytes32 node = leaf; 106 | 107 | for (uint256 i = 0; i < proof.length; i++) { 108 | bytes32 pairNode = proof[i]; 109 | 110 | if (paths[i]) { 111 | // Hash(current element of the proof + current computed hash) 112 | node = _hashBranch(pairNode, node); 113 | } else { 114 | // Hash(current computed hash + current element of the proof) 115 | node = _hashBranch(node, pairNode); 116 | } 117 | } 118 | 119 | return node; 120 | } 121 | 122 | function _hashLeaf(bytes32 leaf) public pure returns (bytes32) { 123 | bytes1 LEAF_PREFIX = 0x00; 124 | return keccak256(abi.encodePacked(LEAF_PREFIX, leaf)); 125 | } 126 | 127 | function _hashBranch(bytes32 left, bytes32 right) public pure returns (bytes32) { 128 | bytes1 BRANCH_PREFIX = 0x01; 129 | return keccak256(abi.encodePacked(BRANCH_PREFIX, left, right)); 130 | } 131 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-solidity-merkle-tree", 3 | "version": "0.3.0", 4 | "description": "TypeScript Merkle tree that comes with Solidity proof verifier. Unit tested for your pleasure!", 5 | "main": "./lib/src/index.js", 6 | "types": "./lib/src/index.d.ts", 7 | "files": [ 8 | "build", 9 | "contracts", 10 | "lib/src" 11 | ], 12 | "scripts": { 13 | "watch": "tsc -W -p tsconfig.json", 14 | "build": "yarn build:typescript && yarn build:contracts", 15 | "build:typescript": "tsc -p tsconfig.json", 16 | "build:contracts": "sol-compiler contracts/ && yarn build:contracts:abis", 17 | "build:contracts:abis": "abi-gen --abis 'build/artifacts/*.json' --template contract_templates/contract.handlebars --partials './contract_templates/partials/**/*.handlebars' --output ./build/wrappers --backend web3", 18 | "test:watch": "tsc -p tsconfig.test.json -W", 19 | "test": "npm run test:solidity && npm run test:typescript", 20 | "test:solidity": "mocha -r source-map-support/register -r make-promises-safe --bail --exit lib/test/solidity.spec.js", 21 | "test:typescript": "mocha -r source-map-support/register -r make-promises-safe --bail --exit lib/test/typescript.spec.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/liamzebedee/typescript-solidity-merkle-tree.git" 26 | }, 27 | "author": "liamzebedee", 28 | "license": "GPL-3.0-only", 29 | "bugs": { 30 | "url": "https://github.com/liamzebedee/typescript-solidity-merkle-tree/issues" 31 | }, 32 | "homepage": "https://github.com/liamzebedee/typescript-solidity-merkle-tree#readme", 33 | "dependencies": { 34 | "@types/node": "^10.12.21", 35 | "ts-node": "^8.0.2", 36 | "typescript": "^3.3.1" 37 | }, 38 | "devDependencies": { 39 | "0x.js": "^5.0.0", 40 | "@0x/abi-gen": "^2.0.3", 41 | "@0x/sol-compiler": "^3.0.3", 42 | "@0x/sol-trace": "^2.0.4", 43 | "@0x/web3-wrapper": "^5.0.0", 44 | "chai": "^4.2.0", 45 | "chai-as-promised": "^7.1.1", 46 | "chai-bytes": "^0.1.2", 47 | "ganache-core": "^2.4.0", 48 | "mocha": "^5.2.0", 49 | "source-map-support": "^0.5.10", 50 | "web3-eth-abi": "^1.0.0-beta.46" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | function firstIndexOf(buf: Buffer, arr: Buffer[]) { 2 | for (let i = 0; i < arr.length; i++) { 3 | if (buf.equals(arr[i])) { 4 | return i; 5 | } 6 | } 7 | 8 | return -1; 9 | } 10 | 11 | let debug = false; 12 | 13 | // Protection against second preimage attacks 14 | // See https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack/ 15 | const LEAF_PREFIX = Buffer.from('00', 'hex'); 16 | const BRANCH_PREFIX = Buffer.from('01', 'hex'); 17 | 18 | type HashFunction = (buf: Buffer) => Buffer; 19 | type MerkleTreeProof = { 20 | root: Buffer; 21 | leaf: Buffer; 22 | proofs: Buffer[]; 23 | paths: boolean[]; 24 | } 25 | 26 | class MerkleTree { 27 | items: Buffer[]; 28 | leaves: Buffer[]; 29 | layers: Buffer[][]; 30 | nLayers: number; 31 | hashFn: (buf: Buffer) => Buffer; 32 | hashSizeBytes: number; 33 | 34 | constructor(items: Buffer[], hashFn: HashFunction) { 35 | let leaves = items; 36 | this.items = items; 37 | this.hashFn = hashFn; 38 | this.hashSizeBytes = hashFn(BRANCH_PREFIX).byteLength; 39 | 40 | // compute the balanced layer 41 | if(leaves.length === 1) leaves = leaves.concat(leaves) 42 | let balancedLeaves = new Array( 43 | Math.pow(2, Math.ceil(Math.log2( 44 | leaves.length 45 | ))) 46 | ); 47 | 48 | for(let j = 0; j < balancedLeaves.length; j++) { 49 | if(j > (leaves.length-1)) { 50 | balancedLeaves[j] = leaves[leaves.length - 1]; 51 | } else { 52 | balancedLeaves[j] = leaves[j]; 53 | } 54 | } 55 | 56 | leaves = balancedLeaves; 57 | 58 | // Now hash all. 59 | this.leaves = leaves.map(leaf => this.hashLeaf(leaf)) 60 | 61 | // And compute tree 62 | this.layers = this.computeTree(this.leaves); 63 | } 64 | 65 | root(): Buffer { 66 | if (this.layers[0].length == 0) throw new Error("no leaves in tree"); 67 | return this.layers[this.nLayers - 1][0]; 68 | } 69 | 70 | hashLeaf(leaf: Buffer): Buffer { 71 | return hashLeaf(this.hashFn, leaf); 72 | } 73 | 74 | hashBranch(left, right: Buffer): Buffer { 75 | if (left.byteLength != this.hashSizeBytes || right.byteLength != this.hashSizeBytes) { 76 | throw new Error("branches should be of hash size already"); 77 | } 78 | return hashBranch(this.hashFn, left, right) 79 | } 80 | 81 | findLeafIndex(item: Buffer) { 82 | let idx = firstIndexOf(this.hashLeaf(item), this.layers[0]); 83 | if(idx == -1) throw new Error('item not found'); 84 | return idx 85 | } 86 | 87 | findLeaf(item: Buffer) { 88 | return this.layers[0][this.findLeafIndex(item)] 89 | } 90 | 91 | generateProof(idx: number): MerkleTreeProof { 92 | let proofs: Buffer[] = new Array(this.nLayers - 1); 93 | let paths = []; 94 | let leaf = this.layers[0][idx] 95 | 96 | for (let i = 0; i < proofs.length; i++) { 97 | let layer = this.layers[i]; 98 | 99 | // if (i == this.nLayers - 1) { 100 | // proof[i] = layer[0]; 101 | // } else { 102 | // const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; 103 | // proof[i] = layer[pairIdx]; 104 | // idx = Math.floor(idx / 2); 105 | // } 106 | let isLeftNode = idx % 2 === 0; 107 | paths.push(!isLeftNode); 108 | 109 | const pairIdx = isLeftNode ? idx + 1 : idx - 1; 110 | proofs[i] = layer[pairIdx]; 111 | idx = Math.floor(idx / 2); 112 | } 113 | 114 | return { proofs, paths, leaf, root: this.root() } 115 | } 116 | 117 | // TODO remove leaf param 118 | verifyProof(proof: MerkleTreeProof, leaf?: Buffer) { 119 | if(!leaf) { 120 | leaf = proof.leaf; 121 | } 122 | if (proof.proofs.length != this.nLayers - 1) throw new Error(`${proof.proofs.length} proof nodes, but only ${this.nLayers} layers in tree`) 123 | if(firstIndexOf(leaf, this.layers[0]) == -1) throw new Error(`Leaf doesn't exist in original tree`); 124 | return verifyProof(this.hashFn, proof, this.root(), leaf); 125 | } 126 | 127 | private computeTree(leaves: Buffer[]) { 128 | // 0th layer is the leaves 129 | this.nLayers = Math.ceil(Math.log2(leaves.length)) + 1; 130 | let layers: Buffer[][] = new Array(this.nLayers); 131 | 132 | for (let i = 0; i < this.nLayers; i++) { 133 | if (i == 0) { 134 | layers[i] = leaves; 135 | continue; 136 | } else { 137 | layers[i] = this.computeLayer(layers[i - 1]); 138 | } 139 | 140 | } 141 | 142 | return layers; 143 | } 144 | 145 | computeLayer(leaves: Buffer[]): Buffer[] { 146 | let nodes: Buffer[] = []; 147 | 148 | // Make sure it's even 149 | if (leaves.length % 2 == 1) { 150 | // Some languages (ie Solidity) don't have prepend, so this makes compatible implementations easier. 151 | leaves = [...leaves, leaves[leaves.length - 1]] 152 | } 153 | 154 | for (let i = 0; i < leaves.length;) { 155 | nodes.push( 156 | this.hashBranch(leaves[i], leaves[i + 1]) 157 | ); 158 | i += 2; 159 | } 160 | 161 | return nodes; 162 | } 163 | 164 | toString() { 165 | let str = ""; 166 | 167 | let j = 0; 168 | 169 | this.layers.map((layer, i) => { 170 | str += `Layer ${i} - \n`; 171 | 172 | for (let node of layer) { 173 | str += '\t ' + node.toString('hex'); 174 | if(i == 0) { 175 | if(j < this.items.length) 176 | str += '\t' + this.items[j++].toString('hex') 177 | } 178 | str += '\n'; 179 | } 180 | }) 181 | return str; 182 | } 183 | } 184 | 185 | 186 | function hashLeaf(hashFn: HashFunction, leaf: Buffer): Buffer { 187 | return hashFn(Buffer.concat([LEAF_PREFIX, leaf])) 188 | } 189 | 190 | function hashBranch(hashFn: HashFunction, left, right: Buffer): Buffer { 191 | return hashFn(Buffer.concat([BRANCH_PREFIX, left, right])) 192 | } 193 | 194 | function verifyProof(hashFn: HashFunction, proof: MerkleTreeProof, root: Buffer, leaf: Buffer) { 195 | let node = leaf; 196 | 197 | // node > proof 198 | // node.compare(proof[0]) == 1 199 | let { proofs, paths } = proof; 200 | 201 | for (let i = 0; i < proofs.length; i++) { 202 | let pairNode = proofs[i]; 203 | 204 | if(debug) { 205 | console.log(`Verifying layer ${i}`) 206 | console.log(`\t`, node) 207 | console.log(`\t`, pairNode) 208 | } 209 | 210 | if(paths[i]) { 211 | node = hashBranch(hashFn, pairNode, node) 212 | } else { 213 | node = hashBranch(hashFn, node, pairNode) 214 | } 215 | } 216 | 217 | if(debug) { 218 | console.log(`Verify root`) 219 | console.log('\t', root) 220 | console.log('\t', node) 221 | } 222 | 223 | return root.equals(node); 224 | } 225 | 226 | export { 227 | MerkleTree, 228 | MerkleTreeProof, 229 | hashLeaf, 230 | hashBranch, 231 | verifyProof, 232 | LEAF_PREFIX, 233 | BRANCH_PREFIX 234 | }; -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { MerkleTree } from "../src"; 2 | const AbiCoder = require('web3-eth-abi').AbiCoder(); 3 | 4 | // @ts-ignore 5 | import { keccak256 } from 'ethereumjs-util'; 6 | import { Web3ProviderEngine } from "0x.js"; 7 | import { AbiDefinition, Provider, TxData } from '@0x/web3-wrapper'; 8 | 9 | function getDeployArgs(name: string, pe: Provider, from: string): [ string, AbiDefinition[], Provider, Partial] { 10 | let json = require(`../../build/artifacts/${name}.json`); 11 | let bytecode = json.compilerOutput.evm.bytecode.object; 12 | let abi = json.compilerOutput.abi; 13 | let provider = pe; 14 | console.log(from) 15 | 16 | return [ 17 | bytecode, 18 | abi, 19 | provider, 20 | { from } 21 | ] 22 | } 23 | 24 | class TestTreeFactory { 25 | static itemsToBuffer(items: string[][]): Buffer[] { 26 | let itemsBuf: Buffer[] = [ 27 | ...items.map(item => AbiCoder.encodeParameter('uint256', item)) 28 | ].map(item => item.slice(2)).map(item => Buffer.from(item, 'hex')) 29 | return itemsBuf; 30 | } 31 | 32 | static newTree(items: string[][]): MerkleTree { 33 | let tree = new MerkleTree( 34 | this.itemsToBuffer(items), 35 | keccak256 36 | ); 37 | return tree; 38 | } 39 | } 40 | 41 | function hexify(buf: Buffer): string { 42 | return `0x${buf.toString('hex')}`; 43 | } 44 | 45 | function prefix0x(x: string): string { 46 | return `0x${x}`; 47 | } 48 | 49 | 50 | const ganache = require("ganache-core"); 51 | 52 | class GanacheTestchain { 53 | static async start(port: string) { 54 | const server = ganache.server({ 55 | ws: true, 56 | logger: { 57 | log: () => false // console.log 58 | }, 59 | total_accounts: 100, 60 | s: "TestRPC is awesome!", // I didn't choose this 61 | gasPrice: 0, 62 | networkId: 420, 63 | debug: false, 64 | defaultBalanceEther: '100000000000000000000000000000', 65 | unlock: [0, 1], 66 | }); 67 | 68 | let blockchainState = await new Promise((res, rej) => { 69 | server.listen(port, (err, state) => { 70 | if(err) rej(err); 71 | else res(state) 72 | }) 73 | }); 74 | 75 | return blockchainState; 76 | } 77 | } 78 | 79 | function waitUntilConnected(pe: Web3ProviderEngine): Promise { 80 | return new Promise((res, rej) => { 81 | pe.on('block', res) 82 | setTimeout(rej, 2000) 83 | }); 84 | } 85 | 86 | export { 87 | getDeployArgs, 88 | TestTreeFactory, 89 | GanacheTestchain, 90 | hexify, 91 | prefix0x, 92 | keccak256, 93 | waitUntilConnected 94 | } -------------------------------------------------------------------------------- /test/solidity.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | const chai = require('chai') 3 | import { expect, assert } from 'chai'; 4 | import { describe, it, before, teardown, Test } from 'mocha'; 5 | chai.use(require('chai-as-promised')).use(require('chai-bytes')); 6 | require('mocha') 7 | 8 | import { 9 | MerkleTree, 10 | hashBranch, 11 | hashLeaf, 12 | } from "../src"; 13 | 14 | 15 | import { MerkleTreeVerifierContract } from '../build/wrappers/merkle_tree_verifier'; 16 | 17 | import { 18 | GanacheTestchain, TestTreeFactory, hexify, keccak256, waitUntilConnected, prefix0x, getDeployArgs 19 | } from './helpers' 20 | 21 | import { Web3ProviderEngine, RPCSubprovider, BigNumber } from "0x.js"; 22 | import { Web3Wrapper, AbiDefinition, Provider, TxData } from '@0x/web3-wrapper'; 23 | import { TruffleArtifactAdapter, RevertTraceSubprovider } from '@0x/sol-trace'; 24 | 25 | let $web3 = require('web3') 26 | const AbiCoder = require('web3-eth-abi').AbiCoder(); 27 | 28 | 29 | 30 | 31 | 32 | function dehexify(str: string): Buffer { 33 | // begins with 0x 34 | if(str[1] == 'x') str = str.slice(2); 35 | return Buffer.from(str, 'hex') 36 | } 37 | 38 | 39 | describe('Solidity verifier', function() { 40 | this.timeout(15000) 41 | 42 | let pe: Web3ProviderEngine, web3: Web3Wrapper; 43 | let accounts: string[]; 44 | 45 | let merkleTreeVerifier: MerkleTreeVerifierContract; 46 | 47 | before(async () => { 48 | const port = '9546'; 49 | let chain = GanacheTestchain.start(port); 50 | 51 | let pe2 = new Web3ProviderEngine(); 52 | pe2.addProvider(new RPCSubprovider(`http://127.0.0.1:${port}`)) 53 | pe2.start() 54 | 55 | web3 = new Web3Wrapper(pe2); 56 | expect(waitUntilConnected(pe2), "didn't connect to chain").to.eventually.be.fulfilled; 57 | 58 | accounts = await web3.getAvailableAddressesAsync(); 59 | 60 | // Now we add tracing in. 61 | const artifactAdapter = new TruffleArtifactAdapter( 62 | require('path').dirname(require.resolve('../../package.json')), 63 | '0.5.0' 64 | ); 65 | const revertTraceSubprovider = new RevertTraceSubprovider( 66 | artifactAdapter, 67 | accounts[0], 68 | true 69 | ); 70 | 71 | pe = new Web3ProviderEngine(); 72 | pe.addProvider(revertTraceSubprovider); 73 | pe.addProvider(new RPCSubprovider(`http://127.0.0.1:${port}`)) 74 | pe.start() 75 | expect(waitUntilConnected(pe), "didn't connect to chain").to.eventually.be.fulfilled; 76 | 77 | web3 = new Web3Wrapper(pe); 78 | 79 | merkleTreeVerifier = await MerkleTreeVerifierContract.deployAsync( 80 | ...getDeployArgs('MerkleTreeVerifier', pe, accounts[0]) 81 | ); 82 | }); 83 | 84 | describe('math_log2', () => { 85 | it('log2(7) = 3', async() => { 86 | let res = await merkleTreeVerifier.math_log2.callAsync(new BigNumber('7')); 87 | expect(res.toString()).to.eq('3'); 88 | }) 89 | 90 | it('log2(8) = 3', async() => { 91 | let res = await merkleTreeVerifier.math_log2.callAsync( 92 | new BigNumber('7') 93 | ); 94 | expect(res.toString()).to.eq('3'); 95 | }) 96 | }) 97 | 98 | describe('_getBalancedLayer', () => { 99 | it('works for 6 items', async() => { 100 | let itemSolEncoded = [ 101 | '12', 102 | '15', 103 | '20', 104 | '25', 105 | '42', 106 | '33', 107 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 108 | let items = await merkleTreeVerifier._getBalancedLayer.callAsync( 109 | itemSolEncoded 110 | ); 111 | expect(items.length).to.eq(8); 112 | let lastItem = itemSolEncoded[itemSolEncoded.length - 1]; 113 | expect(items[6]).to.eq(lastItem) 114 | expect(items[7]).to.eq(lastItem) 115 | }) 116 | 117 | it('works for 4 items', async() => { 118 | let itemSolEncoded = [ 119 | '12', 120 | '15', 121 | '20', 122 | '25', 123 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 124 | let items = await merkleTreeVerifier._getBalancedLayer.callAsync( 125 | itemSolEncoded 126 | ); 127 | expect(items.length).to.eq(4); 128 | expect(items[3]).to.eq(itemSolEncoded[3]) 129 | }) 130 | }) 131 | 132 | describe('_computeLayer', () => { 133 | function makeMockLayer(items: Buffer[]) { 134 | let layerJs = new Array(items.length / 2); 135 | for(let i = 0; i < layerJs.length; i++) { 136 | let left = i*2; 137 | layerJs[i] = hashBranch(keccak256, items[left], items[left+1]); 138 | } 139 | layerJs = layerJs.map(hexify) 140 | return layerJs 141 | } 142 | 143 | it('works for 8 items', async () => { 144 | let itemSolEncoded = [ 145 | '12', 146 | '15', 147 | '20', 148 | '25', 149 | '42', 150 | '33', 151 | '151', 152 | '22' 153 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 154 | 155 | expect(itemSolEncoded.length).to.eq(8) 156 | 157 | let itemsBuf = itemSolEncoded.map(dehexify) 158 | let layerJs = makeMockLayer(itemsBuf) 159 | 160 | let layerSol = await merkleTreeVerifier._computeLayer.callAsync(itemSolEncoded) 161 | expect(layerSol).to.deep.eq(layerJs); 162 | }) 163 | 164 | it('works for 4 items', async () => { 165 | let itemSolEncoded = [ 166 | '12', 167 | '15', 168 | '20', 169 | '25' 170 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 171 | 172 | expect(itemSolEncoded.length).to.eq(4) 173 | 174 | let itemsBuf = itemSolEncoded.map(dehexify) 175 | let layerJs = makeMockLayer(itemsBuf) 176 | 177 | let layerSol = await merkleTreeVerifier._computeLayer.callAsync(itemSolEncoded) 178 | expect(layerSol).to.deep.eq(layerJs); 179 | }) 180 | 181 | // TODO this throws but fails the entire test 182 | // it looks like it is bad but it's not, I swear 183 | it('fails for 6 items', async () => { 184 | // let itemSolEncoded = [ 185 | // '12', 186 | // '15', 187 | // '20', 188 | // '25', 189 | // '42', 190 | // '33', 191 | // ].map(item => AbiCoder.encodeParameter('uint256', item)) 192 | // let layerSol = merkleTreeVerifier._computeLayer.callAsync(itemSolEncoded) 193 | // expect(layerSol).to.throw; 194 | }) 195 | }) 196 | 197 | describe('_computeMerkleRoot', () => { 198 | it('passes on 1 items', async () => { 199 | let itemSolEncoded = [ 200 | '12', 201 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 202 | 203 | let tree = new MerkleTree(itemSolEncoded.map(dehexify), keccak256); 204 | let rootJs = tree.root(); 205 | 206 | let rootSol = await merkleTreeVerifier._computeMerkleRoot.callAsync( 207 | itemSolEncoded 208 | ); 209 | 210 | expect(rootSol).to.eq(hexify(rootJs)); 211 | }) 212 | 213 | it('passes on 6 items', async () => { 214 | let itemSolEncoded = [ 215 | '12', 216 | '15', 217 | '20', 218 | '25', 219 | '542', 220 | '33' 221 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 222 | 223 | // let tree = new MerkleTree(itemSolEncoded.map(dehexify).map(x => hashLeaf(keccak256, x)), keccak256); 224 | let tree = new MerkleTree(itemSolEncoded.map(dehexify), keccak256); 225 | let rootJs = tree.root(); 226 | 227 | let rootSol = await merkleTreeVerifier._computeMerkleRoot.callAsync( 228 | itemSolEncoded 229 | ); 230 | 231 | expect(rootSol).to.eq(hexify(rootJs)); 232 | }) 233 | 234 | it('passes on 8 items', async () => { 235 | let itemSolEncoded = [ 236 | '12', 237 | '15', 238 | '20', 239 | '25', 240 | '42', 241 | '33', 242 | '151', 243 | '22' 244 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 245 | 246 | let tree = new MerkleTree(itemSolEncoded.map(dehexify), keccak256); 247 | let rootJs = tree.root(); 248 | 249 | let rootSol = await merkleTreeVerifier._computeMerkleRoot.callAsync( 250 | itemSolEncoded 251 | ); 252 | 253 | expect(rootSol).to.eq(hexify(rootJs)); 254 | }) 255 | }) 256 | 257 | it('_hashLeaf', async() => { 258 | let data = ['00','2']; 259 | 260 | // TODO 1st param is bytes32, but uint256 encodes simpler 261 | let hex: string[] = [ 262 | AbiCoder.encodeParameter('uint256', data[0]), 263 | AbiCoder.encodeParameter('uint256', data[1]) 264 | ].map(item => item.slice(2)) 265 | 266 | let buf = Buffer.concat(hex.map(x => Buffer.from(x, 'hex'))); 267 | expect(buf.byteLength).to.eq(64); 268 | 269 | let leafJs = hashLeaf(keccak256, buf); 270 | let hashJs = hashLeaf(keccak256, leafJs); 271 | 272 | let hashSol = await merkleTreeVerifier._hashLeaf.callAsync( 273 | hexify(leafJs) 274 | ) 275 | 276 | expect(hashSol).to.eq(hexify(hashJs)); 277 | }) 278 | 279 | it('_hashBranch', async() => { 280 | let left = '123'; 281 | let right = '245'; 282 | 283 | // TODO 1st param is bytes32, but uint256 encodes simpler 284 | let leftHex: string = AbiCoder.encodeParameter('uint256', left) 285 | let rightHex: string = AbiCoder.encodeParameter('uint256', right) 286 | 287 | let leftBuf = Buffer.from(leftHex.slice(2), 'hex') 288 | let rightBuf = Buffer.from(rightHex.slice(2), 'hex') 289 | 290 | expect(leftBuf.byteLength).to.eq(32); 291 | expect(rightBuf.byteLength).to.eq(32); 292 | 293 | let hashJs = hashBranch(keccak256, leftBuf, rightBuf); 294 | 295 | let hashSol = await merkleTreeVerifier._hashBranch.callAsync( 296 | leftHex, 297 | rightHex 298 | ) 299 | 300 | expect(hashSol).to.eq(hexify(hashJs)); 301 | }) 302 | 303 | describe('_verify', async() => { 304 | it('works with 5 items', async() => { 305 | let itemSolEncoded = [ 306 | '12', 307 | '15', 308 | '20', 309 | '25', 310 | '42', 311 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 312 | 313 | let tree = new MerkleTree(itemSolEncoded.map(dehexify), keccak256); 314 | 315 | async function testProof(item, i) { 316 | let proof = tree.generateProof(i) 317 | 318 | expect( 319 | await merkleTreeVerifier._verify.callAsync( 320 | proof.proofs.map(hexify), 321 | proof.paths, 322 | hexify(proof.root), 323 | hexify(proof.leaf) 324 | ), 325 | `item ${i}` 326 | ).to.be.true; 327 | } 328 | 329 | await Promise.all(itemSolEncoded.map(testProof)) 330 | }) 331 | 332 | it('works with 8 items', async() => { 333 | let itemSolEncoded: string[] = [ 334 | '12', 335 | '15', 336 | '20', 337 | '25', 338 | '42', 339 | '33', 340 | '151', 341 | '22' 342 | ].map(item => AbiCoder.encodeParameter('uint256', item)) 343 | 344 | let tree = new MerkleTree(itemSolEncoded.map(dehexify), keccak256); 345 | 346 | async function testProof(item, i) { 347 | let proof = tree.generateProof(i) 348 | 349 | expect( 350 | await merkleTreeVerifier._verify.callAsync( 351 | proof.proofs.map(hexify), 352 | proof.paths, 353 | hexify(proof.root), 354 | hexify(proof.leaf) 355 | ), 356 | `item ${i}` 357 | ).to.be.true; 358 | } 359 | 360 | await Promise.all(itemSolEncoded.map(testProof)) 361 | }) 362 | }) 363 | 364 | it('_verify', async() => { 365 | let items = [ 366 | ['1','2'], 367 | ['3','4'] 368 | ] 369 | let itemsBuffed = TestTreeFactory.itemsToBuffer(items); 370 | let i = 0 371 | let itemToProve = itemsBuffed[i]; 372 | 373 | let tree = TestTreeFactory.newTree(items) 374 | let leafToProve = tree.leaves[i] 375 | 376 | 377 | let proof = tree.generateProof(i); 378 | 379 | expect(tree.verifyProof(proof, leafToProve)).to.be.true; 380 | 381 | let root = await merkleTreeVerifier._computeRoot.callAsync( 382 | proof.proofs.map(hexify), 383 | proof.paths, 384 | hexify(leafToProve) 385 | ) 386 | 387 | expect(root).to.eq(hexify(tree.layers[1][0])); 388 | expect(root).to.eq(hexify(tree.root())) 389 | 390 | let verify = await merkleTreeVerifier._verify.callAsync( 391 | proof.proofs.map(hexify), 392 | proof.paths, 393 | hexify(tree.root()), 394 | hexify(leafToProve) 395 | ) 396 | expect(verify).to.be.true; 397 | }) 398 | 399 | 400 | 401 | }) 402 | -------------------------------------------------------------------------------- /test/typescript.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | const chai = require('chai') 3 | import { expect, assert } from 'chai'; 4 | import { describe, it, before, teardown, Test } from 'mocha'; 5 | chai.use(require('chai-as-promised')).use(require('chai-bytes')); 6 | require('mocha') 7 | 8 | import { 9 | MerkleTree, 10 | hashBranch, 11 | hashLeaf, 12 | } from "../src"; 13 | 14 | 15 | import { MerkleTreeVerifierContract } from '../build/wrappers/merkle_tree_verifier'; 16 | 17 | import { 18 | GanacheTestchain, TestTreeFactory, hexify, keccak256, waitUntilConnected, prefix0x, getDeployArgs 19 | } from './helpers' 20 | 21 | import { Web3ProviderEngine, RPCSubprovider, BigNumber } from "0x.js"; 22 | import { Web3Wrapper, AbiDefinition, Provider, TxData } from '@0x/web3-wrapper'; 23 | import { TruffleArtifactAdapter, RevertTraceSubprovider } from '@0x/sol-trace'; 24 | 25 | let $web3 = require('web3') 26 | const AbiCoder = require('web3-eth-abi').AbiCoder(); 27 | 28 | 29 | 30 | function dehexify(str: string): Buffer { 31 | // begins with 0x 32 | if(str[1] == 'x') str = str.slice(2); 33 | return Buffer.from(str, 'hex') 34 | } 35 | 36 | describe('Typescript Merkle tree', function() { 37 | it('runs example', async () => { 38 | let items = [ 39 | Buffer.from('123', 'hex'), 40 | Buffer.from('foobar') 41 | ]; 42 | 43 | let tree = new MerkleTree(items, keccak256); 44 | 45 | let proof = tree.generateProof(1); 46 | expect(tree.verifyProof(proof, tree.findLeaf(items[1]))).to.be.true; 47 | expect(tree.verifyProof(proof, tree.findLeaf(items[0]))).to.be.false; 48 | }) 49 | 50 | it('handles two duplicate elements', async () => { 51 | let items = [ 52 | Buffer.from('12', 'hex'), 53 | Buffer.from('15', 'hex'), 54 | Buffer.from('20', 'hex'), 55 | Buffer.from('25', 'hex') 56 | ]; 57 | 58 | let tree = new MerkleTree(items, keccak256); 59 | let leaves = tree.layers[0]; 60 | 61 | // console.log(tree.toString()) 62 | 63 | function verify(item, i) { 64 | // console.log('item', item) 65 | let proof = tree.generateProof(i) 66 | // console.log(proof) 67 | let leaf = tree.leaves[i] 68 | expect(tree.verifyProof(proof, leaf)).to.be.true; 69 | } 70 | 71 | items.map(verify) 72 | }) 73 | 74 | it('computes on n=1 items', async () => { 75 | let items = [ 76 | Buffer.from('123', 'hex'), 77 | ]; 78 | 79 | let tree = new MerkleTree(items, keccak256); 80 | 81 | expect(tree.nLayers).to.eq(2) 82 | 83 | function expectArraysEqual(a, b: Buffer) { 84 | expect(hexify(a)).to.eq(hexify(b)) 85 | } 86 | 87 | expectArraysEqual(tree.layers[0][0], hashLeaf(keccak256, items[0])) 88 | expectArraysEqual(tree.layers[0][1], hashLeaf(keccak256, items[0])) 89 | expectArraysEqual(tree.layers[1][0], hashBranch(keccak256, tree.layers[0][0], tree.layers[0][1])) 90 | expectArraysEqual(tree.root(), tree.layers[1][0]) 91 | 92 | let proof = tree.generateProof(1); 93 | expect(tree.verifyProof(proof, tree.findLeaf(items[0]))).to.be.true; 94 | }) 95 | 96 | it('fails on n=0 items', async () => { 97 | let items = [ 98 | ]; 99 | 100 | expect(() => { 101 | let tree = new MerkleTree(items, keccak256); 102 | }).to.throws('Invalid array length') 103 | }) 104 | 105 | 106 | it('throws early on an unknown leaf in a proof', async() => { 107 | let items = [ 108 | ['1','2'], 109 | ['3','0'] 110 | ] 111 | let itemsBuffed = TestTreeFactory.itemsToBuffer(items); 112 | // let itemToProve = itemsBuffed[0]; 113 | 114 | let tree = TestTreeFactory.newTree(items) 115 | let i = 0 116 | let leafToProve = tree.leaves[i]; 117 | 118 | // give it the item, not the leaf (hashed) 119 | let proof = tree.generateProof(i); 120 | expect(tree.verifyProof(proof, leafToProve)).to.throw; 121 | }) 122 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2018", 5 | "dom" 6 | ], 7 | "rootDir": ".", 8 | "outDir": "lib", 9 | "typeRoots": [ 10 | "node_modules/@0x/typescript-typings/types", 11 | "node_modules/@types" 12 | ], 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "module": "commonjs", 17 | "sourceMap": true, 18 | "declaration": true, 19 | "declarationMap": true 20 | }, 21 | "include": [ 22 | "src/", 23 | "build/wrappers/" 24 | ], 25 | "exclude": [ 26 | "node_modules/**" 27 | ] 28 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2018", 5 | "dom" 6 | ], 7 | "rootDir": ".", 8 | "outDir": "lib", 9 | "typeRoots": [ 10 | "node_modules/@0x/typescript-typings/types", 11 | "node_modules/@types" 12 | ], 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "module": "commonjs", 17 | "sourceMap": true 18 | }, 19 | "include": [ 20 | "test/" 21 | ], 22 | "exclude": [ 23 | "node_modules/**" 24 | ] 25 | } --------------------------------------------------------------------------------