perf(mpt): specialize the RLP shapes the witness contains, behind an rlp module - #684
Draft
Qumeric wants to merge 4 commits into
Draft
perf(mpt): specialize the RLP shapes the witness contains, behind an rlp module#684Qumeric wants to merge 4 commits into
Qumeric wants to merge 4 commits into
Conversation
This was referenced Jul 28, 2026
Qumeric
force-pushed
the
valery/mpt-nonzero-children
branch
from
July 28, 2026 07:05
e2a43d0 to
f3394fb
Compare
Qumeric
force-pushed
the
valery/mpt-rlp-module
branch
2 times, most recently
from
July 28, 2026 08:40
36b07c2 to
a6f15d0
Compare
Qumeric
force-pushed
the
valery/mpt-rlp-module
branch
from
July 28, 2026 09:03
a6f15d0 to
327eb0a
Compare
Qumeric
force-pushed
the
valery/mpt-nonzero-children
branch
from
July 28, 2026 09:03
f3394fb to
7d01991
Compare
…l tests The specializations were spread through trie.rs as inline fast paths and match arms. Collecting them behind a module boundary keeps the trie code about tries, and gives the specializations a place to be checked: each one is now compared against alloy_rlp across the header-form boundaries, which is the property that makes them safe to have.
Qumeric
force-pushed
the
valery/mpt-rlp-module
branch
from
July 28, 2026 14:31
327eb0a to
cc42675
Compare
Qumeric
marked this pull request as draft
July 28, 2026 16:53
Qumeric
force-pushed
the
valery/mpt-nonzero-children
branch
from
July 29, 2026 07:30
7d01991 to
cab15d0
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Replaces #664, #666 and #667, which were three stacked PRs expressing one idea: reach the RLP encodings a trie witness actually contains without walking
alloy_rlp's general header space. Splitting one idea across three stacked PRs cost more to review than it saved, so they are collapsed here.Stacked on #658.
Two commits:
trie.rs.decode_rlp_item,decode_node_header,is_null_ref,encode_header,encode_slice,advance_uncheckedandNULL_NODE_REF_SLICEmove tocrates/mpt/src/rlp.rs;bytes_eqandbranch_child_idstay, since they are not RLP.Why the module matters
Hand-specialized RLP invites exactly one question — does it agree with the general implementation on every input? Previously that was only covered indirectly, through trie round-trips. Behind a module boundary each specialization is compared against
alloy_rlpdirectly:decode_node_header_matches_alloy/decode_rlp_item_matches_alloysweep every leading byte0x00..=0xff— the dimension that selects which specialized arm runs — against payload lengths straddling the 1-, 2- and 3-byte length-prefix boundaries, and against three fillers for the bytes after the lead. Most combinations are invalid RLP, which is the point: the specialized decoder must reject exactly whatalloy_rlprejects.The filler dimension matters more than it looks. For the header forms carrying a multi-byte length, the bytes after the lead are the declared length, so a single filler pins each of those arms to one length value. Sweeping all-zero and all-ones fillers alongside the pattern is what reaches the canonicality rules — a length with a leading zero, a long-form length below 56, and a maximal one. I checked this holds by mutation: deleting the
0xf9leading-zero rejection leaves every test passing under a fixed filler, and failsdecode_node_header_matches_alloywith the sweep.encode_header_matches_alloycovers payload lengths0..600plus65_535,65_536and1 << 20, for both lists and strings.encode_slice_matches_alloysweeps all 256 single-byte strings, the only shape with a special encoding.Round-trip and
is_null_reftests cover the remainder.Errors are compared as accept-or-reject rather than by variant: the contract is that invalid input is rejected, not which variant surfaces. On success the tests assert both the decoded value and the buffer advance.
Benchmark results
Marginal against its own parent, #658, on block 24001988, execute-metered — #658 vs this PR:
execute_metered_insnsmetered_rows_unpaddedmetered_main_cells_unpaddedmetered_interaction_cells_unpaddedmetered_memory_unpadded_bytesCumulative for the chain (#647 + #658 + this PR) against
develop-v2.1.0:execute_metered_insnsmetered_rows_unpaddedmetered_main_cells_unpaddedmetered_interaction_cells_unpaddedmetered_memory_unpadded_bytesRuns: block 24001988
develop-v2.1.0/ chain; block 24002549develop-v2.1.0/ chain.Notes
decode_node_headeranddecode_rlp_itemkeepalloy_rlpas the fallback for anything uncommon, so behaviour is unchanged and only the common paths get shorter.The unresolved-node fast path compares the witness digest against the parent's expected reference with
digest_eq, whose length is fixed at compile time so the comparison stays inline as whole-word loads. That comparison is the single hottest operation the fast path performs — a slice comparison of runtime length here costs more than the generic header decode the fast path exists to avoid.