Skip to content

Commit 0b26870

Browse files
committed
feat: add native big number support
1 parent 9cd1693 commit 0b26870

File tree

4 files changed

+191
-79
lines changed

4 files changed

+191
-79
lines changed

src/smt.ts

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from "../src/utils"
1+
import { checkHex, getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from "../src/utils"
22

33
/**
44
* SMT class provides all the functions to create a sparse Merkle tree
@@ -13,7 +13,7 @@ import { getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from
1313
* value to mark the node as leaf node (`H(x, y, 1)`);
1414
* * **entry**: a tree entry is a key/value pair used to create the leaf nodes;
1515
* * **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;
1717
* * **path**: every entry key is a number < 2^256 that can be converted in a binary number,
1818
* and this binary number is the path used to place the entry in the tree (1 or 0 define the
1919
* child node to choose);
@@ -23,28 +23,48 @@ import { getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from
2323
*/
2424
export class SMT {
2525
// 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.
2826
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
3133
// 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>
3537

3638
// The root node of the tree.
37-
root: string
39+
root: Node
3840

3941
/**
4042
* Initializes the SMT attributes.
4143
* @param hash Hash function used to hash the child nodes.
44+
* @param bigNumbers BigInt type enabling.
4245
*/
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+
4461
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"
4665
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.
4868
}
4969

5070
/**
@@ -53,7 +73,9 @@ export class SMT {
5373
* @param key A key of a tree entry.
5474
* @returns A value of a tree entry or 'undefined'.
5575
*/
56-
get(key: string): string | undefined {
76+
get(key: Key): Value | undefined {
77+
this.checkParameterType(key)
78+
5779
const { entry } = this.retrieveEntry(key)
5880

5981
return entry[1]
@@ -66,7 +88,10 @@ export class SMT {
6688
* @param key The key of the new entry.
6789
* @param value The value of the new entry.
6890
*/
69-
add(key: string, value: string) {
91+
add(key: Key, value: Value) {
92+
this.checkParameterType(key)
93+
this.checkParameterType(value)
94+
7095
const { entry, matchingEntry, sidenodes } = this.retrieveEntry(key)
7196

7297
if (entry[1] !== undefined) {
@@ -78,7 +103,7 @@ export class SMT {
78103
// the node is a zero node. This node is used below as the first node
79104
// (starting from the bottom of the tree) to obtain the new nodes
80105
// up to the root.
81-
const node = matchingEntry ? this.hash(matchingEntry) : this.zeroValue
106+
const node = matchingEntry ? this.hash(matchingEntry) : this.zeroNode
82107

83108
// If there are side nodes it deletes all the nodes of the path.
84109
// These nodes will be re-created below with the new hashes.
@@ -95,7 +120,7 @@ export class SMT {
95120
const matchingPath = keyToPath(matchingEntry[0])
96121

97122
for (let i = sidenodes.length; matchingPath[i] === path[i]; i++) {
98-
sidenodes.push(this.zeroValue)
123+
sidenodes.push(this.zeroNode)
99124
}
100125

101126
sidenodes.push(node)
@@ -104,8 +129,8 @@ export class SMT {
104129
// Adds the new entry and re-creates the nodes of the path with the new hashes
105130
// with a bottom-up approach. The `addNewNodes` function returns the last node
106131
// 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])
109134
this.root = this.addNewNodes(newNode, path, sidenodes)
110135
}
111136

@@ -116,7 +141,10 @@ export class SMT {
116141
* @param key The key of the entry.
117142
* @param value The value of the entry.
118143
*/
119-
update(key: string, value: string) {
144+
update(key: Key, value: Value) {
145+
this.checkParameterType(key)
146+
this.checkParameterType(value)
147+
120148
const { entry, sidenodes } = this.retrieveEntry(key)
121149

122150
if (entry[1] === undefined) {
@@ -132,8 +160,8 @@ export class SMT {
132160

133161
// Adds the new entry and re-creates the nodes of the path
134162
// 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])
137165
this.root = this.addNewNodes(newNode, path, sidenodes)
138166
}
139167

@@ -142,7 +170,9 @@ export class SMT {
142170
* the nodes in the path of the entry are updated with a bottom-up approach.
143171
* @param key The key of the entry.
144172
*/
145-
delete(key: string) {
173+
delete(key: Key) {
174+
this.checkParameterType(key)
175+
146176
const { entry, sidenodes } = this.retrieveEntry(key)
147177

148178
if (entry[1] === undefined) {
@@ -155,7 +185,7 @@ export class SMT {
155185
const node = this.hash(entry)
156186
this.nodes.delete(node)
157187

158-
this.root = this.zeroValue
188+
this.root = this.zeroNode
159189

160190
// If there are side nodes it deletes the nodes of the path and
161191
// re-creates them with the new hashes.
@@ -167,9 +197,9 @@ export class SMT {
167197
// it removes the last non-zero side node from the `sidenodes`
168198
// array and it starts from it by skipping the last zero nodes.
169199
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)
171201
} else {
172-
const firstSidenode = sidenodes.pop() as string
202+
const firstSidenode = sidenodes.pop() as Node
173203
const i = getIndexOfLastNonZeroElement(sidenodes)
174204

175205
this.root = this.addNewNodes(firstSidenode, path, sidenodes, i)
@@ -183,7 +213,9 @@ export class SMT {
183213
* @param key A key of an existing or a non-existing entry.
184214
* @returns The membership or the non-membership proof.
185215
*/
186-
createProof(key: string): Proof {
216+
createProof(key: Key): Proof {
217+
this.checkParameterType(key)
218+
187219
const { entry, matchingEntry, sidenodes } = this.retrieveEntry(key)
188220

189221
// If the key exists the function returns a membership proof, otherwise it
@@ -211,7 +243,7 @@ export class SMT {
211243
// and in this case, since there is not a matching entry, the node
212244
// is a zero node. If there is an entry value the proof is a
213245
// 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
215247
const root = this.calculateRoot(node, path, proof.sidenodes)
216248

217249
// If the obtained root is equal to the proof root, then the proof is valid.
@@ -250,13 +282,13 @@ export class SMT {
250282
* @param key The key of the entry to search for.
251283
* @returns The entry response.
252284
*/
253-
private retrieveEntry(key: string): EntryResponse {
285+
private retrieveEntry(key: Key): EntryResponse {
254286
const path = keyToPath(key)
255-
const sidenodes: string[] = []
287+
const sidenodes: SideNodes = []
256288

257289
// Starts from the root and goes down into the tree until it finds
258290
// 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++) {
260292
const childNodes = this.nodes.get(node) as ChildNodes
261293
const direction = path[i]
262294

@@ -277,8 +309,8 @@ export class SMT {
277309
// When it goes down into the tree and follows the path, in every step
278310
// a node is chosen between the left and the right child nodes, and the
279311
// 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)
282314
}
283315

284316
// The path led to a zero node.
@@ -292,7 +324,7 @@ export class SMT {
292324
* @param sidenodes The side nodes of the path.
293325
* @returns The root node.
294326
*/
295-
private calculateRoot(node: string, path: number[], sidenodes: string[]): string {
327+
private calculateRoot(node: Node, path: number[], sidenodes: SideNodes): Node {
296328
for (let i = sidenodes.length - 1; i >= 0; i--) {
297329
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
298330
node = this.hash(childNodes)
@@ -309,7 +341,7 @@ export class SMT {
309341
* @param i The index to start from.
310342
* @returns The root node.
311343
*/
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 {
313345
for (; i >= 0; i--) {
314346
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
315347
node = this.hash(childNodes)
@@ -327,7 +359,7 @@ export class SMT {
327359
* @param sidenodes The side nodes of the path.
328360
* @param i The index to start from.
329361
*/
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) {
331363
for (; i >= 0; i--) {
332364
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
333365
node = this.hash(childNodes)
@@ -341,24 +373,45 @@ export class SMT {
341373
* @param node A node of the tree.
342374
* @returns True if the node is a leaf, false otherwise.
343375
*/
344-
private isLeaf(node: string): boolean {
376+
private isLeaf(node: Node): boolean {
345377
const childNodes = this.nodes.get(node)
346378

347379
return !!(childNodes && childNodes[2])
348380
}
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+
}
349395
}
350396

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[]
352405

353-
export type HashFunction = (childNodes: ChildNodes) => string
406+
export type HashFunction = (childNodes: ChildNodes) => Node
354407

355408
export interface EntryResponse {
356-
entry: ChildNodes
357-
matchingEntry?: ChildNodes
358-
sidenodes: string[]
409+
entry: Entry | Node[]
410+
matchingEntry?: Entry | Node[]
411+
sidenodes: SideNodes
359412
}
360413

361414
export interface Proof extends EntryResponse {
362-
root: string
415+
root: Node
363416
membership: boolean
364417
}

src/utils.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
/**
2-
* Returns the binary representation of a key. The keys of the tree
3-
* are hexadecimals, and for everyone it is possibile to obtain
4-
* an array of 256 padded bits.
5-
* @param key A key of a tree entry.
2+
* Returns the binary representation of a key. For each key it is possibile
3+
* to obtain an array of 256 padded bits.
4+
* @param key The key of a tree entry.
65
* @returns The relative array of bits.
76
*/
8-
export function keyToPath(key: string): number[] {
9-
return hexToBin(key).padStart(256, "0").split("").reverse().map(Number)
7+
export function keyToPath(key: string | bigint): number[] {
8+
const bits = typeof key === "bigint" ? key.toString(16) : hexToBin(key as string)
9+
10+
return bits.padStart(256, "0").split("").reverse().map(Number)
1011
}
1112

1213
/**
1314
* Returns the index of the last non-zero element of an array.
1415
* If there are only zero elements the function returns -1.
15-
* @param array An array of hexadecimal numbers.
16+
* @param array An array of hexadecimal or big numbers.
1617
* @returns The index of the last non-zero element.
1718
*/
18-
export function getIndexOfLastNonZeroElement(array: string[]): number {
19+
export function getIndexOfLastNonZeroElement(array: any[]): number {
1920
for (let i = array.length - 1; i >= 0; i--) {
2021
if (Number(`0x${array[i]}`) !== 0) {
2122
return i
@@ -43,25 +44,12 @@ export function getFirstCommonElements(array1: any[], array2: any[]): any[] {
4344
return minArray.slice()
4445
}
4546

46-
/**
47-
* Checks if a number is a hexadecimal number.
48-
* @param n A hexadecimal number.
49-
* @returns True if the number is a hexadecimal, false otherwise.
50-
*/
51-
export function checkHex(n: string): boolean {
52-
return /^[0-9A-Fa-f]{1,64}$/.test(n)
53-
}
54-
5547
/**
5648
* Converts a hexadecimal number to a binary number.
5749
* @param n A hexadecimal number.
5850
* @returns The relative binary number.
5951
*/
6052
export function hexToBin(n: string): string {
61-
if (!checkHex(n)) {
62-
throw new Error(`Value ${n} is not a hexadecimal number`)
63-
}
64-
6553
let bin = Number(`0x${n[0]}`).toString(2)
6654

6755
for (let i = 1; i < n.length; i++) {
@@ -70,3 +58,12 @@ export function hexToBin(n: string): string {
7058

7159
return bin
7260
}
61+
62+
/**
63+
* Checks if a number is a hexadecimal number.
64+
* @param n A hexadecimal number.
65+
* @returns True if the number is a hexadecimal, false otherwise.
66+
*/
67+
export function checkHex(n: string): boolean {
68+
return typeof n === "string" && /^[0-9A-Fa-f]{1,64}$/.test(n)
69+
}

0 commit comments

Comments
 (0)