From cc28ce90831b47b4eba0f0c32ac32a1f97f66e46 Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Sat, 22 Jul 2017 00:31:30 -0700 Subject: [PATCH] init --- .editorconfig | 23 +++ .gitattributes | 2 + .gitignore | 103 ++++++++++++++ LICENSE.md | 21 +++ README.md | 298 +++++++++++++++++++++++++++++++++++++++ diagrams/merkle-tree.png | Bin 0 -> 42960 bytes diagrams/merkle-tree.xml | 1 + index.js | 225 +++++++++++++++++++++++++++++ package.json | 51 +++++++ test/index.js | 212 ++++++++++++++++++++++++++++ 10 files changed, 936 insertions(+) create mode 100755 .editorconfig create mode 100755 .gitattributes create mode 100755 .gitignore create mode 100755 LICENSE.md create mode 100644 README.md create mode 100644 diagrams/merkle-tree.png create mode 100644 diagrams/merkle-tree.xml create mode 100644 index.js create mode 100644 package.json create mode 100644 test/index.js diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..45a8e81 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig http://editorconfig.org + +# top-most EditorConfig file +root = true + +[*] +# use line feed +end_of_line = lf + +# ensure file ends with a newline when saving +insert_final_newline = true + +# soft tabs +indent_style = space + +# number of columns used for each indentation level +indent_size = 2 + +# remove any whitespace characters preceding newline characters +trim_trailing_whitespace = true + +# character set +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..4c4ff8c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Convert line endings to LF (Line Feed) +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..403e74f --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +//this will affect all the git repos +git config --global core.excludesfile ~/.gitignore + + +//update files since .ignore won't if already tracked +git rm --cached + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +# Icon? +ehthumbs.db +Thumbs.db +.cache +.project +.settings +.tmproj +*.esproj +nbproject + +# Numerous always-ignore extensions # +##################################### +*.diff +*.err +*.orig +*.rej +*.swn +*.swo +*.swp +*.vi +*~ +*.sass-cache +*.grunt +*.tmp + +# Dreamweaver added files # +########################### +_notes +dwsync.xml + +# Komodo # +########################### +*.komodoproject +.komodotools + +# Node # +##################### +node_modules + +# Bower # +##################### +bower_components + +# Folders to ignore # +##################### +.hg +.svn +.CVS +intermediate +publish +.idea +.graphics +_test +_archive +uploads +tmp + +# Vim files to ignore # +####################### +.VimballRecord +.netrwhist diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..eb0736f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT license + +Copyright (C) 2015 Miguel Mota + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d3260e --- /dev/null +++ b/README.md @@ -0,0 +1,298 @@ +# Merkle Tree + +> Construct [Merkle Trees](https://en.wikipedia.org/wiki/Merkle_tree) and verify via proofs in JavaScript. + + + +# Install + +```bash +npm install m-tree +``` + +# Documentation + +## Classes + +
+
MerkleTree
+
+
+ +## Objects + +
+
MerkleTree : object
+

Class reprensenting a Merkle Tree

+
+
+ + + +## MerkleTree +**Kind**: global class + +* [MerkleTree](#MerkleTree) + * [new MerkleTree(leaves, hashAlgo, options)](#new_MerkleTree_new) + * [.getLeaves()](#MerkleTree+getLeaves) ⇒ Array + * [.getLayers()](#MerkleTree+getLayers) ⇒ Array + * [.getRoot()](#MerkleTree+getRoot) ⇒ Buffer + * [.getProof(leaf, [index])](#MerkleTree+getProof) ⇒ Array + * [.verify(proof, targetNode, root)](#MerkleTree+verify) ⇒ Boolean + + + +### new MerkleTree(leaves, hashAlgo, options) +Constructs a Merkle Tree. +All nodes and leaves are stored as Buffers. +Lonely leaf nodes are promoted to the next level up without being hashed again. + + +| Param | Type | Description | +| --- | --- | --- | +| leaves | Array | Array of hashed leaves. Each leaf must be a Buffer. | +| hashAlgo | function | Algorithm used for hashing leaves and nodes | +| options | Object | Additional options | +| options.isBitcoinTree | Boolean | If set to `true`, generates the Merkle Tree with the Bitcoin Merkle Tree implementation. Enable it when you need to replicate Bitcoin constructed Merkle Trees. | + +**Example** +```js +const MerkleTree = require('m-tree') +const crypto = require('crypto') + +function sha256(data) { + // returns Buffer + return crypto.createHash('sha256').update(data).digest() +} + +const leaves = ['a', 'b', 'c'].map(x => sha3(x)) + +const tree = new MerkleTree(leaves, sha256) +``` + + +### merkleTree.getLeaves() ⇒ Array +Returns leaves of Merkle Tree. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - array of leaves +**Example** +```js +const leaves = tree.getLeaves() +``` + + +### merkleTree.getLayers() ⇒ Array +Returns all layers of Merkle Tree, including leaves and root. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - array of layers +**Example** +```js +const layers = tree.getLayers() +``` + + +### merkleTree.getRoot() ⇒ Buffer +Returns the Merkle root hash. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Buffer - - Merkle root hash +**Example** +```js +const root = tree.getRoot() +``` + + +### merkleTree.getProof(leaf, [index]) ⇒ Array +Returns the proof for a target leaf. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - Array of Buffer hashes. + +| Param | Type | Description | +| --- | --- | --- | +| leaf | Buffer | Target leaf | +| [index] | Number | Target leaf index in leaves array. Use if there are leaves containing duplicate data in order to distinguish it. | + +**Example** +```js +const proof = tree.getProof(leaves[2]) +``` +**Example** +```js +const leaves = ['a', 'b', 'a'].map(x => sha3(x)) +const tree = new MerkleTree(leaves, sha3) +const proof = tree.getProof(leaves[2], 2) +``` + + +### merkleTree.verify(proof, targetNode, root) ⇒ Boolean +Returns true if the proof path (array of hashes) can connect the target node +to the Merkle root. + +**Kind**: instance method of [MerkleTree](#MerkleTree) + +| Param | Type | Description | +| --- | --- | --- | +| proof | Array | Array of proof Buffer hashes that should connect target node to Merkle root. | +| targetNode | Buffer | Target node Buffer | +| root | Buffer | Merkle root Buffer | + +**Example** +```js +const root = tree.getRoot() +const proof = tree.getProof(leaves[2]) +const verified = tree.verify(proof, leaves[2], root) +``` + + +## MerkleTree : object +Class reprensenting a Merkle Tree + +**Kind**: global namespace +**Example** +```js +const tree = new MerkleTree(leaves, sha256) +``` + +* [MerkleTree](#MerkleTree) : object + * [new MerkleTree(leaves, hashAlgo, options)](#new_MerkleTree_new) + * [.getLeaves()](#MerkleTree+getLeaves) ⇒ Array + * [.getLayers()](#MerkleTree+getLayers) ⇒ Array + * [.getRoot()](#MerkleTree+getRoot) ⇒ Buffer + * [.getProof(leaf, [index])](#MerkleTree+getProof) ⇒ Array + * [.verify(proof, targetNode, root)](#MerkleTree+verify) ⇒ Boolean + + + +### new MerkleTree(leaves, hashAlgo, options) +Constructs a Merkle Tree. +All nodes and leaves are stored as Buffers. +Lonely leaf nodes are promoted to the next level up without being hashed again. + + +| Param | Type | Description | +| --- | --- | --- | +| leaves | Array | Array of hashed leaves. Each leaf must be a Buffer. | +| hashAlgo | function | Algorithm used for hashing leaves and nodes | +| options | Object | Additional options | +| options.isBitcoinTree | Boolean | If set to `true`, generates the Merkle Tree with the Bitcoin Merkle Tree implementation. Enable it when you need to replicate Bitcoin constructed Merkle Trees. | + +**Example** +```js +const MerkleTree = require('m-tree') +const crypto = require('crypto') + +function sha256(data) { + // returns Buffer + return crypto.createHash('sha256').update(data).digest() +} + +const leaves = ['a', 'b', 'c'].map(x => sha3(x)) + +const tree = new MerkleTree(leaves, sha256) +``` + + +### merkleTree.getLeaves() ⇒ Array +Returns leaves of Merkle Tree. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - array of leaves +**Example** +```js +const leaves = tree.getLeaves() +``` + + +### merkleTree.getLayers() ⇒ Array +Returns all layers of Merkle Tree, including leaves and root. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - array of layers +**Example** +```js +const layers = tree.getLayers() +``` + + +### merkleTree.getRoot() ⇒ Buffer +Returns the Merkle root hash. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Buffer - - Merkle root hash +**Example** +```js +const root = tree.getRoot() +``` + + +### merkleTree.getProof(leaf, [index]) ⇒ Array +Returns the proof for a target leaf. + +**Kind**: instance method of [MerkleTree](#MerkleTree) +**Returns**: Array - - Array of Buffer hashes. + +| Param | Type | Description | +| --- | --- | --- | +| leaf | Buffer | Target leaf | +| [index] | Number | Target leaf index in leaves array. Use if there are leaves containing duplicate data in order to distinguish it. | + +**Example** +```js +const proof = tree.getProof(leaves[2]) +``` +**Example** +```js +const leaves = ['a', 'b', 'a'].map(x => sha3(x)) +const tree = new MerkleTree(leaves, sha3) +const proof = tree.getProof(leaves[2], 2) +``` + + +### merkleTree.verify(proof, targetNode, root) ⇒ Boolean +Returns true if the proof path (array of hashes) can connect the target node +to the Merkle root. + +**Kind**: instance method of [MerkleTree](#MerkleTree) + +| Param | Type | Description | +| --- | --- | --- | +| proof | Array | Array of proof Buffer hashes that should connect target node to Merkle root. | +| targetNode | Buffer | Target node Buffer | +| root | Buffer | Merkle root Buffer | + +**Example** +```js +const root = tree.getRoot() +const proof = tree.getProof(leaves[2]) +const verified = tree.verify(proof, leaves[2], root) +``` + + + +# Test + +``bash +npm test +``` + +# Credits/Resources + +- [Bitcoin mining the hard way: the algorithms, protocols, and bytes](http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html) +- [Raiden Merkle Tree Implemenation](https://github.com/raiden-network/raiden/blob/f9cf12571891cdf54feb4667cd2fffcb3d5daa89/raiden/mtree.py) +- [Bitcoin Talk - Merkle Trees](https://bitcointalk.org/index.php?topic=403231.msg9054025#msg9054025) +- [How Log Proofs Work](https://www.certificate-transparency.org/log-proofs-work) +- [Why aren't Solidity sha3 hashes not matching what other sha3 libraries produce?](https://ethereum.stackexchange.com/questions/559/why-arent-solidity-sha3-hashes-not-matching-what-other-sha3-libraries-produce) + +# License + +MIT diff --git a/diagrams/merkle-tree.png b/diagrams/merkle-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..f3326ed80adce4854e0a33183550676c07a855d7 GIT binary patch literal 42960 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%0DCE*drnn?^7whOIqe@K%ZSJr-DI3K^FU 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 B2*CgV literal 0 HcmV?d00001 diff --git a/diagrams/merkle-tree.xml b/diagrams/merkle-tree.xml new file mode 100644 index 0000000..aef15b3 --- /dev/null +++ b/diagrams/merkle-tree.xml @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..c83fcd7 --- /dev/null +++ b/index.js @@ -0,0 +1,225 @@ +const reverse = require('buffer-reverse') + +/** + * Class reprensenting a Merkle Tree + * @namespace MerkleTree + * @example + * const tree = new MerkleTree(leaves, sha256) + */ +class MerkleTree { + /** + * @desc Constructs a Merkle Tree. + * All nodes and leaves are stored as Buffers. + * Lonely leaf nodes are promoted to the next level up without being hashed again. + * @param {Array} leaves - Array of hashed leaves. Each leaf must be a Buffer. + * @param {Function} hashAlgo - Algorithm used for hashing leaves and nodes + * @param {Object} options - Additional options + * @param {Boolean} options.isBitcoinTree - If set to `true`, generates the Merkle + * Tree with the Bitcoin Merkle Tree implementation. Enable it when you need + * to replicate Bitcoin constructed Merkle Trees. + * @example + * const MerkleTree = require('m-tree') + * const crypto = require('crypto') + * + * function sha256(data) { + * // returns Buffer + * return crypto.createHash('sha256').update(data).digest() + * } + * + * const leaves = ['a', 'b', 'c'].map(x => sha3(x)) + * + * const tree = new MerkleTree(leaves, sha256) + */ + constructor(leaves, hashAlgo, options={}) { + this.hashAlgo = hashAlgo + this.leaves = leaves + this.layers = [leaves] + this.isBitcoinTree = !!options.isBitcoinTree + + this.createHashes(this.leaves) + } + + createHashes(nodes) { + if (nodes.length === 1) { + return false + } + + const layerIndex = this.layers.length + + this.layers.push([]) + + for (let i = 0; i < nodes.length - 1; i += 2) { + const left = nodes[i] + const right = nodes[i+1] + let data = null + + if (this.isBitcoinTree) { + data = Buffer.concat([reverse(left), reverse(right)]) + } else { + data = Buffer.concat([left, right]) + } + + let hash = this.hashAlgo(data) + + // double hash if bitcoin tree + if (this.isBitcoinTree) { + hash = reverse(this.hashAlgo(hash)) + } + + this.layers[layerIndex].push(hash) + } + + // is odd number of nodes + if (nodes.length % 2 === 1) { + let data = nodes[nodes.length-1] + let hash = data + + // is bitcoin tree + if (this.isBitcoinTree) { + // Bitcoin method of duplicating the odd ending nodes + data = Buffer.concat([reverse(data), reverse(data)]) + hash = this.hashAlgo(data) + hash = reverse(this.hashAlgo(hash)) + } + + this.layers[layerIndex].push(hash) + } + + this.createHashes(this.layers[layerIndex]) + } + + /** + * getLeaves + * @desc Returns leaves of Merkle Tree. + * @return {Array} - array of leaves + * @example + * const leaves = tree.getLeaves() + */ + getLeaves() { + return this.leaves + } + + /** + * getLayers + * @desc Returns all layers of Merkle Tree, including leaves and root. + * @return {Array} - array of layers + * @example + * const layers = tree.getLayers() + */ + getLayers() { + return this.layers + } + + /** + * getRoot + * @desc Returns the Merkle root hash. + * @return {Buffer} - Merkle root hash + * @example + * const root = tree.getRoot() + */ + getRoot() { + return this.layers[this.layers.length-1][0] + } + + /** + * getProof + * @desc Returns the proof for a target leaf. + * @param {Buffer} leaf - Target leaf + * @param {Number} [index] - Target leaf index in leaves array. + * Use if there are leaves containing duplicate data in order to distinguish it. + * @return {Array} - Array of Buffer hashes. + * @example + * const proof = tree.getProof(leaves[2]) + * + * @example + * const leaves = ['a', 'b', 'a'].map(x => sha3(x)) + * const tree = new MerkleTree(leaves, sha3) + * const proof = tree.getProof(leaves[2], 2) + */ + getProof(leaf, index) { + const proof = []; + + if (typeof index !== 'number') { + index = -1 + + for (let i = 0; i < this.leaves.length; i++) { + if (Buffer.compare(leaf, this.leaves[i]) === 0) { + index = i + } + } + } + + if (index <= -1) { + return [] + } + + for (let i = 0; i < this.layers.length; i++) { + const layer = this.layers[i] + const isRightNode = index % 2 + const pairIndex = (isRightNode ? index - 1 : index + 1) + + if (pairIndex < layer.length) { + proof.push({ + position: isRightNode ? 'left': 'right', + data: layer[pairIndex] + }) + } + + // set index to parent index + index = (index / 2)|0 + } + + return proof + } + + /** + * verify + * @desc Returns true if the proof path (array of hashes) can connect the target node + * to the Merkle root. + * @param {Array} proof - Array of proof Buffer hashes that should connect + * target node to Merkle root. + * @param {Buffer} targetNode - Target node Buffer + * @param {Buffer} root - Merkle root Buffer + * @return {Boolean} + * @example + * const root = tree.getRoot() + * const proof = tree.getProof(leaves[2]) + * const verified = tree.verify(proof, leaves[2], root) + * + */ + verify(proof, targetNode, root) { + let hash = targetNode + + if (!Array.isArray(proof) || + !proof.length || + !targetNode || + !root) { + return false + } + + for (let i = 0; i < proof.length; i++) { + const node = proof[i] + const isLeftNode = (node.position === 'left') + const buffers = [] + + if (this.isBitcoinTree) { + buffers.push(reverse(hash)) + + buffers[isLeftNode ? 'unshift' : 'push'](reverse(node.data)) + + hash = this.hashAlgo(Buffer.concat(buffers)) + hash = reverse(this.hashAlgo(hash)) + } else { + buffers.push(hash) + + buffers[isLeftNode ? 'unshift' : 'push'](node.data) + + hash = this.hashAlgo(Buffer.concat(buffers)) + } + } + + return Buffer.compare(hash, root) === 0 + } +} + +module.exports = MerkleTree diff --git a/package.json b/package.json new file mode 100644 index 0000000..488985a --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "m-tree", + "version": "0.0.1", + "description": "merkle-tree Filter algorithm in JavaScript.", + "main": "index.js", + "directories": { + "test": "tape test/*" + }, + "scripts": { + "test": "./node_modules/tape/bin/tape test/*.js", + "docs:md": "jsdoc2md index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/miguelmota/merkle-tree" + }, + "keywords": [ + "merkle", + "tree", + "hash", + "algorithm", + "crypto", + "bitcoin", + "ethereum", + "proof" + ], + "author": { + "name": "Miguel Mota", + "email": "hello@miguelmota.com", + "url": "https://miguelmota.com/" + }, + "license": { + "type": "MIT", + "url": "https://github.com/miguelmota/merkle-tree/blob/master/LICENSE.md" + }, + "bugs": { + "url": "https://github.com/miguelmota/merkle-tree/issues" + }, + "homepage": "https://github.com/miguelmota/merkle-tree", + "devDependencies": { + "crypto": "0.0.3", + "ethereumjs-util": "^5.1.2", + "tape": "^3.6.1" + }, + "engines": { + "node": ">= 7.6.0" + }, + "dependencies": { + "buffer-reverse": "^1.0.1" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c0e8d72 --- /dev/null +++ b/test/index.js @@ -0,0 +1,212 @@ +const test = require('tape') +const {sha3} = require('ethereumjs-util') +const crypto = require('crypto') + +const MerkleTree = require('../') + +function sha256(data) { + return crypto.createHash('sha256').update(data).digest() +} + +test('sha256', t => { + t.plan(1) + + const leaves = ['a', 'b', 'c'].map(x => sha3(x)) + + const tree = new MerkleTree(leaves, sha256) + + const root = '311d2e46f49b15fff8b746b74ad57f2cc9e0d9939fda94387141a2d3fdf187ae' + t.equal(tree.getRoot().toString('hex'), root) +}) + +test('solidity sha3 [keccak-256]', t => { + t.plan(20) + + const leaves = ['a', 'b', 'c'].map(x => sha3(x)) + + const a_hash = '3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb' + const b_hash = 'b5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510' + const c_hash = '0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2' + + t.deepEqual(leaves.map(x => x.toString('hex')), [a_hash, b_hash, c_hash]) + + const tree = new MerkleTree(leaves, sha3) + + const layers = tree.getLayers().slice(1) // no leaves + + const layer_1 = sha3(Buffer.concat([leaves[0], leaves[1]])).toString('hex') + t.equal(layers[0][0].toString('hex'), layer_1) + t.equal(layers[0][1].toString('hex'), c_hash) + + const root = Buffer.from('aff1208e69c9e8be9b584b07ebac4e48a1ee9d15ce3afe20b77a4d29e4175aa3', 'hex') + t.equal(tree.getRoot().toString('hex'), root.toString('hex')) + + const proof_0 = tree.getProof(leaves[0]) + t.equal(proof_0.length, 2) + t.equal(proof_0[0].position, 'right') + t.equal(proof_0[0].data.toString('hex'), b_hash) + t.equal(proof_0[1].position, 'right') + t.equal(proof_0[1].data.toString('hex'), c_hash) + + t.equal(tree.verify(proof_0, leaves[0], root), true) + + const proof_1 = tree.getProof(leaves[1]) + t.equal(proof_1.length, 2) + t.equal(proof_1[0].position, 'left') + t.equal(proof_1[0].data.toString('hex'), a_hash) + t.equal(proof_1[1].position, 'right') + t.equal(proof_1[1].data.toString('hex'), c_hash) + + t.equal(tree.verify(proof_1, leaves[1], root), true) + + const proof_2 = tree.getProof(leaves[2]) + t.equal(proof_2.length, 1) + t.equal(proof_2[0].position, 'left') + t.equal(proof_2[0].data.toString('hex'), layer_1) + + t.equal(tree.verify(proof_2, leaves[2], root), true) +}) + +test('solidity sha3 [keccak-256] with duplicate leaves', t => { + t.plan(5) + + const leaves = ['a', 'b', 'a'].map(x => sha3(x)) + + const a_hash = '3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb' + const b_hash = 'b5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510' + + const tree = new MerkleTree(leaves, sha3) + + t.deepEqual(leaves.map(x => x.toString('hex')), [a_hash, b_hash, a_hash]) + + const root = Buffer.from('b8912f7269068901f231a965adfefbc10f0eedcfa61852b103efd54dac7db3d7', 'hex') + t.equal(tree.getRoot().toString('hex'), root.toString('hex')) + + const layer_1 = sha3(Buffer.concat([leaves[0], leaves[1]])).toString('hex') + + const proof_0 = tree.getProof(leaves[2], 2) + t.equal(proof_0.length, 1) + t.equal(proof_0[0].position, 'left') + t.equal(proof_0[0].data.toString('hex'), layer_1) +}) + + +test('sha-256 with option.isBitcoinTree', t => { + t.plan(2) + + /* Derived from: + * http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html + */ + const txHashes = [ + "00baf6626abc2df808da36a518c69f09b0d2ed0a79421ccfde4f559d2e42128b", + "91c5e9f288437262f218c60f986e8bc10fb35ab3b9f6de477ff0eb554da89dea", + "46685c94b82b84fa05b6a0f36de6ff46475520113d5cb8c6fb060e043a0dbc5c", + "ba7ed2544c78ad793ef5bb0ebe0b1c62e8eb9404691165ffcb08662d1733d7a8", + "b8dc1b7b7ed847c3595e7b02dbd7372aa221756b718c5f2943c75654faf48589", + "25074ef168a061fcc8663b4554a31b617683abc33b72d2e2834f9329c93f8214", + "0fb8e311bffffadc6dc4928d7da9e142951d3ba726c8bde2cf1489b62fb9ebc5", + "c67c79204e681c8bb453195db8ca7d61d4692f0098514ca198ccfd1b59dbcee3", + "bd27570a6cbd8ad026bfdb8909fdae9321788f0643dea195f39cd84a60a1901b", + "41a06e53ffc5108358ddcec05b029763d714ae9f33c5403735e8dee78027fe74", + "cc2696b44cb07612c316f24c07092956f7d8b6e0d48f758572e0d611d1da6fb9", + "8fc508772c60ace7bfeb3f5f3a507659285ea6f351ac0474a0a9710c7673d4fd", + "62fed508c095446d971580099f976428fc069f32e966a40a991953b798b28684", + "928eadbc39196b95147416eedf6f635dcff818916da65419904df8fde977d5db", + "b137e685df7c1dffe031fb966a0923bb5d0e56f381e730bc01c6d5244cfe47c1", + "b92207cee1f9e0bfbd797b05a738fab9de9c799b74f54f6b922f20bd5ec23dd6", + "29d6f37ada0481375b6903c6480a81f8deaf2dcdba03411ed9e8d3e5684d02dd", + "48158deb116e4fd0429fbbbae61e8e68cb6d0e0c4465ff9a6a990037f88c489c", + "be64ea86960864cc0a0236bbb11f232faf5b19ae6e2c85518628f5fae37ec1ca", + "081363552e9fff7461f1fc6663e1abd0fb2dd1c54931e177479a18c4c26260e8", + "eb87c25dd2b2537b1ff3dbabc420e422e2a801f1bededa6fa49ef7980feaef70", + "339e16fcc11deb61ccb548239270af43f5ad34c321416bada4b8d66467b1c697", + "4ad6417a3a04179482ed2e4b7251c396e38841c6fba8d2ce9543337ab7c93c02", + "c28a45cded020bf424b400ffc9cb6f2f85601934f18c34a4f78283247192056a", + "882037cc9e3ee6ddc2d3eba86b7ca163533b5d3cbb16eaa38696bb0a2ea1137e", + "179bb936305b46bb0a9df330f8701984c725a60e063ad5892fa97461570b5c04", + "9517c585d1578cb327b7988f38e1a15c663955ea288a2292b40d27f232fbb980", + "2c7e07d0cf42e5520bcbfe2f5ef63761a9ab9d7ccb00ea346195eae030f3b86f", + "534f631fc42ae2d309670e01c7a0890e4bfb65bae798522ca14df09c81b09734", + "104643385619adb848593eb668a8066d1f32650edf35e74b0fc3306cb6719448", + "87ac990808239c768182a752f4f71cd98558397072883c7e137efb49d22b9231", + "9b3e2f1c47d59a444e9b6dc725f0ac6baf160d22f3a9d399434e5e65b14eccb0", + "fbe123066ae5add633a542f151663db4eb5a7053e388faadb40240671ae1b09b", + "1dd07e92e20b3cb9208af040031f7cfc4efd46cc31ec27be20a1047965a42849", + "2709bb9ed27353c1fd76b9240cab7576a44de68945e256ad44b2cb8d849a8060", + "d0174db2c712573432a7869c1508f371f3a1058aeedddc1b53a7e04d7c56c725", + "b4a16f724cddb8f77ddf3d2146a12c4be13d503885eaba3518a03da005009f62", + "2aa706d75decbe57745e01d46f9f5d30a08dedaf3288cee14cc4948e3684e1d4", + "ee49c5f6a5129ccaf2abebbc1d6d07a402a600af6221476b89aafaa683ca95b7", + "bea1011c77874845e9b4c876ed2ceebd530d428dd4a564ad003d9211d40bb091", + "f1e88ffc2b1de2aa4827002f06943ce5468735f7433f960bf01e75885b9f832b", + "19247d017e002fb9143d1a89eb921222a94f8a3d0faaf2e05b0f594989edc4c4", + "13f714ff62ee7d26b6d69ca980c141ebc54e9f71d2697083fe6c5efc1b02bd0f", + "0c78cbb8246572f015fbdc53dc9798fa54d1119ec77c1f07ac310bcbcc40dbf8", + "4bcde0ef92a6d24a2be7be50ac5e5299d776df2e6229ba5d475c2491da94f255", + "0cfd7d1058502730cf0b2ffa880c78ef534651e06832b5d87c0d7eb84eac5b0c", + "3a168f794d6e0c614429ad874317cc4cd67a8177214880ff6ea1704d29228c2f", + "f9a555d817334397b402518d6fd959dc73d981ee7f5fe67969b63974ebbef127", + "24b52691f66eaed4ce391a473902e309018257c98b9f02aaa33b399c9e6f3168", + "a37b5e623dc26a180d9e2c9510d06885b014e86e533adb63ec40511e10b55046", + "9dbaeb485e51d9e25a5621dc46e0bc0aaf51fb26be5acc4e370b96f62c469b80", + "a6431d3d39f6c38c5df48405090752cab03bfdf5c77cf881b18a946807fba74a", + "faa77e309f125373acf19855dd496fffe2f74962e545420844557a3adc7ebc11", + "3523f52543ecfea2f78486dc91550fad0e6467d46d9d9c82ca63b2e0230bfa71", + "a0583e358e42d77d18d1fd0533ff0a65615fc3b3112061ef92f168a00bf640c1", + "42ae900888d5e5dde59c8e3d06e13db9e84ef05d27726d4b67fd00c50cd9406a", + "154940777d3ff78f592ef02790131a59263c36b4958bbc836f9a767ea1a9f178", + "6a0337de6ac75eecf748306e8ebc5bfe5c811a1481ae50f6956a9e7f26a679f5", + "c99530c2148e09688d0b88795625943371183bf1f5d56c7446c6ed51ea133589", + "626421dbe8ad6a0fd0d622d5dd3308a1cdc00b98575a41a91fe01a439e6f40bd", + "b2f3a559f605a158cc395126c3cf394a7e92a53b7514c75157e1dc43a6c7f93e", + "dffe06d1bea81f2a01c76786404bb867258f9e68013bf25454097ce935090738", + "0860159ec7a2a51ce107c182a988c40b4bc2057a734354a1219b6c65e72640ed", + "a405ff1bb51846b1867acc0b0da17f6f9616e592a0a7ff5ef3297c1ecfd60911", + "a7d451924263284765f6343bca8a21b79b89ebfe611c7355dd88e0ec1c29e232", + "41c758d08a4d3fe4d90645711589b832a2cd54dd25bd5b66e463e5d389a53aff", + "a05c1a93a521fa5dbc1790cfbb808893453a428a65f2c6b2d51249fbb12db309", + "90997920aa9786e10f513cfdd14e294feee6739cee1ab61b3fb1e3f42e7a915d", + "99fcb9cb62c20a3135484a70bd3f73983f8f3b7b26266dad34f3993958a7642c", + "e05f9a668b37e5f78bd3b9d047f29f92b33a87f11dd48390410006f858188b7b", + "56dbc65895f7992da4a6985e7edba4d1c00879f1b28442c644c8a07658ceab27", + "5e9004fe262b829563d0804656ba68b1de1690401f08a1915273230d8c902fc0", + "1ea9ed3717523c5e304b7a7ac8058a87fb4f3fed8c6004769f226c9bb67e79c5", + "f0f1a4c009b3f1b2729e89898e2f5c0fcdc312edea5df884a9c897cb90e4c566", + "b5bb4ddf04863e6a60f33cb96c20dac8175d3bae55f335781503143c97a50e43", + "f14cc97a20c6f627b4b78301352ae35463bc359362589cd178a06c0fa90850b7", + "628801c8f614015c0fa0ccb2768cccc3e7b9d41ceed06071ce2534d31f7236d6", + "3be1013c8f8da150e2195408093153b55b08b037fd92db8bb5e803f4c2538aae", + "c9e1f8777685f54ba65c4e02915fd649ee1edcbf9c77ddf584b943d27efb86c3", + "4274e92ed3bd02eb101baa5fb8ff7b96236830762d08273749fbb5166db8ab0b", + "aa84c955bea04c7cee8f5bbbec97d25930fcaca363eed1b8cad37b931556d3e3", + "d6a29c948677fb1f71aaf16debc3d071a4dd349458eb9e056dce3a000ff853da", + "ba84bdb3d78367ca365016ac4bff9269576eb010f874c2967af73e0de5638de0", + "1546c79951e3b541bc64d1957b565b7a2850fc87192c7b374aee6cfc69b9805e", + "f119227d492ebe27fe9aae321980802454dfa64b2691efbe796c5075d5b07f62", + "b8cf13d64818b32f96bbb585998b1bc9505f6a94055488e5a71fee9479c6f2a9", + "1aaf459705b6afef2d7b83e3f181f1af55be0813daf55edce104cc59abc28ed7", + "61ac185c8f520b5e3134953dc52ff292a40e1e96b088dab259558a9d240ec02f", + "2da96e3154d7ec2329f787b73cb8a436b92d64cf3cc28e920d073279ea73b5f8", + "1c4d72ce733b971b9ec4e24f37d733355f6f2ea635cc67ffb3e22748484df446", + "2a6f89769f3272ac8c7a36a42a57627eca6b260ab2c76d8046a27d44d4034893", + "f8d11df51a2cc113698ebf39a958fe81179d7d973d2044322771c0fe63f4d7c9", + "f2287f17a4fa232dca5715c24a92f7112402a8101b9a7b276fb8c8f617376b90", + "bb5ee510a4fda29cae30c97e7eee80569d3ec3598465f2d7e0674c395e0256e9", + "647ab8c84365620d60f2523505d14bd230b5e650c96dee48be47770063ee7461", + "34b06018fcc33ba6ebb01198d785b0629fbdc5d1948f688059158f053093f08b", + "ff58b258dab0d7f36a2908e6c75229ce308d34806289c912a1a5f39a5aa71f9f", + "232fc124803668a9f23b1c3bcb1134274303f5c0e1b0e27c9b6c7db59f0e2a4d", + "27a0797cc5b042ba4c11e72a9555d13a67f00161550b32ede0511718b22dbc2c", + ] + + var leaves = txHashes.map(x => Buffer.from(x, 'hex')) + + const tree = new MerkleTree(leaves, sha256, {isBitcoinTree: true}) + + const root = Buffer.from('871714dcbae6c8193a2bb9b2a69fe1c0440399f38d94b3a0f1b447275a29978a', 'hex') + t.equal(tree.getRoot().toString('hex'), root.toString('hex')) + + const proof_0 = tree.getProof(leaves[0]) + + t.equal(tree.verify(proof_0, leaves[0], root), true) +})