UNPKG

@ethereumjs/mpt

Version:

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

956 lines 41.8 kB
// Some more secure presets when using e.g. JS `call` 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.MerklePatriciaTrie = void 0; const rlp_1 = require("@ethereumjs/rlp"); const util_1 = require("@ethereumjs/util"); const debug_1 = require("debug"); const keccak_js_1 = require("ethereum-cryptography/keccak.js"); const checkpointDB_ts_1 = require("./db/checkpointDB.js"); const index_ts_1 = require("./node/index.js"); const types_ts_1 = require("./types.js"); const asyncWalk_ts_1 = require("./util/asyncWalk.js"); const nibbles_ts_1 = require("./util/nibbles.js"); const walkController_ts_1 = require("./util/walkController.js"); /** * The basic trie interface, use with `import { MerklePatriciaTrie } from '@ethereumjs/mpt'`. * * A MerklePatriciaTrie object can be created with the constructor method: * * - {@link createMPT} * * A sparse MerklePatriciaTrie object can be created from a merkle proof: * * - {@link createMPTFromProof} */ class MerklePatriciaTrie { /** * 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) { this._opts = { useKeyHashing: false, useKeyHashingFunction: keccak_js_1.keccak256, keyPrefix: undefined, useRootPersistence: false, useNodePruning: false, cacheSize: 0, valueEncoding: util_1.ValueEncoding.String, }; this._lock = new util_1.Lock(); this._debug = (0, debug_1.default)('mpt:#'); this.walkTrieIterable = asyncWalk_ts_1._walkTrie.bind(this); let 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 (0, util_1.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 ?? keccak_js_1.keccak256; valueEncoding = opts.db !== undefined ? (opts.valueEncoding ?? util_1.ValueEncoding.String) : util_1.ValueEncoding.Bytes; } else { // No opts are given, so create a MapDB later on // Use `Bytes` for ValueEncoding valueEncoding = util_1.ValueEncoding.Bytes; } this.DEBUG = typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false; this.debug = this.DEBUG ? (message, namespaces = []) => { let log = this._debug; for (const name of namespaces) { log = log.extend(name); } log(message); } : (..._) => { }; this.database(opts?.db ?? new util_1.MapDB(), valueEncoding); this.EMPTY_TRIE_ROOT = this.hash(util_1.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: ${(0, util_1.bytesToHex)(this.root())} || Secure: ${this._opts.useKeyHashing} || Persistent: ${this._opts.useRootPersistence} || Pruning: ${this._opts.useNodePruning} || CacheSize: ${this._opts.cacheSize} || ----------------`); } database(db, valueEncoding) { if (db !== undefined) { if (db instanceof checkpointDB_ts_1.CheckpointDB) { throw (0, util_1.EthereumJSErrorWithoutCode)('Cannot pass in an instance of CheckpointDB'); } this._db = new checkpointDB_ts_1.CheckpointDB({ db, cacheSize: this._opts.cacheSize, valueEncoding }); } return this._db; } /** * Gets and/or Sets the current root of the `trie` */ root(value) { if (value !== undefined) { if (value === null) { value = this.EMPTY_TRIE_ROOT; } this.DEBUG && this.debug(`Setting root to ${(0, util_1.bytesToHex)(value)}`); if (value.length !== this._hashLen) { throw (0, util_1.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) { try { const value = await this.lookupNode(root); return value !== null; } catch (error) { if (error.message === 'Missing node in DB') { return (0, util_1.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, throwIfMissing = false) { this.DEBUG && this.debug(`Key: ${(0, util_1.bytesToHex)(key)}`, ['get']); const { node, remaining } = await this.findPath(this.appliedKey(key), throwIfMissing); let value = null; if (node && remaining.length === 0) { value = node.value(); } this.DEBUG && this.debug(`Value: ${value === null ? 'null' : (0, util_1.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, value, skipKeyTransform = false) { this.DEBUG && this.debug(`Key: ${(0, util_1.bytesToHex)(key)}`, ['put']); this.DEBUG && this.debug(`Value: ${value === null ? 'null' : (0, util_1.bytesToHex)(key)}`, ['put']); if (this._opts.useRootPersistence && (0, util_1.equalsBytes)(key, types_ts_1.ROOT_DB_KEY) === true) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Attempted to set '${(0, util_1.bytesToUtf8)(types_ts_1.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 ((0, util_1.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 = []; 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 || (0, util_1.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 ? (0, util_1.concatBytes)(this._opts.keyPrefix, deletedHash) : deletedHash; return { type: 'del', key, opts: { keyEncoding: util_1.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, skipKeyTransform = false) { this.DEBUG && this.debug(`Key: ${(0, util_1.bytesToHex)(key)}`, ['del']); await this._lock.acquire(); const appliedKey = skipKeyTransform ? key : this.appliedKey(key); const { node, stack } = await this.findPath(appliedKey); let ops = []; // 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 ? (0, util_1.concatBytes)(this._opts.keyPrefix, deletedHash) : deletedHash; return { type: 'del', key, opts: { keyEncoding: util_1.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, throwIfMissing = false, partialPath = { stack: [], }) { const targetKey = (0, nibbles_ts_1.bytesToNibbles)(key); const keyLen = targetKey.length; const stack = 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 index_ts_1.BranchMPTNode ? 1 : stack[i].keyLength(); } this.DEBUG && this.debug(`Target (${targetKey.length}): [${targetKey}]`, ['find_path']); let result = null; const onFound = async (_, node, keyProgress, walkController) => { stack[progress] = node; if (node instanceof index_ts_1.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; if (branchNode === null) { debugString = 'NULL'; } else { debugString = `Branch index: ${branchIndex.toString()} - `; debugString += branchNode instanceof Uint8Array ? `NodeHash: ${(0, util_1.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 index_ts_1.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 index_ts_1.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'}: ${(0, util_1.bytesToHex)(start)}`, ['find_path']); await this.walkTrie(start, onFound); } catch (error) { 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 ${(0, nibbles_ts_1.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, onFound) { await walkController_ts_1.WalkController.newWalk(onFound, this, root); } /** * 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) { 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) { for await (const { node, currentKey } of this.walkTrieIterable(this.root(), [], undefined, async (node) => { return (node instanceof index_ts_1.LeafMPTNode || (node instanceof index_ts_1.BranchMPTNode && node.value() !== null)); })) { await onFound(node, currentKey); } } /** * Creates the initial node from an empty tree. * @private */ async _createInitialNode(key, value) { const newNode = new index_ts_1.LeafMPTNode((0, nibbles_ts_1.bytesToNibbles)(key), value); const encoded = newNode.serialize(); this.root(this.hash(encoded)); let rootKey = this.root(); rootKey = this._opts.keyPrefix ? (0, util_1.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) { if ((0, index_ts_1.isRawMPTNode)(node)) { const decoded = (0, index_ts_1.decodeRawMPTNode)(node); this.DEBUG && this.debug(`${decoded.constructor.name}`, ['lookup_node', 'raw_node']); return decoded; } this.DEBUG && this.debug(`${`${(0, util_1.bytesToHex)(node)}`}`, ['lookup_node', 'by_hash']); const key = this._opts.keyPrefix ? (0, util_1.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 (0, util_1.EthereumJSErrorWithoutCode)('Missing node in DB'); } const decoded = (0, index_ts_1.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 */ async _updateNode(k, value, keyRemainder, stack) { const toSave = []; const lastNode = stack.pop(); if (!lastNode) { throw (0, util_1.EthereumJSErrorWithoutCode)('Stack underflow'); } // add the new nodes const key = (0, nibbles_ts_1.bytesToNibbles)(k); // Check if the last node is a leaf and the key matches to this let matchLeaf = false; if (lastNode instanceof index_ts_1.LeafMPTNode) { let l = 0; for (let i = 0; i < stack.length; i++) { const n = stack[i]; if (n instanceof index_ts_1.BranchMPTNode) { l++; } else { l += n.key().length; } } if (keyRemainder.length === 0 && (0, nibbles_ts_1.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 index_ts_1.BranchMPTNode) { stack.push(lastNode); if (keyRemainder.length !== 0) { // add an extension to a branch node keyRemainder.shift(); // create a new leaf const newLeaf = new index_ts_1.LeafMPTNode(keyRemainder, value); stack.push(newLeaf); } else { lastNode.value(value); } } else { // create a branch node const lastKey = lastNode.key(); const matchingLength = (0, nibbles_ts_1.matchingNibbleLength)(lastKey, keyRemainder); const newBranchMPTNode = new index_ts_1.BranchMPTNode(); // create a new extension node if (matchingLength !== 0) { const newKey = lastNode.key().slice(0, matchingLength); const newExtNode = new index_ts_1.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(); if (lastKey.length !== 0 || lastNode instanceof index_ts_1.LeafMPTNode) { // shrinking extension or leaf lastNode.key(lastKey); const formattedNode = this._formatNode(lastNode, false, toSave); 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 index_ts_1.LeafMPTNode(keyRemainder, value); stack.push(newLeafMPTNode); } else { newBranchMPTNode.value(value); } } await this.saveStack(key, stack, toSave); } /** * Deletes a node from the trie. * @private */ async _deleteNode(k, stack) { const processBranchMPTNode = (key, branchKey, branchNode, parentNode, stack) => { // branchNode is the node ON the branch node not THE branch node if (parentNode === null || parentNode === undefined || parentNode instanceof index_ts_1.BranchMPTNode) { // branch->? if (parentNode !== null && parentNode !== undefined) { stack.push(parentNode); } if (branchNode instanceof index_ts_1.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 index_ts_1.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 index_ts_1.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 (0, util_1.EthereumJSErrorWithoutCode)('missing last node'); let parentNode = stack.pop(); const opStack = []; let key = (0, nibbles_ts_1.bytesToNibbles)(k); if (parentNode === undefined) { // the root here has to be a leaf. this.root(this.EMPTY_TRIE_ROOT); return; } if (lastNode instanceof index_ts_1.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 index_ts_1.BranchMPTNode)) { throw (0, util_1.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(), null); lastNode = parentNode; parentNode = stack.pop(); } // nodes on the branch // count the number of nodes on the branch const branchNodes = 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: (0, index_ts_1.isRawMPTNode)(branchNode) ? this.appliedKey(rlp_1.RLP.encode(branchNode)) : branchNode, }); } // look up node const foundNode = await this.lookupNode(branchNode); key = processBranchMPTNode(key, branchNodeKey, foundNode, parentNode, 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, stack, opStack) { let lastRoot; // update nodes while (stack.length) { const node = stack.pop(); if (node === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)('saveStack: missing node'); } if (node instanceof index_ts_1.LeafMPTNode || node instanceof index_ts_1.ExtensionMPTNode) { key.splice(key.length - node.key().length); } if (node instanceof index_ts_1.ExtensionMPTNode && lastRoot !== undefined) { node.value(lastRoot); } if (node instanceof index_ts_1.BranchMPTNode && lastRoot !== undefined) { const branchKey = key.pop(); node.setBranch(branchKey, lastRoot); } lastRoot = this._formatNode(node, stack.length === 0, opStack); } 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, topLevel, opStack, remove = false) { const encoded = node.serialize(); if (encoded.length >= 32 || topLevel) { const lastRoot = this.hash(encoded); const key = this._opts.keyPrefix ? (0, util_1.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, skipKeyTransform) { for (const op of ops) { if (op.type === 'put') { if (op.value === null || op.value === undefined) { throw (0, util_1.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() { const roots = [ (0, util_1.bytesToUnprefixedHex)(this.root()), (0, util_1.bytesToUnprefixedHex)(this.appliedKey(types_ts_1.ROOT_DB_KEY)), ]; for (const dbkey of this._db.db._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 index_ts_1.BranchMPTNode) { for (const item of node._branches) { // If one of the branches matches the key, then it is found if (item !== null && (0, util_1.bytesToUnprefixedHex)((0, index_ts_1.isRawMPTNode)(item) ? controller.trie.appliedKey(rlp_1.RLP.encode(item)) : item) === dbkey) { found = true; return; } } // Check all children of the branch controller.allChildren(node, key); } if (node instanceof index_ts_1.ExtensionMPTNode) { // If the value of the ExtensionMPTNode points to the dbkey, then it is found if ((0, util_1.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) { 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: ${(0, util_1.bytesToHex)(this.root())}\n|| RootKey: ${(0, util_1.bytesToHex)(this.appliedKey(types_ts_1.ROOT_DB_KEY))}`, ['persist_root']); let key = this.appliedKey(types_ts_1.ROOT_DB_KEY); key = this._opts.keyPrefix ? (0, util_1.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 */ async _findDbNodes(onFound) { const outerOnFound = async (nodeRef, node, key, walkController) => { if ((0, index_ts_1.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 */ appliedKey(key) { if (this._opts.useKeyHashing) { return this.hash(key); } return key; } hash(msg) { 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(`${(0, util_1.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() { if (!this.hasCheckpoints()) { throw (0, util_1.EthereumJSErrorWithoutCode)('trying to commit when not checkpointed'); } this.DEBUG && this.debug(`${(0, util_1.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() { if (!this.hasCheckpoints()) { throw (0, util_1.EthereumJSErrorWithoutCode)('trying to revert when not checkpointed'); } this.DEBUG && this.debug(`${(0, util_1.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(`${(0, util_1.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 = util_1.BIGINT_0, limit) { // If limit is undefined, all keys are inRange let inRange = limit !== undefined ? false : true; let i = 0; const values = {}; let nextKey = null; await this.walkAllValueNodes(async (node, currentKey) => { if (node instanceof index_ts_1.LeafMPTNode) { const keyBytes = (0, nibbles_ts_1.nibblesTypeToPackedBytes)(currentKey.concat(node.key())); if (!inRange) { // Check if the key is already in the correct range. if ((0, util_1.bytesToBigInt)(keyBytes) >= startKey) { inRange = true; } else { return; } } if (limit === undefined || i < limit) { values[(0, util_1.bytesToHex)(keyBytes)] = (0, util_1.bytesToHex)(node._value); i++; } else if (i === limit) { nextKey = (0, util_1.bytesToHex)(keyBytes); } } }); return { values, nextKey, }; } } exports.MerklePatriciaTrie = MerklePatriciaTrie; //# sourceMappingURL=mpt.js.map