@feelyourprotocol/mpt 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 (204) hide show
  1. package/README.md +448 -0
  2. package/dist/cjs/constructors.d.ts +12 -0
  3. package/dist/cjs/constructors.d.ts.map +1 -0
  4. package/dist/cjs/constructors.js +57 -0
  5. package/dist/cjs/constructors.js.map +1 -0
  6. package/dist/cjs/db/checkpointDB.d.ts +87 -0
  7. package/dist/cjs/db/checkpointDB.d.ts.map +1 -0
  8. package/dist/cjs/db/checkpointDB.js +258 -0
  9. package/dist/cjs/db/checkpointDB.js.map +1 -0
  10. package/dist/cjs/db/index.d.ts +2 -0
  11. package/dist/cjs/db/index.d.ts.map +1 -0
  12. package/dist/cjs/db/index.js +18 -0
  13. package/dist/cjs/db/index.js.map +1 -0
  14. package/dist/cjs/index.d.ts +8 -0
  15. package/dist/cjs/index.d.ts.map +1 -0
  16. package/dist/cjs/index.js +24 -0
  17. package/dist/cjs/index.js.map +1 -0
  18. package/dist/cjs/mpt.d.ts +261 -0
  19. package/dist/cjs/mpt.d.ts.map +1 -0
  20. package/dist/cjs/mpt.js +900 -0
  21. package/dist/cjs/mpt.js.map +1 -0
  22. package/dist/cjs/node/branch.d.ts +14 -0
  23. package/dist/cjs/node/branch.d.ts.map +1 -0
  24. package/dist/cjs/node/branch.js +52 -0
  25. package/dist/cjs/node/branch.js.map +1 -0
  26. package/dist/cjs/node/extension.d.ts +7 -0
  27. package/dist/cjs/node/extension.d.ts.map +1 -0
  28. package/dist/cjs/node/extension.js +14 -0
  29. package/dist/cjs/node/extension.js.map +1 -0
  30. package/dist/cjs/node/extensionOrLeafNodeBase.d.ts +15 -0
  31. package/dist/cjs/node/extensionOrLeafNodeBase.d.ts.map +1 -0
  32. package/dist/cjs/node/extensionOrLeafNodeBase.js +42 -0
  33. package/dist/cjs/node/extensionOrLeafNodeBase.js.map +1 -0
  34. package/dist/cjs/node/index.d.ts +5 -0
  35. package/dist/cjs/node/index.d.ts.map +1 -0
  36. package/dist/cjs/node/index.js +21 -0
  37. package/dist/cjs/node/index.js.map +1 -0
  38. package/dist/cjs/node/leaf.d.ts +7 -0
  39. package/dist/cjs/node/leaf.d.ts.map +1 -0
  40. package/dist/cjs/node/leaf.js +14 -0
  41. package/dist/cjs/node/leaf.js.map +1 -0
  42. package/dist/cjs/node/util.d.ts +8 -0
  43. package/dist/cjs/node/util.d.ts.map +1 -0
  44. package/dist/cjs/node/util.js +38 -0
  45. package/dist/cjs/node/util.js.map +1 -0
  46. package/dist/cjs/package.json +3 -0
  47. package/dist/cjs/proof/index.d.ts +3 -0
  48. package/dist/cjs/proof/index.d.ts.map +1 -0
  49. package/dist/cjs/proof/index.js +19 -0
  50. package/dist/cjs/proof/index.js.map +1 -0
  51. package/dist/cjs/proof/proof.d.ts +41 -0
  52. package/dist/cjs/proof/proof.d.ts.map +1 -0
  53. package/dist/cjs/proof/proof.js +119 -0
  54. package/dist/cjs/proof/proof.js.map +1 -0
  55. package/dist/cjs/proof/range.d.ts +35 -0
  56. package/dist/cjs/proof/range.d.ts.map +1 -0
  57. package/dist/cjs/proof/range.js +456 -0
  58. package/dist/cjs/proof/range.js.map +1 -0
  59. package/dist/cjs/types.d.ts +110 -0
  60. package/dist/cjs/types.d.ts.map +1 -0
  61. package/dist/cjs/types.js +6 -0
  62. package/dist/cjs/types.js.map +1 -0
  63. package/dist/cjs/util/asyncWalk.d.ts +20 -0
  64. package/dist/cjs/util/asyncWalk.d.ts.map +1 -0
  65. package/dist/cjs/util/asyncWalk.js +50 -0
  66. package/dist/cjs/util/asyncWalk.js.map +1 -0
  67. package/dist/cjs/util/encoding.d.ts +31 -0
  68. package/dist/cjs/util/encoding.d.ts.map +1 -0
  69. package/dist/cjs/util/encoding.js +200 -0
  70. package/dist/cjs/util/encoding.js.map +1 -0
  71. package/dist/cjs/util/genesisState.d.ts +6 -0
  72. package/dist/cjs/util/genesisState.d.ts.map +1 -0
  73. package/dist/cjs/util/genesisState.js +45 -0
  74. package/dist/cjs/util/genesisState.js.map +1 -0
  75. package/dist/cjs/util/hex.d.ts +20 -0
  76. package/dist/cjs/util/hex.d.ts.map +1 -0
  77. package/dist/cjs/util/hex.js +48 -0
  78. package/dist/cjs/util/hex.js.map +1 -0
  79. package/dist/cjs/util/index.d.ts +4 -0
  80. package/dist/cjs/util/index.d.ts.map +1 -0
  81. package/dist/cjs/util/index.js +20 -0
  82. package/dist/cjs/util/index.js.map +1 -0
  83. package/dist/cjs/util/nibbles.d.ts +30 -0
  84. package/dist/cjs/util/nibbles.d.ts.map +1 -0
  85. package/dist/cjs/util/nibbles.js +79 -0
  86. package/dist/cjs/util/nibbles.js.map +1 -0
  87. package/dist/cjs/util/walkController.d.ts +72 -0
  88. package/dist/cjs/util/walkController.d.ts.map +1 -0
  89. package/dist/cjs/util/walkController.js +138 -0
  90. package/dist/cjs/util/walkController.js.map +1 -0
  91. package/dist/esm/constructors.d.ts +12 -0
  92. package/dist/esm/constructors.d.ts.map +1 -0
  93. package/dist/esm/constructors.js +53 -0
  94. package/dist/esm/constructors.js.map +1 -0
  95. package/dist/esm/db/checkpointDB.d.ts +87 -0
  96. package/dist/esm/db/checkpointDB.d.ts.map +1 -0
  97. package/dist/esm/db/checkpointDB.js +254 -0
  98. package/dist/esm/db/checkpointDB.js.map +1 -0
  99. package/dist/esm/db/index.d.ts +2 -0
  100. package/dist/esm/db/index.d.ts.map +1 -0
  101. package/dist/esm/db/index.js +2 -0
  102. package/dist/esm/db/index.js.map +1 -0
  103. package/dist/esm/index.d.ts +8 -0
  104. package/dist/esm/index.d.ts.map +1 -0
  105. package/dist/esm/index.js +8 -0
  106. package/dist/esm/index.js.map +1 -0
  107. package/dist/esm/mpt.d.ts +261 -0
  108. package/dist/esm/mpt.d.ts.map +1 -0
  109. package/dist/esm/mpt.js +897 -0
  110. package/dist/esm/mpt.js.map +1 -0
  111. package/dist/esm/node/branch.d.ts +14 -0
  112. package/dist/esm/node/branch.d.ts.map +1 -0
  113. package/dist/esm/node/branch.js +48 -0
  114. package/dist/esm/node/branch.js.map +1 -0
  115. package/dist/esm/node/extension.d.ts +7 -0
  116. package/dist/esm/node/extension.d.ts.map +1 -0
  117. package/dist/esm/node/extension.js +10 -0
  118. package/dist/esm/node/extension.js.map +1 -0
  119. package/dist/esm/node/extensionOrLeafNodeBase.d.ts +15 -0
  120. package/dist/esm/node/extensionOrLeafNodeBase.d.ts.map +1 -0
  121. package/dist/esm/node/extensionOrLeafNodeBase.js +38 -0
  122. package/dist/esm/node/extensionOrLeafNodeBase.js.map +1 -0
  123. package/dist/esm/node/index.d.ts +5 -0
  124. package/dist/esm/node/index.d.ts.map +1 -0
  125. package/dist/esm/node/index.js +5 -0
  126. package/dist/esm/node/index.js.map +1 -0
  127. package/dist/esm/node/leaf.d.ts +7 -0
  128. package/dist/esm/node/leaf.d.ts.map +1 -0
  129. package/dist/esm/node/leaf.js +10 -0
  130. package/dist/esm/node/leaf.js.map +1 -0
  131. package/dist/esm/node/util.d.ts +8 -0
  132. package/dist/esm/node/util.d.ts.map +1 -0
  133. package/dist/esm/node/util.js +33 -0
  134. package/dist/esm/node/util.js.map +1 -0
  135. package/dist/esm/package.json +3 -0
  136. package/dist/esm/proof/index.d.ts +3 -0
  137. package/dist/esm/proof/index.d.ts.map +1 -0
  138. package/dist/esm/proof/index.js +3 -0
  139. package/dist/esm/proof/index.js.map +1 -0
  140. package/dist/esm/proof/proof.d.ts +41 -0
  141. package/dist/esm/proof/proof.d.ts.map +1 -0
  142. package/dist/esm/proof/proof.js +113 -0
  143. package/dist/esm/proof/proof.js.map +1 -0
  144. package/dist/esm/proof/range.d.ts +35 -0
  145. package/dist/esm/proof/range.d.ts.map +1 -0
  146. package/dist/esm/proof/range.js +453 -0
  147. package/dist/esm/proof/range.js.map +1 -0
  148. package/dist/esm/types.d.ts +110 -0
  149. package/dist/esm/types.d.ts.map +1 -0
  150. package/dist/esm/types.js +3 -0
  151. package/dist/esm/types.js.map +1 -0
  152. package/dist/esm/util/asyncWalk.d.ts +20 -0
  153. package/dist/esm/util/asyncWalk.d.ts.map +1 -0
  154. package/dist/esm/util/asyncWalk.js +47 -0
  155. package/dist/esm/util/asyncWalk.js.map +1 -0
  156. package/dist/esm/util/encoding.d.ts +31 -0
  157. package/dist/esm/util/encoding.d.ts.map +1 -0
  158. package/dist/esm/util/encoding.js +188 -0
  159. package/dist/esm/util/encoding.js.map +1 -0
  160. package/dist/esm/util/genesisState.d.ts +6 -0
  161. package/dist/esm/util/genesisState.d.ts.map +1 -0
  162. package/dist/esm/util/genesisState.js +42 -0
  163. package/dist/esm/util/genesisState.js.map +1 -0
  164. package/dist/esm/util/hex.d.ts +20 -0
  165. package/dist/esm/util/hex.d.ts.map +1 -0
  166. package/dist/esm/util/hex.js +43 -0
  167. package/dist/esm/util/hex.js.map +1 -0
  168. package/dist/esm/util/index.d.ts +4 -0
  169. package/dist/esm/util/index.d.ts.map +1 -0
  170. package/dist/esm/util/index.js +4 -0
  171. package/dist/esm/util/index.js.map +1 -0
  172. package/dist/esm/util/nibbles.d.ts +30 -0
  173. package/dist/esm/util/nibbles.d.ts.map +1 -0
  174. package/dist/esm/util/nibbles.js +73 -0
  175. package/dist/esm/util/nibbles.js.map +1 -0
  176. package/dist/esm/util/walkController.d.ts +72 -0
  177. package/dist/esm/util/walkController.d.ts.map +1 -0
  178. package/dist/esm/util/walkController.js +134 -0
  179. package/dist/esm/util/walkController.js.map +1 -0
  180. package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
  181. package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
  182. package/package.json +85 -0
  183. package/src/constructors.ts +71 -0
  184. package/src/db/checkpointDB.ts +298 -0
  185. package/src/db/index.ts +1 -0
  186. package/src/index.ts +7 -0
  187. package/src/mpt.ts +1090 -0
  188. package/src/node/branch.ts +60 -0
  189. package/src/node/extension.ts +13 -0
  190. package/src/node/extensionOrLeafNodeBase.ts +54 -0
  191. package/src/node/index.ts +4 -0
  192. package/src/node/leaf.ts +13 -0
  193. package/src/node/util.ts +35 -0
  194. package/src/proof/index.ts +2 -0
  195. package/src/proof/proof.ts +135 -0
  196. package/src/proof/range.ts +542 -0
  197. package/src/types.ts +151 -0
  198. package/src/util/asyncWalk.ts +60 -0
  199. package/src/util/encoding.ts +209 -0
  200. package/src/util/genesisState.ts +52 -0
  201. package/src/util/hex.ts +47 -0
  202. package/src/util/index.ts +3 -0
  203. package/src/util/nibbles.ts +80 -0
  204. package/src/util/walkController.ts +172 -0
package/src/mpt.ts ADDED
@@ -0,0 +1,1090 @@
1
+ // Some more secure presets when using e.g. JS `call`
2
+ 'use strict'
3
+
4
+ import { RLP } from '@feelyourprotocol/rlp'
5
+ import {
6
+ BIGINT_0,
7
+ EthereumJSErrorWithoutCode,
8
+ KeyEncoding,
9
+ Lock,
10
+ MapDB,
11
+ RLP_EMPTY_STRING,
12
+ ValueEncoding,
13
+ bytesToBigInt,
14
+ bytesToHex,
15
+ bytesToUnprefixedHex,
16
+ bytesToUtf8,
17
+ concatBytes,
18
+ equalsBytes,
19
+ isDebugEnabled,
20
+ } from '@feelyourprotocol/util'
21
+ import { keccak_256 } from '@noble/hashes/sha3.js'
22
+ import debug from 'debug'
23
+
24
+ import { CheckpointDB } from './db/checkpointDB.ts'
25
+ import {
26
+ BranchMPTNode,
27
+ ExtensionMPTNode,
28
+ LeafMPTNode,
29
+ decodeMPTNode,
30
+ decodeRawMPTNode,
31
+ isRawMPTNode,
32
+ } from './node/index.ts'
33
+ import { ROOT_DB_KEY } from './types.ts'
34
+ import { _walkTrie } from './util/asyncWalk.ts'
35
+ import { bytesToNibbles, matchingNibbleLength, nibblesTypeToPackedBytes } from './util/nibbles.ts'
36
+ import { WalkController } from './util/walkController.ts'
37
+
38
+ import type { BatchDBOp, DB } from '@feelyourprotocol/util'
39
+ import type { Debugger } from 'debug'
40
+ import type {
41
+ BranchMPTNodeBranchValue,
42
+ FoundNodeFunction,
43
+ MPTNode,
44
+ MPTOpts,
45
+ MPTOptsWithDefaults,
46
+ Nibbles,
47
+ NodeReferenceOrRawMPTNode,
48
+ Path,
49
+ TrieShallowCopyOpts,
50
+ } from './types.ts'
51
+ import type { OnFound } from './util/asyncWalk.ts'
52
+
53
+ /**
54
+ * The basic trie interface, use with `import { MerklePatriciaTrie } from '@feelyourprotocol/mpt'`.
55
+ *
56
+ * A MerklePatriciaTrie object can be created with the constructor method:
57
+ *
58
+ * - {@link createMPT}
59
+ *
60
+ * A sparse MerklePatriciaTrie object can be created from a merkle proof:
61
+ *
62
+ * - {@link createMPTFromProof}
63
+ */
64
+ /**
65
+ * Merkle Patricia Trie - a space-optimized trie where each node with only one child
66
+ * is merged with its parent. Used for Ethereum state and storage.
67
+ *
68
+ * Node types:
69
+ * - Branch: 16-way branch + optional value (for keys ending at this node)
70
+ * - Extension: short path (nibbles) → child node
71
+ * - Leaf: remaining path (nibbles) → value
72
+ */
73
+ export class MerklePatriciaTrie {
74
+ /** Options with defaults applied */
75
+ protected readonly _opts: MPTOptsWithDefaults = {
76
+ useKeyHashing: false,
77
+ useKeyHashingFunction: keccak_256,
78
+ keyPrefix: undefined,
79
+ useRootPersistence: false,
80
+ useNodePruning: false,
81
+ cacheSize: 0,
82
+ valueEncoding: ValueEncoding.String,
83
+ }
84
+
85
+ /** The root for an empty trie */
86
+ EMPTY_TRIE_ROOT: Uint8Array
87
+
88
+ /** The backend DB */
89
+ protected _db!: CheckpointDB
90
+ protected _hashLen: number
91
+ protected _lock = new Lock()
92
+ protected _root: Uint8Array
93
+
94
+ /** Debug logging */
95
+ protected DEBUG: boolean
96
+ protected _debug: Debugger = debug('mpt:#')
97
+ protected debug: (...args: any) => void
98
+
99
+ /**
100
+ * Creates a new trie.
101
+ * @param opts Options for instantiating the trie
102
+ *
103
+ * Note: in most cases, {@link createMPT} constructor should be used. It uses the same API but provides sensible defaults
104
+ */
105
+ constructor(opts?: MPTOpts) {
106
+ if (opts?.valueEncoding !== undefined && opts.db === undefined) {
107
+ throw EthereumJSErrorWithoutCode('`valueEncoding` can only be set if a `db` is provided')
108
+ }
109
+ if (opts !== undefined) {
110
+ this._opts = { ...this._opts, ...opts }
111
+ this._opts.useKeyHashingFunction =
112
+ opts.common?.customCrypto.keccak256 ?? opts.useKeyHashingFunction ?? keccak_256
113
+ }
114
+ const valueEncoding =
115
+ opts?.db !== undefined ? (opts.valueEncoding ?? ValueEncoding.String) : ValueEncoding.Bytes
116
+
117
+ this.DEBUG = isDebugEnabled('ethjs')
118
+ this.debug = this.DEBUG
119
+ ? (message: string, namespaces: string[] = []) => {
120
+ let logger = this._debug
121
+ for (const namespace of namespaces) {
122
+ logger = logger.extend(namespace)
123
+ }
124
+ logger(message)
125
+ }
126
+ : (..._args: unknown[]) => {}
127
+
128
+ this.database(opts?.db ?? new MapDB<string, Uint8Array>(), valueEncoding)
129
+
130
+ this.EMPTY_TRIE_ROOT = this.hash(RLP_EMPTY_STRING)
131
+ this._hashLen = this.EMPTY_TRIE_ROOT.length
132
+ this._root = this.EMPTY_TRIE_ROOT
133
+
134
+ if (opts?.root) {
135
+ this.root(opts.root)
136
+ }
137
+ this.DEBUG &&
138
+ this.debug(`Trie created:
139
+ || Root: ${bytesToHex(this.root())}
140
+ || Secure: ${this._opts.useKeyHashing}
141
+ || Persistent: ${this._opts.useRootPersistence}
142
+ || Pruning: ${this._opts.useNodePruning}
143
+ || CacheSize: ${this._opts.cacheSize}
144
+ || ----------------`)
145
+ }
146
+
147
+ database(db?: DB<string, string | Uint8Array>, valueEncoding?: ValueEncoding) {
148
+ if (db !== undefined) {
149
+ if (db instanceof CheckpointDB) {
150
+ throw EthereumJSErrorWithoutCode('Cannot pass in an instance of CheckpointDB')
151
+ }
152
+
153
+ this._db = new CheckpointDB({ db, cacheSize: this._opts.cacheSize, valueEncoding })
154
+ }
155
+
156
+ return this._db
157
+ }
158
+
159
+ /**
160
+ * Gets and/or Sets the current root of the `trie`
161
+ */
162
+ root(value?: Uint8Array | null): Uint8Array {
163
+ if (value !== undefined) {
164
+ if (value === null) {
165
+ value = this.EMPTY_TRIE_ROOT
166
+ }
167
+ this.DEBUG && this.debug(`Setting root to ${bytesToHex(value)}`)
168
+ if (value.length !== this._hashLen) {
169
+ throw EthereumJSErrorWithoutCode(
170
+ `Invalid root length. Roots are ${this._hashLen} bytes, got ${value.length} bytes`,
171
+ )
172
+ }
173
+
174
+ this._root = value
175
+ }
176
+ return this._root
177
+ }
178
+
179
+ /**
180
+ * Checks if a given root exists.
181
+ */
182
+ async checkRoot(root: Uint8Array): Promise<boolean> {
183
+ try {
184
+ const value = await this.lookupNode(root)
185
+ return value !== null
186
+ } catch (error: any) {
187
+ if (error.message === 'Missing node in DB') {
188
+ return equalsBytes(root, this.EMPTY_TRIE_ROOT)
189
+ } else {
190
+ throw error
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Gets a value given a `key`
197
+ * @param key - the key to search for
198
+ * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false)
199
+ * @returns A Promise that resolves to `Uint8Array` if a value was found or `null` if no value was found.
200
+ */
201
+ async get(key: Uint8Array, throwIfMissing = false): Promise<Uint8Array | null> {
202
+ this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['get'])
203
+ const { node, remaining } = await this.findPath(this.appliedKey(key), throwIfMissing)
204
+ let value: Uint8Array | null = null
205
+ if (node && remaining.length === 0) {
206
+ value = node.value()
207
+ }
208
+ this.DEBUG && this.debug(`Value: ${value === null ? 'null' : bytesToHex(value)}`, ['get'])
209
+ return value
210
+ }
211
+
212
+ /**
213
+ * Stores a given `value` at the given `key` or do a delete if `value` is empty
214
+ * (delete operations are only executed on DB with `deleteFromDB` set to `true`)
215
+ * @param key
216
+ * @param value
217
+ * @returns A Promise that resolves once value is stored.
218
+ */
219
+ async put(
220
+ key: Uint8Array,
221
+ value: Uint8Array | null,
222
+ skipKeyTransform: boolean = false,
223
+ ): Promise<void> {
224
+ this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['put'])
225
+ this.DEBUG && this.debug(`Value: ${value === null ? 'null' : bytesToHex(value)}`, ['put'])
226
+ if (this._opts.useRootPersistence && equalsBytes(key, ROOT_DB_KEY)) {
227
+ throw EthereumJSErrorWithoutCode(
228
+ `Attempted to set '${bytesToUtf8(ROOT_DB_KEY)}' key but it is not allowed.`,
229
+ )
230
+ }
231
+
232
+ // If value is empty, delete
233
+ if (value === null || value.length === 0) {
234
+ return this.del(key)
235
+ }
236
+
237
+ await this._lock.acquire()
238
+ const appliedKey = skipKeyTransform ? key : this.appliedKey(key)
239
+ if (equalsBytes(this.root(), this.EMPTY_TRIE_ROOT)) {
240
+ await this._createInitialNode(appliedKey, value)
241
+ } else {
242
+ const { remaining, stack } = await this.findPath(appliedKey)
243
+ let ops: BatchDBOp[] = []
244
+ if (this._opts.useNodePruning) {
245
+ const val = await this.get(key)
246
+ // Only delete keys if it either does not exist, or if it gets updated
247
+ // (The update will update the hash of the node, thus we can delete the original leaf node)
248
+ if (val === null || !equalsBytes(val, value)) {
249
+ ops = this._createPruneDeleteOps(stack)
250
+ }
251
+ }
252
+ // then update
253
+ await this._updateNode(appliedKey, value, remaining, stack)
254
+ if (this._opts.useNodePruning) {
255
+ // Only after updating the node we can delete the keyHashes
256
+ await this._db.batch(ops)
257
+ }
258
+ }
259
+ await this.persistRoot()
260
+ this._lock.release()
261
+ }
262
+
263
+ /**
264
+ * Deletes a value given a `key` from the trie
265
+ * (delete operations are only executed on DB with `deleteFromDB` set to `true`)
266
+ * @param key
267
+ * @returns A Promise that resolves once value is deleted.
268
+ */
269
+ async del(key: Uint8Array, skipKeyTransform: boolean = false): Promise<void> {
270
+ this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['del'])
271
+ await this._lock.acquire()
272
+ const appliedKey = skipKeyTransform ? key : this.appliedKey(key)
273
+ const { node, stack } = await this.findPath(appliedKey)
274
+
275
+ let ops: BatchDBOp[] = []
276
+ // Only delete if the `key` currently has any value
277
+ if (this._opts.useNodePruning && node !== null) {
278
+ ops = this._createPruneDeleteOps(stack)
279
+ }
280
+ if (node) {
281
+ await this._deleteNode(appliedKey, stack)
282
+ }
283
+ if (this._opts.useNodePruning) {
284
+ // Only after deleting the node it is possible to delete the keyHashes
285
+ await this._db.batch(ops)
286
+ }
287
+ await this.persistRoot()
288
+ this._lock.release()
289
+ }
290
+
291
+ // ─── Path finding ───────────────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Finds the path from root to the node for the given key.
295
+ * Walks the trie, matching nibbles at each level. Returns the target node (if found)
296
+ * and the stack of nodes along the path (needed for updates/deletes).
297
+ *
298
+ * @param key - the search key (bytes)
299
+ * @param throwIfMissing - if true, throws when nodes are missing (e.g. proof verification)
300
+ * @param partialPath - optional pre-loaded stack for resuming from a mid-path node
301
+ */
302
+ async findPath(
303
+ key: Uint8Array,
304
+ throwIfMissing = false,
305
+ partialPath: {
306
+ stack: MPTNode[]
307
+ } = {
308
+ stack: [],
309
+ },
310
+ ): Promise<Path> {
311
+ const targetKey = bytesToNibbles(key)
312
+ const keyLen = targetKey.length
313
+ const stack: MPTNode[] = Array.from({ length: keyLen })
314
+
315
+ // Pre-fill stack from partialPath when resuming a previous walk
316
+ let pathProgress = 0
317
+ for (let stackIndex = 0; stackIndex < partialPath.stack.length - 1; stackIndex++) {
318
+ stack[stackIndex] = partialPath.stack[stackIndex]
319
+ pathProgress +=
320
+ stack[stackIndex] instanceof BranchMPTNode
321
+ ? 1
322
+ : (stack[stackIndex] as ExtensionMPTNode).keyLength()
323
+ }
324
+
325
+ this.DEBUG && this.debug(`Target (${targetKey.length}): [${targetKey}]`, ['find_path'])
326
+ let result: Path | null = null
327
+
328
+ const onFound: FoundNodeFunction = async (
329
+ _nodeRef,
330
+ node,
331
+ currentKeyNibbles,
332
+ walkController,
333
+ ) => {
334
+ stack[pathProgress] = node as MPTNode
335
+
336
+ if (node instanceof BranchMPTNode) {
337
+ if (pathProgress === keyLen) {
338
+ result = { node, remaining: [], stack }
339
+ } else {
340
+ const branchIndex = targetKey[pathProgress]
341
+ const branchNode = node.getBranch(branchIndex)
342
+ this.DEBUG &&
343
+ this.debug(
344
+ branchNode === null
345
+ ? 'NULL'
346
+ : `Branch ${branchIndex}: ${branchNode instanceof Uint8Array ? bytesToHex(branchNode) : 'raw'}`,
347
+ ['find_path', 'branch_node'],
348
+ )
349
+ if (!branchNode) {
350
+ result = { node: null, remaining: targetKey.slice(pathProgress), stack }
351
+ } else {
352
+ pathProgress++
353
+ walkController.onlyBranchIndex(node, currentKeyNibbles, branchIndex)
354
+ }
355
+ }
356
+ } else if (node instanceof LeafMPTNode) {
357
+ const leafStartProgress = pathProgress
358
+ if (keyLen - pathProgress > node.key().length) {
359
+ result = { node: null, remaining: targetKey.slice(leafStartProgress), stack }
360
+ return
361
+ }
362
+ for (const nibble of node.key()) {
363
+ if (nibble !== targetKey[pathProgress]) {
364
+ result = { node: null, remaining: targetKey.slice(leafStartProgress), stack }
365
+ return
366
+ }
367
+ pathProgress++
368
+ }
369
+ result = { node, remaining: [], stack }
370
+ } else if (node instanceof ExtensionMPTNode) {
371
+ const extensionStartProgress = pathProgress
372
+ this.DEBUG &&
373
+ this.debug(
374
+ `Extension key: [${node.key()}] vs expected [${targetKey.slice(pathProgress, pathProgress + node.key().length)}]`,
375
+ ['find_path', 'extension_node'],
376
+ )
377
+ for (const nibble of node.key()) {
378
+ if (nibble !== targetKey[pathProgress]) {
379
+ result = { node: null, remaining: targetKey.slice(extensionStartProgress), stack }
380
+ return
381
+ }
382
+ pathProgress++
383
+ }
384
+ walkController.allChildren(node, currentKeyNibbles)
385
+ }
386
+ }
387
+ const startingNode = partialPath.stack[partialPath.stack.length - 1]
388
+ const start = startingNode !== undefined ? this.hash(startingNode.serialize()) : this.root()
389
+ try {
390
+ this.DEBUG &&
391
+ this.debug(
392
+ `Walking trie from ${startingNode === undefined ? 'ROOT' : 'NODE'}: ${bytesToHex(start)}`,
393
+ ['find_path'],
394
+ )
395
+ await this.walkTrie(start, onFound)
396
+ } catch (error: any) {
397
+ if (error.message !== 'Missing node in DB' || throwIfMissing) {
398
+ throw error
399
+ }
400
+ }
401
+
402
+ if (result === null) {
403
+ result = { node: null, remaining: [], stack }
404
+ }
405
+ this.DEBUG &&
406
+ this.debug(
407
+ result.node !== null
408
+ ? `Target Node FOUND for ${bytesToNibbles(key)}`
409
+ : `Target Node NOT FOUND`,
410
+ ['find_path'],
411
+ )
412
+
413
+ result.stack = result.stack.filter((stackEntry) => stackEntry !== undefined)
414
+ this.DEBUG &&
415
+ this.debug(
416
+ `Result:
417
+ || Node: ${result.node === null ? 'null' : result.node.constructor.name}
418
+ || Remaining: [${result.remaining}]\n|| Stack: ${result.stack
419
+ .map((stackEntry) => stackEntry.constructor.name)
420
+ .join(', ')}`,
421
+ ['find_path'],
422
+ )
423
+ return result
424
+ }
425
+
426
+ /**
427
+ * Walks a trie until finished.
428
+ * @param root
429
+ * @param onFound - callback to call when a node is found. This schedules new tasks. If no tasks are available, the Promise resolves.
430
+ * @returns Resolves when finished walking trie.
431
+ */
432
+ async walkTrie(root: Uint8Array, onFound: FoundNodeFunction): Promise<void> {
433
+ await WalkController.newWalk(onFound, this, root)
434
+ }
435
+
436
+ walkTrieIterable = _walkTrie.bind(this)
437
+
438
+ /**
439
+ * Executes a callback for each node in the trie.
440
+ * @param onFound - callback to call when a node is found.
441
+ * @returns Resolves when finished walking trie.
442
+ */
443
+ async walkAllNodes(onFound: OnFound): Promise<void> {
444
+ for await (const { node, currentKey } of this.walkTrieIterable(this.root())) {
445
+ await onFound(node, currentKey)
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Executes a callback for each value node in the trie.
451
+ * @param onFound - callback to call when a node is found.
452
+ * @returns Resolves when finished walking trie.
453
+ */
454
+ async walkAllValueNodes(onFound: OnFound): Promise<void> {
455
+ for await (const { node, currentKey } of this.walkTrieIterable(
456
+ this.root(),
457
+ [],
458
+ undefined,
459
+ async (node) => {
460
+ return (
461
+ node instanceof LeafMPTNode || (node instanceof BranchMPTNode && node.value() !== null)
462
+ )
463
+ },
464
+ )) {
465
+ await onFound(node, currentKey)
466
+ }
467
+ }
468
+
469
+ // ─── Node persistence (internal) ─────────────────────────────────────────────
470
+
471
+ /**
472
+ * Creates the initial leaf node when inserting into an empty trie.
473
+ * @private
474
+ */
475
+ protected async _createInitialNode(key: Uint8Array, value: Uint8Array): Promise<void> {
476
+ const newNode = new LeafMPTNode(bytesToNibbles(key), value)
477
+
478
+ const encoded = newNode.serialize()
479
+ this.root(this.hash(encoded))
480
+ await this._db.put(this._getDbKey(this.root()), encoded)
481
+ await this.persistRoot()
482
+ }
483
+
484
+ /**
485
+ * Retrieves a node from db by hash.
486
+ */
487
+ async lookupNode(node: Uint8Array | Uint8Array[]): Promise<MPTNode> {
488
+ if (isRawMPTNode(node)) {
489
+ const decoded = decodeRawMPTNode(node)
490
+ this.DEBUG && this.debug(`${decoded.constructor.name}`, ['lookup_node', 'raw_node'])
491
+ return decoded
492
+ }
493
+ this.DEBUG && this.debug(`${bytesToHex(node)}`, ['lookup_node', 'by_hash'])
494
+ const value = (await this._db.get(this._getDbKey(node))) ?? null
495
+
496
+ if (value === null) {
497
+ // Dev note: this error message text is used for error checking in `checkRoot`, `verifyMPTWithMerkleProof`, and `findPath`
498
+ throw EthereumJSErrorWithoutCode('Missing node in DB')
499
+ }
500
+
501
+ const decoded = decodeMPTNode(value)
502
+ this.DEBUG && this.debug(`${decoded.constructor.name} found in DB`, ['lookup_node', 'by_hash'])
503
+ return decoded
504
+ }
505
+
506
+ /**
507
+ * True when we're updating an existing leaf value (key already exists, no structural change).
508
+ * @private
509
+ */
510
+ protected _isMatchingLeafUpdate(
511
+ lastNode: MPTNode,
512
+ stack: MPTNode[],
513
+ fullKeyNibbles: Nibbles,
514
+ keyRemainder: Nibbles,
515
+ ): boolean {
516
+ if (!(lastNode instanceof LeafMPTNode) || keyRemainder.length !== 0) {
517
+ return false
518
+ }
519
+ let keyOffset = 0
520
+ for (const stackNode of stack) {
521
+ keyOffset += stackNode instanceof BranchMPTNode ? 1 : stackNode.key().length
522
+ }
523
+ return (
524
+ matchingNibbleLength(lastNode.key(), fullKeyNibbles.slice(keyOffset)) ===
525
+ lastNode.key().length
526
+ )
527
+ }
528
+
529
+ /**
530
+ * Applies a value update given the path from findPath. Modifies the stack in-place
531
+ * to represent the new structure, then calls saveStack to persist.
532
+ *
533
+ * Three cases:
534
+ * 1. Match leaf: key exists, just update value (no structure change)
535
+ * 2. Branch: add new leaf to branch, or set branch value
536
+ * 3. Extension/Leaf with diverging path: create new branch at divergence, re-hang old + new leaf
537
+ *
538
+ * @private
539
+ */
540
+ protected async _updateNode(
541
+ keyBytes: Uint8Array,
542
+ value: Uint8Array,
543
+ keyRemainder: Nibbles,
544
+ stack: MPTNode[],
545
+ ): Promise<void> {
546
+ const opStack: BatchDBOp[] = []
547
+ const lastNode = stack.pop()
548
+ if (!lastNode) {
549
+ throw EthereumJSErrorWithoutCode('Stack underflow')
550
+ }
551
+
552
+ const fullKeyNibbles = bytesToNibbles(keyBytes)
553
+ const matchLeaf = this._isMatchingLeafUpdate(lastNode, stack, fullKeyNibbles, keyRemainder)
554
+
555
+ if (matchLeaf) {
556
+ // Case 1: Key already exists at this leaf – update value in place
557
+ lastNode.value(value)
558
+ stack.push(lastNode)
559
+ } else if (lastNode instanceof BranchMPTNode) {
560
+ // Case 2: Insert into branch – either new leaf on empty slot or set branch value
561
+ stack.push(lastNode)
562
+ if (keyRemainder.length !== 0) {
563
+ keyRemainder.shift()
564
+ stack.push(new LeafMPTNode(keyRemainder, value))
565
+ } else {
566
+ lastNode.value(value)
567
+ }
568
+ } else {
569
+ // Case 3: Last node is Extension or Leaf – path diverges. Create branch at divergence point
570
+ const lastKey = lastNode.key()
571
+ const matchingLength = matchingNibbleLength(lastKey, keyRemainder)
572
+ const newBranchMPTNode = new BranchMPTNode()
573
+
574
+ if (matchingLength !== 0) {
575
+ const newExtNode = new ExtensionMPTNode(lastNode.key().slice(0, matchingLength), value)
576
+ stack.push(newExtNode)
577
+ lastKey.splice(0, matchingLength)
578
+ keyRemainder.splice(0, matchingLength)
579
+ }
580
+
581
+ stack.push(newBranchMPTNode)
582
+
583
+ if (lastKey.length !== 0) {
584
+ const branchKey = lastKey.shift()!
585
+ if (lastKey.length !== 0 || lastNode instanceof LeafMPTNode) {
586
+ lastNode.key(lastKey)
587
+ const formattedNode = this._formatNode(
588
+ lastNode,
589
+ false,
590
+ opStack,
591
+ ) as NodeReferenceOrRawMPTNode
592
+ newBranchMPTNode.setBranch(branchKey, formattedNode)
593
+ } else {
594
+ this._formatNode(lastNode, false, opStack, true)
595
+ newBranchMPTNode.setBranch(branchKey, lastNode.value())
596
+ }
597
+ } else {
598
+ newBranchMPTNode.value(lastNode.value())
599
+ }
600
+
601
+ if (keyRemainder.length !== 0) {
602
+ keyRemainder.shift()
603
+ stack.push(new LeafMPTNode(keyRemainder, value))
604
+ } else {
605
+ newBranchMPTNode.value(value)
606
+ }
607
+ }
608
+
609
+ await this.saveStack(fullKeyNibbles, stack, opStack)
610
+ }
611
+
612
+ /**
613
+ * Removes a key from the trie. Handles two main cases:
614
+ * - Deleting from a leaf: remove leaf, possibly collapse parent branch
615
+ * - Deleting from a branch value: clear value, possibly collapse if branch has single child
616
+ *
617
+ * When a branch ends up with only one child after deletion, we collapse it into
618
+ * an extension (or merge with parent extension) to keep the trie minimal.
619
+ *
620
+ * @private
621
+ */
622
+ protected async _deleteNode(keyBytes: Uint8Array, stack: MPTNode[]): Promise<void> {
623
+ /**
624
+ * Collapses a branch with one child into a simpler structure.
625
+ * Parent can be: Branch (branch→branch), Extension (ext→branch), or null (branch is root).
626
+ */
627
+ const collapseBranchWithOneChild = (
628
+ pathNibbles: Nibbles,
629
+ branchIndex: number,
630
+ childNode: MPTNode,
631
+ parentNode: MPTNode | null | undefined,
632
+ nodeStack: MPTNode[],
633
+ ): Nibbles => {
634
+ const parentIsBranchOrRoot =
635
+ parentNode === null || parentNode === undefined || parentNode instanceof BranchMPTNode
636
+
637
+ if (parentIsBranchOrRoot) {
638
+ if (parentNode instanceof BranchMPTNode) nodeStack.push(parentNode)
639
+ if (childNode instanceof BranchMPTNode) {
640
+ const extensionNode = new ExtensionMPTNode([branchIndex], new Uint8Array())
641
+ nodeStack.push(extensionNode)
642
+ pathNibbles.push(branchIndex)
643
+ } else {
644
+ const childNodeKey = childNode.key()
645
+ childNodeKey.unshift(branchIndex)
646
+ childNode.key(childNodeKey.slice(0))
647
+ nodeStack.push(childNode)
648
+ return pathNibbles.concat(childNodeKey)
649
+ }
650
+ nodeStack.push(childNode)
651
+ return pathNibbles
652
+ }
653
+
654
+ if (!(parentNode instanceof ExtensionMPTNode)) {
655
+ throw EthereumJSErrorWithoutCode('Expected extension node')
656
+ }
657
+ const parentKey = parentNode.key()
658
+ if (childNode instanceof BranchMPTNode) {
659
+ parentKey.push(branchIndex)
660
+ pathNibbles.push(branchIndex)
661
+ parentNode.key(parentKey)
662
+ nodeStack.push(parentNode)
663
+ } else {
664
+ const childNodeKey = childNode.key()
665
+ childNodeKey.unshift(branchIndex)
666
+ const fullPath = parentKey.concat(childNodeKey)
667
+ childNode.key(fullPath)
668
+ nodeStack.push(childNode)
669
+ return pathNibbles.concat(childNodeKey)
670
+ }
671
+ nodeStack.push(childNode)
672
+ return pathNibbles
673
+ }
674
+
675
+ let lastNode = stack.pop()
676
+ if (lastNode === undefined) throw EthereumJSErrorWithoutCode('missing last node')
677
+ let parentNode = stack.pop()
678
+ const opStack: BatchDBOp[] = []
679
+ let pathNibbles = bytesToNibbles(keyBytes)
680
+
681
+ if (parentNode === undefined) {
682
+ this.root(this.EMPTY_TRIE_ROOT)
683
+ return
684
+ }
685
+
686
+ if (lastNode instanceof BranchMPTNode) {
687
+ lastNode.value(null)
688
+ } else {
689
+ // Deleting a leaf: remove it from parent branch, then consider collapsing
690
+ if (!(parentNode instanceof BranchMPTNode)) {
691
+ throw EthereumJSErrorWithoutCode('Expected branch node')
692
+ }
693
+ const lastNodeKey = lastNode.key()
694
+ pathNibbles.splice(pathNibbles.length - lastNodeKey.length)
695
+ this._formatNode(lastNode, false, opStack, true)
696
+ parentNode.setBranch(pathNibbles.pop()!, null)
697
+ lastNode = parentNode
698
+ parentNode = stack.pop()
699
+ }
700
+
701
+ const branchNodes: [number, NodeReferenceOrRawMPTNode][] = lastNode.getChildren()
702
+
703
+ if (branchNodes.length === 1) {
704
+ // add the one remaining branch node to node above it
705
+ const branchNode = branchNodes[0][1]
706
+ const branchNodeKey = branchNodes[0][0]
707
+
708
+ // Special case where one needs to delete an extra node:
709
+ // In this case, after updating the branch, the branch node has just one branch left
710
+ // However, this violates the trie spec; this should be converted in either an ExtensionMPTNode
711
+ // Or a LeafMPTNode
712
+ // Since this branch is deleted, one can thus also delete this branch from the DB
713
+ // So add this to the `opStack` and mark the keyHash to be deleted
714
+ if (this._opts.useNodePruning) {
715
+ // If the branchNode has length < 32, it will be a RawNode (Uint8Array[]) instead of a Uint8Array
716
+ // In that case, we need to serialize and hash it into a Uint8Array, otherwise the operation will throw
717
+ const hashToDelete = isRawMPTNode(branchNode)
718
+ ? this.hash(RLP.encode(branchNode))
719
+ : branchNode
720
+ opStack.push({ type: 'del', key: this._getDbKey(hashToDelete) })
721
+ }
722
+
723
+ // look up node
724
+ const foundNode = await this.lookupNode(branchNode)
725
+ pathNibbles = collapseBranchWithOneChild(
726
+ pathNibbles,
727
+ branchNodeKey,
728
+ foundNode,
729
+ parentNode as MPTNode,
730
+ stack,
731
+ )
732
+ await this.saveStack(pathNibbles, stack, opStack)
733
+ } else {
734
+ // Branch has multiple children: just persist the updated branch
735
+ if (parentNode) {
736
+ stack.push(parentNode)
737
+ }
738
+
739
+ stack.push(lastNode)
740
+ await this.saveStack(pathNibbles, stack, opStack)
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Persists the modified node stack to the DB. Processes nodes from leaf toward root,
746
+ * wiring each node's references (extension value, branch slot) to its child's hash.
747
+ *
748
+ * @param key - nibble path that corresponds to the stack
749
+ * @param stack - nodes from findPath/update, bottom (leaf) to top (root)
750
+ * @param opStack - put/del operations accumulated by _formatNode
751
+ */
752
+ async saveStack(pathNibbles: Nibbles, stack: MPTNode[], opStack: BatchDBOp[]): Promise<void> {
753
+ let childHash: Uint8Array | undefined
754
+
755
+ while (stack.length > 0) {
756
+ const node = stack.pop()
757
+ if (node === undefined) {
758
+ throw EthereumJSErrorWithoutCode('saveStack: missing node')
759
+ }
760
+ if (node instanceof LeafMPTNode || node instanceof ExtensionMPTNode) {
761
+ pathNibbles.splice(pathNibbles.length - node.key().length)
762
+ }
763
+ if (node instanceof ExtensionMPTNode && childHash !== undefined) {
764
+ node.value(childHash)
765
+ }
766
+ if (node instanceof BranchMPTNode && childHash !== undefined) {
767
+ const branchIndex = pathNibbles.pop()
768
+ node.setBranch(branchIndex!, childHash)
769
+ }
770
+ childHash = this._formatNode(node, stack.length === 0, opStack) as Uint8Array
771
+ }
772
+
773
+ if (childHash !== undefined) {
774
+ this.root(childHash)
775
+ }
776
+ await this._db.batch(opStack)
777
+ await this.persistRoot()
778
+ }
779
+
780
+ /**
781
+ * Serializes a node and either stores it (put) or schedules removal (del).
782
+ * Nodes ≥32 bytes (or top-level) are hashed and stored; smaller nodes are inlined as raw.
783
+ *
784
+ * @param node - the node to persist
785
+ * @param topLevel - if true, always store (root must be in DB)
786
+ * @param opStack - accumulates put/del operations for batch commit
787
+ * @param remove - if true, schedule del (used when pruning)
788
+ * @returns hash (for references) or raw encoding (for inline)
789
+ */
790
+ _formatNode(
791
+ node: MPTNode,
792
+ topLevel: boolean,
793
+ opStack: BatchDBOp[],
794
+ remove: boolean = false,
795
+ ): Uint8Array | NodeReferenceOrRawMPTNode | BranchMPTNodeBranchValue[] {
796
+ const encoded = node.serialize()
797
+
798
+ if (encoded.length >= 32 || topLevel) {
799
+ const nodeHash = this.hash(encoded)
800
+ const dbKey = this._getDbKey(nodeHash)
801
+
802
+ if (remove) {
803
+ if (this._opts.useNodePruning) {
804
+ opStack.push({ type: 'del', key: dbKey })
805
+ }
806
+ } else {
807
+ opStack.push({ type: 'put', key: dbKey, value: encoded })
808
+ }
809
+ return nodeHash
810
+ }
811
+
812
+ return node.raw()
813
+ }
814
+
815
+ /**
816
+ * The given hash of operations (key additions or deletions) are executed on the trie
817
+ * (delete operations are only executed on DB with `deleteFromDB` set to `true`)
818
+ * @example
819
+ * const ops = [
820
+ * { type: 'del', key: Uint8Array.from('father') }
821
+ * , { type: 'put', key: Uint8Array.from('name'), value: Uint8Array.from('Yuri Irsenovich Kim') } // cspell:disable-line
822
+ * , { type: 'put', key: Uint8Array.from('dob'), value: Uint8Array.from('16 February 1941') }
823
+ * , { type: 'put', key: Uint8Array.from('spouse'), value: Uint8Array.from('Kim Young-sook') } // cspell:disable-line
824
+ * , { type: 'put', key: Uint8Array.from('occupation'), value: Uint8Array.from('Clown') }
825
+ * ]
826
+ * await trie.batch(ops)
827
+ * @param ops
828
+ */
829
+ async batch(ops: BatchDBOp[], skipKeyTransform?: boolean): Promise<void> {
830
+ for (const op of ops) {
831
+ if (op.type === 'put') {
832
+ if (op.value === null || op.value === undefined) {
833
+ throw EthereumJSErrorWithoutCode('Invalid batch db operation')
834
+ }
835
+ await this.put(op.key, op.value, skipKeyTransform)
836
+ } else if (op.type === 'del') {
837
+ await this.del(op.key, skipKeyTransform)
838
+ }
839
+ }
840
+ await this.persistRoot()
841
+ }
842
+
843
+ /**
844
+ * Verifies that every key in the DB is reachable from the root. Used to ensure
845
+ * pruning is correct – unreachable keys indicate a bug or corrupt state.
846
+ */
847
+ async verifyPrunedIntegrity(): Promise<boolean> {
848
+ // Using deprecated bytesToUnprefixedHex for performance: used for string comparisons with database keys.
849
+ const roots = [
850
+ bytesToUnprefixedHex(this.root()),
851
+ bytesToUnprefixedHex(this.appliedKey(ROOT_DB_KEY)),
852
+ ]
853
+ for (const dbKeyHex of (this._db.db as any)._database.keys()) {
854
+ if (roots.includes(dbKeyHex)) {
855
+ // The root key can never be found from the trie, otherwise this would
856
+ // convert the tree from a directed acyclic graph to a directed cycling graph
857
+ continue
858
+ }
859
+
860
+ // Track if key is found
861
+ let found = false
862
+ try {
863
+ await this.walkTrie(this.root(), async (_, node, currentKeyNibbles, walkController) => {
864
+ if (found) return
865
+ if (node instanceof BranchMPTNode) {
866
+ for (const branchRef of node._branches) {
867
+ if (
868
+ branchRef !== null &&
869
+ bytesToUnprefixedHex(
870
+ isRawMPTNode(branchRef)
871
+ ? walkController.trie.appliedKey(RLP.encode(branchRef))
872
+ : branchRef,
873
+ ) === dbKeyHex
874
+ ) {
875
+ found = true
876
+ return
877
+ }
878
+ }
879
+ walkController.allChildren(node, currentKeyNibbles)
880
+ }
881
+ if (node instanceof ExtensionMPTNode) {
882
+ if (bytesToUnprefixedHex(node.value()) === dbKeyHex) {
883
+ found = true
884
+ return
885
+ }
886
+ walkController.allChildren(node, currentKeyNibbles)
887
+ }
888
+ })
889
+ } catch {
890
+ return false
891
+ }
892
+ if (!found) {
893
+ return false
894
+ }
895
+ }
896
+ return true
897
+ }
898
+
899
+ /**
900
+ * Returns a copy of the underlying trie.
901
+ *
902
+ * Note on db: the copy will create a reference to the
903
+ * same underlying database.
904
+ *
905
+ * Note on cache: for memory reasons a copy will by default
906
+ * not recreate a new LRU cache but initialize with cache
907
+ * being deactivated. This behavior can be overwritten by
908
+ * explicitly setting `cacheSize` as an option on the method.
909
+ *
910
+ * @param includeCheckpoints - If true and during a checkpoint, the copy will contain the checkpointing metadata and will use the same scratch as underlying db.
911
+ */
912
+ shallowCopy(includeCheckpoints = true, opts?: TrieShallowCopyOpts): MerklePatriciaTrie {
913
+ const trie = new MerklePatriciaTrie({
914
+ ...this._opts,
915
+ db: this._db.db.shallowCopy(),
916
+ root: this.root(),
917
+ cacheSize: 0,
918
+ ...(opts ?? {}),
919
+ })
920
+ if (includeCheckpoints && this.hasCheckpoints()) {
921
+ trie._db.setCheckpoints(this._db.checkpoints)
922
+ }
923
+ return trie
924
+ }
925
+
926
+ /**
927
+ * Persists the root hash in the underlying database
928
+ */
929
+ async persistRoot() {
930
+ if (this._opts.useRootPersistence) {
931
+ this.DEBUG &&
932
+ this.debug(
933
+ `Persisting root: \n|| RootHash: ${bytesToHex(this.root())}\n|| RootKey: ${bytesToHex(
934
+ this.appliedKey(ROOT_DB_KEY),
935
+ )}`,
936
+ ['persist_root'],
937
+ )
938
+ await this._db.put(this._getDbKey(this.appliedKey(ROOT_DB_KEY)), this.root())
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Finds all nodes that are stored directly in the db
944
+ * (some nodes are stored raw inside other nodes)
945
+ * called by {@link ScratchReadStream}
946
+ * @private
947
+ */
948
+ protected async _findDbNodes(onFound: FoundNodeFunction): Promise<void> {
949
+ const outerOnFound: FoundNodeFunction = async (nodeRef, node, key, walkController) => {
950
+ if (isRawMPTNode(nodeRef)) {
951
+ if (node !== null) {
952
+ walkController.allChildren(node, key)
953
+ }
954
+ } else {
955
+ onFound(nodeRef, node, key, walkController)
956
+ }
957
+ }
958
+ await this.walkTrie(this.root(), outerOnFound)
959
+ }
960
+
961
+ // ─── DB helpers ─────────────────────────────────────────────────────────────
962
+
963
+ /** Applies keyPrefix to a hash when multiple tries share a DB. */
964
+ protected _getDbKey(hash: Uint8Array): Uint8Array {
965
+ return this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, hash) : hash
966
+ }
967
+
968
+ /** Builds del ops for nodes that will be replaced (pruning). */
969
+ protected _createPruneDeleteOps(stack: MPTNode[]): BatchDBOp[] {
970
+ return stack.map((node) => {
971
+ const deletedHash = this.hash(node.serialize())
972
+ return {
973
+ type: 'del' as const,
974
+ key: this._getDbKey(deletedHash),
975
+ opts: { keyEncoding: KeyEncoding.Bytes },
976
+ }
977
+ })
978
+ }
979
+
980
+ /** Applies key hashing (keccak) when useKeyHashing is enabled (Ethereum-style). */
981
+ protected appliedKey(key: Uint8Array) {
982
+ if (this._opts.useKeyHashing) {
983
+ return this.hash(key)
984
+ }
985
+ return key
986
+ }
987
+
988
+ protected hash(inputBytes: Uint8Array): Uint8Array {
989
+ return Uint8Array.from(this._opts.useKeyHashingFunction.call(undefined, inputBytes))
990
+ }
991
+
992
+ /**
993
+ * Is the trie during a checkpoint phase?
994
+ */
995
+ hasCheckpoints() {
996
+ return this._db.hasCheckpoints()
997
+ }
998
+
999
+ /**
1000
+ * Creates a checkpoint that can later be reverted to or committed.
1001
+ * After this is called, all changes can be reverted until `commit` is called.
1002
+ */
1003
+ checkpoint() {
1004
+ this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['checkpoint'])
1005
+ this._db.checkpoint(this.root())
1006
+ }
1007
+
1008
+ /**
1009
+ * Commits a checkpoint to disk, if current checkpoint is not nested.
1010
+ * If nested, only sets the parent checkpoint as current checkpoint.
1011
+ * @throws If not during a checkpoint phase
1012
+ */
1013
+ async commit(): Promise<void> {
1014
+ if (!this.hasCheckpoints()) {
1015
+ throw EthereumJSErrorWithoutCode('trying to commit when not checkpointed')
1016
+ }
1017
+ this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['commit'])
1018
+ await this._lock.acquire()
1019
+ await this._db.commit()
1020
+ await this.persistRoot()
1021
+ this._lock.release()
1022
+ }
1023
+
1024
+ /**
1025
+ * Reverts the trie to the state it was at when `checkpoint` was first called.
1026
+ * If during a nested checkpoint, sets root to most recent checkpoint, and sets
1027
+ * parent checkpoint as current.
1028
+ */
1029
+ async revert(): Promise<void> {
1030
+ if (!this.hasCheckpoints()) {
1031
+ throw EthereumJSErrorWithoutCode('trying to revert when not checkpointed')
1032
+ }
1033
+
1034
+ this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['revert', 'before'])
1035
+ await this._lock.acquire()
1036
+ this.root(await this._db.revert())
1037
+ await this.persistRoot()
1038
+ this._lock.release()
1039
+ this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['revert', 'after'])
1040
+ }
1041
+
1042
+ /**
1043
+ * Flushes all checkpoints, restoring the initial checkpoint state.
1044
+ */
1045
+ flushCheckpoints() {
1046
+ this.DEBUG &&
1047
+ this.debug(`Deleting ${this._db.checkpoints.length} checkpoints.`, ['flush_checkpoints'])
1048
+ this._db.checkpoints = []
1049
+ }
1050
+
1051
+ /**
1052
+ * Returns a list of values stored in the trie
1053
+ * @param startKey first unhashed key in the range to be returned (defaults to 0). Note, all keys must be of the same length or undefined behavior will result
1054
+ * @param limit - the number of keys to be returned (undefined means all keys)
1055
+ * @returns an object with two properties (a map of all key/value pairs in the trie - or in the specified range) and then a `nextKey` reference if a range is specified
1056
+ */
1057
+ async getValueMap(
1058
+ startKey = BIGINT_0,
1059
+ limit?: number,
1060
+ ): Promise<{ values: { [key: string]: string }; nextKey: null | string }> {
1061
+ let inRange = limit !== undefined ? false : true
1062
+ let valueCount = 0
1063
+ const values: { [key: string]: string } = {}
1064
+ let nextKey: string | null = null
1065
+ await this.walkAllValueNodes(async (node: MPTNode, currentKey: number[]) => {
1066
+ if (node instanceof LeafMPTNode) {
1067
+ const keyBytes = nibblesTypeToPackedBytes(currentKey.concat(node.key()))
1068
+ if (!inRange) {
1069
+ // Check if the key is already in the correct range.
1070
+ if (bytesToBigInt(keyBytes) >= startKey) {
1071
+ inRange = true
1072
+ } else {
1073
+ return
1074
+ }
1075
+ }
1076
+
1077
+ if (limit === undefined || valueCount < limit) {
1078
+ values[bytesToHex(keyBytes)] = bytesToHex(node._value)
1079
+ valueCount++
1080
+ } else if (valueCount === limit) {
1081
+ nextKey = bytesToHex(keyBytes)
1082
+ }
1083
+ }
1084
+ })
1085
+ return {
1086
+ values,
1087
+ nextKey,
1088
+ }
1089
+ }
1090
+ }