Skip to content

Add RLP and TrieProof libraries #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
wants to merge 68 commits into
base: master
Choose a base branch
from

Conversation

ernestognw
Copy link
Member

@ernestognw ernestognw commented May 10, 2025

Copy link

changeset-bot bot commented May 10, 2025

🦋 Changeset detected

Latest commit: aa26e48

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
Copy link
Member Author

ernestognw commented Jul 9, 2025

This PR must be updated if any of #5792 or #5726 is merged. Essentially replacing these RLP private functions and string's equal

@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.

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.

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