1
1
import { test , testProp , fc } from '@fast-check/ava' ;
2
2
import { HashZero as zero } from '@ethersproject/constants' ;
3
+ import { keccak256 } from '@ethersproject/keccak256' ;
3
4
import { SimpleMerkleTree } from './simple' ;
5
+ import { BytesLike , HexString , concat , compare } from './bytes' ;
6
+
7
+ const reverseNodeHash = ( a : BytesLike , b : BytesLike ) : HexString => keccak256 ( concat ( [ a , b ] . sort ( compare ) . reverse ( ) ) ) ;
8
+ const otherNodeHash = ( a : BytesLike , b : BytesLike ) : HexString => keccak256 ( reverseNodeHash ( a , b ) ) ; // double hash
9
+
4
10
import { toHex } from './bytes' ;
5
11
import { InvalidArgumentError , InvariantError } from './utils/errors' ;
6
12
7
13
const leaf = fc . uint8Array ( { minLength : 32 , maxLength : 32 } ) . map ( toHex ) ;
8
14
const leaves = fc . array ( leaf , { minLength : 1 } ) ;
9
- const options = fc . record ( { sortLeaves : fc . oneof ( fc . constant ( undefined ) , fc . boolean ( ) ) } ) ;
15
+ const options = fc . record ( {
16
+ sortLeaves : fc . oneof ( fc . constant ( undefined ) , fc . boolean ( ) ) ,
17
+ nodeHash : fc . oneof ( fc . constant ( undefined ) , fc . constant ( reverseNodeHash ) ) ,
18
+ } ) ;
10
19
11
- const tree = fc . tuple ( leaves , options ) . map ( ( [ leaves , options ] ) => SimpleMerkleTree . of ( leaves , options ) ) ;
20
+ const tree = fc
21
+ . tuple ( leaves , options )
22
+ . chain ( ( [ leaves , options ] ) => fc . tuple ( fc . constant ( SimpleMerkleTree . of ( leaves , options ) ) , fc . constant ( options ) ) ) ;
12
23
const treeAndLeaf = fc . tuple ( leaves , options ) . chain ( ( [ leaves , options ] ) =>
13
24
fc . tuple (
14
25
fc . constant ( SimpleMerkleTree . of ( leaves , options ) ) ,
26
+ fc . constant ( options ) ,
15
27
fc . nat ( { max : leaves . length - 1 } ) . map ( index => ( { value : leaves [ index ] ! , index } ) ) ,
16
28
) ,
17
29
) ;
18
30
const treeAndLeaves = fc . tuple ( leaves , options ) . chain ( ( [ leaves , options ] ) =>
19
31
fc . tuple (
20
32
fc . constant ( SimpleMerkleTree . of ( leaves , options ) ) ,
33
+ fc . constant ( options ) ,
21
34
fc
22
35
. uniqueArray ( fc . nat ( { max : leaves . length - 1 } ) )
23
36
. map ( indices => indices . map ( index => ( { value : leaves [ index ] ! , index } ) ) ) ,
@@ -26,48 +39,64 @@ const treeAndLeaves = fc.tuple(leaves, options).chain(([leaves, options]) =>
26
39
27
40
fc . configureGlobal ( { numRuns : process . env . CI ? 10000 : 100 } ) ;
28
41
29
- testProp ( 'generates a valid tree' , [ tree ] , ( t , tree ) => {
42
+ testProp ( 'generates a valid tree' , [ tree ] , ( t , [ tree ] ) => {
30
43
t . notThrows ( ( ) => tree . validate ( ) ) ;
31
44
} ) ;
32
45
33
- testProp ( 'generates valid single proofs for all leaves' , [ treeAndLeaf ] , ( t , [ tree , { value : leaf , index } ] ) => {
34
- const proof1 = tree . getProof ( index ) ;
35
- const proof2 = tree . getProof ( leaf ) ;
36
-
37
- t . deepEqual ( proof1 , proof2 ) ;
38
- t . true ( tree . verify ( index , proof1 ) ) ;
39
- t . true ( tree . verify ( leaf , proof1 ) ) ;
40
- t . true ( SimpleMerkleTree . verify ( tree . root , leaf , proof1 ) ) ;
41
- } ) ;
46
+ testProp (
47
+ 'generates valid single proofs for all leaves' ,
48
+ [ treeAndLeaf ] ,
49
+ ( t , [ tree , options , { value : leaf , index } ] ) => {
50
+ const proof1 = tree . getProof ( index ) ;
51
+ const proof2 = tree . getProof ( leaf ) ;
52
+
53
+ t . deepEqual ( proof1 , proof2 ) ;
54
+ t . true ( tree . verify ( index , proof1 ) ) ;
55
+ t . true ( tree . verify ( leaf , proof1 ) ) ;
56
+ t . true ( SimpleMerkleTree . verify ( tree . root , leaf , proof1 , options . nodeHash ) ) ;
57
+ } ,
58
+ ) ;
42
59
43
- testProp ( 'rejects invalid proofs' , [ treeAndLeaf , tree ] , ( t , [ tree , { value : leaf } ] , otherTree ) => {
44
- const proof = tree . getProof ( leaf ) ;
45
- t . false ( otherTree . verify ( leaf , proof ) ) ;
46
- t . false ( SimpleMerkleTree . verify ( otherTree . root , leaf , proof ) ) ;
47
- } ) ;
60
+ testProp (
61
+ 'rejects invalid proofs' ,
62
+ [ treeAndLeaf , tree ] ,
63
+ ( t , [ tree , options , { value : leaf } ] , [ otherTree , otherOptions ] ) => {
64
+ const proof = tree . getProof ( leaf ) ;
65
+ t . false ( otherTree . verify ( leaf , proof ) ) ;
66
+ t . false ( SimpleMerkleTree . verify ( otherTree . root , leaf , proof , options . nodeHash ) ) ;
67
+ t . false ( SimpleMerkleTree . verify ( otherTree . root , leaf , proof , otherOptions . nodeHash ) ) ;
68
+ } ,
69
+ ) ;
48
70
49
- testProp ( 'generates valid multiproofs' , [ treeAndLeaves ] , ( t , [ tree , indices ] ) => {
71
+ testProp ( 'generates valid multiproofs' , [ treeAndLeaves ] , ( t , [ tree , options , indices ] ) => {
50
72
const proof1 = tree . getMultiProof ( indices . map ( e => e . index ) ) ;
51
73
const proof2 = tree . getMultiProof ( indices . map ( e => e . value ) ) ;
52
74
53
75
t . deepEqual ( proof1 , proof2 ) ;
54
76
t . true ( tree . verifyMultiProof ( proof1 ) ) ;
55
- t . true ( SimpleMerkleTree . verifyMultiProof ( tree . root , proof1 ) ) ;
77
+ t . true ( SimpleMerkleTree . verifyMultiProof ( tree . root , proof1 , options . nodeHash ) ) ;
56
78
} ) ;
57
79
58
- testProp ( 'rejects invalid multiproofs' , [ treeAndLeaves , tree ] , ( t , [ tree , indices ] , otherTree ) => {
59
- const multiProof = tree . getMultiProof ( indices . map ( e => e . index ) ) ;
60
-
61
- t . false ( otherTree . verifyMultiProof ( multiProof ) ) ;
62
- t . false ( SimpleMerkleTree . verifyMultiProof ( otherTree . root , multiProof ) ) ;
63
- } ) ;
80
+ testProp (
81
+ 'rejects invalid multiproofs' ,
82
+ [ treeAndLeaves , tree ] ,
83
+ ( t , [ tree , options , indices ] , [ otherTree , otherOptions ] ) => {
84
+ const multiProof = tree . getMultiProof ( indices . map ( e => e . index ) ) ;
85
+
86
+ t . false ( otherTree . verifyMultiProof ( multiProof ) ) ;
87
+ t . false ( SimpleMerkleTree . verifyMultiProof ( otherTree . root , multiProof , options . nodeHash ) ) ;
88
+ t . false ( SimpleMerkleTree . verifyMultiProof ( otherTree . root , multiProof , otherOptions . nodeHash ) ) ;
89
+ } ,
90
+ ) ;
64
91
65
92
testProp (
66
93
'renders tree representation' ,
67
94
[ leaves ] ,
68
95
( t , leaves ) => {
69
96
t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : true } ) . render ( ) ) ;
70
97
t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : false } ) . render ( ) ) ;
98
+ t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : true , nodeHash : reverseNodeHash } ) . render ( ) ) ;
99
+ t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : false , nodeHash : reverseNodeHash } ) . render ( ) ) ;
71
100
} ,
72
101
{ numRuns : 1 , seed : 0 } ,
73
102
) ;
@@ -78,24 +107,34 @@ testProp(
78
107
( t , leaves ) => {
79
108
t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : true } ) . dump ( ) ) ;
80
109
t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : false } ) . dump ( ) ) ;
110
+ t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : true , nodeHash : reverseNodeHash } ) . dump ( ) ) ;
111
+ t . snapshot ( SimpleMerkleTree . of ( leaves , { sortLeaves : false , nodeHash : reverseNodeHash } ) . dump ( ) ) ;
81
112
} ,
82
113
{ numRuns : 1 , seed : 0 } ,
83
114
) ;
84
115
85
- testProp ( 'dump and load' , [ tree ] , ( t , tree ) => {
86
- const recoveredTree = SimpleMerkleTree . load ( tree . dump ( ) ) ;
87
- recoveredTree . validate ( ) ;
116
+ testProp ( 'dump and load' , [ tree ] , ( t , [ tree , options ] ) => {
117
+ const dump = tree . dump ( ) ;
118
+ const recoveredTree = SimpleMerkleTree . load ( dump , options . nodeHash ) ;
119
+ recoveredTree . validate ( ) ; // already done in load
88
120
121
+ t . is ( dump . hash , options . nodeHash ? 'custom' : undefined ) ;
89
122
t . is ( tree . root , recoveredTree . root ) ;
90
123
t . is ( tree . render ( ) , recoveredTree . render ( ) ) ;
91
124
t . deepEqual ( tree . entries ( ) , recoveredTree . entries ( ) ) ;
92
125
t . deepEqual ( tree . dump ( ) , recoveredTree . dump ( ) ) ;
93
126
} ) ;
94
127
95
- testProp ( 'reject out of bounds value index' , [ tree ] , ( t , tree ) => {
128
+ testProp ( 'reject out of bounds value index' , [ tree ] , ( t , [ tree ] ) => {
96
129
t . throws ( ( ) => tree . getProof ( - 1 ) , new InvalidArgumentError ( 'Index out of bounds' ) ) ;
97
130
} ) ;
98
131
132
+ // We need at least 2 leaves for internal node hashing to come into play
133
+ testProp ( 'reject loading dump with wrong node hash' , [ fc . array ( leaf , { minLength : 2 } ) ] , ( t , leaves ) => {
134
+ const dump = SimpleMerkleTree . of ( leaves , { nodeHash : reverseNodeHash } ) . dump ( ) ;
135
+ t . throws ( ( ) => SimpleMerkleTree . load ( dump , otherNodeHash ) , new InvariantError ( 'Merkle tree is invalid' ) ) ;
136
+ } ) ;
137
+
99
138
test ( 'reject invalid leaf size' , t => {
100
139
const invalidLeaf = '0x000000000000000000000000000000000000000000000000000000000000000000' ;
101
140
t . throws ( ( ) => SimpleMerkleTree . of ( [ invalidLeaf ] ) , {
@@ -116,22 +155,28 @@ test('reject unrecognized tree dump', t => {
116
155
} ) ;
117
156
118
157
test ( 'reject malformed tree dump' , t => {
119
- const loadedTree1 = SimpleMerkleTree . load ( {
120
- format : 'simple-v1' ,
121
- tree : [ zero ] ,
122
- values : [
123
- {
124
- value : '0x0000000000000000000000000000000000000000000000000000000000000001' ,
125
- treeIndex : 0 ,
126
- } ,
127
- ] ,
128
- } ) ;
129
- t . throws ( ( ) => loadedTree1 . getProof ( 0 ) , new InvariantError ( 'Merkle tree does not contain the expected value' ) ) ;
158
+ t . throws (
159
+ ( ) =>
160
+ SimpleMerkleTree . load ( {
161
+ format : 'simple-v1' ,
162
+ tree : [ zero ] ,
163
+ values : [
164
+ {
165
+ value : '0x0000000000000000000000000000000000000000000000000000000000000001' ,
166
+ treeIndex : 0 ,
167
+ } ,
168
+ ] ,
169
+ } ) ,
170
+ new InvariantError ( 'Merkle tree does not contain the expected value' ) ,
171
+ ) ;
130
172
131
- const loadedTree2 = SimpleMerkleTree . load ( {
132
- format : 'simple-v1' ,
133
- tree : [ zero , zero , zero ] ,
134
- values : [ { value : zero , treeIndex : 2 } ] ,
135
- } ) ;
136
- t . throws ( ( ) => loadedTree2 . getProof ( 0 ) , new InvariantError ( 'Unable to prove value' ) ) ;
173
+ t . throws (
174
+ ( ) =>
175
+ SimpleMerkleTree . load ( {
176
+ format : 'simple-v1' ,
177
+ tree : [ zero , zero , zero ] ,
178
+ values : [ { value : zero , treeIndex : 2 } ] ,
179
+ } ) ,
180
+ new InvariantError ( 'Merkle tree is invalid' ) ,
181
+ ) ;
137
182
} ) ;
0 commit comments