-
Notifications
You must be signed in to change notification settings - Fork 12.1k
Add RLP library #5680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
ernestognw
wants to merge
78
commits into
OpenZeppelin:master
Choose a base branch
from
ernestognw:feature/rlp
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Add RLP library #5680
Changes from 75 commits
Commits
Show all changes
78 commits
Select commit
Hold shift + click to select a range
9eb5f1c
Add memory utils
ernestognw 2d397f4
Fix tests upgradeable
ernestognw 2a0fb7e
Add docs
ernestognw a7e61c3
Make use of the library
ernestognw 1aae8bb
Update docs/modules/ROOT/pages/utilities.adoc
ernestognw 1b2679a
Merge branch 'master' into utils/memory
Amxx d514606
fix tests
Amxx 14fa04e
Update contracts/utils/Memory.sol
ernestognw d0d55fc
Update contracts/utils/Memory.sol
arr00 7b3cb66
Add RLP library
ernestognw 95149f8
Add TrieProof library
ernestognw ad5d4ac
up
ernestognw 18540ef
Add docs
ernestognw 163f27c
Workaround stack too deep
ernestognw c484289
Add Changesets
ernestognw e0d4790
Add more changesets
ernestognw a6f9053
Add FV and fuzz tests
ernestognw e2b5e4c
Merge branch 'master' into feature/rlp
ernestognw 203d1a2
up
ernestognw 48eabc1
docs
ernestognw 63ced95
up pragma
ernestognw f342756
Add missing Bytes test
ernestognw 23dba37
Add unit tests
ernestognw 0cacca2
up pragma
ernestognw 831e8ab
Move TrieProof
ernestognw 5da111f
Fix countLeadingZeroes
ernestognw ba2293e
nits
ernestognw 9409bc6
Improve
ernestognw e740dac
Fix
ernestognw 0332ffe
Add Memory.sol library
ernestognw 608e3cd
Merge branch 'master' into utils/memory
ernestognw ac92bb4
up
ernestognw 6094bb7
Merge branch 'master' into utils/memory
ernestognw 6bb96d5
WIP: Add more Memory functions
ernestognw 860e5a8
up
ernestognw ecdb768
revert
ernestognw 95907aa
Update docs
ernestognw 124ccee
Nit
ernestognw c3237df
Finish fuzz tests and FV
ernestognw 27f0a9b
up
ernestognw 282ce39
up
ernestognw bdd2cf1
Add operations to Math.sol
ernestognw 42c79f1
Add new equal, nibbles and countLeadingZeroes functions
ernestognw 5754ab8
Rename countLeadingZeroes to clz
ernestognw 44f0e14
up
ernestognw 05c73bd
Pragma changes
ernestognw 3a6fbf6
up
ernestognw e67e8b4
up
ernestognw 3385718
Rename to in Math library and update corresponding tests for consis…
ernestognw 40d7922
Update return types of reverseBits functions to match their respectiv…
ernestognw 89860bc
Refactor reverseBits functions in to use fixed-size byte types
ernestognw 9b58730
Test nits
ernestognw 77ffa8c
Simplify
ernestognw ce91c80
up
ernestognw b3e3add
Move reverse functions to Bytes.sol
ernestognw 2f3107c
Move Bytes.t.sol
ernestognw 4383e01
Merge branch 'master' into feat/bytes-rlp
ernestognw 5a44b11
up
ernestognw d6db2d7
Document
ernestognw 3847050
Remove extra functions
ernestognw 4fd1947
Update docs
ernestognw c4e0375
up
ernestognw acb14cb
Merge branch 'utils/memory' into feature/rlp
ernestognw 2208006
Merge branch 'feat/math-reverse-bits' into feature/rlp
ernestognw 13f4d8f
Merge branch 'feat/bytes-rlp' into feature/rlp
ernestognw 502a520
Merge branch 'master' into feature/rlp
ernestognw aab9274
Merge branch 'master' into feature/rlp
Amxx aa26e48
up
Amxx 948f0a1
Merge branch 'master' into feature/rlp
ernestognw d4bfb8b
Fix compilation
ernestognw 138de7f
Remove dangling clz
ernestognw 5efeb37
Make nibbles function private
ernestognw 00ff228
Remove nibbles test
ernestognw c58c7fd
Remove TrieProof library
ernestognw d1aa944
Add some tests
ernestognw 0925577
Merge branch 'master' into feature/rlp
ernestognw 590b7d7
Use Bytes.concat
ernestognw 8928039
Use Math.ternary
ernestognw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`RLP`: Add library for Ethereum's Recursive Length Prefix encoding/decoding. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, // 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 = 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 encoded items, 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)); | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* @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)); | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// @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 decodeList(Item memory item) internal pure returns (Item[] memory) { | ||
(uint256 listOffset, uint256 listLength, ItemType itemType) = _decodeLength(item); | ||
require(itemType == ItemType.List, RLPUnexpectedType(ItemType.List, 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 {decodeList} but for `bytes`. See {toItem}. | ||
function decodeList(bytes memory value) internal pure returns (Item[] memory) { | ||
return decodeList(toItem(value)); | ||
} | ||
|
||
/// @dev Decodes an RLP encoded item. | ||
function decodeBytes(Item memory item) internal pure returns (bytes memory) { | ||
(uint256 itemOffset, uint256 itemLength, ItemType itemType) = _decodeLength(item); | ||
require(itemType == ItemType.Data, RLPUnexpectedType(ItemType.Data, 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 {decodeBytes} but for `bytes`. See {toItem}. | ||
function decodeBytes(bytes memory item) internal pure returns (bytes memory) { | ||
return decodeBytes(toItem(item)); | ||
} | ||
|
||
/// @dev Reads the raw bytes of an RLP item without decoding the content. Includes prefix bytes. | ||
function decodeRawBytes(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), | ||
_binaryBuffer(length) // already in big-endian, minimal representation | ||
); | ||
} | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// @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() / 8); | ||
} | ||
|
||
/// @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, _addOffset(_asPointer(item), 32), 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); | ||
|
||
// 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); | ||
} | ||
|
||
// 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); | ||
} | ||
|
||
/// @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); | ||
} | ||
|
||
/// @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); | ||
} | ||
|
||
/// @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 | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to remove leeding zeros. is that desirable ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is desirable and part of the spec: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/