diff --git a/contracts/MerkleProof.sol b/contracts/MerkleProof.sol new file mode 100644 index 000000000..1703ee4ea --- /dev/null +++ b/contracts/MerkleProof.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.4.11; + +/* + * @title MerkleProof + * @dev Merkle proof verification + * @note Based on https://github.com/ameensol/merkle-tree-solidity/blob/master/src/MerkleProof.sol + */ +library MerkleProof { + /* + * @dev Verifies a Merkle proof proving the existence of a leaf in a Merkle tree. Assumes that each pair of leaves + * and each pair of pre-images is sorted. + * @param _proof Merkle proof containing sibling hashes on the branch from the leaf to the root of the Merkle tree + * @param _root Merkle root + * @param _leaf Leaf of Merkle tree + */ + function verifyProof(bytes _proof, bytes32 _root, bytes32 _leaf) constant returns (bool) { + // Check if proof length is a multiple of 32 + if (_proof.length % 32 != 0) return false; + + bytes32 proofElement; + bytes32 computedHash = _leaf; + + for (uint256 i = 32; i <= _proof.length; i += 32) { + assembly { + // Load the current element of the proof + proofElement := mload(add(_proof, i)) + } + + if (computedHash < proofElement) { + // Hash(current computed hash + current element of the proof) + computedHash = keccak256(computedHash, proofElement); + } else { + // Hash(current element of the proof + current computed hash) + computedHash = keccak256(proofElement, computedHash); + } + } + + // Check if the computed hash (root) is equal to the provided root + return computedHash == _root; + } +} diff --git a/docs/source/merkleproof.rst b/docs/source/merkleproof.rst new file mode 100644 index 000000000..4dcc94e47 --- /dev/null +++ b/docs/source/merkleproof.rst @@ -0,0 +1,9 @@ +MerkleProof +============================================= + +Merkle proof verification for leaves of a Merkle tree. + +verifyProof(bytes _proof, bytes32 _root, bytes32 _leaf) internal constant returns (bool) +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Verifies a Merkle proof proving the existence of a leaf in a Merkle tree. Assumes that each pair of leaves and each pair of pre-images is sorted. diff --git a/package.json b/package.json index 671173084..55e9c3430 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "chai-as-promised": "^7.0.0", "chai-bignumber": "^2.0.0", "coveralls": "^2.13.1", + "ethereumjs-util": "^5.1.2", "ethereumjs-testrpc": "^4.1.1", "mocha-lcov-reporter": "^1.3.0", "solidity-coverage": "^0.2.2", diff --git a/test/MerkleProof.js b/test/MerkleProof.js new file mode 100644 index 000000000..7f449b431 --- /dev/null +++ b/test/MerkleProof.js @@ -0,0 +1,60 @@ +var MerkleProof = artifacts.require("./MerkleProof.sol"); + +import MerkleTree from "./helpers/merkleTree.js"; +import { sha3, bufferToHex } from "ethereumjs-util"; + +contract('MerkleProof', function(accounts) { + let merkleProof; + + before(async function() { + merkleProof = await MerkleProof.new(); + }); + + describe("verifyProof", function() { + it("should return true for a valid Merkle proof", async function() { + const elements = ["a", "b", "c", "d"]; + const merkleTree = new MerkleTree(elements); + + const root = merkleTree.getHexRoot(); + + const proof = merkleTree.getHexProof(elements[0]); + + const leaf = bufferToHex(sha3(elements[0])); + + const result = await merkleProof.verifyProof(proof, root, leaf); + assert.isOk(result, "verifyProof did not return true for a valid proof"); + }); + + it("should return false for an invalid Merkle proof", async function() { + const correctElements = ["a", "b", "c"] + const correctMerkleTree = new MerkleTree(correctElements); + + const correctRoot = correctMerkleTree.getHexRoot(); + + const correctLeaf = bufferToHex(sha3(correctElements[0])); + + const badElements = ["d", "e", "f"] + const badMerkleTree = new MerkleTree(badElements) + + const badProof = badMerkleTree.getHexProof(badElements[0]) + + const result = await merkleProof.verifyProof(badProof, correctRoot, correctLeaf); + assert.isNotOk(result, "verifyProof did not return false for an invalid proof"); + }); + + it("should return false for a Merkle proof of invalid length", async function() { + const elements = ["a", "b", "c"] + const merkleTree = new MerkleTree(elements); + + const root = merkleTree.getHexRoot(); + + const proof = merkleTree.getHexProof(elements[0]); + const badProof = proof.slice(0, proof.length - 5); + + const leaf = bufferToHex(sha3(elements[0])); + + const result = await merkleProof.verifyProof(badProof, root, leaf); + assert.isNotOk(result, "verifyProof did not return false for proof of invalid length"); + }) + }); +}); diff --git a/test/helpers/merkleTree.js b/test/helpers/merkleTree.js new file mode 100644 index 000000000..0de95eb15 --- /dev/null +++ b/test/helpers/merkleTree.js @@ -0,0 +1,131 @@ +import { sha3, bufferToHex } from "ethereumjs-util"; + +export default class MerkleTree { + constructor(elements) { + // Filter empty strings and hash elements + this.elements = elements.filter(el => el).map(el => sha3(el)); + + // Deduplicate elements + this.elements = this.bufDedup(this.elements); + // Sort elements + this.elements.sort(Buffer.compare); + + // Create layers + this.layers = this.getLayers(this.elements); + } + + getLayers(elements) { + if (elements.length == 0) { + return [[""]]; + } + + const layers = []; + layers.push(elements); + + // Get next layer until we reach the root + while (layers[layers.length - 1].length > 1) { + layers.push(this.getNextLayer(layers[layers.length - 1])); + } + + return layers; + } + + getNextLayer(elements) { + return elements.reduce((layer, el, idx, arr) => { + if (idx % 2 === 0) { + // Hash the current element with its pair element + layer.push(this.combinedHash(el, arr[idx + 1])); + } + + return layer; + }, []); + } + + combinedHash(first, second) { + if (!first) { return second; } + if (!second) { return first; } + + return sha3(this.sortAndConcat(first, second)); + } + + getRoot() { + return this.layers[this.layers.length - 1][0]; + } + + getHexRoot() { + return bufferToHex(this.getRoot()); + } + + getProof(el) { + let idx = this.bufIndexOf(el, this.elements); + + if (idx === -1) { + throw new Error("Element does not exist in Merkle tree"); + } + + return this.layers.reduce((proof, layer) => { + const pairElement = this.getPairElement(idx, layer); + + if (pairElement) { + proof.push(pairElement); + } + + idx = Math.floor(idx / 2); + + return proof; + }, []); + } + + getHexProof(el) { + const proof = this.getProof(el); + + return this.bufArrToHex(proof); + } + + getPairElement(idx, layer) { + const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; + + if (pairIdx < layer.length) { + return layer[pairIdx]; + } else { + return null; + } + } + + bufIndexOf(el, arr) { + let hash; + + // Convert element to 32 byte hash if it is not one already + if (el.length !== 32 || !Buffer.isBuffer(el)) { + hash = sha3(el); + } else { + hash = el; + } + + for (let i = 0; i < arr.length; i++) { + if (hash.equals(arr[i])) { + return i; + } + } + + return -1; + } + + bufDedup(elements) { + return elements.filter((el, idx) => { + return this.bufIndexOf(el, elements) === idx; + }); + } + + bufArrToHex(arr) { + if (arr.some(el => !Buffer.isBuffer(el))) { + throw new Error("Array is not an array of buffers"); + } + + return "0x" + arr.map(el => el.toString("hex")).join(""); + } + + sortAndConcat(...args) { + return Buffer.concat([...args].sort(Buffer.compare)); + } +}