|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.27; |
| 3 | + |
| 4 | +import {Math} from "../math/Math.sol"; |
| 5 | +import {Bytes} from "../Bytes.sol"; |
| 6 | +import {Memory} from "../Memory.sol"; |
| 7 | +import {RLP} from "../RLP.sol"; |
| 8 | + |
| 9 | +/** |
| 10 | + * @dev Library for verifying Ethereum Merkle-Patricia trie inclusion proofs. |
| 11 | + * |
| 12 | + * The {traverse} and {verify} functions can be used to prove the following value: |
| 13 | + * |
| 14 | + * * Transaction against the transactionsRoot of a block. |
| 15 | + * * Event against receiptsRoot of a block. |
| 16 | + * * Account details (RLP encoding of [nonce, balance, storageRoot, codeHash]) against the stateRoot of a block. |
| 17 | + * * Storage slot (RLP encoding of the value) against the storageRoot of a account. |
| 18 | + * |
| 19 | + * Proving a storage slot is usually done in 3 steps: |
| 20 | + * |
| 21 | + * * From the stateRoot of a block, process the account proof (see `eth_getProof`) to get the account details. |
| 22 | + * * RLP decode the account details to extract the storageRoot. |
| 23 | + * * Use storageRoot of that account to process the storageProof (again, see `eth_getProof`). |
| 24 | + * |
| 25 | + * See https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie[Merkle-Patricia trie] |
| 26 | + * |
| 27 | + * Based on https://github.com/ethereum-optimism/optimism/blob/ef970556e668b271a152124023a8d6bb5159bacf/packages/contracts-bedrock/src/libraries/trie/MerkleTrie.sol[this implementation from optimism]. |
| 28 | + */ |
| 29 | +library TrieProof { |
| 30 | + using Bytes for *; |
| 31 | + using RLP for *; |
| 32 | + using Memory for *; |
| 33 | + |
| 34 | + enum Prefix { |
| 35 | + EXTENSION_EVEN, // 0 - Extension node with even length path |
| 36 | + EXTENSION_ODD, // 1 - Extension node with odd length path |
| 37 | + LEAF_EVEN, // 2 - Leaf node with even length path |
| 38 | + LEAF_ODD // 3 - Leaf node with odd length path |
| 39 | + } |
| 40 | + |
| 41 | + enum ProofError { |
| 42 | + NO_ERROR, // No error occurred during proof traversal |
| 43 | + EMPTY_KEY, // The provided key is empty |
| 44 | + INVALID_ROOT, // The validation of the root node failed |
| 45 | + INVALID_LARGE_NODE, // The validation of a large node failed |
| 46 | + INVALID_SHORT_NODE, // The validation of a short node failed |
| 47 | + EMPTY_PATH, // The path in a leaf or extension node is empty |
| 48 | + INVALID_PATH_REMAINDER, // The path remainder in a leaf or extension node is invalid |
| 49 | + EMPTY_EXTENSION_PATH_REMAINDER, // The path remainder in an extension node is empty |
| 50 | + INVALID_EXTRA_PROOF_ELEMENT, // A leaf value should be the last proof element |
| 51 | + EMPTY_VALUE, // The leaf value is empty |
| 52 | + MISMATCH_LEAF_PATH_KEY_REMAINDER, // The path remainder in a leaf node doesn't match the key remainder |
| 53 | + UNKNOWN_NODE_PREFIX, // The node prefix is unknown |
| 54 | + UNPARSEABLE_NODE, // The node cannot be parsed from RLP encoding |
| 55 | + INVALID_PROOF // General failure during proof traversal |
| 56 | + } |
| 57 | + |
| 58 | + error TrieProofTraversalError(ProofError err); |
| 59 | + |
| 60 | + /// @dev The radix of the Ethereum trie |
| 61 | + uint256 internal constant EVM_TREE_RADIX = 16; |
| 62 | + |
| 63 | + /// @dev Number of items in a branch node (16 children + 1 value) |
| 64 | + uint256 internal constant BRANCH_NODE_LENGTH = EVM_TREE_RADIX + 1; |
| 65 | + |
| 66 | + /// @dev Number of items in leaf or extension nodes (always 2) |
| 67 | + uint256 internal constant LEAF_OR_EXTENSION_NODE_LENGTH = 2; |
| 68 | + |
| 69 | + /// @dev Verifies a `proof` against a given `key`, `value`, `and root` hash. |
| 70 | + function verify( |
| 71 | + bytes memory value, |
| 72 | + bytes32 root, |
| 73 | + bytes memory key, |
| 74 | + bytes[] memory proof |
| 75 | + ) internal pure returns (bool) { |
| 76 | + (bytes memory processedValue, ProofError err) = tryTraverse(root, key, proof); |
| 77 | + return processedValue.equal(value) && err == ProofError.NO_ERROR; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * @dev Traverses a proof with a given key and returns the value. |
| 82 | + * |
| 83 | + * Reverts with {TrieProofTraversalError} if proof is invalid. |
| 84 | + */ |
| 85 | + function traverse(bytes32 root, bytes memory key, bytes[] memory proof) internal pure returns (bytes memory) { |
| 86 | + (bytes memory value, ProofError err) = tryTraverse(root, key, proof); |
| 87 | + require(err == ProofError.NO_ERROR, TrieProofTraversalError(err)); |
| 88 | + return value; |
| 89 | + } |
| 90 | + |
| 91 | + /** |
| 92 | + * @dev Traverses a proof with a given key and returns the value and an error flag |
| 93 | + * instead of reverting if the proof is invalid. This function may still revert if |
| 94 | + * malformed input leads to RLP decoding errors. |
| 95 | + */ |
| 96 | + function tryTraverse( |
| 97 | + bytes32 root, |
| 98 | + bytes memory key, |
| 99 | + bytes[] memory proof |
| 100 | + ) internal pure returns (bytes memory value, ProofError err) { |
| 101 | + if (key.length == 0) return (_emptyBytesMemory(), ProofError.EMPTY_KEY); |
| 102 | + |
| 103 | + // Expand the key |
| 104 | + bytes memory keyExpanded = key.toNibbles(); |
| 105 | + |
| 106 | + bytes32 currentNodeId; |
| 107 | + uint256 currentNodeIdLength; |
| 108 | + |
| 109 | + // Free memory pointer cache |
| 110 | + Memory.Pointer fmp = Memory.getFreeMemoryPointer(); |
| 111 | + |
| 112 | + // Traverse proof |
| 113 | + uint256 keyIndex = 0; |
| 114 | + uint256 proofLength = proof.length; |
| 115 | + for (uint256 i = 0; i < proofLength; ++i) { |
| 116 | + // validates the encoded node matches the expected node id |
| 117 | + bytes memory encoded = proof[i]; |
| 118 | + if (keyIndex == 0) { |
| 119 | + // Root node must match root hash |
| 120 | + if (keccak256(encoded) != root) return (_emptyBytesMemory(), ProofError.INVALID_ROOT); |
| 121 | + } else if (encoded.length >= 32) { |
| 122 | + // Large nodes are stored as hashes |
| 123 | + if (currentNodeIdLength != 32 || keccak256(encoded) != currentNodeId) |
| 124 | + return (_emptyBytesMemory(), ProofError.INVALID_LARGE_NODE); |
| 125 | + } else { |
| 126 | + // Small nodes must match directly |
| 127 | + if (currentNodeIdLength != encoded.length || bytes32(encoded) != currentNodeId) |
| 128 | + return (_emptyBytesMemory(), ProofError.INVALID_SHORT_NODE); |
| 129 | + } |
| 130 | + |
| 131 | + // decode the current node as an RLP list, and process it |
| 132 | + Memory.Slice[] memory decoded = encoded.decodeList(); |
| 133 | + if (decoded.length == BRANCH_NODE_LENGTH) { |
| 134 | + // If we've consumed the entire key, the value must be in the last slot |
| 135 | + // Otherwise, continue down the branch specified by the next nibble in the key |
| 136 | + if (keyIndex == keyExpanded.length) { |
| 137 | + return _validateLastItem(decoded[EVM_TREE_RADIX], proofLength, i); |
| 138 | + } else { |
| 139 | + bytes1 branchKey = keyExpanded[keyIndex]; |
| 140 | + (currentNodeId, currentNodeIdLength) = _getNodeId(decoded[uint8(branchKey)]); |
| 141 | + keyIndex += 1; |
| 142 | + } |
| 143 | + } else if (decoded.length == LEAF_OR_EXTENSION_NODE_LENGTH) { |
| 144 | + bytes memory path = decoded[0].readBytes().toNibbles(); // expanded path |
| 145 | + // The following is equivalent to path.length < 2 because toNibbles can't return odd-length buffers |
| 146 | + if (path.length == 0) { |
| 147 | + return (_emptyBytesMemory(), ProofError.EMPTY_PATH); |
| 148 | + } |
| 149 | + uint8 prefix = uint8(path[0]); |
| 150 | + Memory.Slice keyRemainder = keyExpanded.asSlice().slice(keyIndex); // Remaining key to match |
| 151 | + Memory.Slice pathRemainder = path.asSlice().slice(2 - (prefix % 2)); // Path after the prefix |
| 152 | + uint256 pathRemainderLength = pathRemainder.length(); |
| 153 | + |
| 154 | + // pathRemainder must not be longer than keyRemainder, and it must be a prefix of it |
| 155 | + if ( |
| 156 | + pathRemainderLength > keyRemainder.length() || |
| 157 | + !pathRemainder.equal(keyRemainder.slice(0, pathRemainderLength)) |
| 158 | + ) { |
| 159 | + return (_emptyBytesMemory(), ProofError.INVALID_PATH_REMAINDER); |
| 160 | + } |
| 161 | + |
| 162 | + if (prefix <= uint8(Prefix.EXTENSION_ODD)) { |
| 163 | + // Eq to: prefix == EXTENSION_EVEN || prefix == EXTENSION_ODD |
| 164 | + if (pathRemainderLength == 0) { |
| 165 | + return (_emptyBytesMemory(), ProofError.EMPTY_EXTENSION_PATH_REMAINDER); |
| 166 | + } |
| 167 | + // Increment keyIndex by the number of nibbles consumed and continue traversal |
| 168 | + (currentNodeId, currentNodeIdLength) = _getNodeId(decoded[1]); |
| 169 | + keyIndex += pathRemainderLength; |
| 170 | + } else if (prefix <= uint8(Prefix.LEAF_ODD)) { |
| 171 | + // Eq to: prefix == LEAF_EVEN || prefix == LEAF_ODD |
| 172 | + // |
| 173 | + // Leaf node (terminal) - return its value if key matches completely |
| 174 | + // we already know that pathRemainder is a prefix of keyRemainder, so checking the length sufficient |
| 175 | + return |
| 176 | + pathRemainderLength == keyRemainder.length() |
| 177 | + ? _validateLastItem(decoded[1], proofLength, i) |
| 178 | + : (_emptyBytesMemory(), ProofError.MISMATCH_LEAF_PATH_KEY_REMAINDER); |
| 179 | + } else { |
| 180 | + return (_emptyBytesMemory(), ProofError.UNKNOWN_NODE_PREFIX); |
| 181 | + } |
| 182 | + } else { |
| 183 | + return (_emptyBytesMemory(), ProofError.UNPARSEABLE_NODE); |
| 184 | + } |
| 185 | + |
| 186 | + // Reset memory before next iteration. Deallocates `decoded` and `path`. |
| 187 | + Memory.setFreeMemoryPointer(fmp); |
| 188 | + } |
| 189 | + |
| 190 | + // If we've gone through all proof elements without finding a value, the proof is invalid |
| 191 | + return (_emptyBytesMemory(), ProofError.INVALID_PROOF); |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * @dev Validates that we've reached a valid leaf value and this is the last proof element. |
| 196 | + * Ensures the value is not empty and no extra proof elements exist. |
| 197 | + */ |
| 198 | + function _validateLastItem( |
| 199 | + Memory.Slice item, |
| 200 | + uint256 trieProofLength, |
| 201 | + uint256 i |
| 202 | + ) private pure returns (bytes memory, ProofError) { |
| 203 | + if (i != trieProofLength - 1) { |
| 204 | + return (_emptyBytesMemory(), ProofError.INVALID_EXTRA_PROOF_ELEMENT); |
| 205 | + } |
| 206 | + bytes memory value = item.readBytes(); |
| 207 | + return (value, value.length == 0 ? ProofError.EMPTY_VALUE : ProofError.NO_ERROR); |
| 208 | + } |
| 209 | + |
| 210 | + /** |
| 211 | + * @dev Extracts the node ID (hash or raw data based on size) |
| 212 | + * |
| 213 | + * For small nodes (encoded length < 32 bytes) the node ID is the node content itself, |
| 214 | + * For larger nodes, the node ID is the hash of the encoded node data. |
| 215 | + * |
| 216 | + * NOTE: Under normal operation, the input should never be exactly 32-byte inputs. If such an input is provided, |
| 217 | + * it will be used directly, similarly to how small nodes are processed. The following traversal check whether |
| 218 | + * the next node is a large one, and whether its hash matches the raw 32 bytes we have here. If that is the case, |
| 219 | + * the value will be accepted. Otherwise, the next step will return an {INVALID_LARGE_NODE} error. |
| 220 | + */ |
| 221 | + function _getNodeId(Memory.Slice node) private pure returns (bytes32 nodeId, uint256 nodeIdLength) { |
| 222 | + uint256 nodeLength = node.length(); |
| 223 | + return nodeLength < 33 ? (node.load(0), nodeLength) : (node.readBytes32(), 32); |
| 224 | + } |
| 225 | + |
| 226 | + function _emptyBytesMemory() private pure returns (bytes memory result) { |
| 227 | + assembly ("memory-safe") { |
| 228 | + result := 0x60 // mload(0x60) is always 0 |
| 229 | + } |
| 230 | + } |
| 231 | +} |
0 commit comments