diff --git a/.changeset/lovely-cooks-add.md b/.changeset/lovely-cooks-add.md new file mode 100644 index 00000000000..6637c92478d --- /dev/null +++ b/.changeset/lovely-cooks-add.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`RLP`: Add library for Ethereum's Recursive Length Prefix encoding/decoding. diff --git a/.changeset/shaky-phones-mix.md b/.changeset/shaky-phones-mix.md new file mode 100644 index 00000000000..410af473108 --- /dev/null +++ b/.changeset/shaky-phones-mix.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`TrieProof`: Add library for verifying Ethereum Merkle-Patricia trie inclusion proofs. diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index a536e3b51f8..f3e19d0eca2 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -42,6 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {InteroperableAddress}: Library for formatting and parsing ERC-7930 interoperable addresses. * {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality. * {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type. + * {RLP}: Library for encoding and decoding data in Ethereum's Recursive Length Prefix format. [NOTE] ==== @@ -143,3 +144,5 @@ Ethereum contracts have no native concept of an interface, so applications must {{Blockhash}} {{Time}} + +{{RLP}} diff --git a/contracts/utils/RLP.sol b/contracts/utils/RLP.sol new file mode 100644 index 00000000000..2ee7f7d21a1 --- /dev/null +++ b/contracts/utils/RLP.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Math} from "./math/Math.sol"; +import {Bytes} from "./Bytes.sol"; +import {Memory} from "./Memory.sol"; + +/** + * @dev Library for encoding and decoding data in RLP format. + * Recursive Length Prefix (RLP) is the main encoding method used to serialize objects in Ethereum. + * It's used for encoding everything from transactions to blocks to Patricia-Merkle tries. + */ +library RLP { + using Math for uint256; + using Bytes for *; + using Memory for *; + + /// @dev Items with length 0 are not RLP items. + error RLPEmptyItem(); + + /// @dev The `item` is not of the `expected` type. + error RLPUnexpectedType(ItemType expected, ItemType actual); + + /// @dev The item is not long enough to contain the data. + error RLPInvalidDataRemainder(uint256 minLength, uint256 actualLength); + + /// @dev The content length does not match the expected length. + error RLPContentLengthMismatch(uint256 expectedLength, uint256 actualLength); + + struct Item { + uint256 length; // Total length of the item in bytes + Memory.Pointer ptr; // Memory pointer to the start of the item + } + + enum ItemType { + DATA_ITEM, // Single data value + LIST_ITEM // List of RLP encoded items + } + + /** + * @dev Maximum length for data that will be encoded using the short format. + * If `data.length <= 55 bytes`, it will be encoded as: `[0x80 + length]` + data. + */ + uint8 internal constant SHORT_THRESHOLD = 55; + + /// @dev Single byte prefix for short strings (0-55 bytes) + uint8 internal constant SHORT_OFFSET = 128; + /// @dev Prefix for long string length (0xB8) + uint8 internal constant LONG_LENGTH_OFFSET = SHORT_OFFSET + SHORT_THRESHOLD + 1; // 184 + /// @dev Prefix for list items (0xC0) + uint8 internal constant LONG_OFFSET = LONG_LENGTH_OFFSET + 8; // 192 + /// @dev Prefix for long list length (0xF8) + uint8 internal constant SHORT_LIST_OFFSET = LONG_OFFSET + SHORT_THRESHOLD + 1; // 248 + + /** + * @dev Encodes a bytes array using RLP rules. + * Single bytes below 128 are encoded as themselves, otherwise as length prefix + data. + */ + function encode(bytes memory buffer) internal pure returns (bytes memory) { + return _isSingleByte(buffer) ? buffer : bytes.concat(_encodeLength(buffer.length, SHORT_OFFSET), buffer); + } + + /** + * @dev Encodes an array of bytes using RLP (as a list). + * First it {_flatten}s the list of byte arrays, then encodes it with the list prefix. + */ + function encode(bytes[] memory list) internal pure returns (bytes memory) { + bytes memory flattened = _flatten(list); + return bytes.concat(_encodeLength(flattened.length, LONG_OFFSET), flattened); + } + + /// @dev Convenience method to encode a string as RLP. + function encode(string memory str) internal pure returns (bytes memory) { + return encode(bytes(str)); + } + + /// @dev Convenience method to encode an address as RLP bytes (i.e. encoded as packed 20 bytes). + function encode(address addr) internal pure returns (bytes memory) { + return encode(abi.encodePacked(addr)); + } + + /// @dev Convenience method to encode a uint256 as RLP. See {_binaryBuffer}. + function encode(uint256 value) internal pure returns (bytes memory) { + return encode(_binaryBuffer(value)); + } + + /// @dev Same as {encode-uint256-}, but for bytes32. + function encode(bytes32 value) internal pure returns (bytes memory) { + return encode(uint256(value)); + } + + /** + * @dev Convenience method to encode a boolean as RLP. + * + * Boolean `true` is encoded as 0x01, `false` as 0x80 (equivalent to encoding integers 1 and 0). + * This follows the de facto ecosystem standard where booleans are treated as 0/1 integers. + * + * NOTE: Both this and {encodeStrict} produce identical encoded bytes at the output level. + * Use this for ecosystem compatibility; use {encodeStrict} for strict RLP spec compliance. + */ + function encode(bool value) internal pure returns (bytes memory) { + return encode(value ? uint256(1) : uint256(0)); + } + + /** + * @dev Strict RLP encoding of a boolean following literal spec interpretation. + * Boolean `true` is encoded as 0x01, `false` as empty bytes (0x80). + * + * NOTE: This is the strict RLP spec interpretation where false represents "empty". + * Use this for strict RLP spec compliance; use {encode} for ecosystem compatibility. + */ + function encodeStrict(bool value) internal pure returns (bytes memory) { + return value ? abi.encodePacked(bytes1(0x01)) : encode(new bytes(0)); + } + + /// @dev Creates an RLP Item from a bytes array. + function toItem(bytes memory value) internal pure returns (Item memory) { + require(value.length != 0, RLPEmptyItem()); // Empty arrays are not RLP items. + return Item(value.length, _addOffset(_asPointer(value), 32)); + } + + /// @dev Decodes an RLP encoded list into an array of RLP Items. See {_decodeLength} + function readList(Item memory item) internal pure returns (Item[] memory) { + (uint256 listOffset, uint256 listLength, ItemType itemType) = _decodeLength(item); + require(itemType == ItemType.LIST_ITEM, RLPUnexpectedType(ItemType.LIST_ITEM, itemType)); + uint256 expectedLength = listOffset + listLength; + require(expectedLength == item.length, RLPContentLengthMismatch(expectedLength, item.length)); + Item[] memory items = new Item[](32); + + uint256 itemCount; + + for (uint256 currentOffset = listOffset; currentOffset < item.length; ++itemCount) { + (uint256 itemOffset, uint256 itemLength, ) = _decodeLength( + Item(item.length - currentOffset, _addOffset(item.ptr, currentOffset)) + ); + items[itemCount] = Item(itemLength + itemOffset, _addOffset(item.ptr, currentOffset)); + currentOffset += itemOffset + itemLength; + } + + // Decrease the array size to match the actual item count. + assembly ("memory-safe") { + mstore(items, itemCount) + } + return items; + } + + /// @dev Same as {readList} but for `bytes`. See {toItem}. + function readList(bytes memory value) internal pure returns (Item[] memory) { + return readList(toItem(value)); + } + + /// @dev Decodes an RLP encoded item. + function readBytes(Item memory item) internal pure returns (bytes memory) { + (uint256 itemOffset, uint256 itemLength, ItemType itemType) = _decodeLength(item); + require(itemType == ItemType.DATA_ITEM, RLPUnexpectedType(ItemType.DATA_ITEM, itemType)); + uint256 expectedLength = itemOffset + itemLength; + require(expectedLength == item.length, RLPContentLengthMismatch(expectedLength, item.length)); + + bytes memory result = new bytes(itemLength); + _copy(_addOffset(_asPointer(result), 32), _addOffset(item.ptr, itemOffset), itemLength); + + return result; + } + + /// @dev Same as {readBytes} but for `bytes`. See {toItem}. + function readBytes(bytes memory item) internal pure returns (bytes memory) { + return readBytes(toItem(item)); + } + + /// @dev Reads the raw bytes of an RLP item without decoding the content. Includes prefix bytes. + function readRawBytes(Item memory item) internal pure returns (bytes memory) { + uint256 itemLength = item.length; + bytes memory result = new bytes(itemLength); + _copy(_addOffset(_asPointer(result), 32), item.ptr, itemLength); + + return result; + } + + /// @dev Checks if a buffer is a single byte below 128 (0x80). Encoded as-is in RLP. + function _isSingleByte(bytes memory buffer) private pure returns (bool) { + return buffer.length == 1 && uint8(buffer[0]) < SHORT_OFFSET; + } + + /** + * @dev Encodes a length with appropriate RLP prefix. + * + * Uses short encoding for lengths <= 55 bytes (i.e. `abi.encodePacked(bytes1(uint8(length) + uint8(offset)))`). + * Uses long encoding for lengths > 55 bytes See {_encodeLongLength}. + */ + function _encodeLength(uint256 length, uint256 offset) private pure returns (bytes memory) { + return + length <= SHORT_THRESHOLD + ? abi.encodePacked(bytes1(uint8(length) + uint8(offset))) + : _encodeLongLength(length, offset); + } + + /** + * @dev Encodes a long length value (>55 bytes) with a length-of-length prefix. + * Format: [prefix + length of the length] + [length in big-endian] + */ + function _encodeLongLength(uint256 length, uint256 offset) private pure returns (bytes memory) { + uint256 bytesLength = length.log256() + 1; // Result is floored + return + abi.encodePacked( + bytes1(uint8(bytesLength) + uint8(offset) + SHORT_THRESHOLD), + bytes32(length).reverseBytes32() // to big-endian + ); + } + + /// @dev Converts a uint256 to minimal binary representation, removing leading zeros. + function _binaryBuffer(uint256 value) private pure returns (bytes memory) { + return abi.encodePacked(value).slice(value.clz()); + } + + /// @dev Concatenates all byte arrays in the `list` sequentially. Returns a flattened buffer. + function _flatten(bytes[] memory list) private pure returns (bytes memory) { + // TODO: Move to Arrays.sol + bytes memory flattened = new bytes(_totalLength(list)); + Memory.Pointer dataPtr = _addOffset(_asPointer(flattened), 32); + for (uint256 i = 0; i < list.length; i++) { + bytes memory item = list[i]; + uint256 length = item.length; + _copy(dataPtr, _asPointer(item), length); + dataPtr = _addOffset(dataPtr, length); + } + return flattened; + } + + /// @dev Sums up the length of each array in the list. + function _totalLength(bytes[] memory list) private pure returns (uint256) { + // TODO: Move to Arrays.sol + uint256 totalLength; + for (uint256 i = 0; i < list.length; i++) { + totalLength += list[i].length; + } + return totalLength; + } + + /** + * @dev Decodes an RLP `item`'s `length and type from its prefix. + * Returns the offset, length, and type of the RLP item based on the encoding rules. + */ + function _decodeLength(Item memory item) private pure returns (uint256 offset, uint256 length, ItemType) { + require(item.length != 0, RLPEmptyItem()); + uint256 prefix = uint8(_loadByte(item.ptr, 0)); + + // Single byte below 128 + if (prefix < SHORT_OFFSET) return (0, 1, ItemType.DATA_ITEM); + + // Short string (0-55 bytes) + if (prefix < LONG_LENGTH_OFFSET) return _decodeShortString(prefix - SHORT_OFFSET, item); + + // Long string (>55 bytes) + if (prefix < LONG_OFFSET) { + (offset, length) = _decodeLong(prefix - LONG_LENGTH_OFFSET, item); + return (offset, length, ItemType.DATA_ITEM); + } + + // Short list + if (prefix < SHORT_LIST_OFFSET) return _decodeShortList(prefix - LONG_OFFSET, item); + + // Long list + (offset, length) = _decodeLong(prefix - SHORT_LIST_OFFSET, item); + return (offset, length, ItemType.LIST_ITEM); + } + + /// @dev Decodes a short string (0-55 bytes). The first byte contains the length, and the rest is the payload. + function _decodeShortString( + uint256 strLength, + Item memory item + ) private pure returns (uint256 offset, uint256 length, ItemType) { + require(item.length > strLength, RLPInvalidDataRemainder(strLength, item.length)); + require(strLength != 1 || _loadByte(_addOffset(item.ptr, 1), 0) >= bytes1(SHORT_OFFSET)); + return (1, strLength, ItemType.DATA_ITEM); + } + + /// @dev Decodes a short list (0-55 bytes). The first byte contains the length of the entire list. + function _decodeShortList( + uint256 listLength, + Item memory item + ) private pure returns (uint256 offset, uint256 length, ItemType) { + require(item.length > listLength, RLPInvalidDataRemainder(listLength, item.length)); + return (1, listLength, ItemType.LIST_ITEM); + } + + /// @dev Decodes a long string or list (>55 bytes). The first byte indicates the length of the length, followed by the length itself. + function _decodeLong(uint256 lengthLength, Item memory item) private pure returns (uint256 offset, uint256 length) { + lengthLength += 1; // 1 byte for the length itself + require(item.length > lengthLength, RLPInvalidDataRemainder(lengthLength, item.length)); + require(_loadByte(item.ptr, 0) != 0x00); + + // Extract the length value from the next bytes + uint256 len = uint256(_load(_addOffset(item.ptr, 1)) >> (256 - 8 * lengthLength)); + require(len > SHORT_THRESHOLD, RLPInvalidDataRemainder(SHORT_THRESHOLD, len)); + uint256 expectedLength = lengthLength + len; + require(item.length <= expectedLength, RLPContentLengthMismatch(expectedLength, item.length)); + return (lengthLength + 1, len); + } + + function _addOffset(Memory.Pointer ptr, uint256 offset) private pure returns (Memory.Pointer) { + return bytes32(uint256(ptr.asBytes32()) + offset).asPointer(); + } + + function _copy(Memory.Pointer destPtr, Memory.Pointer srcPtr, uint256 length) private pure { + assembly ("memory-safe") { + mcopy(destPtr, srcPtr, length) + } + } + + function _loadByte(Memory.Pointer ptr, uint256 offset) private pure returns (bytes1 v) { + assembly ("memory-safe") { + v := byte(offset, mload(ptr)) + } + } + + function _load(Memory.Pointer ptr) private pure returns (bytes32 v) { + assembly ("memory-safe") { + v := mload(ptr) + } + } + + function _asPointer(bytes memory value) private pure returns (Memory.Pointer ptr) { + assembly ("memory-safe") { + ptr := value + } + } +} diff --git a/contracts/utils/cryptography/README.adoc b/contracts/utils/cryptography/README.adoc index 6c222d7c6ba..5b608c3215b 100644 --- a/contracts/utils/cryptography/README.adoc +++ b/contracts/utils/cryptography/README.adoc @@ -11,6 +11,7 @@ A collection of contracts and libraries that implement various signature validat * {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts. * {Hashes}: Commonly used hash functions. * {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs. + * {TrieProof}: Library for verifying Ethereum Merkle-Patricia trie inclusion proofs. * {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712]. * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739. * {WebAuthn}: Library for verifying WebAuthn Authentication Assertions. @@ -38,6 +39,8 @@ A collection of contracts and libraries that implement various signature validat {{MerkleProof}} +{{TrieProof}} + {{EIP712}} {{ERC7739Utils}} diff --git a/contracts/utils/cryptography/TrieProof.sol b/contracts/utils/cryptography/TrieProof.sol new file mode 100644 index 00000000000..c84068a7b1f --- /dev/null +++ b/contracts/utils/cryptography/TrieProof.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Bytes} from "../Bytes.sol"; +import {Strings} from "../Strings.sol"; +import {RLP} from "../RLP.sol"; +import {Math} from "../math/Math.sol"; + +/** + * @dev Library for verifying Ethereum Merkle-Patricia trie inclusion proofs. + * + * Ethereum's State Trie state layout is a 4-item array of `[nonce, balance, storageRoot, codeHash]` + * See https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie[Merkle-Patricia trie] + */ +library TrieProof { + using Bytes for bytes; + using RLP for *; + using Strings for string; + + enum Prefix { + EXTENSION_EVEN, // 0 - Extension node with even length path + EXTENSION_ODD, // 1 - Extension node with odd length path + LEAF_EVEN, // 2 - Leaf node with even length path + LEAF_ODD // 3 - Leaf node with odd length path + } + + enum ProofError { + NO_ERROR, // No error occurred during proof verification + EMPTY_KEY, // The provided key is empty + INDEX_OUT_OF_BOUNDS, // Array index access is out of bounds + INVALID_ROOT_HASH, // The provided root hash doesn't match the proof + INVALID_LARGE_INTERNAL_HASH, // Internal node hash exceeds expected size + INVALID_INTERNAL_NODE_HASH, // Internal node hash doesn't match expected value + EMPTY_VALUE, // The value to verify is empty + INVALID_EXTRA_PROOF_ELEMENT, // Proof contains unexpected additional elements + INVALID_PATH_REMAINDER, // Path remainder doesn't match expected value + INVALID_KEY_REMAINDER, // Key remainder doesn't match expected value + UNKNOWN_NODE_PREFIX, // Node prefix is not recognized + UNPARSEABLE_NODE, // Node cannot be parsed from RLP encoding + INVALID_PROOF // General proof validation failure + } + + struct Node { + bytes encoded; // Raw RLP encoded node + RLP.Item[] decoded; // Decoded RLP items + } + + /// @dev The radix of the Ethereum trie (hexadecimal = 16) + uint256 internal constant EVM_TREE_RADIX = 16; + /// @dev Number of items in leaf or extension nodes (always 2) + uint256 internal constant LEAF_OR_EXTENSION_NODE_LENGTH = 2; + + /** + * @dev Verifies a `proof` against a given `key`, `value`, `and root` hash + * using the default Ethereum radix (16). + */ + function verify( + bytes memory key, + bytes memory value, + bytes[] memory proof, + bytes32 root + ) internal pure returns (bool) { + return verify(key, value, proof, root, EVM_TREE_RADIX); + } + + /// @dev Same as {verify} but with a custom radix. + function verify( + bytes memory key, + bytes memory value, + bytes[] memory proof, + bytes32 root, + uint256 radix + ) internal pure returns (bool) { + (bytes memory processedValue, ProofError err) = processProof(key, proof, root, radix); + return string(processedValue).equal(string(value)) && err == ProofError.NO_ERROR; + } + + /// @dev Processes a proof for a given key using default Ethereum radix (16) and returns the processed value. + function processProof( + bytes memory key, + bytes[] memory proof, + bytes32 root + ) internal pure returns (bytes memory value, ProofError) { + return processProof(key, proof, root, EVM_TREE_RADIX); + } + + /// @dev Same as {processProof} but with a custom radix. + function processProof( + bytes memory key, + bytes[] memory proof, + bytes32 root, + uint256 radix + ) internal pure returns (bytes memory value, ProofError) { + if (key.length == 0) return ("", ProofError.EMPTY_KEY); + // Convert key to nibbles (4-bit values) and begin processing from the root + return _processInclusionProof(_decodeProof(proof), _nibbles(key), bytes.concat(root), 0, radix); + } + + /// @dev Main recursive function that traverses the trie using the provided proof. + function _processInclusionProof( + Node[] memory trieProof, + bytes memory key, + bytes memory nodeId, + uint256 keyIndex, + uint256 radix + ) private pure returns (bytes memory value, ProofError err) { + uint256 branchNodeLength = radix + 1; // Branch nodes have radix+1 items (values + 1 for stored value) + + for (uint256 i = 0; i < trieProof.length; i++) { + Node memory node = trieProof[i]; + + // ensure we haven't overshot the key + if (keyIndex > key.length) return ("", ProofError.INDEX_OUT_OF_BOUNDS); + err = _validateNodeHashes(nodeId, node, keyIndex); + if (err != ProofError.NO_ERROR) return ("", err); + + uint256 nodeLength = node.decoded.length; + + // must be either a branch or leaf/extension node + if (nodeLength != branchNodeLength && nodeLength != LEAF_OR_EXTENSION_NODE_LENGTH) + return ("", ProofError.UNPARSEABLE_NODE); + + if (nodeLength == branchNodeLength) { + // If we've consumed the entire key, the value must be in the last slot + if (keyIndex == key.length) return _validateLastItem(node.decoded[radix], trieProof, i); + + // Otherwise, continue down the branch specified by the next nibble in the key + uint8 branchKey = uint8(key[keyIndex]); + (nodeId, keyIndex) = (_id(node.decoded[branchKey]), keyIndex + 1); + } else if (nodeLength == LEAF_OR_EXTENSION_NODE_LENGTH) { + return _processLeafOrExtension(node, trieProof, key, nodeId, keyIndex, i); + } + } + + // If we've gone through all proof elements without finding a value, the proof is invalid + return ("", ProofError.INVALID_PROOF); + } + + /// @dev Validates the node hashes at different levels of the proof. + function _validateNodeHashes( + bytes memory nodeId, + Node memory node, + uint256 keyIndex + ) private pure returns (ProofError) { + if (keyIndex == 0 && !string(bytes.concat(keccak256(node.encoded))).equal(string(nodeId))) + return ProofError.INVALID_ROOT_HASH; // Root node must match root hash + if (node.encoded.length >= 32 && !string(bytes.concat(keccak256(node.encoded))).equal(string(nodeId))) + return ProofError.INVALID_LARGE_INTERNAL_HASH; // Large nodes are stored as hashes + if (!string(node.encoded).equal(string(nodeId))) return ProofError.INVALID_INTERNAL_NODE_HASH; // Small nodes must match directly + return ProofError.NO_ERROR; // No error + } + + /** + * @dev Processes a leaf or extension node in the trie proof. + * + * For leaf nodes, validates that the key matches completely and returns the value. + * For extension nodes, continues traversal by updating the node ID and key index. + */ + function _processLeafOrExtension( + Node memory node, + Node[] memory trieProof, + bytes memory key, + bytes memory nodeId, + uint256 keyIndex, + uint256 i + ) private pure returns (bytes memory value, ProofError err) { + bytes memory path = _path(node); + uint8 prefix = uint8(path[0]); + uint8 offset = 2 - (prefix % 2); // Calculate offset based on even/odd path length + bytes memory pathRemainder = Bytes.slice(path, offset); // Path after the prefix + bytes memory keyRemainder = Bytes.slice(key, keyIndex); // Remaining key to match + uint256 sharedNibbleLength = _sharedNibbleLength(pathRemainder, keyRemainder); + + // Path must match at least partially with our key + if (sharedNibbleLength == 0) return ("", ProofError.INVALID_PATH_REMAINDER); + if (prefix > uint8(type(Prefix).max)) return ("", ProofError.UNKNOWN_NODE_PREFIX); + + // Leaf node (terminal) - return its value if key matches completely + if (Prefix(prefix) == Prefix.LEAF_EVEN || Prefix(prefix) == Prefix.LEAF_ODD) { + if (keyRemainder.length == 0) return ("", ProofError.INVALID_KEY_REMAINDER); + return _validateLastItem(node.decoded[1], trieProof, i); + } + + // Extension node (non-terminal) - continue to next node + // Increment keyIndex by the number of nibbles consumed + (nodeId, keyIndex) = (_id(node.decoded[1]), keyIndex + sharedNibbleLength); + } + + /** + * @dev Validates that we've reached a valid leaf value and this is the last proof element. + * Ensures the value is not empty and no extra proof elements exist. + */ + function _validateLastItem( + RLP.Item memory item, + Node[] memory trieProof, + uint256 i + ) private pure returns (bytes memory value, ProofError) { + bytes memory value_ = item.readBytes(); + if (value_.length == 0) return ("", ProofError.EMPTY_VALUE); + if (i != trieProof.length - 1) return ("", ProofError.INVALID_EXTRA_PROOF_ELEMENT); + return (value_, ProofError.NO_ERROR); + } + + /** + * @dev Converts raw proof bytes into structured Node objects with RLP parsing. + * Transforms each proof element into a Node with both encoded and decoded forms. + */ + function _decodeProof(bytes[] memory proof) private pure returns (Node[] memory proof_) { + uint256 length = proof.length; + proof_ = new Node[](length); + for (uint256 i = 0; i < length; i++) { + proof_[i] = Node(proof[i], proof[i].readList()); + } + } + + /** + * @dev Extracts the node ID (hash or raw data based on size). + * For small nodes (<32 bytes), returns the raw bytes; for large nodes, returns the hash. + */ + function _id(RLP.Item memory node) private pure returns (bytes memory) { + return node.length < 32 ? node.readRawBytes() : node.readBytes(); + } + + /** + * @dev Extracts the path from a leaf or extension node. + * The path is stored as the first element in the node's decoded array. + */ + function _path(Node memory node) private pure returns (bytes memory) { + return _nibbles(node.decoded[0].readBytes()); + } + + /** + * @dev Calculates the number of shared nibbles between two byte arrays. + * Used to determine how much of a path matches a key during trie traversal. + */ + function _sharedNibbleLength(bytes memory _a, bytes memory _b) private pure returns (uint256 shared_) { + uint256 max = Math.max(_a.length, _b.length); + uint256 length; + while (length < max && _a[length] == _b[length]) { + length++; + } + return length; + } + + /// @dev Split each byte in `value` into two nibbles (4 bits each). + function _nibbles(bytes memory value) private pure returns (bytes memory) { + uint256 length = value.length; + bytes memory nibbles_ = new bytes(length * 2); + for (uint256 i = 0; i < length; i++) { + (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); + } + return nibbles_; + } +}