UNPKG

merkle-patricia-tree

Version:

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

446 lines 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyRangeProof = void 0; const nibbles_1 = require("./util/nibbles"); const baseTrie_1 = require("./baseTrie"); const trieNode_1 = require("./trieNode"); // 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 trieNode_1.BranchNode) { /** * 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 await unset(trie, child, _child, key, pos + 1, removeLeft, stack); } else if (child instanceof trieNode_1.ExtensionNode || child instanceof trieNode_1.LeafNode) { /** * 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_1.nibblesCompare)(child._nibbles, key.slice(pos, pos + child.keyLength)) !== 0) { if (removeLeft) { if ((0, nibbles_1.nibblesCompare)(child._nibbles, key.slice(pos)) < 0) { ; parent.setBranch(key[pos - 1], null); } } else { if ((0, nibbles_1.nibblesCompare)(child._nibbles, key.slice(pos)) > 0) { ; parent.setBranch(key[pos - 1], null); } } return pos - 1; } if (child instanceof trieNode_1.LeafNode) { // 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 && _child instanceof trieNode_1.LeafNode) { // 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 await unset(trie, child, _child, key, pos + child.keyLength, removeLeft, stack); } } else if (child === null) { return pos - 1; } else { throw new Error('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` // eslint-disable-next-line no-constant-condition while (true) { if (node instanceof trieNode_1.ExtensionNode || node instanceof trieNode_1.LeafNode) { // record this node on the stack stack.push(node); if (left.length - pos < node.keyLength) { shortForkLeft = (0, nibbles_1.nibblesCompare)(left.slice(pos), node._nibbles); } else { shortForkLeft = (0, nibbles_1.nibblesCompare)(left.slice(pos, pos + node.keyLength), node._nibbles); } if (right.length - pos < node.keyLength) { shortForkRight = (0, nibbles_1.nibblesCompare)(right.slice(pos), node._nibbles); } else { shortForkRight = (0, nibbles_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 trieNode_1.LeafNode) { // it shouldn't happen throw new Error('invalid node'); } // continue to the next node parent = node; pos += node.keyLength; node = await trie.lookupNode(node.value); } else if (node instanceof trieNode_1.BranchNode) { // 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 Buffer)) { if (rightNode instanceof Buffer) { break; } if (leftNode.length !== rightNode.length) { break; } let abort = false; for (let i = 0; i < leftNode.length; i++) { if (leftNode[i].compare(rightNode[i]) !== 0) { abort = true; break; } } if (abort) { break; } } else { if (!(rightNode instanceof Buffer)) { break; } if (leftNode.compare(rightNode) !== 0) { break; } } // continue to the next node parent = node; node = await trie.lookupNode(leftNode); pos += 1; } else { throw new Error('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 trieNode_1.ExtensionNode || node instanceof trieNode_1.LeafNode) { /** * 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 new Error('invalid range'); } if (shortForkLeft === 1 && shortForkRight === 1) { throw new Error('invalid range'); } if (shortForkLeft !== 0 && shortForkRight !== 0) { // Unset the entire trie return await removeSelfFromParentAndSaveStack(left); } // Unset left node if (shortForkRight !== 0) { if (node instanceof trieNode_1.LeafNode) { return await removeSelfFromParentAndSaveStack(left); } const child = await trie.lookupNode(node._value); if (child && child instanceof trieNode_1.LeafNode) { return await 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 trieNode_1.LeafNode) { return await removeSelfFromParentAndSaveStack(right); } const child = await trie.lookupNode(node._value); if (child && child instanceof trieNode_1.LeafNode) { return await 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 trieNode_1.BranchNode) { // 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 new Error('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 verifyProof(rootHash, key, proof) { let proofTrie = new baseTrie_1.Trie(null, rootHash); try { proofTrie = await baseTrie_1.Trie.fromProof(proof, proofTrie); } catch (e) { throw new Error('Invalid proof nodes given'); } try { const value = await proofTrie.get(key, true); return { trie: proofTrie, value, }; } catch (err) { if (err.message == 'Missing node in DB') { throw new Error('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 trieNode_1.BranchNode) { 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 trieNode_1.ExtensionNode) { if (key.length - pos < node.keyLength || (0, nibbles_1.nibblesCompare)(node._nibbles, key.slice(pos, pos + node.keyLength)) !== 0) { return (0, nibbles_1.nibblesCompare)(node._nibbles, key.slice(pos)) > 0; } pos += node.keyLength; node = await trie.lookupNode(node._value); } else if (node instanceof trieNode_1.LeafNode) { return false; } else { throw new Error('invalid node'); } } return false; } /** * verifyRangeProof checks whether the given leaf nodes and edge proof * can prove the given trie leaves range is matched with the specific root. * * 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(fisrt and last) should be provided. * * NOTE: Currently only supports verification when the length of firstKey and lastKey are the same. * * @param rootHash - root hash. * @param firstKey - first key. * @param lastKey - last key. * @param keys - key list. * @param values - value list, one-to-one correspondence with keys. * @param proof - proof node list, if proof is null, both `firstKey` and `lastKey` must be null * @returns a flag to indicate whether there exists more trie node in the trie */ async function verifyRangeProof(rootHash, firstKey, lastKey, keys, values, proof) { if (keys.length !== values.length) { throw new Error('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_1.nibblesCompare)(keys[i], keys[i + 1]) >= 0) { throw new Error('invalid keys order'); } } // Make sure all values are present for (const value of values) { if (value.length === 0) { throw new Error('invalid values'); } } // All elements proof if (proof === null && firstKey === null && lastKey === null) { const trie = new baseTrie_1.Trie(); for (let i = 0; i < keys.length; i++) { await trie.put((0, nibbles_1.nibblesToBuffer)(keys[i]), values[i]); } if (rootHash.compare(trie.root) !== 0) { throw new Error('invalid all elements proof: root mismatch'); } return false; } if (proof === null || firstKey === null || lastKey === null) { throw new Error('invalid all elements proof: proof, firstKey, lastKey must be null at the same time'); } // Zero element proof if (keys.length === 0) { const { trie, value } = await verifyProof(rootHash, (0, nibbles_1.nibblesToBuffer)(firstKey), proof); if (value !== null || (await hasRightElement(trie, firstKey))) { throw new Error('invalid zero element proof: value mismatch'); } return false; } // One element proof if (keys.length === 1 && (0, nibbles_1.nibblesCompare)(firstKey, lastKey) === 0) { const { trie, value } = await verifyProof(rootHash, (0, nibbles_1.nibblesToBuffer)(firstKey), proof); if ((0, nibbles_1.nibblesCompare)(firstKey, keys[0]) !== 0) { throw new Error('invalid one element proof: firstKey should be equal to keys[0]'); } if (value === null || value.compare(values[0]) !== 0) { throw new Error('invalid one element proof: value mismatch'); } return hasRightElement(trie, firstKey); } // Two edge elements proof if ((0, nibbles_1.nibblesCompare)(firstKey, lastKey) >= 0) { throw new Error('invalid two edge elements proof: firstKey should be less than lastKey'); } if (firstKey.length !== lastKey.length) { throw new Error('invalid two edge elements proof: the length of firstKey should be equal to the length of lastKey'); } let trie = new baseTrie_1.Trie(null, rootHash); trie = await baseTrie_1.Trie.fromProof(proof, trie); // 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_1.nibblesToBuffer)(keys[i]), values[i]); } // Compare rootHash if (trie.root.compare(rootHash) !== 0) { throw new Error('invalid two edge elements proof: root mismatch'); } return hasRightElement(trie, keys[keys.length - 1]); } exports.verifyRangeProof = verifyRangeProof; //# sourceMappingURL=verifyRangeProof.js.map