UNPKG

@ethereumjs/mpt

Version:

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

456 lines 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyMerkleRangeProof = verifyMerkleRangeProof; const util_1 = require("@ethereumjs/util"); const keccak_js_1 = require("ethereum-cryptography/keccak.js"); const index_ts_1 = require("../index.js"); const mpt_ts_1 = require("../mpt.js"); const index_ts_2 = require("../node/index.js"); const nibbles_ts_1 = require("../util/nibbles.js"); // reference: https://github.com/ethereum/go-ethereum/blob/20356e57b119b4e70ce47665a71964434e15200d/trie/proof.go /** * unset will remove all nodes to the left or right of the target key(decided by `removeLeft`). * @param trie - trie object. * @param parent - parent node, it can be `null`. * @param child - child node. * @param key - target nibbles. * @param pos - key position. * @param removeLeft - remove all nodes to the left or right of the target key. * @param stack - a stack of modified nodes. * @returns The end position of key. */ async function unset(trie, parent, child, key, pos, removeLeft, stack) { if (child instanceof index_ts_2.BranchMPTNode) { /** * This node is a branch node, * remove all branches on the left or right */ if (removeLeft) { for (let i = 0; i < key[pos]; i++) { child.setBranch(i, null); } } else { for (let i = key[pos] + 1; i < 16; i++) { child.setBranch(i, null); } } // record this node on the stack stack.push(child); // continue to the next node const next = child.getBranch(key[pos]); const _child = next && (await trie.lookupNode(next)); return unset(trie, child, _child, key, pos + 1, removeLeft, stack); } else if (child instanceof index_ts_2.ExtensionMPTNode || child instanceof index_ts_2.LeafMPTNode) { /** * This node is an extension node or lead node, * if node._nibbles is less or greater than the target key, * remove self from parent */ if (key.length - pos < child.keyLength() || (0, nibbles_ts_1.nibblesCompare)(child._nibbles, key.slice(pos, pos + child.keyLength())) !== 0) { if (removeLeft) { if ((0, nibbles_ts_1.nibblesCompare)(child._nibbles, key.slice(pos)) < 0) { ; parent.setBranch(key[pos - 1], null); } } else { if ((0, nibbles_ts_1.nibblesCompare)(child._nibbles, key.slice(pos)) > 0) { ; parent.setBranch(key[pos - 1], null); } } return pos - 1; } if (child instanceof index_ts_2.LeafMPTNode) { // This node is a leaf node, directly remove it from parent ; parent.setBranch(key[pos - 1], null); return pos - 1; } else { const _child = await trie.lookupNode(child.value()); if (_child instanceof index_ts_2.LeafMPTNode) { // The child of this node is leaf node, remove it from parent too ; parent.setBranch(key[pos - 1], null); return pos - 1; } // record this node on the stack stack.push(child); // continue to the next node return unset(trie, child, _child, key, pos + child.keyLength(), removeLeft, stack); } } else if (child === null) { return pos - 1; } else { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid node'); } } /** * unsetInternal will remove all nodes between `left` and `right` (including `left` and `right`) * @param trie - trie object. * @param left - left nibbles. * @param right - right nibbles. * @returns Is it an empty trie. */ async function unsetInternal(trie, left, right) { // Key position let pos = 0; // Parent node let parent = null; // Current node let node = await trie.lookupNode(trie.root()); let shortForkLeft; let shortForkRight; // A stack of modified nodes. const stack = []; // 1. Find the fork point of `left` and `right` while (true) { if (node instanceof index_ts_2.ExtensionMPTNode || node instanceof index_ts_2.LeafMPTNode) { // record this node on the stack stack.push(node); if (left.length - pos < node.keyLength()) { shortForkLeft = (0, nibbles_ts_1.nibblesCompare)(left.slice(pos), node._nibbles); } else { shortForkLeft = (0, nibbles_ts_1.nibblesCompare)(left.slice(pos, pos + node.keyLength()), node._nibbles); } if (right.length - pos < node.keyLength()) { shortForkRight = (0, nibbles_ts_1.nibblesCompare)(right.slice(pos), node._nibbles); } else { shortForkRight = (0, nibbles_ts_1.nibblesCompare)(right.slice(pos, pos + node.keyLength()), node._nibbles); } // If one of `left` and `right` is not equal to node._nibbles, it means we found the fork point if (shortForkLeft !== 0 || shortForkRight !== 0) { break; } if (node instanceof index_ts_2.LeafMPTNode) { // it shouldn't happen throw (0, util_1.EthereumJSErrorWithoutCode)('invalid node'); } // continue to the next node parent = node; pos += node.keyLength(); node = await trie.lookupNode(node.value()); } else if (node instanceof index_ts_2.BranchMPTNode) { // record this node on the stack stack.push(node); const leftNode = node.getBranch(left[pos]); const rightNode = node.getBranch(right[pos]); // One of `left` and `right` is `null`, stop searching if (leftNode === null || rightNode === null) { break; } // Stop searching if `left` and `right` are not equal if (!(leftNode instanceof Uint8Array)) { if (rightNode instanceof Uint8Array) { break; } if (leftNode.length !== rightNode.length) { break; } let abort = false; for (let i = 0; i < leftNode.length; i++) { if (!(0, util_1.equalsBytes)(leftNode[i], rightNode[i])) { abort = true; break; } } if (abort) { break; } } else { if (!(rightNode instanceof Uint8Array)) { break; } if (!(0, util_1.equalsBytes)(leftNode, rightNode)) { break; } } // continue to the next node parent = node; node = await trie.lookupNode(leftNode); pos += 1; } else { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid node'); } } // 2. Starting from the fork point, delete all nodes between `left` and `right` const saveStack = (key, stack) => { return trie.saveStack(key, stack, []); }; if (node instanceof index_ts_2.ExtensionMPTNode || node instanceof index_ts_2.LeafMPTNode) { /** * There can have these five scenarios: * - both proofs are less than the trie path => no valid range * - both proofs are greater than the trie path => no valid range * - left proof is less and right proof is greater => valid range, unset the entire trie * - left proof points to the trie node, but right proof is greater => valid range, unset left node * - right proof points to the trie node, but left proof is less => valid range, unset right node */ const removeSelfFromParentAndSaveStack = async (key) => { if (parent === null) { return true; } stack.pop(); parent.setBranch(key[pos - 1], null); await saveStack(key.slice(0, pos - 1), stack); return false; }; if (shortForkLeft === -1 && shortForkRight === -1) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid range'); } if (shortForkLeft === 1 && shortForkRight === 1) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid range'); } if (shortForkLeft !== 0 && shortForkRight !== 0) { // Unset the entire trie return removeSelfFromParentAndSaveStack(left); } // Unset left node if (shortForkRight !== 0) { if (node instanceof index_ts_2.LeafMPTNode) { return removeSelfFromParentAndSaveStack(left); } const child = await trie.lookupNode(node._value); if (child instanceof index_ts_2.LeafMPTNode) { return removeSelfFromParentAndSaveStack(left); } const endPos = await unset(trie, node, child, left.slice(pos), node.keyLength(), false, stack); await saveStack(left.slice(0, pos + endPos), stack); return false; } // Unset right node if (shortForkLeft !== 0) { if (node instanceof index_ts_2.LeafMPTNode) { return removeSelfFromParentAndSaveStack(right); } const child = await trie.lookupNode(node._value); if (child instanceof index_ts_2.LeafMPTNode) { return removeSelfFromParentAndSaveStack(right); } const endPos = await unset(trie, node, child, right.slice(pos), node.keyLength(), true, stack); await saveStack(right.slice(0, pos + endPos), stack); return false; } return false; } else if (node instanceof index_ts_2.BranchMPTNode) { // Unset all internal nodes in the forkPoint for (let i = left[pos] + 1; i < right[pos]; i++) { node.setBranch(i, null); } { /** * `stack` records the path from root to fork point. * Since we need to unset both left and right nodes once, * we need to make a copy here. */ const _stack = [...stack]; const next = node.getBranch(left[pos]); const child = next && (await trie.lookupNode(next)); const endPos = await unset(trie, node, child, left.slice(pos), 1, false, _stack); await saveStack(left.slice(0, pos + endPos), _stack); } { const _stack = [...stack]; const next = node.getBranch(right[pos]); const child = next && (await trie.lookupNode(next)); const endPos = await unset(trie, node, child, right.slice(pos), 1, true, _stack); await saveStack(right.slice(0, pos + endPos), _stack); } return false; } else { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid node'); } } /** * Verifies a proof and return the verified trie. * @param rootHash - root hash. * @param key - target key. * @param proof - proof node list. * @throws If proof is found to be invalid. * @returns The value from the key, or null if valid proof of non-existence. */ async function verifyMPTWithMerkleProof(rootHash, key, proof, useKeyHashingFunction) { const proofTrie = await (0, index_ts_1.createMPTFromProof)(proof, { root: rootHash, useKeyHashingFunction, }); try { const value = await proofTrie.get(key, true); return { trie: proofTrie, value, }; } catch (err) { if (err.message === 'Missing node in DB') { throw (0, util_1.EthereumJSErrorWithoutCode)('Invalid proof provided'); } else { throw err; } } } /** * hasRightElement returns the indicator whether there exists more elements * on the right side of the given path * @param trie - trie object. * @param key - given path. */ async function hasRightElement(trie, key) { let pos = 0; let node = await trie.lookupNode(trie.root()); while (node !== null) { if (node instanceof index_ts_2.BranchMPTNode) { for (let i = key[pos] + 1; i < 16; i++) { if (node.getBranch(i) !== null) { return true; } } const next = node.getBranch(key[pos]); node = next && (await trie.lookupNode(next)); pos += 1; } else if (node instanceof index_ts_2.ExtensionMPTNode) { if (key.length - pos < node.keyLength() || (0, nibbles_ts_1.nibblesCompare)(node._nibbles, key.slice(pos, pos + node.keyLength())) !== 0) { return (0, nibbles_ts_1.nibblesCompare)(node._nibbles, key.slice(pos)) > 0; } pos += node.keyLength(); node = await trie.lookupNode(node._value); } else if (node instanceof index_ts_2.LeafMPTNode) { return false; } else { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid node'); } } return false; } /** * Checks whether the given leaf nodes and edge proof can prove the given trie leaves range is matched with the specific root. * * A range proof is a proof that includes the encoded trie nodes from the root node to leaf node for one or more branches of a trie, * allowing an entire range of leaf nodes to be validated. This is useful in applications such as snap sync where contiguous ranges * of state trie data is received and validated for constructing world state, locally. * * There are four situations: * * - All elements proof. In this case the proof can be null, but the range should * be all the leaves in the trie. * * - One element proof. In this case no matter the edge proof is a non-existent * proof or not, we can always verify the correctness of the proof. * * - Zero element proof. In this case a single non-existent proof is enough to prove. * Besides, if there are still some other leaves available on the right side, then * an error will be returned. * * - Two edge elements proof. In this case two existent or non-existent proof(first and last) should be provided. * * NOTE: Currently only supports verification when the length of firstKey and lastKey are the same. * * @param rootHash - root hash of state trie this proof is being verified against. * @param firstKey - first key of range being proven. * @param lastKey - last key of range being proven. * @param keys - key list of leaf data being proven. * @param values - value list of leaf data being proven, one-to-one correspondence with keys. * @param proof - proof node list, if all-elements-proof where no proof is needed, proof should be null, and both `firstKey` and `lastKey` must be null as well * @param opts - optional, the opts may include a custom hashing function to use with the trie for proof verification * @returns a flag to indicate whether there exists more trie node in the trie */ async function verifyMerkleRangeProof(rootHash, firstKeyRaw, lastKeyRaw, keysRaw, values, proof, useKeyHashingFunction = keccak_js_1.keccak256) { // Convert Uint8Array keys to nibbles const firstKey = firstKeyRaw !== null ? (0, nibbles_ts_1.bytesToNibbles)(firstKeyRaw) : null; const lastKey = lastKeyRaw !== null ? (0, nibbles_ts_1.bytesToNibbles)(lastKeyRaw) : null; const keys = keysRaw.map(nibbles_ts_1.bytesToNibbles); if (keys.length !== values.length) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid keys length or values length'); } // Make sure the keys are in order for (let i = 0; i < keys.length - 1; i++) { if ((0, nibbles_ts_1.nibblesCompare)(keys[i], keys[i + 1]) >= 0) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid keys order'); } } // Make sure all values are present for (const value of values) { if (value.length === 0) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid values'); } } // All elements proof if (proof === null && firstKey === null && lastKey === null) { const trie = new mpt_ts_1.MerklePatriciaTrie({ useKeyHashingFunction }); for (let i = 0; i < keys.length; i++) { await trie.put((0, nibbles_ts_1.nibblesTypeToPackedBytes)(keys[i]), values[i]); } if (!(0, util_1.equalsBytes)(rootHash, trie.root())) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid all elements proof: root mismatch'); } return false; } if (proof !== null && firstKey !== null && lastKey === null) { // Zero element proof if (keys.length === 0) { const { trie, value } = await verifyMPTWithMerkleProof(rootHash, (0, nibbles_ts_1.nibblesTypeToPackedBytes)(firstKey), proof, useKeyHashingFunction); if (value !== null || (await hasRightElement(trie, firstKey))) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid zero element proof: value mismatch'); } return false; } } if (proof === null || firstKey === null || lastKey === null) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid all elements proof: proof, firstKey, lastKey must be null at the same time'); } // One element proof if (keys.length === 1 && (0, nibbles_ts_1.nibblesCompare)(firstKey, lastKey) === 0) { const { trie, value } = await verifyMPTWithMerkleProof(rootHash, (0, nibbles_ts_1.nibblesTypeToPackedBytes)(firstKey), proof, useKeyHashingFunction); if ((0, nibbles_ts_1.nibblesCompare)(firstKey, keys[0]) !== 0) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid one element proof: firstKey should be equal to keys[0]'); } if (value === null || !(0, util_1.equalsBytes)(value, values[0])) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid one element proof: value mismatch'); } return hasRightElement(trie, firstKey); } // Two edge elements proof if ((0, nibbles_ts_1.nibblesCompare)(firstKey, lastKey) >= 0) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid two edge elements proof: firstKey should be less than lastKey'); } if (firstKey.length !== lastKey.length) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid two edge elements proof: the length of firstKey should be equal to the length of lastKey'); } const trie = await (0, index_ts_1.createMPTFromProof)(proof, { useKeyHashingFunction, root: rootHash, }); // Remove all nodes between two edge proofs const empty = await unsetInternal(trie, firstKey, lastKey); if (empty) { trie.root(trie.EMPTY_TRIE_ROOT); } // Put all elements to the trie for (let i = 0; i < keys.length; i++) { await trie.put((0, nibbles_ts_1.nibblesTypeToPackedBytes)(keys[i]), values[i]); } // Compare rootHash if (!(0, util_1.equalsBytes)(trie.root(), rootHash)) { throw (0, util_1.EthereumJSErrorWithoutCode)('invalid two edge elements proof: root mismatch'); } return hasRightElement(trie, keys[keys.length - 1]); } //# sourceMappingURL=range.js.map