|
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