UNPKG

merkle-patricia-tree

Version:

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

805 lines (730 loc) 23.6 kB
import Semaphore from 'semaphore-async-await' import { keccak, KECCAK256_RLP } from 'ethereumjs-util' import { DB, BatchDBOp, PutBatch } from './db' import { TrieReadStream as ReadStream } from './readStream' import { bufferToNibbles, matchingNibbleLength, doKeysMatch } from './util/nibbles' import { WalkController } from './util/walkController' import { TrieNode, decodeNode, decodeRawNode, isRawNode, BranchNode, ExtensionNode, LeafNode, EmbeddedNode, Nibbles, } from './trieNode' import { verifyRangeProof } from './verifyRangeProof' // eslint-disable-next-line implicit-dependencies/no-implicit import type { LevelUp } from 'levelup' const assert = require('assert') export type Proof = Buffer[] interface Path { node: TrieNode | null remaining: Nibbles stack: TrieNode[] } export type FoundNodeFunction = ( nodeRef: Buffer, node: TrieNode | null, key: Nibbles, walkController: WalkController ) => void /** * 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. */ export class Trie { /** The root for an empty trie */ EMPTY_TRIE_ROOT: Buffer protected lock: Semaphore /** The backend DB */ db: DB private _root: Buffer private _deleteFromDB: boolean /** * 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?: LevelUp | null, root?: Buffer, deleteFromDB: boolean = false) { this.EMPTY_TRIE_ROOT = KECCAK256_RLP this.lock = new Semaphore(1) this.db = db ? new DB(db) : new 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: Buffer) { 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(): Buffer { return this._root } /** * This method is deprecated. * Please use {@link Trie.root} instead. * * @param value * @deprecated */ setRoot(value?: Buffer) { this.root = value ?? this.EMPTY_TRIE_ROOT } /** * Checks if a given root exists. */ async checkRoot(root: Buffer): Promise<boolean> { try { const value = await this._lookupNode(root) return value !== null } catch (error: any) { 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: Buffer, throwIfMissing = false): Promise<Buffer | null> { 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: Buffer, value: Buffer): Promise<void> { // If value is empty, delete if (!value || value.toString() === '') { return await this.del(key) } await this.lock.wait() if (this.root.equals(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: Buffer): Promise<void> { 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: Buffer, throwIfMissing = false): Promise<Path> { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const stack: TrieNode[] = [] const targetKey = bufferToNibbles(key) const onFound: FoundNodeFunction = async (nodeRef, node, keyProgress, walkController) => { if (node === null) { return reject(new Error('Path not found')) } const keyRemainder = targetKey.slice(matchingNibbleLength(keyProgress, targetKey)) stack.push(node) if (node instanceof 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 LeafNode) { if (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 ExtensionNode) { const matchingLen = 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: any) { 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: Buffer, onFound: FoundNodeFunction): Promise<void> { await WalkController.newWalk(onFound, this, root) } /** * @hidden * Backwards compatibility * @param root - * @param onFound - */ async _walkTrie(root: Buffer, onFound: FoundNodeFunction): Promise<void> { await this.walkTrie(root, onFound) } /** * Creates the initial node from an empty tree. * @private */ async _createInitialNode(key: Buffer, value: Buffer): Promise<void> { const newNode = new LeafNode(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: Buffer | Buffer[]): Promise<TrieNode | null> { if (isRawNode(node)) { return decodeRawNode(node as Buffer[]) } let value = null let foundNode = null value = await this.db.get(node as Buffer) if (value) { foundNode = 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: Buffer | Buffer[]): Promise<TrieNode | null> { return this.lookupNode(node) } /** * Updates a node. * @private * @param key * @param value * @param keyRemainder * @param stack */ async _updateNode( k: Buffer, value: Buffer, keyRemainder: Nibbles, stack: TrieNode[] ): Promise<void> { const toSave: BatchDBOp[] = [] const lastNode = stack.pop() if (!lastNode) { throw new Error('Stack underflow') } // add the new nodes const key = bufferToNibbles(k) // Check if the last node is a leaf and the key matches to this let matchLeaf = false if (lastNode instanceof LeafNode) { let l = 0 for (let i = 0; i < stack.length; i++) { const n = stack[i] if (n instanceof BranchNode) { l++ } else { l += n.key.length } } if ( 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 as TrieNode) } else if (lastNode instanceof BranchNode) { stack.push(lastNode) if (keyRemainder.length !== 0) { // add an extension to a branch node keyRemainder.shift() // create a new leaf const newLeaf = new LeafNode(keyRemainder, value) stack.push(newLeaf) } else { lastNode.value = value } } else { // create a branch node const lastKey = lastNode.key const matchingLength = matchingNibbleLength(lastKey, keyRemainder) const newBranchNode = new BranchNode() // create a new extension node if (matchingLength !== 0) { const newKey = lastNode.key.slice(0, matchingLength) const newExtNode = new 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() as number if (lastKey.length !== 0 || lastNode instanceof LeafNode) { // shrinking extension or leaf lastNode.key = lastKey const formattedNode = this._formatNode(lastNode, false, toSave) newBranchNode.setBranch(branchKey, formattedNode as EmbeddedNode) } 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 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: Buffer, stack: TrieNode[]): Promise<void> { const processBranchNode = ( key: Nibbles, branchKey: number, branchNode: TrieNode, parentNode: TrieNode, stack: TrieNode[] ) => { // branchNode is the node ON the branch node not THE branch node if (!parentNode || parentNode instanceof BranchNode) { // branch->? if (parentNode) { stack.push(parentNode) } if (branchNode instanceof BranchNode) { // create an extension node // branch->extension->branch // @ts-ignore const extensionNode = new 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 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() as TrieNode assert(lastNode) let parentNode = stack.pop() const opStack: BatchDBOp[] = [] let key = bufferToNibbles(k) if (!parentNode) { // the root here has to be a leaf. this.root = this.EMPTY_TRIE_ROOT return } if (lastNode instanceof 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 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() as number, null) lastNode = parentNode parentNode = stack.pop() } // nodes on the branch // count the number of nodes on the branch const branchNodes: [number, EmbeddedNode][] = 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 as TrieNode, parentNode as TrieNode, 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: Nibbles, stack: TrieNode[], opStack: BatchDBOp[]): Promise<void> { let lastRoot // update nodes while (stack.length) { const node = stack.pop() as TrieNode if (node instanceof LeafNode) { key.splice(key.length - node.key.length) } else if (node instanceof ExtensionNode) { key.splice(key.length - node.key.length) if (lastRoot) { node.value = lastRoot } } else if (node instanceof BranchNode) { if (lastRoot) { const branchKey = key.pop() node.setBranch(branchKey!, lastRoot) } } lastRoot = this._formatNode(node, stack.length === 0, opStack) as Buffer } 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: TrieNode, topLevel: boolean, opStack: BatchDBOp[], remove: boolean = false ): Buffer | (EmbeddedNode | null)[] { const rlpNode = node.serialize() if (rlpNode.length >= 32 || topLevel) { // Do not use TrieNode.hash() here otherwise serialize() // is applied twice (performance) const hashRoot = 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: BatchDBOp[]): Promise<void> { 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: Proof, trie?: Trie): Promise<Trie> { const opStack = proof.map((nodeValue) => { return { type: 'put', key: keccak(nodeValue), value: nodeValue, } as PutBatch }) 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: Trie, key: Buffer): Promise<Proof> { 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: Trie, key: Buffer): Promise<Proof> { 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: Buffer, key: Buffer, proof: Proof): Promise<Buffer | null> { let proofTrie = new Trie(null, rootHash) try { proofTrie = await Trie.fromProof(proof, proofTrie) } catch (e: any) { throw new Error('Invalid proof nodes given') } try { const value = await proofTrie.get(key, true) return value } catch (err: any) { if (err.message == 'Missing node in DB') { throw new Error('Invalid proof provided') } else { throw err } } } /** * {@link verifyRangeProof} */ static verifyRangeProof( rootHash: Buffer, firstKey: Buffer | null, lastKey: Buffer | null, keys: Buffer[], values: Buffer[], proof: Buffer[] | null ): Promise<boolean> { return verifyRangeProof( rootHash, firstKey && bufferToNibbles(firstKey), lastKey && bufferToNibbles(lastKey), keys.map(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(): ReadStream { return new ReadStream(this) } /** * Creates a new trie backed by the same db. */ copy(): Trie { 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: FoundNodeFunction): Promise<void> { const outerOnFound: FoundNodeFunction = async (nodeRef, node, key, walkController) => { if (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: FoundNodeFunction): Promise<void> { const outerOnFound: FoundNodeFunction = async (nodeRef, node, key, walkController) => { let fullKey = key if (node instanceof LeafNode) { fullKey = key.concat(node.key) // found leaf node! onFound(nodeRef, node, fullKey, walkController) } else if (node instanceof 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) } }