Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## forks/amsterdam #3600 +/- ##
===================================================
- Coverage 94.44% 94.01% -0.43%
===================================================
Files 624 624
Lines 36928 36969 +41
Branches 3326 3334 +8
===================================================
- Hits 34875 34757 -118
- Misses 1450 1547 +97
- Partials 603 665 +62
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CPerezz
marked this pull request as draft
September 16, 2026 13:50
2 tasks
This was referenced Sep 16, 2026
Fixtures whose committed state root is the cross-client oracle for the Merkle Patricia Trie itself: storage slots and addresses are mined so their keccak256 hashes force specific node operations, and every test asserts the node path it claims against the reference `patricialize` at fill time. Valid from Osaka on so every client consumes one set. Covers leaf and extension splits, branch collapses onto a leaf, an extension and a branch, extension merges, 16-way branches, deletes of missing keys, the 31/32/33-byte inlining boundary for storage and withdrawal leaves, and account-leaf deletion through a same-transaction CREATE2 + SELFDESTRUCT onto a pre-funded mined address (EIP-6780).
pre: empty
post: ext(1) -> branch@1 -> {leaf(62), leaf(62)}
The shortest extension a leaf split can create, between the
four-nibble split and the prefix-free root branch.
pre: branch@0 -> {leaf(63), leaf(63)}
post: branch@0 -> {ext(1) -> branch@2, leaf} or {branch@1, leaf}
The split result is re-parented into an existing branch slot instead of
becoming the root, a different frame in every node-walking client.
pre: ext(4) -> branch@4 / ext(1) -> branch@1
post: branch@0 -> {ext(3) -> ..} / branch@0 -> {branch@1 -> ..}
A 4-nibble extension is shortened; a 1-nibble one vanishes without
leaving a zero-length extension behind.
pre: branch@0 -> {branch@1 -> {leaf, leaf}, leaf}
post: branch@0 -> {leaf(63), leaf(63)}
The survivor gains the branch nibble and stays in its parent's slot;
nothing merges because the parent is a branch.
pre: branch@0 -> {branch@1 -> {leaf, branch@2}, leaf}
post: branch@0 -> {ext(1) -> branch@2, leaf}
A fresh 1-nibble extension appears between two branches instead of
merging into an existing one.
pre: branch@0 -> {branch@1 -> {leaf, leaf}, leaf}
post: ext(1) -> branch@1 -> {leaf, leaf}
The root changes kind from branch to extension.
pre: branch@0 -> {branch@1 -> {leaf, ext(2) -> branch@4}, leaf}
post: branch@0 -> {ext(3) -> branch@4, leaf}
The extension absorbs the branch nibble; nothing above it merges.
pre: ext(4) -> branch@4 -> {leaf, leaf, leaf}
post: ext(4) -> branch@4 -> {leaf, leaf}
The smallest non-collapsing delete; a client that reduces at two
remaining children passes 16 -> 15 and fails here.
pre: leaf(64) post: leaf(64) Control: the walk for the absent key ends at a different leaf; the root must equal the pre-state root.
pre: ext(4) -> branch@4 -> {16 x leaf}
post: leaf(64)
One block diff takes the branch from 16 children to a collapsed and
merged leaf; the existing cases cover 16 -> 15 and 16 -> 0.
pre: leaf(64)
post: ext(4) -> branch@4 -> {16 x leaf}
One split of a committed leaf followed by fourteen slot fills of the
branch it just created, all in one block.
pre: leaf(64) post: leaf(64), storage root byte-identical to genesis Control: the transient write would have split the leaf; the block diff must leave no residue of it.
pre: ext(4) -> branch@4 -> {leaf, leaf}
post: the same shape, with the same or a new value
Control: a per-key delete marker must not shadow the later write.
pre: ext(4) -> branch@4 -> {leaf = 1, leaf = 2}
post: leaf(64) = 7
The collapsed leaf must carry the survivor's new value, not its
committed one.
pre: branch@0 -> {leaf a, leaf}
post: branch@0 -> {leaf b, leaf}
Child count unchanged while the slot's content is swapped; a
path-keyed cache must not serve the old leaf.
pre: ext(2) -> branch@2 -> {l3, ext(2) -> branch@5}
post: ext(3) -> branch@3 -> {new, ext(1) -> branch@5}
A collapse at depth 2 and a split at depth 3 in one diff; neither the
pre shape nor either single-step result.
pre: ext(4) -> branch@4 -> {c, ext(3) -> branch@8 -> {a, b}}, a and b
31 B inline
post: a = leaf(59) 33 B hashed under branch@4, then inline again
The flip comes from the path growing on collapse, not from the value.
pre: .. -> branch@4 -> {survivor, doomed}
post: the same shape; doomed is a balance-only leaf again
A per-block destruction record must not shadow the later write.
pre: .. -> branch@4 -> {survivor contract, doomed}
post: .. -> leaf whose value carries the flipped storage root
Storage root and account re-pathing change in one block diff.
pre: no withdrawals post: one 30-byte root leaf, withdrawalsRoot = keccak256 of its RLP The root is hashed even below the inlining threshold; only the withdrawals trie can reach the case.
pre: no withdrawals
post: branch@0 -> {8: leaf(1), 0: branch@1 -> {leaf(0) ..}}, 3 or 16
entries, inline and hashed leaves in one branch
Empty-path leaves (prefix 0x20) cannot occur in a state trie.
pre: one sender, one recipient
post: the transaction and receipt tries at 17, 129, 130 and 258
transfers: nibble-1 slot, two-byte keys, a 1-nibble and a
3-nibble extension
The only extension nodes an index trie can contain.
Slots: siblings of TWO_SLOTS_EXT4 at depths 2 and 3 (extension splits under a branch parent), a pair sharing exactly 10 nibbles (32-byte leaf from the value side), a triple where two keys share 9 nibbles and a third shares 8 (inline-to-inline collapse), and sixteen slots with distinct first nibbles (root-level 16-way branch). Addresses: siblings of TWO_ADDRS_EXT4 at depths 1 and 3, salts sharing exactly 2 and 3 nibbles with it, and fifteen salts filling the SIXTEEN_ADDRS_BRANCH4 prefix, all for account-trie collapse cells with a controlled parent. ROOT_PAIR is now spelled through SINGLE_SLOT, the identity five tests rely on.
Every docstring now states the node path as `pre:`/`post:` lines in one notation; client symbol citations are gone. Shape helpers move to `trie_shape` (`storage_shape`, `account_shape`, `index_shape`, `covering`, `leaf_rlp_size`). Delete tests are named `test_delete_*`, genesis-only account tests `test_genesis_*`. Account shapes are computed over the full genesis set (pre-alloc plus the fork's system contracts, plus the coinbase after the block) and pin the parent kind around the mined group with `covering`; the previous `all(... if d == 4)` was vacuous when no node sat at depth 4. The storage-root flip test now collapses onto the contract itself, created through the factory with a salt whose address shares four nibbles with the doomed one, so storage root and account re-pathing change in one diff. The three delete-missing tests merge into one parametrized control (the write never reaches trie code), the trie-identical per-transaction variant of the mass delete is dropped, insert-then-delete now covers a leaf split, an extension split and a branch slot, and the index-trie test adds counts 145, 257 and 273. Withdrawal values are encoded as a plain RLP tuple instead of a fork-pinned class.
pre: branch@0 -> {ext(3) -> branch@4 -> {p, q}, leaf(63)}
post: ext(4) -> branch@4 -> {p, q}
Closes the survivor-extension x parent-root cell; the root changes kind
from branch to extension. A client with a dedicated "extension becomes
root" path computes a different root here and nowhere else.
pre: branch@0 -> {ext(3) -> branch@4 -> {p, q}, leaf(63)}
post: the extension split at offset 0, in the middle, at its last
nibble, and the 1-nibble case where it vanishes
Every earlier extension split happened at the root; here the result is
re-parented into an existing branch slot, a different frame in every
node-walking client.
pre: ext(2) -> branch@2 -> {l3, ext(2) -> branch@5 -> {l1, l2}}
post: ext(2) -> branch@2 -> {l3, leaf(61)}
The only extension-parent collapse whose merged leaf does not become
the root: the merge and the update of the parent branch's slot happen
in the same step.
pre: ext(4) -> branch@4 / branch@0 / leaf post: empty Adds root branch -> empty and root leaf -> empty next to the existing root extension case, so every root kind reaches the empty trie root.
pre: ext(4) -> branch@4 -> {c, ext(3) -> branch@8 -> {a, b}} with a,
b inline (31 B) or hashed (33 B) by value size, c always hashed;
and ext(8) -> branch@8 -> {s, branch@9 -> {x, y}} all inline
post: deleted inline / hashed x survivor flips / stays hashed / stays
inline, each restored in a second block
Six cells of the (deleted child, survivor encoding) matrix; the
previous single flip test covered one. Hash-keyed stores decode inline
children from the parent and must re-inline a rebuilt survivor.
pre: empty
post: ext(10) -> branch@10 -> {leaf(53), leaf(53)}, 30 B with a 1-byte
value and exactly 32 B with a 2-byte value
The existing 32-byte case comes from the key side (56 nibbles); a
client computing leaf lengths arithmetically has a separate branch for
a value byte >= 0x80, which only this cell reaches at the threshold.
pre: ext(8) -> branch@8 -> {leaf(55) 31 B, leaf(55) 31 B}
post: the leaf holds a different 1-byte value, still 31 B inline
Next to the existing flip to 33 B: the inline case rewrites the leaf
inside its parent's RLP and never gives it a hash, so dirty tracking
keyed by child hash has nothing to key on.
pre: empty; block 1 builds each geometry, block 2 deletes, block 3
re-inserts
post: the geometry's shape after the round trip
Adds three-to-two, leaf/ext/branch survivors under a branch parent,
both root collapses and the nested-extension leaf to the three-block
test. Genesis lands in a client's base layer; block-1 nodes live in
its diff layer or dirty cache, which is where these cells were still
untested.
pre: branch@0 -> {ext(3) -> branch@4 -> {p, q}, branch@1 -> {1, 14}};
two contracts with ext(4)/ext(5) tries; two contracts holding the
same single-leaf trie
post: both root children collapse, or one collapses while the other
splits; two storage roots change in one diff; one contract's copy
of the shared node is deleted, the other's grown next block
Touched-prefix sets with two entries under one root, parallel storage
roots, and hash-keyed stores that refcount or drop a node still used
by another trie.
pre: ext(2) -> branch@2 -> {l3, ext(2) -> branch@5}; branch@0 with 16
leaves; ext(4) -> branch@4 -> {p, q}
post: the inner extension replaced by a branch in one step; the root
branch filled, reduced to 15 and to 1; q round-trips to its
original value while p really changes
Extension-first-nibble splits so far happened at the root only; root
nodes take dedicated code in several clients; a touched-key applier
must neither skip the changed subtree nor rewrite the unchanged leaf.
pre: .. -> branch@4 -> {survivor, doomed}
post: the survivor credited in the collapse block; the doomed leaf
deleted, re-created by a transfer and deleted again across three
blocks; re-created by a withdrawal, or not when it carries 0 Gwei
The collapse must read the survivor's updated balance; a resurrected
balance-only account is deployable again; withdrawals apply after the
transactions and a zero credit leaves no empty account.
pre: mined siblings at depths 1 and 3 make the parent of the
collapsing branch a controlled branch or extension; three and
sixteen accounts under one branch
post: leaf, extension and branch survivors under a branch parent, a
branch survivor under an extension, 3 -> 2, and 16 -> 1 via
fifteen CREATE2 deletions in one block
Brings the account trie to parity with the storage cells; each cell's
parent kind is pinned over the full genesis set, so an uncontrolled
account sharing the prefix fails the fill instead of changing the cell.
`fund_address(addr, 0)` yields an empty account that the filler omits from the genesis allocation, so EIP-161 clearing of a committed empty leaf cannot be built through the public API. Recorded next to the other inexpressible account cases.
The package docstring now carries the survivor x parent grid and the enumerated classes (splits, root transitions, arity, encoding, one-block sequences, index tries, account bookkeeping) so the "what is covered" statement lives in the repository next to the tests.
CPerezz
force-pushed
the
mpt-structural-tests
branch
from
September 17, 2026 20:55
52cb9ec to
9057067
Compare
This was referenced Sep 18, 2026
This branch has not been deployed
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.
Description
Adds
tests/mpt/: fixtures that exercise the Merkle Patricia Trie as a data structure.Consumed exactly like any other
blockchain_testfixture set.Results of running within hive (NO NEED TO RUN THE FIXTURES THERE THOUGH)
evm blocktest/statetest(pathdb)14273f79a6evm blocktest53358159nethtest --blockTest/ state89e1602ef_tests-blockchain/ef_tests-state(LEVM)157433beest_blockchain_test(Aristo)evmtool block-test/consume direct(needs #3610)d3d70d3fc3ef-test-runneree0bf39evmone test(from-scratch rebuild)Reth's offline
ef-test-runnerdiscards the trie updates it computes, so it rebuilds each block's root from the flat hashed state; the hive runs above cover reth's incremental paths instead. We need to PR or do something about this one.Found along the way, filed separately: #3608, #3609, #3610.
Related Issues or PRs
#3608, #3609, #3610
Checklist
tests/mpt/<type>(<area>): <title>Cute Animal Picture