Skip to content

Conversation

ernestognw
Copy link
Member

@ernestognw ernestognw commented May 10, 2025

Copy link

changeset-bot bot commented May 10, 2025

🦋 Changeset detected

Latest commit: 8928039

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ernestognw ernestognw changed the title Add RLP library Add RLP and TrieProof libraries May 11, 2025
@ernestognw ernestognw added this to the 5.x milestone Jun 4, 2025
@ernestognw ernestognw mentioned this pull request Jul 9, 2025
3 tasks
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]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my point before I don't think it's necessary to convert the key to nibbles beforehand, makes more sense to extract the Nth nibble here.

Comment on lines 103 to 110
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_;
}
Copy link

@0xClandestine 0xClandestine Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toNibbles function converts a bytes array like 0xABCD into a nibble-expanded format like 0xA00BC00D. The existing implementation does this by iterating linearly over the input, which works correctly but is extremely inefficient.

My approach processes the input in parallel, reducing time complexity from O(n) to O(n / 32). This is inspired by bit-hacking techniques for interleaving bits, as described in the following resources:

https://stackoverflow.com/questions/38881877/bit-hack-expanding-bits
https://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN

    // 0xABCD -> 0xA0B0C0D0
    function expandNibbles(uint128 x) internal pure returns (uint256) {
        uint256 y = uint256(x);
        y = (y | (y << 64)) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF;
        y = (y | (y << 32)) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF;
        y = (y | (y << 16)) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF;
        y = (y | (y << 8)) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF;
        y = (y | (y << 4)) & 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F;
        return y;
    }
    
    // Puts nibbles in their respective height (high/low). 
    // 0xA0B0C0D0 -> 0xA00BC00D
    function toNibbles(uint128 x) internal pure returns (uint256) {
        uint256 y = expandNibbles(x);
        uint256 high = y & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF;
        uint256 low = y & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00;
        high = high >> 4;
        return high | low;
    }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bringing it all together:

function toNibbles(bytes memory input) internal pure returns (bytes memory output) {
    /// @solidity memory-safe-assembly
    assembly {
        output := mload(0x40)
        mstore(output, mul(mload(input), 2))
        mstore(0x40, add(output, add(0x20, mul(mload(input), 2))))
        
        for { let i := 0 } lt(i, mload(input)) { i := add(i, 16) } {
            let x := shr(128, mload(add(add(input, 0x20), i)))
            x := and(or(x, shl(64, x)), 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF)
            x := and(or(x, shl(32, x)), 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF)
            x := and(or(x, shl(16, x)), 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF)
            x := and(or(x, shl(8, x)),  0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF)
            let mask := 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F
            x := and(or(x, shl(4, x)),  mask)
            x := or(shl(4, and(shr(4, x), mask)), and(x, mask))
            mstore(add(add(output, 0x20), mul(i, 2)), x)
        }
    }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably be simplified further by adjusting the first 4 masks, working on that now.

Comment on lines 236 to 243
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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be much more efficient by comparing 32 bytes at a time. Once a mismatch is found, use xor and clz to count leading matching bytes within the word.

O(n) -> O(n/32)

Copy link

@0xClandestine 0xClandestine Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function _getSharedNibbleLength(bytes memory _a, bytes memory _b) private pure returns (uint256 shared) {
    uint256 minLen = _a.length < _b.length ? _a.length : _b.length;

    unchecked {
        // Compare 32 bytes at a time
        for (; shared < minLen; shared += 32) {
            uint256 wordA;
            uint256 wordB;

            assembly {
                wordA := mload(add(_a, add(32, shared)))
                wordB := mload(add(_b, add(32, shared)))
            }

            if (wordA != wordB) {
                uint256 diff = wordA ^ wordB;
                uint256 leadingBits = clz(diff); // clz counts bits
                return shared + (leadingBits / 8);
            }
        }

        // Fallback to byte-by-byte for remaining bytes
        while (shared < minLen && _a[shared] == _b[shared]) shared++;
    }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With proofs in calldata, and the above the optimizations I'm hitting 175k gas for an account proof verification (down from over 400k).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind you I'm working off of optimisms implementation, this version looks a bit more expensive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests. Then I optimize

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, just wanted to write this out while I had a minute.

Copy link
Member Author

@ernestognw ernestognw Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thanks for the review though, it's just I haven't put time into making sure the implementation fully works.

It's a priority for 5.5 regardless. Just, you know, the release will take a couple of months 😓

Comment on lines 262 to 264
// Long list
(offset, length) = _decodeLong(prefix - SHORT_LIST_OFFSET, item);
return (offset, length, ItemType.LIST_ITEM);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel like we should use an if statement with a revert case after.

@ernestognw ernestognw changed the title Add RLP and TrieProof libraries Add RLP library Jul 31, 2025
@ernestognw ernestognw mentioned this pull request Jul 31, 2025
3 tasks
Comment on lines +82 to +90
/// @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));
}
Copy link
Collaborator

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 ?

Copy link
Member Author

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/

Positive integers must be represented in big-endian binary form with no leading zeroes (thus making the integer value zero equivalent to the empty byte array). Deserialized positive integers with leading zeroes must be treated as invalid by any higher-order protocol using RLP.

@Amxx Amxx mentioned this pull request Aug 25, 2025
3 tasks
Copy link

coderabbitai bot commented Aug 27, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants