@@ -5,18 +5,23 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
55const { StandardMerkleTree } = require ( '@openzeppelin/merkle-tree' ) ;
66
77const { generators } = require ( '../../helpers/random' ) ;
8+ const { range } = require ( '../../helpers/iterate' ) ;
89
9- const makeTree = ( leaves = [ ethers . ZeroHash ] ) =>
10+ const DEPTH = 4 ; // 16 slots
11+
12+ const makeTree = ( leaves = [ ] , length = 2 ** DEPTH , zero = ethers . ZeroHash ) =>
1013 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 ] ) ,
1220 [ 'bytes32' ] ,
1321 { sortLeaves : false } ,
1422 ) ;
1523
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 ] ) ;
2025
2126async function fixture ( ) {
2227 const mock = await ethers . deployContract ( 'MerkleTreeMock' ) ;
@@ -30,69 +35,144 @@ describe('MerkleTree', function () {
3035 } ) ;
3136
3237 it ( 'sets initial values at setup' , async function ( ) {
33- const merkleTree = makeTree ( Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ) ;
38+ const merkleTree = makeTree ( ) ;
3439
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 ) ;
3843 } ) ;
3944
4045 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 = [ ] ;
4348
4449 // 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 ( ) ) ;
4853
49- // update leaf list and rebuild tree.
54+ // rebuild tree.
5055 const tree = makeTree ( leaves ) ;
56+ const hash = tree . leafHash ( tree . at ( i ) ) ;
5157
5258 // 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 ) ;
5460
5561 // 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 ) ;
5864 }
5965 } ) ;
6066
61- it ( 'revert when tree is full' , async function ( ) {
67+ it ( 'pushing to a full tree reverts ' , async function ( ) {
6268 await Promise . all ( Array . from ( { length : 2 ** Number ( DEPTH ) } ) . map ( ( ) => this . mock . push ( ethers . ZeroHash ) ) ) ;
6369
6470 await expect ( this . mock . push ( ethers . ZeroHash ) ) . to . be . revertedWithPanic ( PANIC_CODES . TOO_MUCH_MEMORY_ALLOCATED ) ;
6571 } ) ;
6672 } ) ;
6773
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+
68149 it ( 'reset' , async function ( ) {
69150 // empty tree
70- const zeroLeaves = Array . from ( { length : 2 ** Number ( DEPTH ) } , ( ) => ethers . ZeroHash ) ;
71- const zeroTree = makeTree ( zeroLeaves ) ;
151+ const emptyTree = makeTree ( ) ;
72152
73153 // 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 ( ) ] ;
76155 const tree = makeTree ( leaves ) ;
156+ const hash = tree . leafHash ( tree . at ( 0 ) ) ;
77157
78158 // 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 ) ;
80160 expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 0n ) ;
81161
82162 // 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 ) ;
84164
85165 expect ( await this . mock . root ( ) ) . to . equal ( tree . root ) ;
86166 expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 1n ) ;
87167
88168 // reset tree
89169 await this . mock . setup ( DEPTH , ZERO ) ;
90170
91- expect ( await this . mock . root ( ) ) . to . equal ( zeroTree . root ) ;
171+ expect ( await this . mock . root ( ) ) . to . equal ( emptyTree . root ) ;
92172 expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 0n ) ;
93173
94174 // 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 ) ;
96176
97177 expect ( await this . mock . root ( ) ) . to . equal ( tree . root ) ;
98178 expect ( await this . mock . nextLeafIndex ( ) ) . to . equal ( 1n ) ;
0 commit comments