@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.
- package/README.md +448 -0
- package/dist/cjs/constructors.d.ts +12 -0
- package/dist/cjs/constructors.d.ts.map +1 -0
- package/dist/cjs/constructors.js +57 -0
- package/dist/cjs/constructors.js.map +1 -0
- package/dist/cjs/db/checkpointDB.d.ts +87 -0
- package/dist/cjs/db/checkpointDB.d.ts.map +1 -0
- package/dist/cjs/db/checkpointDB.js +258 -0
- package/dist/cjs/db/checkpointDB.js.map +1 -0
- package/dist/cjs/db/index.d.ts +2 -0
- package/dist/cjs/db/index.d.ts.map +1 -0
- package/dist/cjs/db/index.js +18 -0
- package/dist/cjs/db/index.js.map +1 -0
- package/dist/cjs/index.d.ts +8 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mpt.d.ts +261 -0
- package/dist/cjs/mpt.d.ts.map +1 -0
- package/dist/cjs/mpt.js +900 -0
- package/dist/cjs/mpt.js.map +1 -0
- package/dist/cjs/node/branch.d.ts +14 -0
- package/dist/cjs/node/branch.d.ts.map +1 -0
- package/dist/cjs/node/branch.js +52 -0
- package/dist/cjs/node/branch.js.map +1 -0
- package/dist/cjs/node/extension.d.ts +7 -0
- package/dist/cjs/node/extension.d.ts.map +1 -0
- package/dist/cjs/node/extension.js +14 -0
- package/dist/cjs/node/extension.js.map +1 -0
- package/dist/cjs/node/extensionOrLeafNodeBase.d.ts +15 -0
- package/dist/cjs/node/extensionOrLeafNodeBase.d.ts.map +1 -0
- package/dist/cjs/node/extensionOrLeafNodeBase.js +42 -0
- package/dist/cjs/node/extensionOrLeafNodeBase.js.map +1 -0
- package/dist/cjs/node/index.d.ts +5 -0
- package/dist/cjs/node/index.d.ts.map +1 -0
- package/dist/cjs/node/index.js +21 -0
- package/dist/cjs/node/index.js.map +1 -0
- package/dist/cjs/node/leaf.d.ts +7 -0
- package/dist/cjs/node/leaf.d.ts.map +1 -0
- package/dist/cjs/node/leaf.js +14 -0
- package/dist/cjs/node/leaf.js.map +1 -0
- package/dist/cjs/node/util.d.ts +8 -0
- package/dist/cjs/node/util.d.ts.map +1 -0
- package/dist/cjs/node/util.js +38 -0
- package/dist/cjs/node/util.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proof/index.d.ts +3 -0
- package/dist/cjs/proof/index.d.ts.map +1 -0
- package/dist/cjs/proof/index.js +19 -0
- package/dist/cjs/proof/index.js.map +1 -0
- package/dist/cjs/proof/proof.d.ts +41 -0
- package/dist/cjs/proof/proof.d.ts.map +1 -0
- package/dist/cjs/proof/proof.js +119 -0
- package/dist/cjs/proof/proof.js.map +1 -0
- package/dist/cjs/proof/range.d.ts +35 -0
- package/dist/cjs/proof/range.d.ts.map +1 -0
- package/dist/cjs/proof/range.js +456 -0
- package/dist/cjs/proof/range.js.map +1 -0
- package/dist/cjs/types.d.ts +110 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/util/asyncWalk.d.ts +20 -0
- package/dist/cjs/util/asyncWalk.d.ts.map +1 -0
- package/dist/cjs/util/asyncWalk.js +50 -0
- package/dist/cjs/util/asyncWalk.js.map +1 -0
- package/dist/cjs/util/encoding.d.ts +31 -0
- package/dist/cjs/util/encoding.d.ts.map +1 -0
- package/dist/cjs/util/encoding.js +200 -0
- package/dist/cjs/util/encoding.js.map +1 -0
- package/dist/cjs/util/genesisState.d.ts +6 -0
- package/dist/cjs/util/genesisState.d.ts.map +1 -0
- package/dist/cjs/util/genesisState.js +45 -0
- package/dist/cjs/util/genesisState.js.map +1 -0
- package/dist/cjs/util/hex.d.ts +20 -0
- package/dist/cjs/util/hex.d.ts.map +1 -0
- package/dist/cjs/util/hex.js +48 -0
- package/dist/cjs/util/hex.js.map +1 -0
- package/dist/cjs/util/index.d.ts +4 -0
- package/dist/cjs/util/index.d.ts.map +1 -0
- package/dist/cjs/util/index.js +20 -0
- package/dist/cjs/util/index.js.map +1 -0
- package/dist/cjs/util/nibbles.d.ts +30 -0
- package/dist/cjs/util/nibbles.d.ts.map +1 -0
- package/dist/cjs/util/nibbles.js +79 -0
- package/dist/cjs/util/nibbles.js.map +1 -0
- package/dist/cjs/util/walkController.d.ts +72 -0
- package/dist/cjs/util/walkController.d.ts.map +1 -0
- package/dist/cjs/util/walkController.js +138 -0
- package/dist/cjs/util/walkController.js.map +1 -0
- package/dist/esm/constructors.d.ts +12 -0
- package/dist/esm/constructors.d.ts.map +1 -0
- package/dist/esm/constructors.js +53 -0
- package/dist/esm/constructors.js.map +1 -0
- package/dist/esm/db/checkpointDB.d.ts +87 -0
- package/dist/esm/db/checkpointDB.d.ts.map +1 -0
- package/dist/esm/db/checkpointDB.js +254 -0
- package/dist/esm/db/checkpointDB.js.map +1 -0
- package/dist/esm/db/index.d.ts +2 -0
- package/dist/esm/db/index.d.ts.map +1 -0
- package/dist/esm/db/index.js +2 -0
- package/dist/esm/db/index.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mpt.d.ts +261 -0
- package/dist/esm/mpt.d.ts.map +1 -0
- package/dist/esm/mpt.js +897 -0
- package/dist/esm/mpt.js.map +1 -0
- package/dist/esm/node/branch.d.ts +14 -0
- package/dist/esm/node/branch.d.ts.map +1 -0
- package/dist/esm/node/branch.js +48 -0
- package/dist/esm/node/branch.js.map +1 -0
- package/dist/esm/node/extension.d.ts +7 -0
- package/dist/esm/node/extension.d.ts.map +1 -0
- package/dist/esm/node/extension.js +10 -0
- package/dist/esm/node/extension.js.map +1 -0
- package/dist/esm/node/extensionOrLeafNodeBase.d.ts +15 -0
- package/dist/esm/node/extensionOrLeafNodeBase.d.ts.map +1 -0
- package/dist/esm/node/extensionOrLeafNodeBase.js +38 -0
- package/dist/esm/node/extensionOrLeafNodeBase.js.map +1 -0
- package/dist/esm/node/index.d.ts +5 -0
- package/dist/esm/node/index.d.ts.map +1 -0
- package/dist/esm/node/index.js +5 -0
- package/dist/esm/node/index.js.map +1 -0
- package/dist/esm/node/leaf.d.ts +7 -0
- package/dist/esm/node/leaf.d.ts.map +1 -0
- package/dist/esm/node/leaf.js +10 -0
- package/dist/esm/node/leaf.js.map +1 -0
- package/dist/esm/node/util.d.ts +8 -0
- package/dist/esm/node/util.d.ts.map +1 -0
- package/dist/esm/node/util.js +33 -0
- package/dist/esm/node/util.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proof/index.d.ts +3 -0
- package/dist/esm/proof/index.d.ts.map +1 -0
- package/dist/esm/proof/index.js +3 -0
- package/dist/esm/proof/index.js.map +1 -0
- package/dist/esm/proof/proof.d.ts +41 -0
- package/dist/esm/proof/proof.d.ts.map +1 -0
- package/dist/esm/proof/proof.js +113 -0
- package/dist/esm/proof/proof.js.map +1 -0
- package/dist/esm/proof/range.d.ts +35 -0
- package/dist/esm/proof/range.d.ts.map +1 -0
- package/dist/esm/proof/range.js +453 -0
- package/dist/esm/proof/range.js.map +1 -0
- package/dist/esm/types.d.ts +110 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +3 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util/asyncWalk.d.ts +20 -0
- package/dist/esm/util/asyncWalk.d.ts.map +1 -0
- package/dist/esm/util/asyncWalk.js +47 -0
- package/dist/esm/util/asyncWalk.js.map +1 -0
- package/dist/esm/util/encoding.d.ts +31 -0
- package/dist/esm/util/encoding.d.ts.map +1 -0
- package/dist/esm/util/encoding.js +188 -0
- package/dist/esm/util/encoding.js.map +1 -0
- package/dist/esm/util/genesisState.d.ts +6 -0
- package/dist/esm/util/genesisState.d.ts.map +1 -0
- package/dist/esm/util/genesisState.js +42 -0
- package/dist/esm/util/genesisState.js.map +1 -0
- package/dist/esm/util/hex.d.ts +20 -0
- package/dist/esm/util/hex.d.ts.map +1 -0
- package/dist/esm/util/hex.js +43 -0
- package/dist/esm/util/hex.js.map +1 -0
- package/dist/esm/util/index.d.ts +4 -0
- package/dist/esm/util/index.d.ts.map +1 -0
- package/dist/esm/util/index.js +4 -0
- package/dist/esm/util/index.js.map +1 -0
- package/dist/esm/util/nibbles.d.ts +30 -0
- package/dist/esm/util/nibbles.d.ts.map +1 -0
- package/dist/esm/util/nibbles.js +73 -0
- package/dist/esm/util/nibbles.js.map +1 -0
- package/dist/esm/util/walkController.d.ts +72 -0
- package/dist/esm/util/walkController.d.ts.map +1 -0
- package/dist/esm/util/walkController.js +134 -0
- package/dist/esm/util/walkController.js.map +1 -0
- package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
- package/package.json +85 -0
- package/src/constructors.ts +71 -0
- package/src/db/checkpointDB.ts +298 -0
- package/src/db/index.ts +1 -0
- package/src/index.ts +7 -0
- package/src/mpt.ts +1090 -0
- package/src/node/branch.ts +60 -0
- package/src/node/extension.ts +13 -0
- package/src/node/extensionOrLeafNodeBase.ts +54 -0
- package/src/node/index.ts +4 -0
- package/src/node/leaf.ts +13 -0
- package/src/node/util.ts +35 -0
- package/src/proof/index.ts +2 -0
- package/src/proof/proof.ts +135 -0
- package/src/proof/range.ts +542 -0
- package/src/types.ts +151 -0
- package/src/util/asyncWalk.ts +60 -0
- package/src/util/encoding.ts +209 -0
- package/src/util/genesisState.ts +52 -0
- package/src/util/hex.ts +47 -0
- package/src/util/index.ts +3 -0
- package/src/util/nibbles.ts +80 -0
- 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
|
+
}
|