diff --git a/.changeset/itchy-turkeys-allow.md b/.changeset/itchy-turkeys-allow.md new file mode 100644 index 00000000000..caab673c15c --- /dev/null +++ b/.changeset/itchy-turkeys-allow.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`RLP`: Add a library for encoding and decoding data in Ethereum's Recursive Length Prefix format. 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/modern-moments-raise.md b/.changeset/modern-moments-raise.md new file mode 100644 index 00000000000..38526551e0f --- /dev/null +++ b/.changeset/modern-moments-raise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Memory`: Add a UDVT for handling slices or memory space similarly to calldata slices. diff --git a/.changeset/wise-webs-fly.md b/.changeset/wise-webs-fly.md new file mode 100644 index 00000000000..5fe2c548c6f --- /dev/null +++ b/.changeset/wise-webs-fly.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Accumulators`: A library for merging an arbitrary dynamic number of bytes buffers. diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 232fbe83d4e..0b74c9a0ee3 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.26; // We keep these imports and a dummy contract just to we can run the test suite after transpilation. +import {Accumulators} from "../utils/structs/Accumulators.sol"; import {Address} from "../utils/Address.sol"; import {Arrays} from "../utils/Arrays.sol"; import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol"; @@ -44,6 +45,7 @@ import {P256} from "../utils/cryptography/P256.sol"; import {Packing} from "../utils/Packing.sol"; import {Panic} from "../utils/Panic.sol"; import {RelayedCall} from "../utils/RelayedCall.sol"; +import {RLP} from "../utils/RLP.sol"; import {RSA} from "../utils/cryptography/RSA.sol"; import {SafeCast} from "../utils/math/SafeCast.sol"; import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 51bbf8f42f9..38ae716e79a 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/ERC4626.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index f09e1c8d626..9e3722e6ba9 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; + +import {Panic} from "./Panic.sol"; /** * @dev Utilities to manipulate memory. @@ -41,4 +43,71 @@ library Memory { function asPointer(bytes32 value) internal pure returns (Pointer) { return Pointer.wrap(value); } + + type Slice is bytes32; + + /// @dev Get a slice representation of a bytes object in memory + function asSlice(bytes memory self) internal pure returns (Slice result) { + assembly ("memory-safe") { + result := or(shl(128, mload(self)), add(self, 0x20)) + } + } + + /// @dev Private helper: create a slice from raw values (length and pointer) + function _asSlice(uint256 len, Memory.Pointer ptr) private pure returns (Slice result) { + // TODO: Fail if len or ptr is larger than type(uint128).max ? + assembly ("memory-safe") { + result := or(shl(128, len), ptr) + } + } + + /// @dev Returns the memory location of a given slice (equiv to self.offset for calldata slices) + function _pointer(Slice self) private pure returns (Memory.Pointer result) { + assembly ("memory-safe") { + result := and(self, shr(128, not(0))) + } + } + + /// @dev Returns the length of a given slice (equiv to self.length for calldata slices) + function length(Slice self) internal pure returns (uint256 result) { + assembly ("memory-safe") { + result := shr(128, self) + } + } + + /// @dev Offset a memory slice (equivalent to self[start:] for calldata slices) + function slice(Slice self, uint256 offset) internal pure returns (Slice) { + if (offset > length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + return _asSlice(length(self) - offset, asPointer(bytes32(uint256(asBytes32(_pointer(self))) + offset))); + } + + /// @dev Offset and cut a Slice (equivalent to self[start:start+length] for calldata slices) + function slice(Slice self, uint256 offset, uint256 len) internal pure returns (Slice) { + if (offset + len > length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + return _asSlice(len, asPointer(bytes32(uint256(asBytes32(_pointer(self))) + offset))); + } + + /** + * @dev Read a bytes32 buffer from a given Slice at a specific offset + * + * Note:If offset > length(slice) - 32, part of the return value will be out of bound and should be ignored. + */ + function load(Slice self, uint256 offset) internal pure returns (bytes32 value) { + if (offset >= length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + assembly ("memory-safe") { + value := mload(add(and(self, shr(128, not(0))), offset)) + } + } + + /// @dev Extract the data corresponding to a Slice (allocate new memory) + function toBytes(Slice self) internal pure returns (bytes memory result) { + uint256 len = length(self); + Memory.Pointer ptr = _pointer(self); + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, len) + mcopy(add(result, 0x20), ptr, len) + mstore(0x40, add(add(result, len), 0x20)) + } + } } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 3542d10849c..3e1a4adad21 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -13,6 +13,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions. * {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]). * {ERC165}, {ERC165Checker}: Utilities for inspecting interfaces supported by contracts. + * {Accumulators}: A library for merging an arbitrary dynamic number of bytes buffers. * {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way. * {Checkpoints}: A data structure to store values mapped to a strictly increasing key. Can be used for storing and accessing values over time. * {CircularBuffer}: A data structure to store the last N values pushed to it. @@ -38,6 +39,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Packing}: A library for packing and unpacking multiple values into bytes32. * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. * {RelayedCall}: A library for performing calls that use minimal and predictable relayers to hide the sender. + * {RLP}: Library for encoding and decoding data in Ethereum's Recursive Length Prefix format. * {ShortStrings}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. * {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays. * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. @@ -84,6 +86,8 @@ Ethereum contracts have no native concept of an interface, so applications must == Data Structures +{{Accumulators}} + {{BitMaps}} {{Checkpoints}} @@ -138,6 +142,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{RelayedCall}} +{{RLP}} + {{ShortStrings}} {{SlotDerivation}} diff --git a/contracts/utils/RLP.sol b/contracts/utils/RLP.sol new file mode 100644 index 00000000000..e8a7c78dd2e --- /dev/null +++ b/contracts/utils/RLP.sol @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Math} from "./math/Math.sol"; +import {Accumulators} from "./structs/Accumulators.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. + * + * Inspired by + * * https://github.com/succinctlabs/optimism-bedrock-contracts/blob/main/rlp/RLPWriter.sol + * * https://github.com/succinctlabs/optimism-bedrock-contracts/blob/main/rlp/RLPReader.sol + */ +library RLP { + using Accumulators for *; + 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); + + enum ItemType { + Data, // Single data value + List // 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 = 0x80; + /// @dev Prefix for list items (0xC0) + uint8 internal constant LONG_OFFSET = 0xC0; + + /**************************************************************************************************************** + * ENCODING - ENCODER * + ****************************************************************************************************************/ + + struct Encoder { + Accumulators.Accumulator acc; + } + + /// @dev Create an empty RLP Encoder. + function encoder() internal pure returns (Encoder memory enc) { + enc.acc = Accumulators.accumulator(); + } + + /// @dev Add a boolean to a given RLP Encoder. + function push(Encoder memory self, bool input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add an address to a given RLP Encoder. + function push(Encoder memory self, address input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add a uint256 to a given RLP Encoder. + function push(Encoder memory self, uint256 input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add a bytes32 to a given RLP Encoder. + function push(Encoder memory self, bytes32 input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add a bytes buffer to a given RLP Encoder. + function push(Encoder memory self, bytes memory input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add a string to a given RLP Encoder. + function push(Encoder memory self, string memory input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add an array of bytes to a given RLP Encoder. + function push(Encoder memory self, bytes[] memory input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /// @dev Add an (input) Encoder to a (target) Encoder. The input is RLP encoded as a list of bytes, and added to the target Encoder. + function push(Encoder memory self, Encoder memory input) internal pure returns (Encoder memory) { + self.acc.push(encode(input)); + return self; + } + + /**************************************************************************************************************** + * ENCODING - TO BYTES * + ****************************************************************************************************************/ + + /** + * @dev 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. + */ + function encode(bool input) internal pure returns (bytes memory result) { + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, 0x01) // length of the encoded data: 1 byte + mstore8(add(result, 0x20), add(mul(iszero(input), 0x7f), 1)) // input + mstore(0x40, add(result, 0x21)) // reserve memory + } + } + + /// @dev Encode an address as RLP. + function encode(address input) internal pure returns (bytes memory result) { + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, 0x15) // length of the encoded data: 1 (prefix) + 14 (address) + mstore(add(result, 0x20), or(shl(248, 0x94), shl(88, input))) // prefix (0x94 = SHORT_OFFSET + 14) + input + mstore(0x40, add(result, 0x35)) // reserve memory + } + } + + /// @dev Encode a uint256 as RLP. + function encode(uint256 input) internal pure returns (bytes memory result) { + if (input < SHORT_OFFSET) { + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, 1) // length of the encoded data: 1 byte + mstore8(add(result, 0x20), or(input, mul(0x80, iszero(input)))) // input (zero is encoded as 0x80) + mstore(0x40, add(result, 0x21)) // reserve memory + } + } else { + uint256 length = Math.log256(input) + 1; + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, add(length, 1)) // length of the encoded data: 1 (prefix) + length + mstore8(add(result, 0x20), add(length, SHORT_OFFSET)) // prefix: SHORT_OFFSET + length + mstore(add(result, 0x21), shl(sub(256, mul(8, length)), input)) // input (aligned left) + mstore(0x40, add(result, add(length, 0x21))) // reserve memory + } + } + } + + /// @dev Encode a bytes32 as RLP. Type alias for {encode-uint256-}. + function encode(bytes32 input) internal pure returns (bytes memory) { + return encode(uint256(input)); + } + + /// @dev Encode a bytes buffer as RLP. + function encode(bytes memory input) internal pure returns (bytes memory) { + return (input.length == 1 && uint8(input[0]) < SHORT_OFFSET) ? input : _encode(input, SHORT_OFFSET); + } + + /// @dev Encode a string as RLP. Type alias for {encode-bytes-}. + function encode(string memory input) internal pure returns (bytes memory) { + return encode(bytes(input)); + } + + /// @dev Encode an array of bytes as RLP. + function encode(bytes[] memory input) internal pure returns (bytes memory) { + return _encode(input.concat(), LONG_OFFSET); + } + + /// @dev Encode an encoder (list of bytes) as RLP + function encode(Encoder memory self) internal pure returns (bytes memory result) { + return _encode(self.acc.flatten(), LONG_OFFSET); + } + + function _encode(bytes memory input, uint256 offset) private pure returns (bytes memory result) { + uint256 length = input.length; + if (length <= SHORT_THRESHOLD) { + // Encode "short-bytes" as + // [ offset + input.length | input ] + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, add(length, 1)) // length of the encoded data: 1 (prefix) + input.length + mstore8(add(result, 0x20), add(length, offset)) // prefix: offset + input.length + mcopy(add(result, 0x21), add(input, 0x20), length) // input + mstore(0x40, add(result, add(length, 0x21))) // reserve memory + } + } else { + // Encode "long-bytes" as + // [ SHORT_THRESHOLD + offset + input.length.length | input.length | input ] + uint256 lenlength = Math.log256(length) + 1; + assembly ("memory-safe") { + result := mload(0x40) + mstore(result, add(add(length, lenlength), 1)) // length of the encoded data: 1 (prefix) + input.length.length + input.length + mstore8(add(result, 0x20), add(add(lenlength, offset), SHORT_THRESHOLD)) // prefix: SHORT_THRESHOLD + offset + input.length.length + mstore(add(result, 0x21), shl(sub(256, mul(8, lenlength)), length)) // input.length + mcopy(add(result, add(lenlength, 0x21)), add(input, 0x20), length) // input + mstore(0x40, add(result, add(add(length, lenlength), 0x21))) // reserve memory + } + } + } + + /**************************************************************************************************************** + * DECODING - READ FROM AN RLP ENCODED MEMORY SLICE * + ****************************************************************************************************************/ + + /// @dev Decode an RLP encoded bool. See {encode-bool} + function readBool(Memory.Slice item) internal pure returns (bool) { + return readUint256(item) != 0; + } + + /// @dev Decode an RLP encoded address. See {encode-address} + function readAddress(Memory.Slice item) internal pure returns (address) { + uint256 length = item.length(); + require(length == 1 || length == 21, RLPContentLengthMismatch(21, length)); + return address(uint160(readUint256(item))); + } + + /// @dev Decode an RLP encoded uint256. See {encode-uint256} + function readUint256(Memory.Slice item) internal pure returns (uint256) { + uint256 length = item.length(); + require(length <= 33, RLPContentLengthMismatch(32, length)); + + (uint256 itemOffset, uint256 itemLength, ItemType itemType) = _decodeLength(item); + require(itemType == ItemType.Data, RLPUnexpectedType(ItemType.Data, itemType)); + + return itemLength == 0 ? 0 : uint256(item.load(itemOffset)) >> (256 - 8 * itemLength); + } + + /// @dev Decode an RLP encoded bytes32. See {encode-bytes32} + function readBytes32(Memory.Slice item) internal pure returns (bytes32) { + return bytes32(readUint256(item)); + } + + /// @dev Decodes an RLP encoded bytes. See {encode-bytes} + function readBytes(Memory.Slice item) internal pure returns (bytes memory) { + (uint256 offset, uint256 length, ItemType itemType) = _decodeLength(item); + require(itemType == ItemType.Data, RLPUnexpectedType(ItemType.Data, itemType)); + + // Length is checked by {toBytes} + return item.slice(offset, length).toBytes(); + } + + function readString(Memory.Slice item) internal pure returns (string memory) { + return string(readBytes(item)); + } + + /// @dev Decodes an RLP encoded list into an array of RLP Items. This function supports list up to 32 elements + function readList(Memory.Slice item) internal pure returns (Memory.Slice[] memory) { + return readList(item, 32); + } + + /** + * @dev Variant of {readList-bytes32} that supports long lists up to `maxListLength`. Setting `maxListLength` to + * a high value will cause important, and costly, memory expansion. + */ + function readList(Memory.Slice item, uint256 maxListLength) internal pure returns (Memory.Slice[] memory) { + uint256 itemLength = item.length(); + + (uint256 listOffset, uint256 listLength, ItemType itemType) = _decodeLength(item); + require(itemType == ItemType.List, RLPUnexpectedType(ItemType.List, itemType)); + require(itemLength == listOffset + listLength, RLPContentLengthMismatch(listOffset + listLength, itemLength)); + + Memory.Slice[] memory list = new Memory.Slice[](maxListLength); + + uint256 itemCount; + for (uint256 currentOffset = listOffset; currentOffset < itemLength; ++itemCount) { + (uint256 elementOffset, uint256 elementLength, ) = _decodeLength(item.slice(currentOffset)); + list[itemCount] = item.slice(currentOffset, elementLength + elementOffset); + currentOffset += elementOffset + elementLength; + } + + // Decrease the array size to match the actual item count. + assembly ("memory-safe") { + mstore(list, itemCount) + } + return list; + } + + /**************************************************************************************************************** + * DECODING - FROM BYTES * + ****************************************************************************************************************/ + + function decodeBool(bytes memory item) internal pure returns (bool) { + return readBool(item.asSlice()); + } + + function decodeAddress(bytes memory item) internal pure returns (address) { + return readAddress(item.asSlice()); + } + + function decodeUint256(bytes memory item) internal pure returns (uint256) { + return readUint256(item.asSlice()); + } + + function decodeBytes32(bytes memory item) internal pure returns (bytes32) { + return readBytes32(item.asSlice()); + } + + function decodeBytes(bytes memory item) internal pure returns (bytes memory) { + return readBytes(item.asSlice()); + } + + function decodeString(bytes memory item) internal pure returns (string memory) { + return readString(item.asSlice()); + } + + function decodeList(bytes memory value) internal pure returns (Memory.Slice[] memory) { + return readList(value.asSlice()); + } + + /** + * @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( + Memory.Slice item + ) private pure returns (uint256 _offset, uint256 _length, ItemType _itemtype) { + uint256 itemLength = item.length(); + + require(itemLength != 0, RLPEmptyItem()); + uint8 prefix = uint8(bytes1(item.load(0))); + + if (prefix < LONG_OFFSET) { + // CASE: item + if (prefix < SHORT_OFFSET) { + // Case: Single byte below 128 + return (0, 1, ItemType.Data); + } else if (prefix <= SHORT_OFFSET + SHORT_THRESHOLD) { + // Case: Short string (0-55 bytes) + uint256 strLength = prefix - SHORT_OFFSET; + require(itemLength > strLength, RLPInvalidDataRemainder(strLength, itemLength)); + if (strLength == 1) { + require(bytes1(item.load(1)) >= bytes1(SHORT_OFFSET)); // TODO: custom error for sanity checks + } + return (1, strLength, ItemType.Data); + } else { + // Case: Long string (>55 bytes) + uint256 lengthLength = prefix - SHORT_OFFSET - SHORT_THRESHOLD; + + require(itemLength > lengthLength, RLPInvalidDataRemainder(lengthLength, itemLength)); + require(bytes1(item.load(0)) != 0x00); // TODO: custom error for sanity checks + + uint256 len = uint256(item.load(1)) >> (256 - 8 * lengthLength); + require(len > SHORT_THRESHOLD, RLPInvalidDataRemainder(SHORT_THRESHOLD, len)); + require(itemLength > lengthLength + len, RLPContentLengthMismatch(lengthLength + len, itemLength)); + + return (lengthLength + 1, len, ItemType.Data); + } + } else { + // Case: list + if (prefix <= LONG_OFFSET + SHORT_THRESHOLD) { + // Case: Short list + uint256 listLength = prefix - LONG_OFFSET; + require(item.length() > listLength, RLPInvalidDataRemainder(listLength, itemLength)); + return (1, listLength, ItemType.List); + } else { + // Case: Long list + uint256 lengthLength = prefix - LONG_OFFSET - SHORT_THRESHOLD; + + require(itemLength > lengthLength, RLPInvalidDataRemainder(lengthLength, itemLength)); + require(bytes1(item.load(0)) != 0x00); + + uint256 len = uint256(item.load(1)) >> (256 - 8 * lengthLength); + require(len > SHORT_THRESHOLD, RLPInvalidDataRemainder(SHORT_THRESHOLD, len)); + require(itemLength > lengthLength + len, RLPContentLengthMismatch(lengthLength + len, itemLength)); + + return (lengthLength + 1, len, ItemType.List); + } + } + } +} diff --git a/contracts/utils/structs/Accumulators.sol b/contracts/utils/structs/Accumulators.sol new file mode 100644 index 00000000000..78230654f93 --- /dev/null +++ b/contracts/utils/structs/Accumulators.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Memory} from "../Memory.sol"; + +/** + * @dev Structure concatenating an arbitrary number of bytes buffers with limited memory allocation. + */ +library Accumulators { + /** + * @dev Bytes accumulator: a linked list of `bytes`. + * + * Note: This is a memory structure that SHOULD not be put in storage. + */ + struct Accumulator { + Memory.Pointer head; + Memory.Pointer tail; + } + + /// @dev Item (list node) in a bytes accumulator + struct AccumulatorEntry { + Memory.Pointer next; + Memory.Slice data; + } + + /// @dev Create a new (empty) accumulator + function accumulator() internal pure returns (Accumulator memory self) { + self.head = Memory.asPointer(0x00); + self.tail = Memory.asPointer(0x00); + } + + /// @dev Add a bytes buffer to (the end of) an Accumulator + function push(Accumulator memory self, bytes memory data) internal pure returns (Accumulator memory) { + Memory.Pointer ptr = _asPtr(AccumulatorEntry({next: Memory.asPointer(0x00), data: Memory.asSlice(data)})); + + if (Memory.asBytes32(self.head) == 0x00) { + self.head = ptr; + self.tail = ptr; + } else { + _asAccumulatorEntry(self.tail).next = ptr; + self.tail = ptr; + } + + return self; + } + + /// @dev Add a bytes buffer to (the beginning of) an Accumulator + function shift(Accumulator memory self, bytes memory data) internal pure returns (Accumulator memory) { + Memory.Pointer ptr = _asPtr(AccumulatorEntry({next: self.head, data: Memory.asSlice(data)})); + + if (Memory.asBytes32(self.head) == 0x00) { + self.head = ptr; + self.tail = ptr; + } else { + self.head = ptr; + } + + return self; + } + + /// @dev Flatten all the bytes entries in an Accumulator into a single buffer + function flatten(Accumulator memory self) internal pure returns (bytes memory result) { + assembly ("memory-safe") { + result := mload(0x40) + let ptr := add(result, 0x20) + for { + let it := mload(self) + } iszero(iszero(it)) { + it := mload(it) + } { + let slice := mload(add(it, 0x20)) + let offset := and(slice, shr(128, not(0))) + let length := shr(128, slice) + mcopy(ptr, offset, length) + ptr := add(ptr, length) + } + mstore(result, sub(ptr, add(result, 0x20))) + mstore(0x40, ptr) + } + } + + function _asPtr(AccumulatorEntry memory item) private pure returns (Memory.Pointer ptr) { + assembly ("memory-safe") { + ptr := item + } + } + + function _asAccumulatorEntry(Memory.Pointer ptr) private pure returns (AccumulatorEntry memory item) { + assembly ("memory-safe") { + item := ptr + } + } +} diff --git a/test/utils/Accumulators.t.sol b/test/utils/Accumulators.t.sol new file mode 100644 index 00000000000..83c64e9f17f --- /dev/null +++ b/test/utils/Accumulators.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Accumulators} from "@openzeppelin/contracts/utils/structs/Accumulators.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +contract AccumulatorsTest is Test { + using Accumulators for *; + + // Accumulator + function testAccumulatorPushShift() public pure { + Accumulators.Accumulator memory acc = Accumulators.accumulator(); // + acc.push(hex"11"); // 11 + acc.push(hex"22"); // 1122 + acc.shift(hex"33"); // 331122 + acc.shift(hex"44"); // 44331122 + acc.push(hex"55"); // 4433112255 + acc.shift(hex"66"); // 664433112255 + assertEq(acc.flatten(), hex"664433112255"); + } + + function testAccumulatorPush(bytes[] calldata input) public pure { + Accumulators.Accumulator memory acc = Accumulators.accumulator(); + for (uint256 i = 0; i < input.length; ++i) acc.push(input[i]); + assertEq(acc.flatten(), Bytes.concat(input)); + } + + function testAccumulatorShift(bytes[] calldata input) public pure { + Accumulators.Accumulator memory acc = Accumulators.accumulator(); + for (uint256 i = input.length; i > 0; --i) acc.shift(input[i - 1]); + assertEq(acc.flatten(), Bytes.concat(input)); + } +} diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 8ed9b4c43bc..86fb901b38c 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -3,9 +3,11 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; contract MemoryTest is Test { + using Bytes for *; using Memory for *; // - first 0x80 bytes are reserved (scratch + FMP + zero) @@ -18,4 +20,19 @@ contract MemoryTest is Test { ptr.asPointer().setFreeMemoryPointer(); assertEq(Memory.getFreeMemoryPointer().asBytes32(), ptr); } + + function testAsSliceToBytes(bytes memory input) public pure { + assertEq(input.asSlice().toBytes(), input); + } + + function testSlice(bytes memory input, uint256 offset) public pure { + offset = bound(offset, 0, input.length); + assertEq(input.asSlice().slice(offset).toBytes(), input.slice(offset)); + } + + function testSlice(bytes memory input, uint256 offset, uint256 length) public pure { + offset = bound(offset, 0, input.length); + length = bound(length, 0, input.length - offset); + assertEq(input.asSlice().slice(offset, length).toBytes(), input.slice(offset, offset + length)); + } } diff --git a/test/utils/RLP.t.sol b/test/utils/RLP.t.sol new file mode 100644 index 00000000000..7bc40d6024d --- /dev/null +++ b/test/utils/RLP.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RLP} from "@openzeppelin/contracts/utils/RLP.sol"; +import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; + +contract RLPTest is Test { + using RLP for *; + + // Encode -> Decode + + function testEncodeDecodeBool(bool input) external pure { + assertEq(input.encode().decodeBool(), input); + } + + function testEncodeDecodeAddress(address input) external pure { + assertEq(input.encode().decodeAddress(), input); + } + + function testEncodeDecodeUint256(uint256 input) external pure { + assertEq(input.encode().decodeUint256(), input); + } + + function testEncodeDecodeBytes32(bytes32 input) external pure { + assertEq(input.encode().decodeBytes32(), input); + } + + function testEncodeDecodeBytes(bytes memory input) external pure { + assertEq(input.encode().decodeBytes(), input); + } + + function testEncodeDecodeString(string memory input) external pure { + assertEq(input.encode().decodeString(), input); + } + + /// forge-config: default.fuzz.runs = 512 + function testEncodeDecodeList(bytes[] memory input) external pure { + // max length for list decoding by default + vm.assume(input.length <= 32); + + bytes[] memory encoded = new bytes[](input.length); + for (uint256 i = 0; i < input.length; ++i) { + encoded[i] = input[i].encode(); + } + + // encode list + decode as list of RLP items + Memory.Slice[] memory list = encoded.encode().decodeList(); + + assertEq(list.length, input.length); + for (uint256 i = 0; i < input.length; ++i) { + assertEq(list[i].readBytes(), input[i]); + } + } + + // List encoder + + function testEncodeEmpty() external pure { + assertEq(RLP.encoder().encode(), hex"c0"); + } + + function testEncodeBool(bool input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeAddress(address input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeUint256(uint256 input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeBytes32(bytes32 input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeBytes(bytes memory input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeString(string memory input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + /// forge-config: default.fuzz.runs = 512 + function testEncodeBytesArray(bytes[] memory input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + + assertEq(RLP.encoder().push(input).encode(), RLP.encode(list)); + } + + function testEncodeEncoder(bytes memory input) external pure { + bytes[] memory list = new bytes[](1); + list[0] = RLP.encode(input); + list[0] = RLP.encode(list); + + assertEq(RLP.encoder().push(RLP.encoder().push(input)).encode(), RLP.encode(list)); + } + + function testEncodeMultiType(uint256 u, bytes memory b, address a) external pure { + bytes[] memory list = new bytes[](3); + list[0] = RLP.encode(u); + list[1] = RLP.encode(b); + list[2] = RLP.encode(a); + + assertEq(RLP.encoder().push(u).push(b).push(a).encode(), RLP.encode(list)); + + list[0] = RLP.encode(b); + list[1] = RLP.encode(a); + list[2] = RLP.encode(u); + + assertEq(RLP.encoder().push(b).push(a).push(u).encode(), RLP.encode(list)); + } +} diff --git a/test/utils/RLP.test.js b/test/utils/RLP.test.js new file mode 100644 index 00000000000..856a93a48fa --- /dev/null +++ b/test/utils/RLP.test.js @@ -0,0 +1,150 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { generators } = require('../helpers/random'); + +async function fixture() { + const mock = await ethers.deployContract('$RLP'); + + // Resolve function overload ambiguities like in Math.test.js + mock.$encode_bool = mock['$encode(bool)']; + mock.$encode_address = mock['$encode(address)']; + mock.$encode_uint256 = mock['$encode(uint256)']; + mock.$encode_bytes32 = mock['$encode(bytes32)']; + mock.$encode_bytes = mock['$encode(bytes)']; + mock.$encode_string = mock['$encode(string)']; + mock.$encode_list = mock['$encode(bytes[])']; + + return { mock }; +} + +describe('RLP', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('encode/decode booleans', async function () { + await expect(this.mock.$encode_bool(false)).to.eventually.equal('0x80'); // 0 + await expect(this.mock.$encode_bool(true)).to.eventually.equal('0x01'); // 1 + + await expect(this.mock.$decodeBool('0x80')).to.eventually.equal(false); // 0 + await expect(this.mock.$decodeBool('0x01')).to.eventually.equal(true); // 1 + }); + + it('encode/decode addresses', async function () { + const addr = generators.address(); + const expected = ethers.encodeRlp(addr); + + await expect(this.mock.$encode_address(addr)).to.eventually.equal(expected); + await expect(this.mock.$decodeAddress(expected)).to.eventually.equal(addr); + }); + + it('encode/decode uint256', async function () { + for (const input of [0, 1, 127, 128, 256, 1024, 0xffffff, ethers.MaxUint256]) { + const expected = ethers.encodeRlp(ethers.toBeArray(input)); + + await expect(this.mock.$encode_uint256(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeUint256(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode bytes32', async function () { + for (const { input, expected } of [ + { input: '0x0000000000000000000000000000000000000000000000000000000000000000', expected: '0x80' }, + { input: '0x0000000000000000000000000000000000000000000000000000000000000001', expected: '0x01' }, + { + input: '0x1000000000000000000000000000000000000000000000000000000000000000', + expected: '0xa01000000000000000000000000000000000000000000000000000000000000000', + }, + ]) { + await expect(this.mock.$encode_bytes32(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes32(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode empty byte', async function () { + const input = '0x'; + const expected = ethers.encodeRlp(input); + + await expect(this.mock.$encode_bytes(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes(expected)).to.eventually.equal(input); + }); + + it('encode/decode single byte < 128', async function () { + for (const input of ['0x00', '0x01', '0x7f']) { + const expected = ethers.encodeRlp(input); + + await expect(this.mock.$encode_bytes(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode single byte >= 128', async function () { + for (const input of ['0x80', '0xff']) { + const expected = ethers.encodeRlp(input); + + await expect(this.mock.$encode_bytes(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode short buffers (1-55 bytes)', async function () { + for (const input of [ + '0xab', // 1 byte + '0x1234', // 2 bytes + generators.bytes(55), // 55 bytes (maximum for short encoding) + ]) { + const expected = ethers.encodeRlp(input); + + await expect(this.mock.$encode_bytes(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode long buffers (>55 bytes)', async function () { + for (const input of [ + generators.bytes(56), // 56 bytes (minimum for long encoding) + generators.bytes(128), // 128 bytes + ]) { + const expected = ethers.encodeRlp(input); + + await expect(this.mock.$encode_bytes(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeBytes(expected)).to.eventually.equal(input); + } + }); + + it('encode/decode strings', async function () { + for (const input of [ + '', // empty string + 'dog', + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit', + ]) { + const expected = ethers.encodeRlp(ethers.toUtf8Bytes(input)); + + await expect(this.mock.$encode_string(input)).to.eventually.equal(expected); + await expect(this.mock.$decodeString(expected)).to.eventually.equal(input); + } + }); + + it('encodes array (bytes[])', async function () { + for (const input of [[], ['0x'], ['0x00'], ['0x17', '0x42'], ['0x17', '0x', '0x42', '0x0123456789abcdef', '0x']]) { + await expect(this.mock.$encode_list(input.map(ethers.encodeRlp))).to.eventually.equal(ethers.encodeRlp(input)); + } + }); + + // const invalidTests = [ + // { name: 'short string with invalid length', input: '0x8100' }, + // { name: 'long string with invalid length prefix', input: '0xb800' }, + // { name: 'list with invalid length', input: '0xc100' }, + // { name: 'truncated long string', input: '0xb838' }, + // { name: 'invalid single byte encoding (non-minimal)', input: '0x8100' }, + // ]; + + // invalidTests.forEach(({ name, input }) => { + // it(`encodes ${name} into invalid RLP`, async function () { + // const item = await this.mock.$toItem(input); + // await expect(this.mock.$decodeBytes_bytes(item)).to.be.reverted; + // }); + // }); +});