@feelyourprotocol/binarytree 8141.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +46 -0
  3. package/dist/cjs/binaryTree.d.ts +167 -0
  4. package/dist/cjs/binaryTree.d.ts.map +1 -0
  5. package/dist/cjs/binaryTree.js +606 -0
  6. package/dist/cjs/binaryTree.js.map +1 -0
  7. package/dist/cjs/constructors.d.ts +4 -0
  8. package/dist/cjs/constructors.d.ts.map +1 -0
  9. package/dist/cjs/constructors.js +44 -0
  10. package/dist/cjs/constructors.js.map +1 -0
  11. package/dist/cjs/db/checkpoint.d.ts +87 -0
  12. package/dist/cjs/db/checkpoint.d.ts.map +1 -0
  13. package/dist/cjs/db/checkpoint.js +257 -0
  14. package/dist/cjs/db/checkpoint.js.map +1 -0
  15. package/dist/cjs/db/index.d.ts +2 -0
  16. package/dist/cjs/db/index.d.ts.map +1 -0
  17. package/dist/cjs/db/index.js +18 -0
  18. package/dist/cjs/db/index.js.map +1 -0
  19. package/dist/cjs/index.d.ts +7 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +23 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/node/index.d.ts +4 -0
  24. package/dist/cjs/node/index.d.ts.map +1 -0
  25. package/dist/cjs/node/index.js +20 -0
  26. package/dist/cjs/node/index.js.map +1 -0
  27. package/dist/cjs/node/internalNode.d.ts +37 -0
  28. package/dist/cjs/node/internalNode.d.ts.map +1 -0
  29. package/dist/cjs/node/internalNode.js +92 -0
  30. package/dist/cjs/node/internalNode.js.map +1 -0
  31. package/dist/cjs/node/stemNode.d.ts +34 -0
  32. package/dist/cjs/node/stemNode.d.ts.map +1 -0
  33. package/dist/cjs/node/stemNode.js +75 -0
  34. package/dist/cjs/node/stemNode.js.map +1 -0
  35. package/dist/cjs/node/types.d.ts +35 -0
  36. package/dist/cjs/node/types.d.ts.map +1 -0
  37. package/dist/cjs/node/types.js +9 -0
  38. package/dist/cjs/node/types.js.map +1 -0
  39. package/dist/cjs/node/util.d.ts +9 -0
  40. package/dist/cjs/node/util.d.ts.map +1 -0
  41. package/dist/cjs/node/util.js +40 -0
  42. package/dist/cjs/node/util.js.map +1 -0
  43. package/dist/cjs/package.json +3 -0
  44. package/dist/cjs/proof.d.ts +16 -0
  45. package/dist/cjs/proof.d.ts.map +1 -0
  46. package/dist/cjs/proof.js +49 -0
  47. package/dist/cjs/proof.js.map +1 -0
  48. package/dist/cjs/types.d.ts +45 -0
  49. package/dist/cjs/types.d.ts.map +1 -0
  50. package/dist/cjs/types.js +6 -0
  51. package/dist/cjs/types.js.map +1 -0
  52. package/dist/cjs/util.d.ts +17 -0
  53. package/dist/cjs/util.d.ts.map +1 -0
  54. package/dist/cjs/util.js +71 -0
  55. package/dist/cjs/util.js.map +1 -0
  56. package/dist/esm/binaryTree.d.ts +167 -0
  57. package/dist/esm/binaryTree.d.ts.map +1 -0
  58. package/dist/esm/binaryTree.js +602 -0
  59. package/dist/esm/binaryTree.js.map +1 -0
  60. package/dist/esm/constructors.d.ts +4 -0
  61. package/dist/esm/constructors.d.ts.map +1 -0
  62. package/dist/esm/constructors.js +41 -0
  63. package/dist/esm/constructors.js.map +1 -0
  64. package/dist/esm/db/checkpoint.d.ts +87 -0
  65. package/dist/esm/db/checkpoint.d.ts.map +1 -0
  66. package/dist/esm/db/checkpoint.js +253 -0
  67. package/dist/esm/db/checkpoint.js.map +1 -0
  68. package/dist/esm/db/index.d.ts +2 -0
  69. package/dist/esm/db/index.d.ts.map +1 -0
  70. package/dist/esm/db/index.js +2 -0
  71. package/dist/esm/db/index.js.map +1 -0
  72. package/dist/esm/index.d.ts +7 -0
  73. package/dist/esm/index.d.ts.map +1 -0
  74. package/dist/esm/index.js +7 -0
  75. package/dist/esm/index.js.map +1 -0
  76. package/dist/esm/node/index.d.ts +4 -0
  77. package/dist/esm/node/index.d.ts.map +1 -0
  78. package/dist/esm/node/index.js +4 -0
  79. package/dist/esm/node/index.js.map +1 -0
  80. package/dist/esm/node/internalNode.d.ts +37 -0
  81. package/dist/esm/node/internalNode.d.ts.map +1 -0
  82. package/dist/esm/node/internalNode.js +88 -0
  83. package/dist/esm/node/internalNode.js.map +1 -0
  84. package/dist/esm/node/stemNode.d.ts +34 -0
  85. package/dist/esm/node/stemNode.d.ts.map +1 -0
  86. package/dist/esm/node/stemNode.js +71 -0
  87. package/dist/esm/node/stemNode.js.map +1 -0
  88. package/dist/esm/node/types.d.ts +35 -0
  89. package/dist/esm/node/types.d.ts.map +1 -0
  90. package/dist/esm/node/types.js +6 -0
  91. package/dist/esm/node/types.js.map +1 -0
  92. package/dist/esm/node/util.d.ts +9 -0
  93. package/dist/esm/node/util.d.ts.map +1 -0
  94. package/dist/esm/node/util.js +33 -0
  95. package/dist/esm/node/util.js.map +1 -0
  96. package/dist/esm/package.json +3 -0
  97. package/dist/esm/proof.d.ts +16 -0
  98. package/dist/esm/proof.d.ts.map +1 -0
  99. package/dist/esm/proof.js +45 -0
  100. package/dist/esm/proof.js.map +1 -0
  101. package/dist/esm/types.d.ts +45 -0
  102. package/dist/esm/types.d.ts.map +1 -0
  103. package/dist/esm/types.js +3 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/util.d.ts +17 -0
  106. package/dist/esm/util.d.ts.map +1 -0
  107. package/dist/esm/util.js +66 -0
  108. package/dist/esm/util.js.map +1 -0
  109. package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
  110. package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
  111. package/package.json +77 -0
  112. package/src/binaryTree.ts +742 -0
  113. package/src/constructors.ts +50 -0
  114. package/src/db/checkpoint.ts +297 -0
  115. package/src/db/index.ts +1 -0
  116. package/src/index.ts +6 -0
  117. package/src/node/index.ts +3 -0
  118. package/src/node/internalNode.ts +112 -0
  119. package/src/node/stemNode.ts +87 -0
  120. package/src/node/types.ts +41 -0
  121. package/src/node/util.ts +38 -0
  122. package/src/proof.ts +54 -0
  123. package/src/types.ts +58 -0
  124. package/src/util.ts +80 -0
@@ -0,0 +1,742 @@
1
+ import {
2
+ EthereumJSErrorWithoutCode,
3
+ Lock,
4
+ bitsToBytes,
5
+ bytesToBits,
6
+ bytesToHex,
7
+ concatBytes,
8
+ equalsBits,
9
+ equalsBytes,
10
+ isDebugEnabled,
11
+ matchingBitsLength,
12
+ setLengthRight,
13
+ } from '@feelyourprotocol/util'
14
+ import debug from 'debug'
15
+
16
+ import { CheckpointDB } from './db/index.ts'
17
+ import { InternalBinaryNode } from './node/internalNode.ts'
18
+ import { StemBinaryNode } from './node/stemNode.ts'
19
+ import { decodeBinaryNode, isInternalBinaryNode, isStemBinaryNode } from './node/util.ts'
20
+ import { type BinaryTreeOpts, ROOT_DB_KEY } from './types.ts'
21
+
22
+ import type { PutBatch } from '@feelyourprotocol/util'
23
+ import type { Debugger } from 'debug'
24
+ import type { BinaryNode } from './node/types.ts'
25
+
26
+ interface Path {
27
+ node: BinaryNode | null
28
+ remaining: number[]
29
+ stack: Array<[BinaryNode, number[]]>
30
+ }
31
+
32
+ /**
33
+ * The basic binary tree interface, use with `import { BinaryTree } from '@feelyourprotocol/binarytree'`.
34
+ *
35
+ * A BinaryTree object can be created with the constructor method:
36
+ *
37
+ * - {@link createBinaryTree}
38
+ */
39
+ export class BinaryTree {
40
+ /** The options for instantiating the binary tree */
41
+ protected _opts: BinaryTreeOpts
42
+
43
+ /** The root for an empty tree */
44
+ EMPTY_TREE_ROOT: Uint8Array
45
+
46
+ protected _db!: CheckpointDB
47
+ protected _hashLen: number
48
+ protected _lock = new Lock()
49
+ protected _root: Uint8Array
50
+
51
+ protected DEBUG: boolean
52
+ protected _debug: Debugger = debug('binarytree:#')
53
+ protected debug: (...args: any) => void
54
+ /**
55
+ * Creates a new binary tree.
56
+ * @param opts Options for instantiating the binary tree
57
+ *
58
+ * Note: in most cases, the static {@link createBinaryTree} constructor should be used. It uses the same API but provides sensible defaults
59
+ */
60
+ constructor(opts: BinaryTreeOpts) {
61
+ this._opts = opts
62
+
63
+ if (opts.db instanceof CheckpointDB) {
64
+ throw EthereumJSErrorWithoutCode('Cannot pass in an instance of CheckpointDB')
65
+ }
66
+ this._db = new CheckpointDB({ db: opts.db, cacheSize: opts.cacheSize })
67
+
68
+ this.EMPTY_TREE_ROOT = new Uint8Array(32)
69
+ this._hashLen = 32
70
+ this._root = this.EMPTY_TREE_ROOT
71
+
72
+ if (opts?.root) {
73
+ this.root(opts.root)
74
+ }
75
+
76
+ this.DEBUG = isDebugEnabled('ethjs')
77
+ this.debug = this.DEBUG
78
+ ? (message: string, namespaces: string[] = []) => {
79
+ let log = this._debug
80
+ for (const name of namespaces) {
81
+ log = log.extend(name)
82
+ }
83
+ log(message)
84
+ }
85
+ : (..._: any) => {}
86
+
87
+ this.DEBUG &&
88
+ this.debug(`Trie created:
89
+ || Root: ${bytesToHex(this._root)}
90
+ || Persistent: ${this._opts.useRootPersistence}
91
+ || CacheSize: ${this._opts.cacheSize}
92
+ || ----------------`)
93
+ }
94
+
95
+ /**
96
+ * Gets and/or Sets the current root of the `tree`
97
+ */
98
+ root(value?: Uint8Array | null): Uint8Array {
99
+ if (value !== undefined) {
100
+ if (value === null) {
101
+ value = this.EMPTY_TREE_ROOT
102
+ }
103
+
104
+ if (value.length !== this._hashLen) {
105
+ throw EthereumJSErrorWithoutCode(`Invalid root length. Roots are ${this._hashLen} bytes`)
106
+ }
107
+
108
+ this._root = value
109
+ }
110
+
111
+ return this._root
112
+ }
113
+
114
+ /**
115
+ * Checks if a given root exists.
116
+ */
117
+ async checkRoot(root: Uint8Array): Promise<boolean> {
118
+ try {
119
+ const value = await this._db.get(root)
120
+ return value !== undefined
121
+ } catch (error: any) {
122
+ if (error.message === 'Missing node in DB') {
123
+ return equalsBytes(root, this.EMPTY_TREE_ROOT)
124
+ } else {
125
+ throw error
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Gets values at a given binary tree `stem` and set of suffixes
132
+ * @param stem - the stem of the stem node where we're seeking values
133
+ * @param suffixes - an array of suffixes corresponding to the values desired
134
+ * @returns A Promise that resolves to an array of `Uint8Array`s or `null` depending on if values were found.
135
+ * If the stem is not found, will return an empty array.
136
+ */
137
+ async get(stem: Uint8Array, suffixes: number[]): Promise<(Uint8Array | null)[]> {
138
+ if (stem.length !== 31)
139
+ throw EthereumJSErrorWithoutCode(`expected stem with length 31; got ${stem.length}`)
140
+ this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix: ${suffixes}`, ['get'])
141
+ const stemPath = await this.findPath(stem)
142
+ if (stemPath.node instanceof StemBinaryNode) {
143
+ // The retrieved stem node contains an array of 256 possible values.
144
+ // We read all the suffixes to get the desired values
145
+ const values = []
146
+ for (const suffix of suffixes) {
147
+ const value = stemPath.node.getValue(suffix)
148
+ this.DEBUG &&
149
+ this.debug(`Suffix: ${suffix}; Value: ${value === null ? 'null' : bytesToHex(value)}`, [
150
+ 'get',
151
+ ])
152
+ values.push(value)
153
+ }
154
+ return values
155
+ }
156
+
157
+ return []
158
+ }
159
+
160
+ /**
161
+ * Stores a given `value` at the given `key` or performs a deletion if `value` is null.
162
+ * @param stem - the stem (must be 31 bytes) to store the value at.
163
+ * @param suffixes - array of suffixes at which to store individual values.
164
+ * @param values - the value(s) to store (or null for deletion).
165
+ * @returns A Promise that resolves once the value is stored.
166
+ */
167
+ async put(stem: Uint8Array, suffixes: number[], values: (Uint8Array | null)[]): Promise<void> {
168
+ if (stem.length !== 31)
169
+ throw EthereumJSErrorWithoutCode(`expected stem with length 31, got ${stem.length}`)
170
+ if (values.length > 0 && values.length !== suffixes.length)
171
+ throw EthereumJSErrorWithoutCode(
172
+ `expected number of values (${values.length}) to equal number of suffixes (${suffixes.length})`,
173
+ )
174
+
175
+ this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}`, ['put'])
176
+ const putStack: [Uint8Array, BinaryNode | null][] = [] // A stack of updated nodes starting with the stem node being updated/created to be saved to the DB
177
+
178
+ // If the tree is empty, initialize it.
179
+ if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) {
180
+ await this._createInitialNode(stem, suffixes, values)
181
+ return
182
+ }
183
+
184
+ // Find the path to the node (or the nearest node) for the given stem.
185
+ const foundPath = await this.findPath(stem)
186
+
187
+ // We should always at least get the root node back
188
+ if (foundPath.stack.length === 0)
189
+ throw EthereumJSErrorWithoutCode(`Root node not found in trie`)
190
+
191
+ // Step 1) Create or update the stem node
192
+ let stemNode: StemBinaryNode
193
+ // If we found a stem node with the same stem, we'll update it.
194
+ if (
195
+ foundPath.node &&
196
+ isStemBinaryNode(foundPath.node) &&
197
+ equalsBytes(foundPath.node.stem, stem)
198
+ ) {
199
+ stemNode = foundPath.node
200
+ } else {
201
+ // Otherwise, we'll create a new stem node.
202
+ stemNode = StemBinaryNode.create(stem)
203
+ this.DEBUG && this.debug(`Creating new stem node for stem: ${bytesToHex(stem)}`, ['put'])
204
+ }
205
+
206
+ // Update the values in the stem node
207
+ for (let i = 0; i < suffixes.length; i++) {
208
+ const suffix = suffixes[i]
209
+ const value = values[i]
210
+ stemNode.setValue(suffix, value)
211
+ this.DEBUG &&
212
+ this.debug(
213
+ `Setting value for suffix: ${suffix} to value: ${value instanceof Uint8Array ? bytesToHex(value) : value} at stem node with stem: ${bytesToHex(stem)}`,
214
+ ['put'],
215
+ )
216
+ }
217
+
218
+ // If all values are null then we treat this as a deletion.
219
+ if (stemNode.values.every((val) => val === null)) {
220
+ if (foundPath.node !== null) {
221
+ this.DEBUG && this.debug(`Deleting stem node for stem: ${bytesToHex(stem)}`, ['put'])
222
+ putStack.push([this.merkelize(stemNode), null])
223
+ } else {
224
+ return // nothing to delete
225
+ }
226
+ } else {
227
+ // Otherwise, we add the new or updated stemNode to the putStack
228
+ putStack.push([this.merkelize(stemNode), stemNode])
229
+ }
230
+
231
+ // Get the bit representation of the stem.
232
+ const stemBits = bytesToBits(stemNode.stem)
233
+ // We keep a reference to the current "parent" node path as we update up the tree.
234
+ let lastUpdatedParentPath: number[] = []
235
+
236
+ // Step 2: Add any needed new internal nodes if inserting a new stem.
237
+ // If updating an existing stem, just update the parent internal node reference
238
+ if (foundPath.stack.length > 1) {
239
+ // Pop the nearest node on the path.
240
+ const [nearestNode, nearestNodePath] = foundPath.stack.pop()!
241
+ const parentPath = foundPath.stack[foundPath.stack.length - 1]?.[1] ?? []
242
+ this.DEBUG && this.debug(`Adding necessary internal nodes.`, ['put'])
243
+ // Update the parent branch if necessary.
244
+ // If an update was necessary, updateBranch returns a stack of internal nodes
245
+ // that connect the new stem node to the previous parent inner node
246
+ const updated = this.updateBranch(stemNode, nearestNode, nearestNodePath, parentPath)
247
+ if (updated !== undefined) {
248
+ for (const update of updated) {
249
+ putStack.push([this.merkelize(update.node), update.node])
250
+ lastUpdatedParentPath = update.parentPath
251
+ }
252
+ }
253
+ }
254
+
255
+ // Step 3: Update remaining parent node hashes
256
+ while (foundPath.stack.length > 1) {
257
+ const [node, path] = foundPath.stack.pop()!
258
+ if (isInternalBinaryNode(node)) {
259
+ // Set child pointer to the last internal node in the putStack (last updated internal node)
260
+ node.setChild(lastUpdatedParentPath[lastUpdatedParentPath.length - 1], {
261
+ hash: putStack[putStack.length - 1][0], // Reuse hash already computed above
262
+ path: lastUpdatedParentPath,
263
+ })
264
+ putStack.push([this.merkelize(node), node]) // Update node hash and add to putStack
265
+ lastUpdatedParentPath = path
266
+ this.DEBUG &&
267
+ this.debug(`Updated parent internal node hash for path ${path.join(',')}`, ['put'])
268
+ } else {
269
+ throw EthereumJSErrorWithoutCode(
270
+ `Expected internal node at path ${path.join(',')}, got ${node}`,
271
+ )
272
+ }
273
+ }
274
+
275
+ // Step 4: Update the root node.
276
+ let rootNode = foundPath.stack.pop()![0] // The root node.
277
+ const childReference = putStack[putStack.length - 1][1]
278
+
279
+ if (isStemBinaryNode(rootNode)) {
280
+ // If the root is a stem node but its stem differs from the one we're updating,
281
+ // then we need to split the root. Per the spec, when two stems share a common prefix,
282
+ // we create one internal node per bit in that common prefix, and then at the first
283
+ // divergence, an internal node that points to both stem nodes.
284
+ if (!equalsBytes(rootNode.stem, stem)) {
285
+ this.DEBUG && this.debug(`Root stem differs from new stem. Splitting root.`, ['put'])
286
+ const rootBits = bytesToBits(rootNode.stem)
287
+ const commonPrefixLength = matchingBitsLength(rootBits, stemBits)
288
+ // Create the split node at the divergence bit.
289
+ const splitNode = InternalBinaryNode.create()
290
+ const branchForNew = stemBits[commonPrefixLength]
291
+ const branchForExisting = rootBits[commonPrefixLength]
292
+ splitNode.setChild(branchForNew, {
293
+ hash: this.merkelize(stemNode),
294
+ path: stemBits,
295
+ })
296
+ splitNode.setChild(branchForExisting, {
297
+ hash: this.merkelize(rootNode),
298
+ path: rootBits,
299
+ })
300
+
301
+ let newRoot = splitNode
302
+
303
+ // If there is a common prefix (i.e. commonPrefixLength > 0), we build a chain
304
+ // of internal nodes representing that prefix.
305
+ for (let depth = commonPrefixLength - 1; depth >= 0; depth--) {
306
+ this.DEBUG && this.debug(`Creating internal node at depth ${depth}`, ['put'])
307
+ putStack.push([this.merkelize(newRoot), newRoot])
308
+ const parent = InternalBinaryNode.create()
309
+ // At each level, the branch is determined by the bit of the new stem at position i.
310
+ parent.setChild(stemBits[depth], {
311
+ hash: this.merkelize(newRoot),
312
+ path: stemBits.slice(0, depth + 1),
313
+ })
314
+ newRoot = parent
315
+ }
316
+ // Now newRoot is an internal node chain that represents the entire common prefix,
317
+ // ending in a split node that distinguishes the two different stems.
318
+ rootNode = newRoot
319
+ }
320
+ } else {
321
+ // For an internal root node, we assign the last update child reference to the root.
322
+ if (childReference !== null) {
323
+ rootNode.setChild(
324
+ stemBits[0],
325
+ childReference !== null
326
+ ? {
327
+ hash: this.merkelize(childReference),
328
+ path: isStemBinaryNode(childReference) ? stemBits : lastUpdatedParentPath,
329
+ }
330
+ : null,
331
+ )
332
+ }
333
+ }
334
+
335
+ this.root(this.merkelize(rootNode))
336
+ putStack.push([this._root, rootNode])
337
+ this.DEBUG && this.debug(`Updated root hash to ${bytesToHex(this._root)}`, ['put'])
338
+ await this.saveStack(putStack)
339
+ }
340
+
341
+ /**
342
+ * Helper method for updating or creating the parent internal node for a given stem node.
343
+ * If the nearest node is a stem node with a different stem, a new internal node is created
344
+ * to branch at the first differing bit.
345
+ * If the nearest node is an internal node, its child reference is updated.
346
+ *
347
+ * @param stemNode - The child stem node that will be referenced by the new/updated internal node.
348
+ * @param nearestNode - The nearest node to the new stem node.
349
+ * @param pathToNode - The path (in bits) to `nearestNode` as known from the trie.
350
+ * @returns An array of nodes and their partial paths from the new stem node to the branch parent node
351
+ * or `undefined` if no changes were made.
352
+ */
353
+ updateBranch(
354
+ stemNode: StemBinaryNode,
355
+ nearestNode: BinaryNode,
356
+ pathToNode: number[],
357
+ pathToParent: number[],
358
+ ): { node: BinaryNode; parentPath: number[] }[] | undefined {
359
+ const stemBits = bytesToBits(stemNode.stem)
360
+ if (isStemBinaryNode(nearestNode)) {
361
+ // For two different stems, find the first differing bit.
362
+ const nearestNodeStemBits = bytesToBits(nearestNode.stem)
363
+ const diffIndex = matchingBitsLength(stemBits, nearestNodeStemBits)
364
+ const parentDiffIndex = matchingBitsLength(pathToNode, pathToParent)
365
+
366
+ const newInternal = InternalBinaryNode.create()
367
+ // Set the child pointer for the new stem node using the bit at diffIndex.
368
+ newInternal.setChild(stemBits[diffIndex], {
369
+ hash: this.merkelize(stemNode),
370
+ path: stemBits,
371
+ })
372
+
373
+ // Set the child pointer for the existing stem node.
374
+ newInternal.setChild(nearestNodeStemBits[diffIndex], {
375
+ hash: this.merkelize(nearestNode),
376
+ path: nearestNodeStemBits,
377
+ })
378
+ const putStack = [{ node: newInternal, parentPath: stemBits.slice(0, diffIndex) }]
379
+
380
+ let parent = newInternal
381
+ for (let depth = diffIndex - 1; depth > parentDiffIndex; depth--) {
382
+ this.DEBUG && this.debug(`Creating internal node at depth ${depth}`, ['put'])
383
+ const newParent = InternalBinaryNode.create()
384
+ // At each level, the branch is determined by the bit of the new stem at position i.
385
+ newParent.setChild(stemBits[depth], {
386
+ hash: this.merkelize(parent),
387
+ path: stemBits.slice(0, depth + 1),
388
+ })
389
+ putStack.push({ node: newParent, parentPath: stemBits.slice(0, depth) })
390
+ parent = newParent
391
+ }
392
+
393
+ // Return the stack of new internal nodes that connect the new stem node to the previous parent inner node
394
+ return putStack
395
+ } else if (isInternalBinaryNode(nearestNode)) {
396
+ // For an internal node, determine the branch index using the parent's known path length.
397
+ const branchIndex = stemBits[pathToNode.length]
398
+ nearestNode.setChild(branchIndex, {
399
+ hash: this.merkelize(stemNode),
400
+ path: stemBits,
401
+ })
402
+ return [{ node: nearestNode, parentPath: pathToNode }]
403
+ }
404
+ return undefined
405
+ }
406
+
407
+ /**
408
+ * Tries to find a path to the node for the given key.
409
+ * It returns a `Path` object containing:
410
+ * - `node`: the found node (if any),
411
+ * - `stack`: an array of tuples [node, path] representing the nodes encountered,
412
+ * - `remaining`: the bits of the key that were not matched.
413
+ *
414
+ * @param keyInBytes - the search key as a byte array.
415
+ * @returns A Promise that resolves to a Path object.
416
+ */
417
+ async findPath(keyInBytes: Uint8Array): Promise<Path> {
418
+ const keyInBits = bytesToBits(keyInBytes)
419
+ this.DEBUG && this.debug(`Searching for key: ${bytesToHex(keyInBytes)}`, ['find_path'])
420
+ const result: Path = {
421
+ node: null,
422
+ stack: [],
423
+ remaining: keyInBits,
424
+ }
425
+
426
+ // If tree is empty, return empty path.
427
+ if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) return result
428
+
429
+ // Get the root node.
430
+ let rawNode = await this._db.get(this.root())
431
+ if (rawNode === undefined) throw EthereumJSErrorWithoutCode('root node should exist')
432
+ const rootNode = decodeBinaryNode(rawNode)
433
+
434
+ this.DEBUG && this.debug(`Starting with Root Node: [${bytesToHex(this.root())}]`, ['find_path'])
435
+ // Treat the root as being at an empty path.
436
+ result.stack.push([rootNode, []])
437
+
438
+ // If the root node is a stem node, we're done.
439
+ if (isStemBinaryNode(rootNode)) {
440
+ this.DEBUG && this.debug(`Found stem node at root.`, ['find_path'])
441
+ if (equalsBytes(keyInBytes, rootNode.stem)) {
442
+ result.node = rootNode
443
+ result.remaining = []
444
+ }
445
+ return result
446
+ }
447
+
448
+ // The root is an internal node. Determine the branch to follow using the first bit of the key
449
+ let childNode = rootNode.getChild(keyInBits[0])
450
+
451
+ let finished = false
452
+ while (!finished) {
453
+ if (childNode === null) break
454
+
455
+ // Look up child node by its node hash.
456
+ rawNode = await this._db.get(childNode.hash)
457
+ if (rawNode === undefined)
458
+ throw EthereumJSErrorWithoutCode(`missing node at ${childNode.path}`)
459
+ const decodedNode = decodeBinaryNode(rawNode)
460
+
461
+ // Determine how many bits match between keyInBits and the stored path in childNode.
462
+ const matchingKeyLength = matchingBitsLength(keyInBits, childNode.path)
463
+
464
+ // If we have an exact match (i.e. the stored path equals a prefix of the key)
465
+ // and either the key is fully consumed or we have reached a stem node, we stop.
466
+ if (
467
+ matchingKeyLength === childNode.path.length &&
468
+ (matchingKeyLength === keyInBits.length || isStemBinaryNode(decodedNode))
469
+ ) {
470
+ finished = true
471
+ if (
472
+ matchingKeyLength === keyInBits.length &&
473
+ equalsBits(keyInBits, childNode.path) === true
474
+ ) {
475
+ // We found the sought node
476
+ this.DEBUG &&
477
+ this.debug(
478
+ `Path ${bytesToHex(keyInBytes)} - found full path to node ${bytesToHex(
479
+ this.merkelize(decodedNode),
480
+ )}.`,
481
+ ['find_path'],
482
+ )
483
+ result.node = decodedNode
484
+ result.remaining = []
485
+ return result
486
+ }
487
+ // We didn't find the sought node so record the unmatched tail of the key.
488
+ result.remaining = keyInBits.slice(matchingKeyLength)
489
+ result.stack.push([decodedNode, childNode.path])
490
+ return result
491
+ }
492
+ // Otherwise, push this internal node and continue.
493
+ result.stack.push([decodedNode, keyInBits.slice(0, matchingKeyLength)])
494
+ this.DEBUG &&
495
+ this.debug(
496
+ `Partial Path ${keyInBits.slice(0, matchingKeyLength)} - found next node in path ${bytesToHex(
497
+ this.merkelize(decodedNode),
498
+ )}.`,
499
+ ['find_path'],
500
+ )
501
+
502
+ // If the decoded node is not internal, then we cannot traverse further.
503
+ if (!isInternalBinaryNode(decodedNode)) {
504
+ result.remaining = keyInBits.slice(matchingKeyLength)
505
+ finished = true
506
+ break
507
+ }
508
+ // The next branch is determined by the next bit after the matched prefix.
509
+ const childIndex = keyInBits[matchingKeyLength]
510
+ childNode = decodedNode.getChild(childIndex)
511
+ if (childNode === null) {
512
+ result.remaining = keyInBits.slice(matchingKeyLength)
513
+ finished = true
514
+ }
515
+ }
516
+ this.DEBUG &&
517
+ this.debug(
518
+ `Found partial path ${bytesToHex(bitsToBytes(keyInBits.slice(256 - result.remaining.length)))} but sought node is not present in trie.`,
519
+ ['find_path'],
520
+ )
521
+ return result
522
+ }
523
+
524
+ /**
525
+ * Deletes a given `key` from the tree.
526
+ * @param stem - the stem of the stem node to delete from
527
+ * @param suffixes - the suffixes to delete
528
+ * @returns A Promise that resolves once the key is deleted.
529
+ */
530
+ async del(stem: Uint8Array, suffixes: number[]): Promise<void> {
531
+ this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix(es): ${suffixes}`, ['del'])
532
+ await this.put(stem, suffixes, new Array(suffixes.length).fill(null))
533
+ }
534
+
535
+ /**
536
+ * Create empty root node for initializing an empty tree.
537
+ */
538
+ async createRootNode(): Promise<void> {
539
+ const rootNode = null
540
+ this.DEBUG && this.debug(`No root node. Creating new root node`, ['initialize'])
541
+ this.root(this.merkelize(rootNode))
542
+ await this.saveStack([[this.root(), rootNode]])
543
+ return
544
+ }
545
+
546
+ /**
547
+ * Creates the initial node from an empty tree.
548
+ * @private
549
+ */
550
+ protected async _createInitialNode(
551
+ stem: Uint8Array,
552
+ indexes: number[],
553
+ values: (Uint8Array | null)[],
554
+ ): Promise<void> {
555
+ const initialNode = StemBinaryNode.create(stem)
556
+ for (let i = 0; i < indexes.length; i++) {
557
+ initialNode.setValue(indexes[i], values[i])
558
+ }
559
+ this.root(this.merkelize(initialNode))
560
+ await this._db.put(this.root(), initialNode.serialize())
561
+ await this.persistRoot()
562
+ }
563
+
564
+ /**
565
+ * Saves a stack of nodes to the database.
566
+ *
567
+ * @param putStack - an array of tuples of keys (the partial path of the node in the trie) and nodes (BinaryNodes)
568
+ */
569
+
570
+ async saveStack(putStack: [Uint8Array, BinaryNode | null][]): Promise<void> {
571
+ const opStack = putStack.map(([key, node]) => {
572
+ return {
573
+ type: node !== null ? 'put' : 'del',
574
+ key,
575
+ value: node !== null ? node.serialize() : null,
576
+ } as PutBatch
577
+ })
578
+ await this._db.batch(opStack)
579
+ }
580
+
581
+ /**
582
+ * Creates a proof from a tree and key that can be verified using {@link BinaryTree.verifyBinaryProof}.
583
+ * @param key a 32 byte binary tree key (31 byte stem + 1 byte suffix)
584
+ */
585
+ async createBinaryProof(key: Uint8Array): Promise<Uint8Array[]> {
586
+ this.DEBUG && this.debug(`creating proof for ${bytesToHex(key)}`, ['create_proof'])
587
+ // We only use the stem (i.e. the first 31 bytes) to find the path to the node
588
+
589
+ const { node, stack } = await this.findPath(key.slice(0, 31))
590
+ const proof = stack.map(([node, _]) => node.serialize())
591
+ if (node !== null) {
592
+ // If node is found, add node to proof
593
+ proof.push(node.serialize())
594
+ }
595
+
596
+ return proof
597
+ }
598
+
599
+ /**
600
+ * The `data` event is given an `Object` that has two properties; the `key` and the `value`. Both should be Uint8Arrays.
601
+ * @return Returns a [stream](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_readable) of the contents of the `tree`
602
+ */
603
+ createReadStream(): any {
604
+ throw EthereumJSErrorWithoutCode('Not implemented')
605
+ }
606
+
607
+ /**
608
+ * Returns a copy of the underlying tree.
609
+ *
610
+ * Note on db: the copy will create a reference to the
611
+ * same underlying database.
612
+ *
613
+ * Note on cache: for memory reasons a copy will not
614
+ * recreate a new LRU cache but initialize with cache
615
+ * being deactivated.
616
+ *
617
+ * @param includeCheckpoints - If true and during a checkpoint, the copy will contain the checkpointing metadata and will use the same scratch as underlying db.
618
+ */
619
+ shallowCopy(includeCheckpoints = true): BinaryTree {
620
+ const tree = new BinaryTree({
621
+ ...this._opts,
622
+ db: this._db.db.shallowCopy(),
623
+ root: this.root(),
624
+ cacheSize: 0,
625
+ })
626
+ if (includeCheckpoints && this.hasCheckpoints()) {
627
+ tree._db.setCheckpoints(this._db.checkpoints)
628
+ }
629
+ return tree
630
+ }
631
+
632
+ /**
633
+ * Persists the root hash in the underlying database
634
+ */
635
+ async persistRoot() {
636
+ if (this._opts.useRootPersistence === true) {
637
+ await this._db.put(ROOT_DB_KEY, this.root())
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Is the tree during a checkpoint phase?
643
+ */
644
+ hasCheckpoints() {
645
+ return this._db.hasCheckpoints()
646
+ }
647
+
648
+ /**
649
+ * Creates a checkpoint that can later be reverted to or committed.
650
+ * After this is called, all changes can be reverted until `commit` is called.
651
+ */
652
+ checkpoint() {
653
+ this._db.checkpoint(this.root())
654
+ }
655
+
656
+ /**
657
+ * Commits a checkpoint to disk, if current checkpoint is not nested.
658
+ * If nested, only sets the parent checkpoint as current checkpoint.
659
+ * @throws If not during a checkpoint phase
660
+ */
661
+ async commit(): Promise<void> {
662
+ if (!this.hasCheckpoints()) {
663
+ throw EthereumJSErrorWithoutCode('trying to commit when not checkpointed')
664
+ }
665
+
666
+ await this._lock.acquire()
667
+ await this._db.commit()
668
+ await this.persistRoot()
669
+ this._lock.release()
670
+ }
671
+
672
+ /**
673
+ * Reverts the tree to the state it was at when `checkpoint` was first called.
674
+ * If during a nested checkpoint, sets root to most recent checkpoint, and sets
675
+ * parent checkpoint as current.
676
+ */
677
+ async revert(): Promise<void> {
678
+ if (!this.hasCheckpoints()) {
679
+ throw EthereumJSErrorWithoutCode('trying to revert when not checkpointed')
680
+ }
681
+
682
+ await this._lock.acquire()
683
+ this.root(await this._db.revert())
684
+ await this.persistRoot()
685
+ this._lock.release()
686
+ }
687
+
688
+ /**
689
+ * Flushes all checkpoints, restoring the initial checkpoint state.
690
+ */
691
+ flushCheckpoints() {
692
+ this._db.checkpoints = []
693
+ }
694
+
695
+ protected hash(msg: Uint8Array | null): Uint8Array {
696
+ // As per spec, if value is null or a 64-byte array of 0s, hash(msg) is a 32-byte array of 0s
697
+ if (msg === null || (msg.length === 64 && msg.every((byte) => byte === 0))) {
698
+ return new Uint8Array(32)
699
+ }
700
+
701
+ if (msg.length !== 32 && msg.length !== 64) {
702
+ throw EthereumJSErrorWithoutCode('Data must be 32 or 64 bytes')
703
+ }
704
+
705
+ return Uint8Array.from(this._opts.hashFunction.call(undefined, msg))
706
+ }
707
+
708
+ protected merkelize(node: BinaryNode | null): Uint8Array {
709
+ if (node === null) {
710
+ return new Uint8Array(32)
711
+ }
712
+
713
+ if (isInternalBinaryNode(node)) {
714
+ const [leftChild, rightChild] = node.children
715
+
716
+ return this.hash(
717
+ concatBytes(
718
+ leftChild === null ? this.hash(null) : leftChild.hash,
719
+ rightChild === null ? this.hash(null) : rightChild.hash,
720
+ ),
721
+ )
722
+ }
723
+
724
+ // Otherwise, it's a stem node.
725
+ // Map each value in node.values through the hash function.
726
+ let currentLayerHashes = node.values.map((value) => this.hash(value))
727
+
728
+ // While there is more than one hash at the current layer, combine them pairwise.
729
+ while (currentLayerHashes.length > 1) {
730
+ const newLayerHashes = []
731
+ for (let i = 0; i < currentLayerHashes.length; i += 2) {
732
+ newLayerHashes.push(
733
+ this.hash(concatBytes(currentLayerHashes[i], currentLayerHashes[i + 1])),
734
+ )
735
+ }
736
+ currentLayerHashes = newLayerHashes
737
+ }
738
+
739
+ // Return the hash of the concatenation of node.stem appended with 00 and the final level hash.
740
+ return this.hash(concatBytes(setLengthRight(node.stem, 32), currentLayerHashes[0]))
741
+ }
742
+ }