// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "./RLPReader.sol"; /** * @title MerklePatriciaProof — hexary Merkle-Patricia trie inclusion/exclusion verifier. * * @notice Solidity port of the validated Rust verifier in * `zk-interop/interop-core/src/mpt.rs` (used verbatim inside the AERE SP1 * interop guest). Given a trie `root`, a key as a nibble path, and the set of * trie nodes along that path (as `eth_getProof` / a receipts-trie builder * emits), it returns the trie value for an included key or the empty flag for a * proven absent key. Byte-exact: a tampered node or wrong root fails rather * than lies. * * Node model (keccak-hashed refs, hex-prefix paths): * leaf [ HP(path, leaf), value ] * extension [ HP(path, ext), child_ref ] * branch [ c0, c1, ..., c15, value ] */ library MerklePatriciaProof { using RLPReader for bytes; /// @notice Verify inclusion of `key` under `root` given `nodes`. /// @param root the 32-byte trie root (e.g. a header's receiptsRoot). /// @param key the key bytes (for a receipts trie: rlp(transactionIndex)). /// @param nodes the proof node set; order irrelevant (resolved by hash). /// @return included true if the key is present, false if proven absent. /// @return value the trie value bytes when included (empty otherwise). function verify(bytes32 root, bytes memory key, bytes[] memory nodes) internal pure returns (bool included, bytes memory value) { bytes memory path = _nibbles(key); // Empty trie: root = keccak(rlp("")) = keccak(0x80). Any key is absent. if (root == keccak256(hex"80")) { return (false, ""); } bytes memory node = _findByHash(nodes, root); uint256 idx = 0; // Bounded loop: a valid path is at most 64 nibbles (+ node hops); 512 is safe. for (uint256 hop = 0; hop < 512; hop++) { bytes[] memory items = node.splitList(); if (items.length == 2) { (bool isLeaf, bytes memory frag) = _hpDecode(items[0].readBytes()); if (isLeaf) { if (_eq(_slice(path, idx), frag)) { return (true, items[1].readBytes()); } return (false, ""); // diverges at leaf -> exclusion } // extension: remaining path must start with frag bytes memory rem = _slice(path, idx); if (rem.length < frag.length || !_startsWith(rem, frag)) { return (false, ""); } idx += frag.length; node = _resolve(items[1], nodes); } else if (items.length == 17) { if (idx >= path.length) { // value in slot 16 if (_isEmptyRef(items[16])) return (false, ""); return (true, items[16].readBytes()); } uint256 nib = uint8(path[idx]); idx += 1; bytes memory child = items[nib]; if (_isEmptyRef(child)) return (false, ""); node = _resolve(child, nodes); } else { revert("MPT:bad arity"); } } revert("MPT:path too long"); } // ---- node reference resolution ------------------------------------------ /// @dev Resolve a child reference: an inline (embedded) list node IS the node; /// a 32-byte string is a hash to look up in the node set. function _resolve(bytes memory ref, bytes[] memory nodes) private pure returns (bytes memory) { if (RLPReader.isList(ref)) { return ref; // embedded node } bytes memory content = ref.readBytes(); require(content.length == 32, "MPT:bad ref"); return _findByHash(nodes, _toBytes32(content)); } function _findByHash(bytes[] memory nodes, bytes32 h) private pure returns (bytes memory) { for (uint256 i = 0; i < nodes.length; i++) { if (keccak256(nodes[i]) == h) return nodes[i]; } revert("MPT:missing node"); } function _isEmptyRef(bytes memory ref) private pure returns (bool) { return ref.length == 0 || (ref.length == 1 && ref[0] == 0x80); } // ---- path / nibble helpers ---------------------------------------------- function _nibbles(bytes memory b) private pure returns (bytes memory out) { out = new bytes(b.length * 2); for (uint256 i = 0; i < b.length; i++) { out[2 * i] = bytes1(uint8(b[i]) >> 4); out[2 * i + 1] = bytes1(uint8(b[i]) & 0x0f); } } /// @dev Hex-prefix ("compact") decode. First nibble: bit1=leaf, bit0=odd. function _hpDecode(bytes memory enc) private pure returns (bool isLeaf, bytes memory nibbles) { require(enc.length != 0, "MPT:empty hp"); uint8 flags = uint8(enc[0]) >> 4; isLeaf = (flags & 0x2) != 0; bool odd = (flags & 0x1) != 0; uint256 count = (enc.length - 1) * 2 + (odd ? 1 : 0); nibbles = new bytes(count); uint256 k = 0; if (odd) { nibbles[k++] = bytes1(uint8(enc[0]) & 0x0f); } for (uint256 i = 1; i < enc.length; i++) { nibbles[k++] = bytes1(uint8(enc[i]) >> 4); nibbles[k++] = bytes1(uint8(enc[i]) & 0x0f); } } function _slice(bytes memory b, uint256 start) private pure returns (bytes memory out) { if (start >= b.length) return ""; out = new bytes(b.length - start); for (uint256 i = 0; i < out.length; i++) out[i] = b[start + i]; } function _startsWith(bytes memory a, bytes memory prefix) private pure returns (bool) { if (a.length < prefix.length) return false; for (uint256 i = 0; i < prefix.length; i++) { if (a[i] != prefix[i]) return false; } return true; } function _eq(bytes memory a, bytes memory b) private pure returns (bool) { return a.length == b.length && keccak256(a) == keccak256(b); } function _toBytes32(bytes memory b) private pure returns (bytes32 r) { require(b.length == 32, "MPT:not b32"); assembly { r := mload(add(b, 32)) } } }