UNPKG

merkle-patricia-tree

Version:

This is an implementation of the modified merkle patricia tree as specified in Ethereum's yellow paper.

703 lines 26.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Trie = void 0; const semaphore_async_await_1 = __importDefault(require("semaphore-async-await")); const ethereumjs_util_1 = require("ethereumjs-util"); const db_1 = require("./db"); const readStream_1 = require("./readStream"); const nibbles_1 = require("./util/nibbles"); const walkController_1 = require("./util/walkController"); const trieNode_1 = require("./trieNode"); const verifyRangeProof_1 = require("./verifyRangeProof"); const assert = require('assert'); /** * The basic trie interface, use with `import { BaseTrie as Trie } from 'merkle-patricia-tree'`. * In Ethereum applications stick with the {@link SecureTrie} overlay. * The API for the base and the secure interface are about the same. */ class Trie { /** * test * @param db - A [levelup](https://github.com/Level/levelup) instance. By default (if the db is `null` or * left undefined) creates an in-memory [memdown](https://github.com/Level/memdown) instance. * @param root - A `Buffer` for the root of a previously stored trie * @param deleteFromDB - Delete nodes from DB on delete operations (disallows switching to an older state root) (default: `false`) */ constructor(db, root, deleteFromDB = false) { this.EMPTY_TRIE_ROOT = ethereumjs_util_1.KECCAK256_RLP; this.lock = new semaphore_async_await_1.default(1); this.db = db ? new db_1.DB(db) : new db_1.DB(); this._root = this.EMPTY_TRIE_ROOT; this._deleteFromDB = deleteFromDB; if (root) { this.root = root; } } /** * Sets the current root of the `trie` */ set root(value) { if (!value) { value = this.EMPTY_TRIE_ROOT; } assert(value.length === 32, 'Invalid root length. Roots are 32 bytes'); this._root = value; } /** * Gets the current root of the `trie` */ get root() { return this._root; } /** * This method is deprecated. * Please use {@link Trie.root} instead. * * @param value * @deprecated */ setRoot(value) { this.root = value !== null && value !== void 0 ? value : this.EMPTY_TRIE_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 false; } else { throw error; } } } /** * BaseTrie has no checkpointing so return false */ get isCheckpoint() { return false; } /** * 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 `Buffer` if a value was found or `null` if no value was found. */ async get(key, throwIfMissing = false) { const { node, remaining } = await this.findPath(key, throwIfMissing); let value = null; if (node && remaining.length === 0) { value = node.value; } 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) { // If value is empty, delete if (!value || value.toString() === '') { return await this.del(key); } await this.lock.wait(); if (this.root.equals(ethereumjs_util_1.KECCAK256_RLP)) { // If no root, initialize this trie await this._createInitialNode(key, value); } else { // First try to find the given key or its nearest node const { remaining, stack } = await this.findPath(key); // then update await this._updateNode(key, value, remaining, stack); } this.lock.signal(); } /** * 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) { await this.lock.wait(); const { node, stack } = await this.findPath(key); if (node) { await this._deleteNode(key, stack); } this.lock.signal(); } /** * 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) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const stack = []; const targetKey = (0, nibbles_1.bufferToNibbles)(key); const onFound = async (nodeRef, node, keyProgress, walkController) => { if (node === null) { return reject(new Error('Path not found')); } const keyRemainder = targetKey.slice((0, nibbles_1.matchingNibbleLength)(keyProgress, targetKey)); stack.push(node); if (node instanceof trieNode_1.BranchNode) { if (keyRemainder.length === 0) { // we exhausted the key without finding a node resolve({ node, remaining: [], stack }); } else { const branchIndex = keyRemainder[0]; const branchNode = node.getBranch(branchIndex); if (!branchNode) { // there are no more nodes to find and we didn't find the key resolve({ node: null, remaining: keyRemainder, stack }); } else { // node found, continuing search // this can be optimized as this calls getBranch again. walkController.onlyBranchIndex(node, keyProgress, branchIndex); } } } else if (node instanceof trieNode_1.LeafNode) { if ((0, nibbles_1.doKeysMatch)(keyRemainder, node.key)) { // keys match, return node with empty key resolve({ node, remaining: [], stack }); } else { // reached leaf but keys dont match resolve({ node: null, remaining: keyRemainder, stack }); } } else if (node instanceof trieNode_1.ExtensionNode) { const matchingLen = (0, nibbles_1.matchingNibbleLength)(keyRemainder, node.key); if (matchingLen !== node.key.length) { // keys don't match, fail resolve({ node: null, remaining: keyRemainder, stack }); } else { // keys match, continue search walkController.allChildren(node, keyProgress); } } }; // walk trie and process nodes try { await this.walkTrie(this.root, onFound); } catch (error) { if (error.message == 'Missing node in DB' && !throwIfMissing) { // pass } else { reject(error); } } // Resolve if _walkTrie finishes without finding any nodes resolve({ node: null, remaining: [], stack }); }); } /** * 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_1.WalkController.newWalk(onFound, this, root); } /** * @hidden * Backwards compatibility * @param root - * @param onFound - */ async _walkTrie(root, onFound) { await this.walkTrie(root, onFound); } /** * Creates the initial node from an empty tree. * @private */ async _createInitialNode(key, value) { const newNode = new trieNode_1.LeafNode((0, nibbles_1.bufferToNibbles)(key), value); this.root = newNode.hash(); await this.db.put(this.root, newNode.serialize()); } /** * Retrieves a node from db by hash. */ async lookupNode(node) { if ((0, trieNode_1.isRawNode)(node)) { return (0, trieNode_1.decodeRawNode)(node); } let value = null; let foundNode = null; value = await this.db.get(node); if (value) { foundNode = (0, trieNode_1.decodeNode)(value); } else { // Dev note: this error message text is used for error checking in `checkRoot`, `verifyProof`, and `findPath` throw new Error('Missing node in DB'); } return foundNode; } /** * @hidden * Backwards compatibility * @param node The node hash to lookup from the DB */ async _lookupNode(node) { return this.lookupNode(node); } /** * 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 new Error('Stack underflow'); } // add the new nodes const key = (0, nibbles_1.bufferToNibbles)(k); // Check if the last node is a leaf and the key matches to this let matchLeaf = false; if (lastNode instanceof trieNode_1.LeafNode) { let l = 0; for (let i = 0; i < stack.length; i++) { const n = stack[i]; if (n instanceof trieNode_1.BranchNode) { l++; } else { l += n.key.length; } } if ((0, nibbles_1.matchingNibbleLength)(lastNode.key, key.slice(l)) === lastNode.key.length && keyRemainder.length === 0) { matchLeaf = true; } } if (matchLeaf) { // just updating a found value lastNode.value = value; stack.push(lastNode); } else if (lastNode instanceof trieNode_1.BranchNode) { stack.push(lastNode); if (keyRemainder.length !== 0) { // add an extension to a branch node keyRemainder.shift(); // create a new leaf const newLeaf = new trieNode_1.LeafNode(keyRemainder, value); stack.push(newLeaf); } else { lastNode.value = value; } } else { // create a branch node const lastKey = lastNode.key; const matchingLength = (0, nibbles_1.matchingNibbleLength)(lastKey, keyRemainder); const newBranchNode = new trieNode_1.BranchNode(); // create a new extension node if (matchingLength !== 0) { const newKey = lastNode.key.slice(0, matchingLength); const newExtNode = new trieNode_1.ExtensionNode(newKey, value); stack.push(newExtNode); lastKey.splice(0, matchingLength); keyRemainder.splice(0, matchingLength); } stack.push(newBranchNode); if (lastKey.length !== 0) { const branchKey = lastKey.shift(); if (lastKey.length !== 0 || lastNode instanceof trieNode_1.LeafNode) { // shrinking extension or leaf lastNode.key = lastKey; const formattedNode = this._formatNode(lastNode, false, toSave); newBranchNode.setBranch(branchKey, formattedNode); } else { // remove extension or attaching this._formatNode(lastNode, false, toSave, true); newBranchNode.setBranch(branchKey, lastNode.value); } } else { newBranchNode.value = lastNode.value; } if (keyRemainder.length !== 0) { keyRemainder.shift(); // add a leaf node to the new branch node const newLeafNode = new trieNode_1.LeafNode(keyRemainder, value); stack.push(newLeafNode); } else { newBranchNode.value = value; } } await this._saveStack(key, stack, toSave); } /** * Deletes a node from the trie. * @private */ async _deleteNode(k, stack) { const processBranchNode = (key, branchKey, branchNode, parentNode, stack) => { // branchNode is the node ON the branch node not THE branch node if (!parentNode || parentNode instanceof trieNode_1.BranchNode) { // branch->? if (parentNode) { stack.push(parentNode); } if (branchNode instanceof trieNode_1.BranchNode) { // create an extension node // branch->extension->branch // @ts-ignore const extensionNode = new trieNode_1.ExtensionNode([branchKey], null); 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 trieNode_1.BranchNode) { // 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 exstention // add two keys together // dont 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(); assert(lastNode); let parentNode = stack.pop(); const opStack = []; let key = (0, nibbles_1.bufferToNibbles)(k); if (!parentNode) { // the root here has to be a leaf. this.root = this.EMPTY_TRIE_ROOT; return; } if (lastNode instanceof trieNode_1.BranchNode) { 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 trieNode_1.BranchNode)) { throw new Error('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 remaing branch node to node above it const branchNode = branchNodes[0][1]; const branchNodeKey = branchNodes[0][0]; // look up node const foundNode = await this._lookupNode(branchNode); if (foundNode) { key = processBranchNode(key, branchNodeKey, foundNode, parentNode, stack); await this._saveStack(key, stack, opStack); } } else { // simple removing a leaf and recaluclation the stack if (parentNode) { stack.push(parentNode); } stack.push(lastNode); await this._saveStack(key, stack, opStack); } } /** * Saves a stack of nodes to the database. * @private * @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 funciton */ async _saveStack(key, stack, opStack) { let lastRoot; // update nodes while (stack.length) { const node = stack.pop(); if (node instanceof trieNode_1.LeafNode) { key.splice(key.length - node.key.length); } else if (node instanceof trieNode_1.ExtensionNode) { key.splice(key.length - node.key.length); if (lastRoot) { node.value = lastRoot; } } else if (node instanceof trieNode_1.BranchNode) { if (lastRoot) { const branchKey = key.pop(); node.setBranch(branchKey, lastRoot); } } lastRoot = this._formatNode(node, stack.length === 0, opStack); } if (lastRoot) { this.root = lastRoot; } await this.db.batch(opStack); } /** * 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 (only used for CheckpointTrie). * @returns The node's hash used as the key or the rawNode. */ _formatNode(node, topLevel, opStack, remove = false) { const rlpNode = node.serialize(); if (rlpNode.length >= 32 || topLevel) { // Do not use TrieNode.hash() here otherwise serialize() // is applied twice (performance) const hashRoot = (0, ethereumjs_util_1.keccak)(rlpNode); if (remove) { if (this._deleteFromDB) { opStack.push({ type: 'del', key: hashRoot, }); } } else { opStack.push({ type: 'put', key: hashRoot, value: rlpNode, }); } return hashRoot; } 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: Buffer.from('father') } * , { type: 'put', key: Buffer.from('name'), value: Buffer.from('Yuri Irsenovich Kim') } * , { type: 'put', key: Buffer.from('dob'), value: Buffer.from('16 February 1941') } * , { type: 'put', key: Buffer.from('spouse'), value: Buffer.from('Kim Young-sook') } * , { type: 'put', key: Buffer.from('occupation'), value: Buffer.from('Clown') } * ] * await trie.batch(ops) * @param ops */ async batch(ops) { for (const op of ops) { if (op.type === 'put') { if (!op.value) { throw new Error('Invalid batch db operation'); } await this.put(op.key, op.value); } else if (op.type === 'del') { await this.del(op.key); } } } /** * Saves the nodes from a proof into the trie. If no trie is provided a new one wil be instantiated. * @param proof * @param trie */ static async fromProof(proof, trie) { const opStack = proof.map((nodeValue) => { return { type: 'put', key: (0, ethereumjs_util_1.keccak)(nodeValue), value: nodeValue, }; }); if (!trie) { trie = new Trie(); if (opStack[0]) { trie.root = opStack[0].key; } } await trie.db.batch(opStack); return trie; } /** * prove has been renamed to {@link Trie.createProof}. * @deprecated * @param trie * @param key */ static async prove(trie, key) { return this.createProof(trie, key); } /** * Creates a proof from a trie and key that can be verified using {@link Trie.verifyProof}. * @param trie * @param key */ static async createProof(trie, key) { const { stack } = await trie.findPath(key); const p = stack.map((stackElem) => { return stackElem.serialize(); }); return p; } /** * Verifies a proof. * @param rootHash * @param key * @param proof * @throws If proof is found to be invalid. * @returns The value from the key, or null if valid proof of non-existence. */ static async verifyProof(rootHash, key, proof) { let proofTrie = new Trie(null, rootHash); try { proofTrie = await Trie.fromProof(proof, proofTrie); } catch (e) { throw new Error('Invalid proof nodes given'); } try { const value = await proofTrie.get(key, true); return value; } catch (err) { if (err.message == 'Missing node in DB') { throw new Error('Invalid proof provided'); } else { throw err; } } } /** * {@link verifyRangeProof} */ static verifyRangeProof(rootHash, firstKey, lastKey, keys, values, proof) { return (0, verifyRangeProof_1.verifyRangeProof)(rootHash, firstKey && (0, nibbles_1.bufferToNibbles)(firstKey), lastKey && (0, nibbles_1.bufferToNibbles)(lastKey), keys.map(nibbles_1.bufferToNibbles), values, proof); } /** * The `data` event is given an `Object` that has two properties; the `key` and the `value`. Both should be Buffers. * @return Returns a [stream](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_readable) of the contents of the `trie` */ createReadStream() { return new readStream_1.TrieReadStream(this); } /** * Creates a new trie backed by the same db. */ copy() { const db = this.db.copy(); return new Trie(db._leveldb, 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, trieNode_1.isRawNode)(nodeRef)) { if (node !== null) { walkController.allChildren(node, key); } } else { onFound(nodeRef, node, key, walkController); } }; await this.walkTrie(this.root, outerOnFound); } /** * Finds all nodes that store k,v values * called by {@link TrieReadStream} * @private */ async _findValueNodes(onFound) { const outerOnFound = async (nodeRef, node, key, walkController) => { let fullKey = key; if (node instanceof trieNode_1.LeafNode) { fullKey = key.concat(node.key); // found leaf node! onFound(nodeRef, node, fullKey, walkController); } else if (node instanceof trieNode_1.BranchNode && node.value) { // found branch with value onFound(nodeRef, node, fullKey, walkController); } else { // keep looking for value nodes if (node !== null) { walkController.allChildren(node, key); } } }; await this.walkTrie(this.root, outerOnFound); } } exports.Trie = Trie; //# sourceMappingURL=baseTrie.js.map