|
1 | 1 | import assert from 'assert/strict';
|
2 | 2 | import { keccak256 } from 'ethereum-cryptography/keccak';
|
3 | 3 | import { hex } from './bytes';
|
| 4 | +import { MerkleTreeOptions } from './options'; |
4 | 5 | import { StandardMerkleTree } from './standard';
|
5 | 6 |
|
6 | 7 | const zeroBytes = new Uint8Array(32);
|
7 | 8 | const zero = hex(zeroBytes);
|
8 | 9 |
|
9 |
| -const characters = (s: string) => { |
| 10 | +const makeTree = (s: string, opts: MerkleTreeOptions = {}) => { |
10 | 11 | const l = s.split('').map(c => [c]);
|
11 |
| - const t = StandardMerkleTree.of(l, ['string']); |
| 12 | + const t = StandardMerkleTree.of(l, ['string'], opts); |
12 | 13 | return { l, t };
|
13 | 14 | }
|
14 | 15 |
|
15 | 16 | describe('standard merkle tree', () => {
|
16 |
| - it('generates valid single proofs for all leaves', () => { |
17 |
| - const { t } = characters('abcdef'); |
18 |
| - t.validate(); |
19 |
| - }); |
20 |
| - |
21 |
| - it('generates valid single proofs for all leaves', () => { |
22 |
| - const { t } = characters('abcdef'); |
23 |
| - |
24 |
| - for (const [id, leaf] of t.entries()) { |
25 |
| - const proof1 = t.getProof(id); |
26 |
| - const proof2 = t.getProof(leaf); |
27 |
| - |
28 |
| - assert.deepEqual(proof1, proof2); |
29 |
| - |
30 |
| - assert(t.verify(id, proof1)); |
31 |
| - assert(t.verify(leaf, proof1)); |
32 |
| - assert(StandardMerkleTree.verify(t.root, ['string'], leaf, proof1)); |
33 |
| - } |
34 |
| - }); |
35 |
| - |
36 |
| - it('rejects invalid proofs', () => { |
37 |
| - const { t } = characters('abcdef'); |
38 |
| - const { t: otherTree } = characters('abc'); |
39 |
| - |
40 |
| - const leaf = ['a']; |
41 |
| - const invalidProof = otherTree.getProof(leaf); |
42 |
| - |
43 |
| - assert(!t.verify(leaf, invalidProof)); |
44 |
| - assert(!StandardMerkleTree.verify(t.root, ['string'], leaf, invalidProof)); |
45 |
| - }); |
46 |
| - |
47 |
| - it('generates valid multiproofs', () => { |
48 |
| - const { t, l } = characters('abcdef'); |
49 |
| - |
50 |
| - for (const ids of [[], [0, 1], [0, 1, 5], [1, 3, 4, 5], [0, 2, 4, 5], [0, 1, 2, 3, 4, 5]]) { |
51 |
| - const proof1 = t.getMultiProof(ids); |
52 |
| - const proof2 = t.getMultiProof(ids.map(i => l[i]!)); |
53 |
| - |
54 |
| - assert.deepEqual(proof1, proof2); |
55 |
| - |
56 |
| - assert(t.verifyMultiProof(proof1)); |
57 |
| - assert(StandardMerkleTree.verifyMultiProof(t.root, ['string'], proof1)); |
58 |
| - } |
59 |
| - }); |
60 |
| - |
61 |
| - it('rejects invalid multiproofs', () => { |
62 |
| - const { t } = characters('abcdef'); |
63 |
| - const { t: otherTree } = characters('abc'); |
64 |
| - |
65 |
| - const leaves = [['a'], ['b'], ['c']]; |
66 |
| - const multiProof = otherTree.getMultiProof(leaves); |
67 |
| - |
68 |
| - assert(!t.verifyMultiProof(multiProof)); |
69 |
| - assert(!StandardMerkleTree.verifyMultiProof(t.root, ['string'], multiProof)); |
70 |
| - }); |
71 |
| - |
72 |
| - it('renders tree representation', () => { |
73 |
| - const { t } = characters('abc'); |
74 |
| - |
75 |
| - const expected = `\ |
76 |
| -0) f2129b5a697531ef818f644564a6552b35c549722385bc52aa7fe46c0b5f46b1 |
77 |
| -├─ 1) fa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016 |
78 |
| -│ ├─ 3) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c |
79 |
| -│ └─ 4) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681 |
80 |
| -└─ 2) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b`; |
81 |
| - |
82 |
| - assert.equal(t.render(), expected); |
83 |
| - }); |
84 |
| - |
85 |
| - it('dump and load', () => { |
86 |
| - const { t } = characters('abcdef'); |
87 |
| - const t2 = StandardMerkleTree.load(t.dump()); |
88 |
| - |
89 |
| - t2.validate(); |
90 |
| - assert.deepEqual(t2, t); |
91 |
| - }); |
92 |
| - |
93 |
| - it('reject out of bounds value index', () => { |
94 |
| - const { t } = characters('a'); |
95 |
| - assert.throws( |
96 |
| - () => t.getProof(1), |
97 |
| - /^Error: Index out of bounds$/, |
98 |
| - ); |
99 |
| - }); |
100 |
| - |
101 |
| - it('reject unrecognized tree dump', () => { |
102 |
| - assert.throws( |
103 |
| - () => StandardMerkleTree.load({ format: 'nonstandard' } as any), |
104 |
| - /^Error: Unknown format 'nonstandard'$/, |
105 |
| - ); |
106 |
| - }); |
107 |
| - |
108 |
| - it('reject malformed tree dump', () => { |
109 |
| - const t1 = StandardMerkleTree.load({ |
110 |
| - format: 'standard-v1', |
111 |
| - tree: [zero], |
112 |
| - values: [{ value: ['0'], treeIndex: 0 }], |
113 |
| - leafEncoding: ['uint256'], |
| 17 | + for (const opts of [ |
| 18 | + {}, |
| 19 | + { sortLeaves: true }, |
| 20 | + { sortLeaves: false }, |
| 21 | + ]) { |
| 22 | + describe(`with options '${JSON.stringify(opts)}'`, () => { |
| 23 | + const { l: leaves, t: tree } = makeTree('abcdef', opts); |
| 24 | + const { l: otherLeaves, t: otherTree } = makeTree('abc', opts); |
| 25 | + |
| 26 | + it('generates valid single proofs for all leaves', () => { |
| 27 | + tree.validate(); |
| 28 | + }); |
| 29 | + |
| 30 | + it('generates valid single proofs for all leaves', () => { |
| 31 | + for (const [id, leaf] of tree.entries()) { |
| 32 | + const proof1 = tree.getProof(id); |
| 33 | + const proof2 = tree.getProof(leaf); |
| 34 | + |
| 35 | + assert.deepEqual(proof1, proof2); |
| 36 | + |
| 37 | + assert(tree.verify(id, proof1)); |
| 38 | + assert(tree.verify(leaf, proof1)); |
| 39 | + assert(StandardMerkleTree.verify(tree.root, ['string'], leaf, proof1)); |
| 40 | + } |
| 41 | + }); |
| 42 | + |
| 43 | + it('rejects invalid proofs', () => { |
| 44 | + const leaf = ['a']; |
| 45 | + const invalidProof = otherTree.getProof(leaf); |
| 46 | + |
| 47 | + assert(!tree.verify(leaf, invalidProof)); |
| 48 | + assert(!StandardMerkleTree.verify(tree.root, ['string'], leaf, invalidProof)); |
| 49 | + }); |
| 50 | + |
| 51 | + it('generates valid multiproofs', () => { |
| 52 | + for (const ids of [[], [0, 1], [0, 1, 5], [1, 3, 4, 5], [0, 2, 4, 5], [0, 1, 2, 3, 4, 5], [4, 1, 5, 0, 2]]) { |
| 53 | + const proof1 = tree.getMultiProof(ids); |
| 54 | + const proof2 = tree.getMultiProof(ids.map(i => leaves[i]!)); |
| 55 | + |
| 56 | + assert.deepEqual(proof1, proof2); |
| 57 | + |
| 58 | + assert(tree.verifyMultiProof(proof1)); |
| 59 | + assert(StandardMerkleTree.verifyMultiProof(tree.root, ['string'], proof1)); |
| 60 | + } |
| 61 | + }); |
| 62 | + |
| 63 | + it('rejects invalid multiproofs', () => { |
| 64 | + const multiProof = otherTree.getMultiProof([['a'], ['b'], ['c']]); |
| 65 | + |
| 66 | + assert(!tree.verifyMultiProof(multiProof)); |
| 67 | + assert(!StandardMerkleTree.verifyMultiProof(tree.root, ['string'], multiProof)); |
| 68 | + }); |
| 69 | + |
| 70 | + it('renders tree representation', () => { |
| 71 | + assert.equal( |
| 72 | + tree.render(), |
| 73 | + opts.sortLeaves == false |
| 74 | + ? [ |
| 75 | + "0) 23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b", |
| 76 | + "├─ 1) 8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9", |
| 77 | + "│ ├─ 3) 03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9", |
| 78 | + "│ │ ├─ 7) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", |
| 79 | + "│ │ └─ 8) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", |
| 80 | + "│ └─ 4) fa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016", |
| 81 | + "│ ├─ 9) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", |
| 82 | + "│ └─ 10) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", |
| 83 | + "└─ 2) 7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece", |
| 84 | + " ├─ 5) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", |
| 85 | + " └─ 6) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", |
| 86 | + ].join("\n") |
| 87 | + : [ |
| 88 | + "0) 6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8", |
| 89 | + "├─ 1) 52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3", |
| 90 | + "│ ├─ 3) 8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f", |
| 91 | + "│ │ ├─ 7) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", |
| 92 | + "│ │ └─ 8) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", |
| 93 | + "│ └─ 4) 965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c", |
| 94 | + "│ ├─ 9) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", |
| 95 | + "│ └─ 10) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", |
| 96 | + "└─ 2) fd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51", |
| 97 | + " ├─ 5) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", |
| 98 | + " └─ 6) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", |
| 99 | + ].join("\n"), |
| 100 | + ); |
| 101 | + }); |
| 102 | + |
| 103 | + it('dump and load', () => { |
| 104 | + const recoveredTree = StandardMerkleTree.load(tree.dump()); |
| 105 | + |
| 106 | + recoveredTree.validate(); |
| 107 | + assert.deepEqual(tree, recoveredTree); |
| 108 | + }); |
| 109 | + |
| 110 | + it('reject out of bounds value index', () => { |
| 111 | + assert.throws( |
| 112 | + () => tree.getProof(leaves.length), |
| 113 | + /^Error: Index out of bounds$/, |
| 114 | + ); |
| 115 | + }); |
| 116 | + |
| 117 | + it('reject unrecognized tree dump', () => { |
| 118 | + assert.throws( |
| 119 | + () => StandardMerkleTree.load({ format: 'nonstandard' } as any), |
| 120 | + /^Error: Unknown format 'nonstandard'$/, |
| 121 | + ); |
| 122 | + }); |
| 123 | + |
| 124 | + it('reject malformed tree dump', () => { |
| 125 | + const loadedTree1 = StandardMerkleTree.load({ |
| 126 | + format: 'standard-v1', |
| 127 | + tree: [zero], |
| 128 | + values: [{ value: ['0'], treeIndex: 0 }], |
| 129 | + leafEncoding: ['uint256'], |
| 130 | + }); |
| 131 | + assert.throws( |
| 132 | + () => loadedTree1.getProof(0), |
| 133 | + /^Error: Merkle tree does not contain the expected value$/, |
| 134 | + ); |
| 135 | + |
| 136 | + const loadedTree2 = StandardMerkleTree.load({ |
| 137 | + format: 'standard-v1', |
| 138 | + tree: [zero, zero, hex(keccak256(keccak256(zeroBytes)))], |
| 139 | + values: [{ value: ['0'], treeIndex: 2 }], |
| 140 | + leafEncoding: ['uint256'], |
| 141 | + }); |
| 142 | + assert.throws( |
| 143 | + () => loadedTree2.getProof(0), |
| 144 | + /^Error: Unable to prove value$/, |
| 145 | + ); |
| 146 | + }); |
114 | 147 | });
|
115 |
| - assert.throws( |
116 |
| - () => t1.getProof(0), |
117 |
| - /^Error: Merkle tree does not contain the expected value$/, |
118 |
| - ); |
119 |
| - |
120 |
| - const t2 = StandardMerkleTree.load({ |
121 |
| - format: 'standard-v1', |
122 |
| - tree: [zero, zero, hex(keccak256(keccak256(zeroBytes)))], |
123 |
| - values: [{ value: ['0'], treeIndex: 2 }], |
124 |
| - leafEncoding: ['uint256'], |
125 |
| - }); |
126 |
| - assert.throws( |
127 |
| - () => t2.getProof(0), |
128 |
| - /^Error: Unable to prove value$/, |
129 |
| - ); |
130 |
| - }); |
| 148 | + } |
131 | 149 | });
|
0 commit comments