@ethereumjs/mpt
Version:
Implementation of the modified merkle patricia tree as specified in Ethereum's yellow paper.
956 lines • 41.8 kB
JavaScript
// 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