-
Notifications
You must be signed in to change notification settings - Fork 12.1k
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
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Hadrien Croubois <[email protected]>
Co-authored-by: Arr00 <[email protected]>
🦋 Changeset detectedLatest commit: aa26e48 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
This PR must be updated if any of #5792 or #5726 is merged. Essentially replacing these RLP private functions and string's |
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]); |
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.
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.
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_; | ||
} |
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.
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;
}
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.
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)
}
}
}
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.
Can probably be simplified further by adjusting the first 4 masks, working on that now.
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; | ||
} |
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 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)
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.
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++;
}
}
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.
With proofs in calldata, and the above the optimizations I'm hitting 175k gas for an account proof verification (down from over 400k).
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.
Mind you I'm working off of optimisms implementation, this version looks a bit more expensive.
// Long list | ||
(offset, length) = _decodeLong(prefix - SHORT_LIST_OFFSET, item); | ||
return (offset, length, ItemType.LIST_ITEM); |
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.
Feel like we should use an if statement with a revert case after.
Requires:
Memory
utility library #5189reverseBits
operations to Bytes.sol #5724clz(bytes)
andclz(uint256)
functions #5725PR Checklist
npx changeset add
)