Skip to content

Commit ff63ac2

Browse files
authored
Merge pull request OpenZeppelin#260 from yondonfu/feature/merkleproof
Merkle proof library with tests and docs
2 parents 526c75d + 6bf622b commit ff63ac2

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed

contracts/MerkleProof.sol

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
pragma solidity ^0.4.11;
2+
3+
/*
4+
* @title MerkleProof
5+
* @dev Merkle proof verification
6+
* @note Based on https://github.com/ameensol/merkle-tree-solidity/blob/master/src/MerkleProof.sol
7+
*/
8+
library MerkleProof {
9+
/*
10+
* @dev Verifies a Merkle proof proving the existence of a leaf in a Merkle tree. Assumes that each pair of leaves
11+
* and each pair of pre-images is sorted.
12+
* @param _proof Merkle proof containing sibling hashes on the branch from the leaf to the root of the Merkle tree
13+
* @param _root Merkle root
14+
* @param _leaf Leaf of Merkle tree
15+
*/
16+
function verifyProof(bytes _proof, bytes32 _root, bytes32 _leaf) constant returns (bool) {
17+
// Check if proof length is a multiple of 32
18+
if (_proof.length % 32 != 0) return false;
19+
20+
bytes32 proofElement;
21+
bytes32 computedHash = _leaf;
22+
23+
for (uint256 i = 32; i <= _proof.length; i += 32) {
24+
assembly {
25+
// Load the current element of the proof
26+
proofElement := mload(add(_proof, i))
27+
}
28+
29+
if (computedHash < proofElement) {
30+
// Hash(current computed hash + current element of the proof)
31+
computedHash = keccak256(computedHash, proofElement);
32+
} else {
33+
// Hash(current element of the proof + current computed hash)
34+
computedHash = keccak256(proofElement, computedHash);
35+
}
36+
}
37+
38+
// Check if the computed hash (root) is equal to the provided root
39+
return computedHash == _root;
40+
}
41+
}

docs/source/merkleproof.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MerkleProof
2+
=============================================
3+
4+
Merkle proof verification for leaves of a Merkle tree.
5+
6+
verifyProof(bytes _proof, bytes32 _root, bytes32 _leaf) internal constant returns (bool)
7+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
8+
9+
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.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"chai-as-promised": "^7.0.0",
3636
"chai-bignumber": "^2.0.0",
3737
"coveralls": "^2.13.1",
38+
"ethereumjs-util": "^5.1.2",
3839
"ethereumjs-testrpc": "^4.1.1",
3940
"mocha-lcov-reporter": "^1.3.0",
4041
"solidity-coverage": "^0.2.2",

test/MerkleProof.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
var MerkleProof = artifacts.require("./MerkleProof.sol");
2+
3+
import MerkleTree from "./helpers/merkleTree.js";
4+
import { sha3, bufferToHex } from "ethereumjs-util";
5+
6+
contract('MerkleProof', function(accounts) {
7+
let merkleProof;
8+
9+
before(async function() {
10+
merkleProof = await MerkleProof.new();
11+
});
12+
13+
describe("verifyProof", function() {
14+
it("should return true for a valid Merkle proof", async function() {
15+
const elements = ["a", "b", "c", "d"];
16+
const merkleTree = new MerkleTree(elements);
17+
18+
const root = merkleTree.getHexRoot();
19+
20+
const proof = merkleTree.getHexProof(elements[0]);
21+
22+
const leaf = bufferToHex(sha3(elements[0]));
23+
24+
const result = await merkleProof.verifyProof(proof, root, leaf);
25+
assert.isOk(result, "verifyProof did not return true for a valid proof");
26+
});
27+
28+
it("should return false for an invalid Merkle proof", async function() {
29+
const correctElements = ["a", "b", "c"]
30+
const correctMerkleTree = new MerkleTree(correctElements);
31+
32+
const correctRoot = correctMerkleTree.getHexRoot();
33+
34+
const correctLeaf = bufferToHex(sha3(correctElements[0]));
35+
36+
const badElements = ["d", "e", "f"]
37+
const badMerkleTree = new MerkleTree(badElements)
38+
39+
const badProof = badMerkleTree.getHexProof(badElements[0])
40+
41+
const result = await merkleProof.verifyProof(badProof, correctRoot, correctLeaf);
42+
assert.isNotOk(result, "verifyProof did not return false for an invalid proof");
43+
});
44+
45+
it("should return false for a Merkle proof of invalid length", async function() {
46+
const elements = ["a", "b", "c"]
47+
const merkleTree = new MerkleTree(elements);
48+
49+
const root = merkleTree.getHexRoot();
50+
51+
const proof = merkleTree.getHexProof(elements[0]);
52+
const badProof = proof.slice(0, proof.length - 5);
53+
54+
const leaf = bufferToHex(sha3(elements[0]));
55+
56+
const result = await merkleProof.verifyProof(badProof, root, leaf);
57+
assert.isNotOk(result, "verifyProof did not return false for proof of invalid length");
58+
})
59+
});
60+
});

test/helpers/merkleTree.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { sha3, bufferToHex } from "ethereumjs-util";
2+
3+
export default class MerkleTree {
4+
constructor(elements) {
5+
// Filter empty strings and hash elements
6+
this.elements = elements.filter(el => el).map(el => sha3(el));
7+
8+
// Deduplicate elements
9+
this.elements = this.bufDedup(this.elements);
10+
// Sort elements
11+
this.elements.sort(Buffer.compare);
12+
13+
// Create layers
14+
this.layers = this.getLayers(this.elements);
15+
}
16+
17+
getLayers(elements) {
18+
if (elements.length == 0) {
19+
return [[""]];
20+
}
21+
22+
const layers = [];
23+
layers.push(elements);
24+
25+
// Get next layer until we reach the root
26+
while (layers[layers.length - 1].length > 1) {
27+
layers.push(this.getNextLayer(layers[layers.length - 1]));
28+
}
29+
30+
return layers;
31+
}
32+
33+
getNextLayer(elements) {
34+
return elements.reduce((layer, el, idx, arr) => {
35+
if (idx % 2 === 0) {
36+
// Hash the current element with its pair element
37+
layer.push(this.combinedHash(el, arr[idx + 1]));
38+
}
39+
40+
return layer;
41+
}, []);
42+
}
43+
44+
combinedHash(first, second) {
45+
if (!first) { return second; }
46+
if (!second) { return first; }
47+
48+
return sha3(this.sortAndConcat(first, second));
49+
}
50+
51+
getRoot() {
52+
return this.layers[this.layers.length - 1][0];
53+
}
54+
55+
getHexRoot() {
56+
return bufferToHex(this.getRoot());
57+
}
58+
59+
getProof(el) {
60+
let idx = this.bufIndexOf(el, this.elements);
61+
62+
if (idx === -1) {
63+
throw new Error("Element does not exist in Merkle tree");
64+
}
65+
66+
return this.layers.reduce((proof, layer) => {
67+
const pairElement = this.getPairElement(idx, layer);
68+
69+
if (pairElement) {
70+
proof.push(pairElement);
71+
}
72+
73+
idx = Math.floor(idx / 2);
74+
75+
return proof;
76+
}, []);
77+
}
78+
79+
getHexProof(el) {
80+
const proof = this.getProof(el);
81+
82+
return this.bufArrToHex(proof);
83+
}
84+
85+
getPairElement(idx, layer) {
86+
const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
87+
88+
if (pairIdx < layer.length) {
89+
return layer[pairIdx];
90+
} else {
91+
return null;
92+
}
93+
}
94+
95+
bufIndexOf(el, arr) {
96+
let hash;
97+
98+
// Convert element to 32 byte hash if it is not one already
99+
if (el.length !== 32 || !Buffer.isBuffer(el)) {
100+
hash = sha3(el);
101+
} else {
102+
hash = el;
103+
}
104+
105+
for (let i = 0; i < arr.length; i++) {
106+
if (hash.equals(arr[i])) {
107+
return i;
108+
}
109+
}
110+
111+
return -1;
112+
}
113+
114+
bufDedup(elements) {
115+
return elements.filter((el, idx) => {
116+
return this.bufIndexOf(el, elements) === idx;
117+
});
118+
}
119+
120+
bufArrToHex(arr) {
121+
if (arr.some(el => !Buffer.isBuffer(el))) {
122+
throw new Error("Array is not an array of buffers");
123+
}
124+
125+
return "0x" + arr.map(el => el.toString("hex")).join("");
126+
}
127+
128+
sortAndConcat(...args) {
129+
return Buffer.concat([...args].sort(Buffer.compare));
130+
}
131+
}

0 commit comments

Comments
 (0)