diff --git a/.changeset/khaki-hats-leave.md b/.changeset/khaki-hats-leave.md new file mode 100644 index 00000000000..021df0ff083 --- /dev/null +++ b/.changeset/khaki-hats-leave.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `nibbles` function to split each byte into two nibbles. 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/major-feet-write.md b/.changeset/major-feet-write.md new file mode 100644 index 00000000000..da2966f00cd --- /dev/null +++ b/.changeset/major-feet-write.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Add `reverseBitsUint256`, `reverseBitsUint128`, `reverseBitsUint64`, `reverseBitsUint32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. 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/.changeset/whole-cats-find.md b/.changeset/whole-cats-find.md new file mode 100644 index 00000000000..e5ba8df6e5d --- /dev/null +++ b/.changeset/whole-cats-find.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `clz` function to count the leading zero bytes in a `uint256` value. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1234b845513..b6829f666f4 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,6 +99,28 @@ library Bytes { return result; } + /// @dev Split each byte in `value` into two nibbles (4 bits each). + function nibbles(bytes memory value) internal 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_; + } + + /// @dev Counts the number of leading zero bytes in a uint256. + function clz(uint256 x) internal pure returns (uint256) { + if (x == 0) return 32; // All 32 bytes are zero + uint256 r = 0; + if (x > 0xffffffffffffffffffffffffffffffff) r = 128; // Upper 128 bits + if ((x >> r) > 0xffffffffffffffff) r |= 64; // Next 64 bits + if ((x >> r) > 0xffffffff) r |= 32; // Next 32 bits + if ((x >> r) > 0xffff) r |= 16; // Next 16 bits + if ((x >> r) > 0xff) r |= 8; // Next 8 bits + return 31 ^ (r >> 3); // Convert to leading zero bytes count + } + /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..5435e9ff5fe --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Utilities to manipulate memory. + * + * Memory is a contiguous and dynamic byte array in which Solidity stores non-primitive types. + * This library provides functions to manipulate pointers to this dynamic array. + * + * WARNING: When manipulating memory, make sure to follow the Solidity documentation + * guidelines for https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[Memory Safety]. + */ +library Memory { + type Pointer is bytes32; + + /// @dev Returns a memory pointer to the current free memory pointer. + function getFreePointer() internal pure returns (Pointer ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + /// @dev Sets the free memory pointer to a specific value. + /// + /// WARNING: Everything after the pointer may be overwritten. + function setFreePointer(Pointer ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } + + /// @dev Returns a memory pointer to the content of a buffer. Skips the length word. + function contentPointer(bytes memory buffer) internal pure returns (Pointer) { + bytes32 ptr; + assembly ("memory-safe") { + ptr := add(buffer, 32) + } + return asPointer(ptr); + } + + /// @dev Copies `length` bytes from `srcPtr` to `destPtr`. + function copy(Pointer destPtr, Pointer srcPtr, uint256 length) internal pure { + assembly ("memory-safe") { + mcopy(destPtr, srcPtr, length) + } + } + + /// @dev Extracts a byte from a memory pointer. + function extractByte(Pointer ptr) internal pure returns (bytes1 v) { + assembly ("memory-safe") { + v := byte(0, mload(ptr)) + } + } + + /// @dev Extracts a word from a memory pointer. + function extractWord(Pointer ptr) internal pure returns (uint256 v) { + assembly ("memory-safe") { + v := mload(ptr) + } + } + + /// @dev Adds an offset to a memory pointer. + function addOffset(Pointer ptr, uint256 offset) internal pure returns (Pointer) { + return asPointer(bytes32(uint256(asBytes32(ptr)) + offset)); + } + + /// @dev Pointer to `bytes32`. + function asBytes32(Pointer ptr) internal pure returns (bytes32) { + return Pointer.unwrap(ptr); + } + + /// @dev `bytes32` to pointer. + function asPointer(bytes32 value) internal pure returns (Pointer) { + return Pointer.wrap(value); + } + + /// @dev `bytes` to pointer. + function asPointer(bytes memory value) internal pure returns (Pointer) { + bytes32 ptr; + assembly ("memory-safe") { + ptr := value + } + return asPointer(ptr); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 231bccd9738..4834613834f 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -40,7 +40,8 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. * {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] ==== Because Solidity does not support generic types, {EnumerableMap} and {EnumerableSet} are specialized to a limited number of key-value types. @@ -137,3 +138,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..e5203a24062 --- /dev/null +++ b/contracts/utils/RLP.sol @@ -0,0 +1,299 @@ +// 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, value.contentPointer()); + } + + /// @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, item.ptr.addOffset(currentOffset)) + ); + items[itemCount] = Item(itemLength + itemOffset, item.ptr.addOffset(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); + result.contentPointer().copy(item.ptr.addOffset(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); + result.contentPointer().copy(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), + length.reverseBitsUint256() // 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 = flattened.contentPointer(); + for (uint256 i = 0; i < list.length; i++) { + bytes memory item = list[i]; + uint256 length = item.length; + dataPtr.copy(item.contentPointer(), length); + dataPtr = dataPtr.addOffset(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(item.ptr.extractByte()); + + // 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 || item.ptr.addOffset(1).extractByte() >= 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(item.ptr.extractByte() != 0x00); + + // Extract the length value from the next bytes + uint256 len = item.ptr.addOffset(1).extractWord() >> (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); + } +} diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 4cc597646f2..65e349f034e 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.3.0) (utils/Strings.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Math} from "./math/Math.sol"; import {SafeCast} from "./math/SafeCast.sol"; @@ -132,7 +132,7 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + return a.length == b.length && keccak256(bytes(a)) == keccak256(bytes(b)); } /** diff --git a/contracts/utils/cryptography/README.adoc b/contracts/utils/cryptography/README.adoc index 79b10437322..fd822f7bbfd 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. * {AbstractSigner}: Abstract contract for internal signature validation in smart contracts. @@ -36,6 +37,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..9ee44f708f0 --- /dev/null +++ b/contracts/utils/cryptography/TrieProof.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Bytes} from "../Bytes.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 *; + + 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 processedValue.equal(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), key.nibbles(), 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 && !bytes.concat(keccak256(node.encoded)).equal(nodeId)) return ProofError.INVALID_ROOT_HASH; // Root node must match root hash + if (node.encoded.length >= 32 && !bytes.concat(keccak256(node.encoded)).equal(nodeId)) + return ProofError.INVALID_LARGE_INTERNAL_HASH; // Large nodes are stored as hashes + if (!node.encoded.equal(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 node.decoded[0].readBytes().nibbles(); + } + + /** + * @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; + } +} diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index f0d608a2dea..12546593ffe 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -740,6 +740,58 @@ library Math { } } + /** + * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. + * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] + */ + function reverseBitsUint256(uint256 value) internal pure returns (uint256) { + value = // swap bytes + ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + value = // swap 8-byte long pairs + ((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) | + ((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + return (value >> 128) | (value << 128); // swap 16-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 128-bit values. + function reverseBitsUint128(uint128 value) internal pure returns (uint256) { + value = // swap bytes + ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32); + return (value >> 64) | (value << 64); // swap 8-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 64-bit values. + function reverseBitsUint64(uint64 value) internal pure returns (uint256) { + value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes + value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs + return (value >> 32) | (value << 32); // swap 4-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 32-bit values. + function reverseBitsUint32(uint32 value) internal pure returns (uint256) { + value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes + return (value >> 16) | (value << 16); // swap 2-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 16-bit values. + function reverseBits16(uint16 value) internal pure returns (uint256) { + return (value >> 8) | (value << 8); + } + /** * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. */ diff --git a/test/helpers/constants.js b/test/helpers/constants.js index eb9b43e5549..d08c3ec0455 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -1,5 +1,7 @@ module.exports = { + MAX_UINT16: 2n ** 16n - 1n, MAX_UINT32: 2n ** 32n - 1n, MAX_UINT48: 2n ** 48n - 1n, MAX_UINT64: 2n ** 64n - 1n, + MAX_UINT128: 2n ** 128n - 1n, }; diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol new file mode 100644 index 00000000000..cec57caafd3 --- /dev/null +++ b/test/utils/Bytes.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +contract BytesTest is Test { + function testIndexOf(bytes memory buffer, bytes1 s) public pure { + testIndexOf(buffer, s, 0); + } + + function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + uint256 result = Bytes.indexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s) public pure { + testLastIndexOf(buffer, s, 0); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + pos = bound(pos, 0, buffer.length); + uint256 result = Bytes.lastIndexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testSlice(bytes memory buffer, uint256 start) public pure { + testSlice(buffer, start, buffer.length); + } + + function testSlice(bytes memory buffer, uint256 start, uint256 end) public pure { + bytes memory result = Bytes.slice(buffer, start, end); + uint256 sanitizedEnd = Math.min(end, buffer.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + assertEq(result.length, sanitizedEnd - sanitizedStart); + for (uint256 i = 0; i < result.length; i++) assertEq(result[i], buffer[sanitizedStart + i]); + } + + function testNibbles(bytes memory value) public pure { + bytes memory result = Bytes.nibbles(value); + assertEq(result.length, value.length * 2); + for (uint256 i = 0; i < value.length; i++) { + bytes1 originalByte = value[i]; + bytes1 highNibble = result[i * 2]; + bytes1 lowNibble = result[i * 2 + 1]; + + assertEq(highNibble, originalByte & 0xf0); + assertEq(lowNibble, originalByte & 0x0f); + } + } + + function testSymbolicEqual(bytes memory a, bytes memory b) public pure { + assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); + } + + function testSymbolicCountLeadingZeroes(uint256 x) public pure { + uint256 result = Bytes.clz(x); + assertLe(result, 32); // [0, 32] + + if (x != 0) { + uint256 firstNonZeroBytePos = 32 - result - 1; + uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; + assertNotEq(byteValue, 0); + + // x != 0 implies result < 32 + // most significant byte should be non-zero + uint256 msbValue = (x >> (248 - result * 8)) & 0xff; + assertNotEq(msbValue, 0); + } + } +} diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 52a1ae95e77..05ba530d94d 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -85,4 +85,98 @@ describe('Bytes', function () { } }); }); + + describe('nibbles', function () { + it('converts single byte', async function () { + await expect(this.mock.$nibbles('0xab')).to.eventually.equal('0xa00b'); + }); + + it('converts multiple bytes', async function () { + await expect(this.mock.$nibbles('0x1234')).to.eventually.equal('0x10023004'); + }); + + it('handles empty bytes', async function () { + await expect(this.mock.$nibbles('0x')).to.eventually.equal('0x'); + }); + + it('converts lorem text', async function () { + const result = await this.mock.$nibbles(lorem); + expect(ethers.dataLength(result)).to.equal(lorem.length * 2); + + // Check nibble extraction for first few bytes + for (let i = 0; i < Math.min(lorem.length, 5); i++) { + const originalByte = lorem[i]; + const highNibble = ethers.dataSlice(result, i * 2, i * 2 + 1); + const lowNibble = ethers.dataSlice(result, i * 2 + 1, i * 2 + 2); + + expect(highNibble).to.equal(ethers.toBeHex(originalByte & 0xf0, 1)); + expect(lowNibble).to.equal(ethers.toBeHex(originalByte & 0x0f, 1)); + } + }); + }); + + describe('equal', function () { + it('identical arrays', async function () { + await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true; + }); + + it('same content', async function () { + const copy = new Uint8Array(lorem); + await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true; + }); + + it('different content', async function () { + const different = ethers.toUtf8Bytes('Different content'); + await expect(this.mock.$equal(lorem, different)).to.eventually.be.false; + }); + + it('different lengths', async function () { + const shorter = lorem.slice(0, 10); + await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false; + }); + + it('empty arrays', async function () { + const empty1 = new Uint8Array(0); + const empty2 = new Uint8Array(0); + await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true; + }); + + it('one empty one not', async function () { + const empty = new Uint8Array(0); + await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false; + }); + }); + + describe('clz', function () { + it('zero value', async function () { + await expect(this.mock.$clz(0)).to.eventually.equal(32); + }); + + it('small values', async function () { + await expect(this.mock.$clz(1)).to.eventually.equal(31); + await expect(this.mock.$clz(255)).to.eventually.equal(31); + }); + + it('larger values', async function () { + await expect(this.mock.$clz(256)).to.eventually.equal(30); + await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); + await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); + }); + + it('max value', async function () { + await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0); + }); + + it('specific patterns', async function () { + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), + ).to.eventually.equal(30); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), + ).to.eventually.equal(29); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), + ).to.eventually.equal(28); + }); + }); }); diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 3c83febe9df..9f501b0e367 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -308,6 +308,27 @@ contract MathTest is Test { } } + // REVERSE BITS + function testSymbolicReverseBitsUint256(uint256 value) public pure { + assertEq(Math.reverseBitsUint256(Math.reverseBitsUint256(value)), value); + } + + function testSymbolicReverseBitsUint128(uint128 value) public pure { + assertEq(Math.reverseBitsUint128(uint128(Math.reverseBitsUint128(value))), value); + } + + function testSymbolicReverseBitsUint64(uint64 value) public pure { + assertEq(Math.reverseBitsUint64(uint64(Math.reverseBitsUint64(value))), value); + } + + function testSymbolicReverseBitsUint32(uint32 value) public pure { + assertEq(Math.reverseBitsUint32(uint32(Math.reverseBitsUint32(value))), value); + } + + function testSymbolicReverseBits16(uint16 value) public pure { + assertEq(Math.reverseBits16(uint16(Math.reverseBits16(value))), value); + } + // Helpers function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6a09938148a..ce1abdd8a09 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -7,6 +7,7 @@ const { Rounding } = require('../../helpers/enums'); const { min, max, modExp } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); const { product, range } = require('../../helpers/iterate'); +const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../../helpers/constants'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -710,4 +711,118 @@ describe('Math', function () { }); }); }); + + describe('reverseBits', function () { + describe('reverseBitsUint256', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint256(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint256(ethers.MaxUint256)).to.eventually.equal(ethers.MaxUint256); + + // Test simple pattern + await expect( + this.mock.$reverseBitsUint256('0x0000000000000000000000000000000000000000000000000000000000000001'), + ).to.eventually.equal('0x0100000000000000000000000000000000000000000000000000000000000000'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint256(value); + await expect(this.mock.$reverseBitsUint256(reversed)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint128', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint128(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint128(MAX_UINT128)).to.eventually.equal(MAX_UINT128); + + // Test simple pattern + await expect(this.mock.$reverseBitsUint128('0x00000000000000000000000000000001')).to.eventually.equal( + '0x01000000000000000000000000000000', + ); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT128]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint128(value); + // Cast back to uint128 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint128(reversed & MAX_UINT128)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint64', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint64(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint64(MAX_UINT64)).to.eventually.equal(MAX_UINT64); + + // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 + await expect(this.mock.$reverseBitsUint64('0x123456789ABCDEF0')).to.eventually.equal('0xF0DEBC9A78563412'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT64]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint64(value); + // Cast back to uint64 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint64(reversed & MAX_UINT64)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint32', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint32(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint32(MAX_UINT32)).to.eventually.equal(MAX_UINT32); + + // Test known pattern: 0x12345678 -> 0x78563412 + await expect(this.mock.$reverseBitsUint32(0x12345678)).to.eventually.equal(0x78563412); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT32]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint32(value); + // Cast back to uint32 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint32(reversed & MAX_UINT32)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBits16', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits16(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBits16(MAX_UINT16)).to.eventually.equal(MAX_UINT16); + + // Test known pattern: 0x1234 -> 0x3412 + await expect(this.mock.$reverseBits16(0x1234)).to.eventually.equal(0x3412); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x1234n, MAX_UINT16]; + for (const value of values) { + const reversed = await this.mock.$reverseBits16(value); + // Cast back to uint16 for comparison since function returns uint256 + await expect(this.mock.$reverseBits16(reversed & MAX_UINT16)).to.eventually.equal(value); + } + }); + }); + + describe('edge cases', function () { + it('handles single byte values', async function () { + await expect(this.mock.$reverseBits16(0x00ff)).to.eventually.equal(0xff00); + await expect(this.mock.$reverseBitsUint32(0x000000ff)).to.eventually.equal(0xff000000); + }); + + it('handles alternating patterns', async function () { + await expect(this.mock.$reverseBits16(0xaaaa)).to.eventually.equal(0xaaaa); + await expect(this.mock.$reverseBits16(0x5555)).to.eventually.equal(0x5555); + await expect(this.mock.$reverseBitsUint32(0xaaaaaaaa)).to.eventually.equal(0xaaaaaaaa); + await expect(this.mock.$reverseBitsUint32(0x55555555)).to.eventually.equal(0x55555555); + }); + }); + }); });