UNPKG

@ethereumjs/mpt

Version:

Implementation of the modified merkle patricia tree as specified in Ethereum's yellow paper.

1,125 lines (1,031 loc) 37.5 kB
// Some more secure presets when using e.g. JS `call` 'use strict' import { RLP } from '@ethereumjs/rlp' import { BIGINT_0, EthereumJSErrorWithoutCode, KeyEncoding, Lock, MapDB, RLP_EMPTY_STRING, ValueEncoding, bytesToBigInt, bytesToHex, bytesToUnprefixedHex, bytesToUtf8, concatBytes, equalsBytes, } from '@ethereumjs/util' import debug from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { CheckpointDB } from './db/checkpointDB.ts' import { BranchMPTNode, ExtensionMPTNode, LeafMPTNode, decodeMPTNode, decodeRawMPTNode, isRawMPTNode, } from './node/index.ts' import { ROOT_DB_KEY } from './types.ts' import { _walkTrie } from './util/asyncWalk.ts' import { bytesToNibbles, matchingNibbleLength, nibblesTypeToPackedBytes } from './util/nibbles.ts' import { WalkController } from './util/walkController.ts' import type { BatchDBOp, DB } from '@ethereumjs/util' import type { Debugger } from 'debug' import type { BranchMPTNodeBranchValue, FoundNodeFunction, MPTNode, MPTOpts, MPTOptsWithDefaults, Nibbles, NodeReferenceOrRawMPTNode, Path, TrieShallowCopyOpts, } from './types.ts' import type { OnFound } from './util/asyncWalk.ts' /** * The basic trie interface, use with `import { MerklePatriciaTrie } from '@ethereumjs/mpt'`. */ export class MerklePatriciaTrie { protected readonly _opts: MPTOptsWithDefaults = { useKeyHashing: false, useKeyHashingFunction: keccak256, keyPrefix: undefined, useRootPersistence: false, useNodePruning: false, cacheSize: 0, valueEncoding: ValueEncoding.String, } /** The root for an empty trie */ EMPTY_TRIE_ROOT: Uint8Array /** The backend DB */ protected _db!: CheckpointDB protected _hashLen: number protected _lock = new Lock() protected _root: Uint8Array /** Debug logging */ protected DEBUG: boolean protected _debug: Debugger = debug('mpt:#') protected debug: (...args: any) => void /** * Creates a new trie. * @param opts Options for instantiating the trie * * Note: in most cases, {@link createMPT} constructor should be used. It uses the same API but provides sensible defaults */ constructor(opts?: MPTOpts) { let valueEncoding: ValueEncoding if (opts !== undefined) { // Sanity check: can only set valueEncoding if a db is provided // The valueEncoding defaults to `Bytes` if no DB is provided (use a MapDB in memory) if (opts?.valueEncoding !== undefined && opts.db === undefined) { throw EthereumJSErrorWithoutCode('`valueEncoding` can only be set if a `db` is provided') } this._opts = { ...this._opts, ...opts } this._opts.useKeyHashingFunction = opts.common?.customCrypto.keccak256 ?? opts.useKeyHashingFunction ?? keccak256 valueEncoding = opts.db !== undefined ? (opts.valueEncoding ?? ValueEncoding.String) : ValueEncoding.Bytes } else { // No opts are given, so create a MapDB later on // Use `Bytes` for ValueEncoding valueEncoding = ValueEncoding.Bytes } this.DEBUG = typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false this.debug = this.DEBUG ? (message: string, namespaces: string[] = []) => { let log = this._debug for (const name of namespaces) { log = log.extend(name) } log(message) } : (..._: any) => {} this.database(opts?.db ?? new MapDB<string, Uint8Array>(), valueEncoding) this.EMPTY_TRIE_ROOT = this.hash(RLP_EMPTY_STRING) this._hashLen = this.EMPTY_TRIE_ROOT.length this._root = this.EMPTY_TRIE_ROOT if (opts?.root) { this.root(opts.root) } this.DEBUG && this.debug(`Trie created: || Root: ${bytesToHex(this.root())} || Secure: ${this._opts.useKeyHashing} || Persistent: ${this._opts.useRootPersistence} || Pruning: ${this._opts.useNodePruning} || CacheSize: ${this._opts.cacheSize} || ----------------`) } database(db?: DB<string, string | Uint8Array>, valueEncoding?: ValueEncoding) { if (db !== undefined) { if (db instanceof CheckpointDB) { throw EthereumJSErrorWithoutCode('Cannot pass in an instance of CheckpointDB') } this._db = new CheckpointDB({ db, cacheSize: this._opts.cacheSize, valueEncoding }) } return this._db } /** * Gets and/or Sets the current root of the `trie` */ root(value?: Uint8Array | null): Uint8Array { if (value !== undefined) { if (value === null) { value = this.EMPTY_TRIE_ROOT } this.DEBUG && this.debug(`Setting root to ${bytesToHex(value)}`) if (value.length !== this._hashLen) { throw EthereumJSErrorWithoutCode( `Invalid root length. Roots are ${this._hashLen} bytes, got ${value.length} bytes`, ) } this._root = value } return this._root } /** * Checks if a given root exists. */ async checkRoot(root: Uint8Array): Promise<boolean> { try { const value = await this.lookupNode(root) return value !== null } catch (error: any) { if (error.message === 'Missing node in DB') { return equalsBytes(root, this.EMPTY_TRIE_ROOT) } else { throw error } } } /** * Gets a value given a `key` * @param key - the key to search for * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) * @returns A Promise that resolves to `Uint8Array` if a value was found or `null` if no value was found. */ async get(key: Uint8Array, throwIfMissing = false): Promise<Uint8Array | null> { this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['get']) const { node, remaining } = await this.findPath(this.appliedKey(key), throwIfMissing) let value: Uint8Array | null = null if (node && remaining.length === 0) { value = node.value() } this.DEBUG && this.debug(`Value: ${value === null ? 'null' : bytesToHex(value)}`, ['get']) return value } /** * Stores a given `value` at the given `key` or do a delete if `value` is empty * (delete operations are only executed on DB with `deleteFromDB` set to `true`) * @param key * @param value * @returns A Promise that resolves once value is stored. */ async put( key: Uint8Array, value: Uint8Array | null, skipKeyTransform: boolean = false, ): Promise<void> { this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['put']) this.DEBUG && this.debug(`Value: ${value === null ? 'null' : bytesToHex(key)}`, ['put']) if (this._opts.useRootPersistence && equalsBytes(key, ROOT_DB_KEY) === true) { throw EthereumJSErrorWithoutCode( `Attempted to set '${bytesToUtf8(ROOT_DB_KEY)}' key but it is not allowed.`, ) } // If value is empty, delete if (value === null || value.length === 0) { return this.del(key) } await this._lock.acquire() const appliedKey = skipKeyTransform ? key : this.appliedKey(key) if (equalsBytes(this.root(), this.EMPTY_TRIE_ROOT) === true) { // If no root, initialize this trie await this._createInitialNode(appliedKey, value) } else { // First try to find the given key or its nearest node const { remaining, stack } = await this.findPath(appliedKey) let ops: BatchDBOp[] = [] if (this._opts.useNodePruning) { const val = await this.get(key) // Only delete keys if it either does not exist, or if it gets updated // (The update will update the hash of the node, thus we can delete the original leaf node) if (val === null || equalsBytes(val, value) === false) { // All items of the stack are going to change. // (This is the path from the root node to wherever it needs to insert nodes) // The items change, because the leaf value is updated, thus all keyHashes in the // stack should be updated as well, so that it points to the right key/value pairs of the path const deleteHashes = stack.map((e) => this.hash(e.serialize())) ops = deleteHashes.map((deletedHash) => { const key = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, deletedHash) : deletedHash return { type: 'del', key, opts: { keyEncoding: KeyEncoding.Bytes, }, } }) } } // then update await this._updateNode(appliedKey, value, remaining, stack) if (this._opts.useNodePruning) { // Only after updating the node we can delete the keyHashes await this._db.batch(ops) } } await this.persistRoot() this._lock.release() } /** * Deletes a value given a `key` from the trie * (delete operations are only executed on DB with `deleteFromDB` set to `true`) * @param key * @returns A Promise that resolves once value is deleted. */ async del(key: Uint8Array, skipKeyTransform: boolean = false): Promise<void> { this.DEBUG && this.debug(`Key: ${bytesToHex(key)}`, ['del']) await this._lock.acquire() const appliedKey = skipKeyTransform ? key : this.appliedKey(key) const { node, stack } = await this.findPath(appliedKey) let ops: BatchDBOp[] = [] // Only delete if the `key` currently has any value if (this._opts.useNodePruning && node !== null) { const deleteHashes = stack.map((e) => this.hash(e.serialize())) // Just as with `put`, the stack items all will have their keyHashes updated // So after deleting the node, one can safely delete these from the DB ops = deleteHashes.map((deletedHash) => { const key = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, deletedHash) : deletedHash return { type: 'del', key, opts: { keyEncoding: KeyEncoding.Bytes, }, } }) } if (node) { await this._deleteNode(appliedKey, stack) } if (this._opts.useNodePruning) { // Only after deleting the node it is possible to delete the keyHashes await this._db.batch(ops) } await this.persistRoot() this._lock.release() } /** * Tries to find a path to the node for the given key. * It returns a `stack` of nodes to the closest node. * @param key - the search key * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) */ async findPath( key: Uint8Array, throwIfMissing = false, partialPath: { stack: MPTNode[] } = { stack: [], }, ): Promise<Path> { const targetKey = bytesToNibbles(key) const keyLen = targetKey.length const stack: MPTNode[] = Array.from({ length: keyLen }) let progress = 0 for (let i = 0; i < partialPath.stack.length - 1; i++) { stack[i] = partialPath.stack[i] progress += stack[i] instanceof BranchMPTNode ? 1 : (stack[i] as ExtensionMPTNode).keyLength() } this.DEBUG && this.debug(`Target (${targetKey.length}): [${targetKey}]`, ['find_path']) let result: Path | null = null const onFound: FoundNodeFunction = async (_, node, keyProgress, walkController) => { stack[progress] = node as MPTNode if (node instanceof BranchMPTNode) { if (progress === keyLen) { result = { node, remaining: [], stack } } else { const branchIndex = targetKey[progress] this.DEBUG && this.debug(`Looking for node on branch index: [${branchIndex}]`, [ 'find_path', 'branch_node', ]) const branchNode = node.getBranch(branchIndex) if (this.DEBUG) { let debugString: string if (branchNode === null) { debugString = 'NULL' } else { debugString = `Branch index: ${branchIndex.toString()} - ` debugString += branchNode instanceof Uint8Array ? `NodeHash: ${bytesToHex(branchNode)}` : `Raw_Node: ${branchNode.toString()}` } this.debug(debugString, ['find_path', 'branch_node']) } if (!branchNode) { result = { node: null, remaining: targetKey.slice(progress), stack } } else { progress++ walkController.onlyBranchIndex(node, keyProgress, branchIndex) } } } else if (node instanceof LeafMPTNode) { const _progress = progress if (keyLen - progress > node.key().length) { result = { node: null, remaining: targetKey.slice(_progress), stack } return } for (const k of node.key()) { if (k !== targetKey[progress]) { result = { node: null, remaining: targetKey.slice(_progress), stack } return } progress++ } result = { node, remaining: [], stack } } else if (node instanceof ExtensionMPTNode) { this.DEBUG && this.debug( `Comparing node key to expected\n|| Node_Key: [${node.key()}]\n|| Expected: [${targetKey.slice( progress, progress + node.key().length, )}]\n|| Matching: [${ targetKey.slice(progress, progress + node.key().length).toString() === node.key().toString() }] `, ['find_path', 'extension_node'], ) const _progress = progress for (const k of node.key()) { this.DEBUG && this.debug(`NextNode: ${node.value()}`, ['find_path', 'extension_node']) if (k !== targetKey[progress]) { result = { node: null, remaining: targetKey.slice(_progress), stack } return } progress++ } walkController.allChildren(node, keyProgress) } } const startingNode = partialPath.stack[partialPath.stack.length - 1] const start = startingNode !== undefined ? this.hash(startingNode.serialize()) : this.root() try { this.DEBUG && this.debug( `Walking trie from ${startingNode === undefined ? 'ROOT' : 'NODE'}: ${bytesToHex(start)}`, ['find_path'], ) await this.walkTrie(start, onFound) } catch (error: any) { if (error.message !== 'Missing node in DB' || throwIfMissing) { throw error } } if (result === null) { result = { node: null, remaining: [], stack } } this.DEBUG && this.debug( result.node !== null ? `Target Node FOUND for ${bytesToNibbles(key)}` : `Target Node NOT FOUND`, ['find_path'], ) result.stack = result.stack.filter((e) => e !== undefined) this.DEBUG && this.debug( `Result: || Node: ${result.node === null ? 'null' : result.node.constructor.name} || Remaining: [${result.remaining}]\n|| Stack: ${result.stack .map((e) => e.constructor.name) .join(', ')}`, ['find_path'], ) return result } /** * Walks a trie until finished. * @param root * @param onFound - callback to call when a node is found. This schedules new tasks. If no tasks are available, the Promise resolves. * @returns Resolves when finished walking trie. */ async walkTrie(root: Uint8Array, onFound: FoundNodeFunction): Promise<void> { await WalkController.newWalk(onFound, this, root) } walkTrieIterable = _walkTrie.bind(this) /** * Executes a callback for each node in the trie. * @param onFound - callback to call when a node is found. * @returns Resolves when finished walking trie. */ async walkAllNodes(onFound: OnFound): Promise<void> { for await (const { node, currentKey } of this.walkTrieIterable(this.root())) { await onFound(node, currentKey) } } /** * Executes a callback for each value node in the trie. * @param onFound - callback to call when a node is found. * @returns Resolves when finished walking trie. */ async walkAllValueNodes(onFound: OnFound): Promise<void> { for await (const { node, currentKey } of this.walkTrieIterable( this.root(), [], undefined, async (node) => { return ( node instanceof LeafMPTNode || (node instanceof BranchMPTNode && node.value() !== null) ) }, )) { await onFound(node, currentKey) } } /** * Creates the initial node from an empty tree. * @private */ protected async _createInitialNode(key: Uint8Array, value: Uint8Array): Promise<void> { const newNode = new LeafMPTNode(bytesToNibbles(key), value) const encoded = newNode.serialize() this.root(this.hash(encoded)) let rootKey = this.root() rootKey = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, rootKey) : rootKey await this._db.put(rootKey, encoded) await this.persistRoot() } /** * Retrieves a node from db by hash. */ async lookupNode(node: Uint8Array | Uint8Array[]): Promise<MPTNode> { if (isRawMPTNode(node)) { const decoded = decodeRawMPTNode(node) this.DEBUG && this.debug(`${decoded.constructor.name}`, ['lookup_node', 'raw_node']) return decoded } this.DEBUG && this.debug(`${`${bytesToHex(node)}`}`, ['lookup_node', 'by_hash']) const key = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, node) : node const value = (await this._db.get(key)) ?? null if (value === null) { // Dev note: this error message text is used for error checking in `checkRoot`, `verifyMPTWithMerkleProof`, and `findPath` throw EthereumJSErrorWithoutCode('Missing node in DB') } const decoded = decodeMPTNode(value) this.DEBUG && this.debug(`${decoded.constructor.name} found in DB`, ['lookup_node', 'by_hash']) return decoded } /** * Updates a node. * @private * @param key * @param value * @param keyRemainder * @param stack */ protected async _updateNode( k: Uint8Array, value: Uint8Array, keyRemainder: Nibbles, stack: MPTNode[], ): Promise<void> { const toSave: BatchDBOp[] = [] const lastNode = stack.pop() if (!lastNode) { throw EthereumJSErrorWithoutCode('Stack underflow') } // add the new nodes const key = bytesToNibbles(k) // Check if the last node is a leaf and the key matches to this let matchLeaf = false if (lastNode instanceof LeafMPTNode) { let l = 0 for (let i = 0; i < stack.length; i++) { const n = stack[i] if (n instanceof BranchMPTNode) { l++ } else { l += n.key().length } } if ( keyRemainder.length === 0 && matchingNibbleLength(lastNode.key(), key.slice(l)) === lastNode.key().length ) { matchLeaf = true } } if (matchLeaf) { // just updating a found value lastNode.value(value) stack.push(lastNode) } else if (lastNode instanceof BranchMPTNode) { stack.push(lastNode) if (keyRemainder.length !== 0) { // add an extension to a branch node keyRemainder.shift() // create a new leaf const newLeaf = new LeafMPTNode(keyRemainder, value) stack.push(newLeaf) } else { lastNode.value(value) } } else { // create a branch node const lastKey = lastNode.key() const matchingLength = matchingNibbleLength(lastKey, keyRemainder) const newBranchMPTNode = new BranchMPTNode() // create a new extension node if (matchingLength !== 0) { const newKey = lastNode.key().slice(0, matchingLength) const newExtNode = new ExtensionMPTNode(newKey, value) stack.push(newExtNode) lastKey.splice(0, matchingLength) keyRemainder.splice(0, matchingLength) } stack.push(newBranchMPTNode) if (lastKey.length !== 0) { const branchKey = lastKey.shift() as number if (lastKey.length !== 0 || lastNode instanceof LeafMPTNode) { // shrinking extension or leaf lastNode.key(lastKey) const formattedNode = this._formatNode( lastNode, false, toSave, ) as NodeReferenceOrRawMPTNode newBranchMPTNode.setBranch(branchKey, formattedNode) } else { // remove extension or attaching this._formatNode(lastNode, false, toSave, true) newBranchMPTNode.setBranch(branchKey, lastNode.value()) } } else { newBranchMPTNode.value(lastNode.value()) } if (keyRemainder.length !== 0) { keyRemainder.shift() // add a leaf node to the new branch node const newLeafMPTNode = new LeafMPTNode(keyRemainder, value) stack.push(newLeafMPTNode) } else { newBranchMPTNode.value(value) } } await this.saveStack(key, stack, toSave) } /** * Deletes a node from the trie. * @private */ protected async _deleteNode(k: Uint8Array, stack: MPTNode[]): Promise<void> { const processBranchMPTNode = ( key: Nibbles, branchKey: number, branchNode: MPTNode, parentNode: MPTNode, stack: MPTNode[], ) => { // branchNode is the node ON the branch node not THE branch node if (parentNode === null || parentNode === undefined || parentNode instanceof BranchMPTNode) { // branch->? if (parentNode !== null && parentNode !== undefined) { stack.push(parentNode) } if (branchNode instanceof BranchMPTNode) { // create an extension node // branch->extension->branch // We push an extension value with a temporarily empty value to the stack. // It will be replaced later on with the correct value in saveStack const extensionNode = new ExtensionMPTNode([branchKey], new Uint8Array()) stack.push(extensionNode) key.push(branchKey) } else { const branchNodeKey = branchNode.key() // branch key is an extension or a leaf // branch->(leaf or extension) branchNodeKey.unshift(branchKey) branchNode.key(branchNodeKey.slice(0)) key = key.concat(branchNodeKey) } stack.push(branchNode) } else { // parent is an extension let parentKey = parentNode.key() if (branchNode instanceof BranchMPTNode) { // ext->branch parentKey.push(branchKey) key.push(branchKey) parentNode.key(parentKey) stack.push(parentNode) } else { const branchNodeKey = branchNode.key() // branch node is an leaf or extension and parent node is an extension // add two keys together // don't push the parent node branchNodeKey.unshift(branchKey) key = key.concat(branchNodeKey) parentKey = parentKey.concat(branchNodeKey) branchNode.key(parentKey) } stack.push(branchNode) } return key } let lastNode = stack.pop() if (lastNode === undefined) throw EthereumJSErrorWithoutCode('missing last node') let parentNode = stack.pop() const opStack: BatchDBOp[] = [] let key = bytesToNibbles(k) if (parentNode === undefined) { // the root here has to be a leaf. this.root(this.EMPTY_TRIE_ROOT) return } if (lastNode instanceof BranchMPTNode) { lastNode.value(null) } else { // the lastNode has to be a leaf if it's not a branch. // And a leaf's parent, if it has one, must be a branch. if (!(parentNode instanceof BranchMPTNode)) { throw EthereumJSErrorWithoutCode('Expected branch node') } const lastNodeKey = lastNode.key() key.splice(key.length - lastNodeKey.length) // delete the value this._formatNode(lastNode, false, opStack, true) parentNode.setBranch(key.pop() as number, null) lastNode = parentNode parentNode = stack.pop() } // nodes on the branch // count the number of nodes on the branch const branchNodes: [number, NodeReferenceOrRawMPTNode][] = lastNode.getChildren() // if there is only one branch node left, collapse the branch node if (branchNodes.length === 1) { // add the one remaining branch node to node above it const branchNode = branchNodes[0][1] const branchNodeKey = branchNodes[0][0] // Special case where one needs to delete an extra node: // In this case, after updating the branch, the branch node has just one branch left // However, this violates the trie spec; this should be converted in either an ExtensionMPTNode // Or a LeafMPTNode // Since this branch is deleted, one can thus also delete this branch from the DB // So add this to the `opStack` and mark the keyHash to be deleted if (this._opts.useNodePruning) { // If the branchNode has length < 32, it will be a RawNode (Uint8Array[]) instead of a Uint8Array // In that case, we need to serialize and hash it into a Uint8Array, otherwise the operation will throw opStack.push({ type: 'del', key: isRawMPTNode(branchNode) ? this.appliedKey(RLP.encode(branchNode)) : branchNode, }) } // look up node const foundNode = await this.lookupNode(branchNode) key = processBranchMPTNode(key, branchNodeKey, foundNode, parentNode as MPTNode, stack) await this.saveStack(key, stack, opStack) } else { // simple removing a leaf and recalculation the stack if (parentNode) { stack.push(parentNode) } stack.push(lastNode) await this.saveStack(key, stack, opStack) } } /** * Saves a stack of nodes to the database. * * @param key - the key. Should follow the stack * @param stack - a stack of nodes to the value given by the key * @param opStack - a stack of levelup operations to commit at the end of this function */ async saveStack(key: Nibbles, stack: MPTNode[], opStack: BatchDBOp[]): Promise<void> { let lastRoot // update nodes while (stack.length) { const node = stack.pop() if (node === undefined) { throw EthereumJSErrorWithoutCode('saveStack: missing node') } if (node instanceof LeafMPTNode || node instanceof ExtensionMPTNode) { key.splice(key.length - node.key().length) } if (node instanceof ExtensionMPTNode && lastRoot !== undefined) { node.value(lastRoot) } if (node instanceof BranchMPTNode && lastRoot !== undefined) { const branchKey = key.pop() node.setBranch(branchKey!, lastRoot) } lastRoot = this._formatNode(node, stack.length === 0, opStack) as Uint8Array } if (lastRoot !== undefined) { this.root(lastRoot) } await this._db.batch(opStack) await this.persistRoot() } /** * Formats node to be saved by `levelup.batch`. * @private * @param node - the node to format. * @param topLevel - if the node is at the top level. * @param opStack - the opStack to push the node's data. * @param remove - whether to remove the node * @returns The node's hash used as the key or the rawNode. */ _formatNode( node: MPTNode, topLevel: boolean, opStack: BatchDBOp[], remove: boolean = false, ): Uint8Array | NodeReferenceOrRawMPTNode | BranchMPTNodeBranchValue[] { const encoded = node.serialize() if (encoded.length >= 32 || topLevel) { const lastRoot = this.hash(encoded) const key = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, lastRoot) : lastRoot if (remove) { if (this._opts.useNodePruning) { opStack.push({ type: 'del', key, }) } } else { opStack.push({ type: 'put', key, value: encoded, }) } return lastRoot } return node.raw() } /** * The given hash of operations (key additions or deletions) are executed on the trie * (delete operations are only executed on DB with `deleteFromDB` set to `true`) * @example * const ops = [ * { type: 'del', key: Uint8Array.from('father') } * , { type: 'put', key: Uint8Array.from('name'), value: Uint8Array.from('Yuri Irsenovich Kim') } // cspell:disable-line * , { type: 'put', key: Uint8Array.from('dob'), value: Uint8Array.from('16 February 1941') } * , { type: 'put', key: Uint8Array.from('spouse'), value: Uint8Array.from('Kim Young-sook') } // cspell:disable-line * , { type: 'put', key: Uint8Array.from('occupation'), value: Uint8Array.from('Clown') } * ] * await trie.batch(ops) * @param ops */ async batch(ops: BatchDBOp[], skipKeyTransform?: boolean): Promise<void> { for (const op of ops) { if (op.type === 'put') { if (op.value === null || op.value === undefined) { throw EthereumJSErrorWithoutCode('Invalid batch db operation') } await this.put(op.key, op.value, skipKeyTransform) } else if (op.type === 'del') { await this.del(op.key, skipKeyTransform) } } await this.persistRoot() } // This method verifies if all keys in the trie (except the root) are reachable // If one of the key is not reachable, then that key could be deleted from the DB // (i.e. the Trie is not correctly pruned) // If this method returns `true`, the Trie is correctly pruned and all keys are reachable async verifyPrunedIntegrity(): Promise<boolean> { const roots = [ bytesToUnprefixedHex(this.root()), bytesToUnprefixedHex(this.appliedKey(ROOT_DB_KEY)), ] for (const dbkey of (this._db.db as any)._database.keys()) { if (roots.includes(dbkey)) { // The root key can never be found from the trie, otherwise this would // convert the tree from a directed acyclic graph to a directed cycling graph continue } // Track if key is found let found = false try { await this.walkTrie(this.root(), async function (_, node, key, controller) { if (found) { // Abort all other children checks return } if (node instanceof BranchMPTNode) { for (const item of node._branches) { // If one of the branches matches the key, then it is found if ( item !== null && bytesToUnprefixedHex( isRawMPTNode(item) ? controller.trie.appliedKey(RLP.encode(item)) : item, ) === dbkey ) { found = true return } } // Check all children of the branch controller.allChildren(node, key) } if (node instanceof ExtensionMPTNode) { // If the value of the ExtensionMPTNode points to the dbkey, then it is found if (bytesToUnprefixedHex(node.value()) === dbkey) { found = true return } controller.allChildren(node, key) } }) } catch { return false } if (!found) { return false } } return true } /** * Returns a copy of the underlying trie. * * Note on db: the copy will create a reference to the * same underlying database. * * Note on cache: for memory reasons a copy will by default * not recreate a new LRU cache but initialize with cache * being deactivated. This behavior can be overwritten by * explicitly setting `cacheSize` as an option on the method. * * @param includeCheckpoints - If true and during a checkpoint, the copy will contain the checkpointing metadata and will use the same scratch as underlying db. */ shallowCopy(includeCheckpoints = true, opts?: TrieShallowCopyOpts): MerklePatriciaTrie { const trie = new MerklePatriciaTrie({ ...this._opts, db: this._db.db.shallowCopy(), root: this.root(), cacheSize: 0, ...(opts ?? {}), }) if (includeCheckpoints && this.hasCheckpoints()) { trie._db.setCheckpoints(this._db.checkpoints) } return trie } /** * Persists the root hash in the underlying database */ async persistRoot() { if (this._opts.useRootPersistence) { this.DEBUG && this.debug( `Persisting root: \n|| RootHash: ${bytesToHex(this.root())}\n|| RootKey: ${bytesToHex( this.appliedKey(ROOT_DB_KEY), )}`, ['persist_root'], ) let key = this.appliedKey(ROOT_DB_KEY) key = this._opts.keyPrefix ? concatBytes(this._opts.keyPrefix, key) : key await this._db.put(key, this.root()) } } /** * Finds all nodes that are stored directly in the db * (some nodes are stored raw inside other nodes) * called by {@link ScratchReadStream} * @private */ protected async _findDbNodes(onFound: FoundNodeFunction): Promise<void> { const outerOnFound: FoundNodeFunction = async (nodeRef, node, key, walkController) => { if (isRawMPTNode(nodeRef)) { if (node !== null) { walkController.allChildren(node, key) } } else { onFound(nodeRef, node, key, walkController) } } await this.walkTrie(this.root(), outerOnFound) } /** * Returns the key practically applied for trie construction * depending on the `useKeyHashing` option being set or not. * @param key */ protected appliedKey(key: Uint8Array) { if (this._opts.useKeyHashing) { return this.hash(key) } return key } protected hash(msg: Uint8Array): Uint8Array { return Uint8Array.from(this._opts.useKeyHashingFunction.call(undefined, msg)) } /** * Is the trie during a checkpoint phase? */ hasCheckpoints() { return this._db.hasCheckpoints() } /** * Creates a checkpoint that can later be reverted to or committed. * After this is called, all changes can be reverted until `commit` is called. */ checkpoint() { this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['checkpoint']) this._db.checkpoint(this.root()) } /** * Commits a checkpoint to disk, if current checkpoint is not nested. * If nested, only sets the parent checkpoint as current checkpoint. * @throws If not during a checkpoint phase */ async commit(): Promise<void> { if (!this.hasCheckpoints()) { throw EthereumJSErrorWithoutCode('trying to commit when not checkpointed') } this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['commit']) await this._lock.acquire() await this._db.commit() await this.persistRoot() this._lock.release() } /** * Reverts the trie to the state it was at when `checkpoint` was first called. * If during a nested checkpoint, sets root to most recent checkpoint, and sets * parent checkpoint as current. */ async revert(): Promise<void> { if (!this.hasCheckpoints()) { throw EthereumJSErrorWithoutCode('trying to revert when not checkpointed') } this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['revert', 'before']) await this._lock.acquire() this.root(await this._db.revert()) await this.persistRoot() this._lock.release() this.DEBUG && this.debug(`${bytesToHex(this.root())}`, ['revert', 'after']) } /** * Flushes all checkpoints, restoring the initial checkpoint state. */ flushCheckpoints() { this.DEBUG && this.debug(`Deleting ${this._db.checkpoints.length} checkpoints.`, ['flush_checkpoints']) this._db.checkpoints = [] } /** * Returns a list of values stored in the trie * @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 * @param limit - the number of keys to be returned (undefined means all keys) * @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 */ async getValueMap( startKey = BIGINT_0, limit?: number, ): Promise<{ values: { [key: string]: string }; nextKey: null | string }> { // If limit is undefined, all keys are inRange let inRange = limit !== undefined ? false : true let i = 0 const values: { [key: string]: string } = {} let nextKey: string | null = null await this.walkAllValueNodes(async (node: MPTNode, currentKey: number[]) => { if (node instanceof LeafMPTNode) { const keyBytes = nibblesTypeToPackedBytes(currentKey.concat(node.key())) if (!inRange) { // Check if the key is already in the correct range. if (bytesToBigInt(keyBytes) >= startKey) { inRange = true } else { return } } if (limit === undefined || i < limit) { values[bytesToHex(keyBytes)] = bytesToHex(node._value) i++ } else if (i === limit) { nextKey = bytesToHex(keyBytes) } } }) return { values, nextKey, } } }