merkle-patricia-tree
Version:
This is an implementation of the modified merkle patricia tree as specified in Ethereum's yellow paper.
507 lines (445 loc) • 15.7 kB
text/typescript
import { nibblesToBuffer, nibblesCompare } from './util/nibbles'
import { Trie } from './baseTrie'
import { TrieNode, BranchNode, ExtensionNode, LeafNode, Nibbles } from './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: Trie,
parent: TrieNode,
child: TrieNode | null,
key: Nibbles,
pos: number,
removeLeft: boolean,
stack: TrieNode[]
): Promise<number> {
if (child instanceof 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 ExtensionNode || child instanceof 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 ||
nibblesCompare(child._nibbles, key.slice(pos, pos + child.keyLength)) !== 0
) {
if (removeLeft) {
if (nibblesCompare(child._nibbles, key.slice(pos)) < 0) {
;(parent as BranchNode).setBranch(key[pos - 1], null)
}
} else {
if (nibblesCompare(child._nibbles, key.slice(pos)) > 0) {
;(parent as BranchNode).setBranch(key[pos - 1], null)
}
}
return pos - 1
}
if (child instanceof LeafNode) {
// This node is a leaf node, directly remove it from parent
;(parent as BranchNode).setBranch(key[pos - 1], null)
return pos - 1
} else {
const _child = await trie.lookupNode(child.value)
if (_child && _child instanceof LeafNode) {
// The child of this node is leaf node, remove it from parent too
;(parent as BranchNode).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: Trie, left: Nibbles, right: Nibbles): Promise<boolean> {
// Key position
let pos = 0
// Parent node
let parent: TrieNode | null = null
// Current node
let node: TrieNode | null = await trie.lookupNode(trie.root)
let shortForkLeft!: number
let shortForkRight!: number
// A stack of modified nodes.
const stack: TrieNode[] = []
// 1. Find the fork point of `left` and `right`
// eslint-disable-next-line no-constant-condition
while (true) {
if (node instanceof ExtensionNode || node instanceof LeafNode) {
// record this node on the stack
stack.push(node)
if (left.length - pos < node.keyLength) {
shortForkLeft = nibblesCompare(left.slice(pos), node._nibbles)
} else {
shortForkLeft = nibblesCompare(left.slice(pos, pos + node.keyLength), node._nibbles)
}
if (right.length - pos < node.keyLength) {
shortForkRight = nibblesCompare(right.slice(pos), node._nibbles)
} else {
shortForkRight = 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 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 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: Nibbles, stack: TrieNode[]) => {
return trie._saveStack(key, stack, [])
}
if (node instanceof ExtensionNode || node instanceof 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: Nibbles) => {
if (parent === null) {
return true
}
stack.pop()
;(parent as BranchNode).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 LeafNode) {
return await removeSelfFromParentAndSaveStack(left)
}
const child = await trie.lookupNode(node._value)
if (child && child instanceof 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 LeafNode) {
return await removeSelfFromParentAndSaveStack(right)
}
const child = await trie.lookupNode(node._value)
if (child && child instanceof 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 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: Buffer,
key: Buffer,
proof: Buffer[]
): Promise<{ value: Buffer | null; trie: Trie }> {
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 {
trie: proofTrie,
value,
}
} catch (err: any) {
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: Trie, key: Nibbles): Promise<boolean> {
let pos = 0
let node = await trie.lookupNode(trie.root)
while (node !== null) {
if (node instanceof 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 ExtensionNode) {
if (
key.length - pos < node.keyLength ||
nibblesCompare(node._nibbles, key.slice(pos, pos + node.keyLength)) !== 0
) {
return nibblesCompare(node._nibbles, key.slice(pos)) > 0
}
pos += node.keyLength
node = await trie.lookupNode(node._value)
} else if (node instanceof 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
*/
export async function verifyRangeProof(
rootHash: Buffer,
firstKey: Nibbles | null,
lastKey: Nibbles | null,
keys: Nibbles[],
values: Buffer[],
proof: Buffer[] | null
): Promise<boolean> {
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 (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 Trie()
for (let i = 0; i < keys.length; i++) {
await trie.put(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, 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 && nibblesCompare(firstKey, lastKey) === 0) {
const { trie, value } = await verifyProof(rootHash, nibblesToBuffer(firstKey), proof)
if (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 (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 Trie(null, rootHash)
trie = await 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(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])
}