@@ -5,18 +5,23 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
5
5
const { StandardMerkleTree } = require ( '@openzeppelin/merkle-tree' ) ;
6
6
7
7
const { generators } = require ( '../../helpers/random' ) ;
8
+ const { range } = require ( '../../helpers/iterate' ) ;
8
9
9
- const makeTree = ( leaves = [ ethers . ZeroHash ] ) =>
10
+ const DEPTH = 4 ; // 16 slots
11
+
12
+ const makeTree = ( leaves = [ ] , length = 2 ** DEPTH , zero = ethers . ZeroHash ) =>
10
13
StandardMerkleTree . of (
11
- leaves . map ( leaf => [ leaf ] ) ,
14
+ [ ]
15
+ . concat (
16
+ leaves ,
17
+ Array . from ( { length : length - leaves . length } , ( ) => zero ) ,
18
+ )
19
+ . map ( leaf => [ leaf ] ) ,
12
20
[ 'bytes32' ] ,
13
21
{ sortLeaves : false } ,
14
22
) ;
15
23
16
- const hashLeaf = leaf => makeTree ( ) . leafHash ( [ leaf ] ) ;
17
-
18
- const DEPTH = 4n ; // 16 slots
19
- const ZERO = hashLeaf ( ethers . ZeroHash ) ;
24
+ const ZERO = makeTree ( ) . leafHash ( [ ethers . ZeroHash ] ) ;
20
25
21
26
async function fixture ( ) {
22
27
const mock = await ethers . deployContract ( 'MerkleTreeMock' ) ;
@@ -30,69 +35,144 @@ describe('MerkleTree', function () {
30
35
} ) ;
31
36
32
37
it ( 'sets initial values at setup' , async function ( ) {
33
- const merkleTree = makeTree ( Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ) ;
38
+ const merkleTree = makeTree ( ) ;
34
39
35
- expect ( await this . mock . root ( ) ) . to . equal ( merkleTree . root ) ;
36
- expect ( await this . mock . depth ( ) ) . to . equal ( DEPTH ) ;
37
- expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 0n ) ;
40
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( merkleTree . root ) ;
41
+ await expect ( this . mock . depth ( ) ) . to . eventually . equal ( DEPTH ) ;
42
+ await expect ( this . mock . nextLeafIndex ( ) ) . to . eventually . equal ( 0n ) ;
38
43
} ) ;
39
44
40
45
describe ( 'push' , function ( ) {
41
- it ( 'tree is correctly updated ' , async function ( ) {
42
- const leaves = Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ;
46
+ it ( 'pushing correctly updates the tree ' , async function ( ) {
47
+ const leaves = [ ] ;
43
48
44
49
// for each leaf slot
45
- for ( const i in leaves ) {
46
- // generate random leaf and hash it
47
- const hashedLeaf = hashLeaf ( ( leaves [ i ] = generators . bytes32 ( ) ) ) ;
50
+ for ( const i in range ( 2 ** DEPTH ) ) {
51
+ // generate random leaf
52
+ leaves . push ( generators . bytes32 ( ) ) ;
48
53
49
- // update leaf list and rebuild tree.
54
+ // rebuild tree.
50
55
const tree = makeTree ( leaves ) ;
56
+ const hash = tree . leafHash ( tree . at ( i ) ) ;
51
57
52
58
// push value to tree
53
- await expect ( this . mock . push ( hashedLeaf ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hashedLeaf , i , tree . root ) ;
59
+ await expect ( this . mock . push ( hash ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hash , i , tree . root ) ;
54
60
55
61
// check tree
56
- expect ( await this . mock . root ( ) ) . to . equal ( tree . root ) ;
57
- expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( BigInt ( i ) + 1n ) ;
62
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( tree . root ) ;
63
+ await expect ( this . mock . nextLeafIndex ( ) ) . to . eventually . equal ( BigInt ( i ) + 1n ) ;
58
64
}
59
65
} ) ;
60
66
61
- it ( 'revert when tree is full' , async function ( ) {
67
+ it ( 'pushing to a full tree reverts ' , async function ( ) {
62
68
await Promise . all ( Array . from ( { length : 2 ** Number ( DEPTH ) } ) . map ( ( ) => this . mock . push ( ethers . ZeroHash ) ) ) ;
63
69
64
70
await expect ( this . mock . push ( ethers . ZeroHash ) ) . to . be . revertedWithPanic ( PANIC_CODES . TOO_MUCH_MEMORY_ALLOCATED ) ;
65
71
} ) ;
66
72
} ) ;
67
73
74
+ describe ( 'update' , function ( ) {
75
+ for ( const { leafCount, leafIndex } of range ( 2 ** DEPTH + 1 ) . flatMap ( leafCount =>
76
+ range ( leafCount ) . map ( leafIndex => ( { leafCount, leafIndex } ) ) ,
77
+ ) )
78
+ it ( `updating a leaf correctly updates the tree (leaf #${ leafIndex + 1 } /${ leafCount } )` , async function ( ) {
79
+ // initial tree
80
+ const leaves = Array . from ( { length : leafCount } , generators . bytes32 ) ;
81
+ const oldTree = makeTree ( leaves ) ;
82
+
83
+ // fill tree and verify root
84
+ for ( const i in leaves ) {
85
+ await this . mock . push ( oldTree . leafHash ( oldTree . at ( i ) ) ) ;
86
+ }
87
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( oldTree . root ) ;
88
+
89
+ // create updated tree
90
+ leaves [ leafIndex ] = generators . bytes32 ( ) ;
91
+ const newTree = makeTree ( leaves ) ;
92
+
93
+ const oldLeafHash = oldTree . leafHash ( oldTree . at ( leafIndex ) ) ;
94
+ const newLeafHash = newTree . leafHash ( newTree . at ( leafIndex ) ) ;
95
+
96
+ // perform update
97
+ await expect ( this . mock . update ( leafIndex , oldLeafHash , newLeafHash , oldTree . getProof ( leafIndex ) ) )
98
+ . to . emit ( this . mock , 'LeafUpdated' )
99
+ . withArgs ( oldLeafHash , newLeafHash , leafIndex , newTree . root ) ;
100
+
101
+ // verify updated root
102
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( newTree . root ) ;
103
+
104
+ // if there is still room in the tree, fill it
105
+ for ( const i of range ( leafCount , 2 ** DEPTH ) ) {
106
+ // push new value and rebuild tree
107
+ leaves . push ( generators . bytes32 ( ) ) ;
108
+ const nextTree = makeTree ( leaves ) ;
109
+
110
+ // push and verify root
111
+ await this . mock . push ( nextTree . leafHash ( nextTree . at ( i ) ) ) ;
112
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( nextTree . root ) ;
113
+ }
114
+ } ) ;
115
+
116
+ it ( 'replacing a leaf that was not previously pushed reverts' , async function ( ) {
117
+ // changing leaf 0 on an empty tree
118
+ await expect ( this . mock . update ( 1 , ZERO , ZERO , [ ] ) )
119
+ . to . be . revertedWithCustomError ( this . mock , 'MerkleTreeUpdateInvalidIndex' )
120
+ . withArgs ( 1 , 0 ) ;
121
+ } ) ;
122
+
123
+ it ( 'replacing a leaf using an invalid proof reverts' , async function ( ) {
124
+ const leafCount = 4 ;
125
+ const leafIndex = 2 ;
126
+
127
+ const leaves = Array . from ( { length : leafCount } , generators . bytes32 ) ;
128
+ const tree = makeTree ( leaves ) ;
129
+
130
+ // fill tree and verify root
131
+ for ( const i in leaves ) {
132
+ await this . mock . push ( tree . leafHash ( tree . at ( i ) ) ) ;
133
+ }
134
+ await expect ( this . mock . root ( ) ) . to . eventually . equal ( tree . root ) ;
135
+
136
+ const oldLeafHash = tree . leafHash ( tree . at ( leafIndex ) ) ;
137
+ const newLeafHash = generators . bytes32 ( ) ;
138
+ const proof = tree . getProof ( leafIndex ) ;
139
+ // invalid proof (tamper)
140
+ proof [ 1 ] = generators . bytes32 ( ) ;
141
+
142
+ await expect ( this . mock . update ( leafIndex , oldLeafHash , newLeafHash , proof ) ) . to . be . revertedWithCustomError (
143
+ this . mock ,
144
+ 'MerkleTreeUpdateInvalidProof' ,
145
+ ) ;
146
+ } ) ;
147
+ } ) ;
148
+
68
149
it ( 'reset' , async function ( ) {
69
150
// empty tree
70
- const zeroLeaves = Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ;
71
- const zeroTree = makeTree ( zeroLeaves ) ;
151
+ const emptyTree = makeTree ( ) ;
72
152
73
153
// tree with one element
74
- const leaves = Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ;
75
- const hashedLeaf = hashLeaf ( ( leaves [ 0 ] = generators . bytes32 ( ) ) ) ; // fill first leaf and hash it
154
+ const leaves = [ generators . bytes32 ( ) ] ;
76
155
const tree = makeTree ( leaves ) ;
156
+ const hash = tree . leafHash ( tree . at ( 0 ) ) ;
77
157
78
158
// root should be that of a zero tree
79
- expect ( await this . mock . root ( ) ) . to . equal ( zeroTree . root ) ;
159
+ expect ( await this . mock . root ( ) ) . to . equal ( emptyTree . root ) ;
80
160
expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 0n ) ;
81
161
82
162
// push leaf and check root
83
- await expect ( this . mock . push ( hashedLeaf ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hashedLeaf , 0 , tree . root ) ;
163
+ await expect ( this . mock . push ( hash ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hash , 0 , tree . root ) ;
84
164
85
165
expect ( await this . mock . root ( ) ) . to . equal ( tree . root ) ;
86
166
expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 1n ) ;
87
167
88
168
// reset tree
89
169
await this . mock . setup ( DEPTH , ZERO ) ;
90
170
91
- expect ( await this . mock . root ( ) ) . to . equal ( zeroTree . root ) ;
171
+ expect ( await this . mock . root ( ) ) . to . equal ( emptyTree . root ) ;
92
172
expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 0n ) ;
93
173
94
174
// re-push leaf and check root
95
- await expect ( this . mock . push ( hashedLeaf ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hashedLeaf , 0 , tree . root ) ;
175
+ await expect ( this . mock . push ( hash ) ) . to . emit ( this . mock , 'LeafInserted' ) . withArgs ( hash , 0 , tree . root ) ;
96
176
97
177
expect ( await this . mock . root ( ) ) . to . equal ( tree . root ) ;
98
178
expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 1n ) ;
0 commit comments