1
- import { getFirstCommonElements , getIndexOfLastNonZeroElement , keyToPath } from "../src/utils"
1
+ import { checkHex , getFirstCommonElements , getIndexOfLastNonZeroElement , keyToPath } from "../src/utils"
2
2
3
3
/**
4
4
* SMT class provides all the functions to create a sparse Merkle tree
@@ -13,7 +13,7 @@ import { getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from
13
13
* value to mark the node as leaf node (`H(x, y, 1)`);
14
14
* * **entry**: a tree entry is a key/value pair used to create the leaf nodes;
15
15
* * **zero nodes**: a zero node is an hash of zeros and in this implementation `H(0,0) = 0`;
16
- * * **side node**: if you take one of the two child nodes, the other node is its side node;
16
+ * * **side node**: if you take one of the two child nodes, the other one is its side node;
17
17
* * **path**: every entry key is a number < 2^256 that can be converted in a binary number,
18
18
* and this binary number is the path used to place the entry in the tree (1 or 0 define the
19
19
* child node to choose);
@@ -23,28 +23,48 @@ import { getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from
23
23
*/
24
24
export class SMT {
25
25
// Hash function used to hash the child nodes.
26
- // The child nodes are hexadecimals and the hash function
27
- // must return the hash of the child nodes as hexadecimal.
28
26
private hash : HashFunction
29
- // Hexadecimal value for zero nodes.
30
- private zeroValue : string
27
+ // Value for zero nodes.
28
+ private zeroNode : Node
29
+ // Additional entry value to mark the leaf nodes.
30
+ private entryMark : EntryMark
31
+ // If true it sets `BigInt` type as default type of the tree hashes.
32
+ private bigNumbers : boolean
31
33
// Key/value map in which the key is a node of the tree and
32
- // the value is an array of the child nodes. When the node is
33
- // a leaf node the child nodes are an entry of the tree.
34
- private nodes : Map < string , ChildNodes >
34
+ // the value is an array of child nodes. When the node is
35
+ // a leaf node the child nodes are an entry (key/value) of the tree.
36
+ private nodes : Map < Node , ChildNodes >
35
37
36
38
// The root node of the tree.
37
- root : string
39
+ root : Node
38
40
39
41
/**
40
42
* Initializes the SMT attributes.
41
43
* @param hash Hash function used to hash the child nodes.
44
+ * @param bigNumbers BigInt type enabling.
42
45
*/
43
- constructor ( hash : HashFunction ) {
46
+ constructor ( hash : HashFunction , bigNumbers = false ) {
47
+ if ( bigNumbers ) {
48
+ if ( typeof BigInt !== "function" ) {
49
+ throw new Error ( "Big numbers are not supported" )
50
+ }
51
+
52
+ if ( typeof hash ( [ BigInt ( 1 ) , BigInt ( 1 ) ] ) !== "bigint" ) {
53
+ throw new Error ( "The hash function must return a big number" )
54
+ }
55
+ } else {
56
+ if ( ! checkHex ( hash ( [ "1" , "1" ] ) as string ) ) {
57
+ throw new Error ( "The hash function must return a hexadecimal" )
58
+ }
59
+ }
60
+
44
61
this . hash = hash
45
- this . zeroValue = "0"
62
+ this . bigNumbers = bigNumbers
63
+ this . zeroNode = bigNumbers ? BigInt ( 0 ) : "0"
64
+ this . entryMark = bigNumbers ? BigInt ( 1 ) : "1"
46
65
this . nodes = new Map ( )
47
- this . root = this . zeroValue // The root node is initially a zero node.
66
+
67
+ this . root = this . zeroNode // The root node is initially a zero node.
48
68
}
49
69
50
70
/**
@@ -53,7 +73,9 @@ export class SMT {
53
73
* @param key A key of a tree entry.
54
74
* @returns A value of a tree entry or 'undefined'.
55
75
*/
56
- get ( key : string ) : string | undefined {
76
+ get ( key : Key ) : Value | undefined {
77
+ this . checkParameterType ( key )
78
+
57
79
const { entry } = this . retrieveEntry ( key )
58
80
59
81
return entry [ 1 ]
@@ -66,7 +88,10 @@ export class SMT {
66
88
* @param key The key of the new entry.
67
89
* @param value The value of the new entry.
68
90
*/
69
- add ( key : string , value : string ) {
91
+ add ( key : Key , value : Value ) {
92
+ this . checkParameterType ( key )
93
+ this . checkParameterType ( value )
94
+
70
95
const { entry, matchingEntry, sidenodes } = this . retrieveEntry ( key )
71
96
72
97
if ( entry [ 1 ] !== undefined ) {
@@ -78,7 +103,7 @@ export class SMT {
78
103
// the node is a zero node. This node is used below as the first node
79
104
// (starting from the bottom of the tree) to obtain the new nodes
80
105
// up to the root.
81
- const node = matchingEntry ? this . hash ( matchingEntry ) : this . zeroValue
106
+ const node = matchingEntry ? this . hash ( matchingEntry ) : this . zeroNode
82
107
83
108
// If there are side nodes it deletes all the nodes of the path.
84
109
// These nodes will be re-created below with the new hashes.
@@ -95,7 +120,7 @@ export class SMT {
95
120
const matchingPath = keyToPath ( matchingEntry [ 0 ] )
96
121
97
122
for ( let i = sidenodes . length ; matchingPath [ i ] === path [ i ] ; i ++ ) {
98
- sidenodes . push ( this . zeroValue )
123
+ sidenodes . push ( this . zeroNode )
99
124
}
100
125
101
126
sidenodes . push ( node )
@@ -104,8 +129,8 @@ export class SMT {
104
129
// Adds the new entry and re-creates the nodes of the path with the new hashes
105
130
// with a bottom-up approach. The `addNewNodes` function returns the last node
106
131
// added, which is the root node.
107
- const newNode = this . hash ( [ key , value , "1" ] )
108
- this . nodes . set ( newNode , [ key , value , "1" ] )
132
+ const newNode = this . hash ( [ key , value , this . entryMark ] )
133
+ this . nodes . set ( newNode , [ key , value , this . entryMark ] )
109
134
this . root = this . addNewNodes ( newNode , path , sidenodes )
110
135
}
111
136
@@ -116,7 +141,10 @@ export class SMT {
116
141
* @param key The key of the entry.
117
142
* @param value The value of the entry.
118
143
*/
119
- update ( key : string , value : string ) {
144
+ update ( key : Key , value : Value ) {
145
+ this . checkParameterType ( key )
146
+ this . checkParameterType ( value )
147
+
120
148
const { entry, sidenodes } = this . retrieveEntry ( key )
121
149
122
150
if ( entry [ 1 ] === undefined ) {
@@ -132,8 +160,8 @@ export class SMT {
132
160
133
161
// Adds the new entry and re-creates the nodes of the path
134
162
// with the new hashes.
135
- const newNode = this . hash ( [ key , value , "1" ] )
136
- this . nodes . set ( newNode , [ key , value , "1" ] )
163
+ const newNode = this . hash ( [ key , value , this . entryMark ] )
164
+ this . nodes . set ( newNode , [ key , value , this . entryMark ] )
137
165
this . root = this . addNewNodes ( newNode , path , sidenodes )
138
166
}
139
167
@@ -142,7 +170,9 @@ export class SMT {
142
170
* the nodes in the path of the entry are updated with a bottom-up approach.
143
171
* @param key The key of the entry.
144
172
*/
145
- delete ( key : string ) {
173
+ delete ( key : Key ) {
174
+ this . checkParameterType ( key )
175
+
146
176
const { entry, sidenodes } = this . retrieveEntry ( key )
147
177
148
178
if ( entry [ 1 ] === undefined ) {
@@ -155,7 +185,7 @@ export class SMT {
155
185
const node = this . hash ( entry )
156
186
this . nodes . delete ( node )
157
187
158
- this . root = this . zeroValue
188
+ this . root = this . zeroNode
159
189
160
190
// If there are side nodes it deletes the nodes of the path and
161
191
// re-creates them with the new hashes.
@@ -167,9 +197,9 @@ export class SMT {
167
197
// it removes the last non-zero side node from the `sidenodes`
168
198
// array and it starts from it by skipping the last zero nodes.
169
199
if ( ! this . isLeaf ( sidenodes [ sidenodes . length - 1 ] ) ) {
170
- this . root = this . addNewNodes ( this . zeroValue , path , sidenodes )
200
+ this . root = this . addNewNodes ( this . zeroNode , path , sidenodes )
171
201
} else {
172
- const firstSidenode = sidenodes . pop ( ) as string
202
+ const firstSidenode = sidenodes . pop ( ) as Node
173
203
const i = getIndexOfLastNonZeroElement ( sidenodes )
174
204
175
205
this . root = this . addNewNodes ( firstSidenode , path , sidenodes , i )
@@ -183,7 +213,9 @@ export class SMT {
183
213
* @param key A key of an existing or a non-existing entry.
184
214
* @returns The membership or the non-membership proof.
185
215
*/
186
- createProof ( key : string ) : Proof {
216
+ createProof ( key : Key ) : Proof {
217
+ this . checkParameterType ( key )
218
+
187
219
const { entry, matchingEntry, sidenodes } = this . retrieveEntry ( key )
188
220
189
221
// If the key exists the function returns a membership proof, otherwise it
@@ -211,7 +243,7 @@ export class SMT {
211
243
// and in this case, since there is not a matching entry, the node
212
244
// is a zero node. If there is an entry value the proof is a
213
245
// membership proof and the node is the hash of the entry.
214
- const node = proof . entry [ 1 ] !== undefined ? this . hash ( proof . entry ) : this . zeroValue
246
+ const node = proof . entry [ 1 ] !== undefined ? this . hash ( proof . entry ) : this . zeroNode
215
247
const root = this . calculateRoot ( node , path , proof . sidenodes )
216
248
217
249
// If the obtained root is equal to the proof root, then the proof is valid.
@@ -250,13 +282,13 @@ export class SMT {
250
282
* @param key The key of the entry to search for.
251
283
* @returns The entry response.
252
284
*/
253
- private retrieveEntry ( key : string ) : EntryResponse {
285
+ private retrieveEntry ( key : Key ) : EntryResponse {
254
286
const path = keyToPath ( key )
255
- const sidenodes : string [ ] = [ ]
287
+ const sidenodes : SideNodes = [ ]
256
288
257
289
// Starts from the root and goes down into the tree until it finds
258
290
// the entry, a zero node or a matching entry.
259
- for ( let i = 0 , node = this . root ; node !== this . zeroValue ; i ++ ) {
291
+ for ( let i = 0 , node = this . root ; node !== this . zeroNode ; i ++ ) {
260
292
const childNodes = this . nodes . get ( node ) as ChildNodes
261
293
const direction = path [ i ]
262
294
@@ -277,8 +309,8 @@ export class SMT {
277
309
// When it goes down into the tree and follows the path, in every step
278
310
// a node is chosen between the left and the right child nodes, and the
279
311
// opposite node is saved as side node.
280
- node = childNodes [ direction ] as string
281
- sidenodes . push ( childNodes [ Number ( ! direction ) ] as string )
312
+ node = childNodes [ direction ] as Node
313
+ sidenodes . push ( childNodes [ Number ( ! direction ) ] as Node )
282
314
}
283
315
284
316
// The path led to a zero node.
@@ -292,7 +324,7 @@ export class SMT {
292
324
* @param sidenodes The side nodes of the path.
293
325
* @returns The root node.
294
326
*/
295
- private calculateRoot ( node : string , path : number [ ] , sidenodes : string [ ] ) : string {
327
+ private calculateRoot ( node : Node , path : number [ ] , sidenodes : SideNodes ) : Node {
296
328
for ( let i = sidenodes . length - 1 ; i >= 0 ; i -- ) {
297
329
const childNodes : ChildNodes = path [ i ] ? [ sidenodes [ i ] , node ] : [ node , sidenodes [ i ] ]
298
330
node = this . hash ( childNodes )
@@ -309,7 +341,7 @@ export class SMT {
309
341
* @param i The index to start from.
310
342
* @returns The root node.
311
343
*/
312
- private addNewNodes ( node : string , path : number [ ] , sidenodes : string [ ] , i = sidenodes . length - 1 ) : string {
344
+ private addNewNodes ( node : Node , path : number [ ] , sidenodes : SideNodes , i = sidenodes . length - 1 ) : Node {
313
345
for ( ; i >= 0 ; i -- ) {
314
346
const childNodes : ChildNodes = path [ i ] ? [ sidenodes [ i ] , node ] : [ node , sidenodes [ i ] ]
315
347
node = this . hash ( childNodes )
@@ -327,7 +359,7 @@ export class SMT {
327
359
* @param sidenodes The side nodes of the path.
328
360
* @param i The index to start from.
329
361
*/
330
- private deleteOldNodes ( node : string , path : number [ ] , sidenodes : string [ ] , i = sidenodes . length - 1 ) {
362
+ private deleteOldNodes ( node : Node , path : number [ ] , sidenodes : SideNodes , i = sidenodes . length - 1 ) {
331
363
for ( ; i >= 0 ; i -- ) {
332
364
const childNodes : ChildNodes = path [ i ] ? [ sidenodes [ i ] , node ] : [ node , sidenodes [ i ] ]
333
365
node = this . hash ( childNodes )
@@ -341,24 +373,45 @@ export class SMT {
341
373
* @param node A node of the tree.
342
374
* @returns True if the node is a leaf, false otherwise.
343
375
*/
344
- private isLeaf ( node : string ) : boolean {
376
+ private isLeaf ( node : Node ) : boolean {
345
377
const childNodes = this . nodes . get ( node )
346
378
347
379
return ! ! ( childNodes && childNodes [ 2 ] )
348
380
}
381
+
382
+ /**
383
+ * Checks the parameter type.
384
+ * @param parameter The parameter to check.
385
+ */
386
+ private checkParameterType ( parameter : Key | Value ) {
387
+ if ( this . bigNumbers && typeof parameter !== "bigint" ) {
388
+ throw new Error ( `Parameter ${ parameter } must be a big number` )
389
+ }
390
+
391
+ if ( ! this . bigNumbers && ! checkHex ( parameter as string ) ) {
392
+ throw new Error ( `Parameter ${ parameter } must be a hexadecimal` )
393
+ }
394
+ }
349
395
}
350
396
351
- export type ChildNodes = string [ ]
397
+ export type Node = string | bigint
398
+ export type Key = Node
399
+ export type Value = Node
400
+ export type EntryMark = Node
401
+
402
+ export type Entry = [ Key , Value , EntryMark ]
403
+ export type ChildNodes = Node [ ]
404
+ export type SideNodes = Node [ ]
352
405
353
- export type HashFunction = ( childNodes : ChildNodes ) => string
406
+ export type HashFunction = ( childNodes : ChildNodes ) => Node
354
407
355
408
export interface EntryResponse {
356
- entry : ChildNodes
357
- matchingEntry ?: ChildNodes
358
- sidenodes : string [ ]
409
+ entry : Entry | Node [ ]
410
+ matchingEntry ?: Entry | Node [ ]
411
+ sidenodes : SideNodes
359
412
}
360
413
361
414
export interface Proof extends EntryResponse {
362
- root : string
415
+ root : Node
363
416
membership : boolean
364
417
}
0 commit comments