Skip to content

feat(spec-tests, spec-tools): run fork-transition fixtures through EELS - #3564

Open
jochem-brouwer wants to merge 6 commits into
ethereum:forks/amsterdamfrom
jochem-brouwer:json-loader/fork-transition-fixtures
Open

jochem-brouwer wants to merge 6 commits into
ethereum:forks/amsterdamfrom
jochem-brouwer:json-loader/fork-transition-fixtures

Conversation

@jochem-brouwer

@jochem-brouwer jochem-brouwer commented Sep 11, 2026

Copy link
Copy Markdown
Member

Description

Teach the json-loader to run fork-transition fixtures through EELS.

Fork-transition tests are filled in CI, which executes the spec block by block through the in-process t8n. The resulting fixtures were never replayed through the spec's state_transition, though: a fixture whose network names a transition, such as BPO2ToAmsterdamAtTime15k or BerlinToLondonAt5, is not in the fork table, so the json-loader job and the validate-blocks step from #3552 dropped it at collection. Spec code that runs only in that block-level path at a fork boundary, header validation against the previous fork's parent header, apply_fork, and fork-block detection from FORK_CRITERIA, was therefore not exercised across a boundary in CI.

The loader now parses the transition name into the starting fork, the activating fork and the activation criteria (timestamp for AtTime, block number for At). The genesis and the blocks before the activation run on the starting fork. The first block that meets the criteria hands the chain to the activating fork through its apply_fork, the same hook the mainnet sync tool uses, and the remaining blocks run there. For the duration of the test the activating fork's FORK_CRITERIA is set to the fixture's criteria, the way a client's chain configuration would schedule it, so the fork detects its own fork block. A transition fixture is collected when the fork it activates is among the requested forks.

ForkLoad gains an apply_fork accessor, and the network parsing has unit tests.

Verified locally by filling every not slow and primary_format transition test up to Amsterdam and running just validate-blocks over the result; non-transition fixtures are unaffected.

Related Issues or PRs

Prepares for #3535, whose fork-block nonce bump can only be exercised in the spec by a transition fixture.

Checklist

  • Ran fast static checks to avoid CI fails, see Code Standards & Verifying Changes: just static
  • PR title has the form <type>(<area>): <title>, where <type> and <area> come from an appropriate C-<type>, respectively A-<area>, label. The title should match the target squash commit message.

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.49%. Comparing base (622620d) to head (0671907).
⚠️ Report is 37 commits behind head on forks/amsterdam.

Additional details and impacted files
@@                 Coverage Diff                 @@
##           forks/amsterdam    #3564      +/-   ##
===================================================
+ Coverage            94.39%   94.49%   +0.10%     
===================================================
  Files                  624      624              
  Lines                36927    36969      +42     
  Branches              3326     3334       +8     
===================================================
+ Hits                 34857    34935      +78     
+ Misses                1459     1435      -24     
+ Partials               611      599      -12     
Flag Coverage Δ
unittests 94.49% <ø> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Fork-transition tests are filled in CI, which executes the spec block by
block through the in-process t8n. The resulting fixtures were never
replayed through the spec's `state_transition`: a fixture whose network
names a transition, such as `BPO2ToAmsterdamAtTime15k` or
`BerlinToLondonAt5`, is not in the fork table, so the json-loader job and
the `validate-blocks` step of the fill jobs dropped it at collection.
Spec code that runs only in that block-level path at a fork boundary,
header validation against the previous fork's parent header,
`apply_fork`, and fork-block detection from `FORK_CRITERIA`, was
therefore not exercised across a boundary in CI.

Parse the transition name into the starting fork, the activating fork,
and the activation criteria. Run the genesis and the blocks before the
activation on the starting fork, hand the chain to the activating fork
through `apply_fork` at the first block that meets the criteria, and set
that fork's `FORK_CRITERIA` to the criteria for the duration of the test
so it detects its own fork block. Blocks that carry only their RLP, as
invalid blocks do, are placed by the number and timestamp read from the
encoded header.

Add an `apply_fork` accessor to `ForkLoad` and unit tests for the
network parsing.
@jochem-brouwer
jochem-brouwer force-pushed the json-loader/fork-transition-fixtures branch from 63c8c7d to 9777a1a Compare September 11, 2026 04:21
@jochem-brouwer jochem-brouwer added the backport benchmarks/amsterdam On merge, bot cherry-picks the squash commit to `benchmarks/amsterdam` and opens a PR. Squash only. label Sep 11, 2026
jochem-brouwer added a commit to jochem-brouwer/execution-specs that referenced this pull request Sep 11, 2026
…e EIP-8253 case map

Add a `nonce-balance-storage` variant to the EXTCODEHASH empty-account
test: a codeless account with storage and nonce one fails the EIP-684
creation check, so its storage can never be adopted and the case is
valid at every fork.

Bring `test_cases.md` in line with the consolidated tests: the renamed
`test_targeted_accounts_without_storage`, the activation delay on the
bump test, the synthetic factory wording, and the fact that the spec's
own fork-block detection is exercised when `validate-blocks` replays
transition fixtures (ethereum#3564).
@jochem-brouwer

Copy link
Copy Markdown
Member Author

Follow-up on #3552, now also tests transitions to directly add this to coverage. CC @danceratopz 😄 👍 This was found in #3535 with a low coverage warning despite having #3552 on there.

@spencer-tb spencer-tb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Woo! Lets go! Love to see coverage increments even the smallest amounts haha

LGTM, didn't see anything obvious (claude/codex like it also)!

@danceratopz

Copy link
Copy Markdown
Member

Nothing sticks out, but review incoming :-)

The json-loader parsed a transition fixture's network name with a regex to
recover the starting fork, the activating fork and the activation
criteria. The testing package already declares every transition fork with
exactly those three facts, and the transition fork's name is the fixture's
network, so look the name up there instead. A network that names no
transition fork is a plain fork name, as before. A unit test checks that
both ends of every transition fork map to a fork of the spec.
…locks

To decide which fork an invalid block of a transition fixture belongs to,
the json-loader decoded the block's RLP itself and read the number and the
timestamp at fixed header positions. The filler already writes the decoded
block under `rlp_decoded` for every invalid block whose RLP decodes, which
is exactly the case the manual decoding covered, so read that header
instead. A block without a decoded header stays with the fork before it,
as before, and so does a header whose timestamp does not fit in a `U256`,
which previously escaped as an `OverflowError` before the expected
exception was checked.
For a transition fixture, the json-loader sets the activating fork's
`FORK_CRITERIA` to the fixture's schedule so that the fork detects its
own fork block. It patched the name only in the fork's `fork` module, and
only when that module binds it, which today is the case for London and
the DAO fork alone. For every other fork the helper did nothing, and the
fork tooling that reads the constant from the fork package kept reporting
the mainnet schedule. Patch the package attribute as well, unconditionally,
and keep patching the module's own binding when there is one. Unit tests
cover both shapes and check that the patches are undone.

@danceratopz danceratopz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice, thanks Jochem! Suggestions for the three larger points are in jochem-brouwer#9.

Can you update the description? It says fork-block detection from FORK_CRITERIA is now exercised across a boundary in CI. It is not yet: only London and the DAO fork read it in their fork module and no London transition test exists, so that part becomes live with #3535.

fork_loader.py also gains accessors in #3535, it'll need a rebase if we apply the suggestions.

from .exceptional_test_patterns import exceptional_blockchain_test_patterns
from .fixtures import Fixture, FixturesFile, FixtureTestItem

TRANSITION_NETWORK = re.compile(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be a lookup into the testing package's transition forks, which already declare these facts and whose names are the fixture networks: danceratopz@193b416

fork activates at the given timestamp (`AtTime`) or block number (`At`).
"""

HEADER_NUMBER_INDEX = 8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The filler already writes the decoded block of every decodable invalid block under rlp_decoded, so the header can be read from there instead of walking the RLP: danceratopz@a3645c7

with a matching chain configuration would.
"""
fork_module = load.fork.hardfork.module("fork")
if hasattr(fork_module, "FORK_CRITERIA"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only London and the DAO fork bind FORK_CRITERIA in their fork module, so this is a no-op for every transition fixture CI fills today. Patching the package attribute as well, with a unit test: danceratopz@778aee1

for json_block in json_data["blocks"]:
if (
self.transition is not None
and current is not load

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Optional renaming suggestion.

load now means the activating fork's loader while current carries the old meaning, so current is not load reads as an implicit "not handed over yet". Renaming load to activating, or keeping an explicit pending transition, would make these lines self-describing. Two smaller ones in the same spirit: _schedule_fork could return a context manager instead of mutating the caller's stack, and mock_pow can stay a bool, since proof_of_stake cannot differ across any transition the loader accepts.

…xtures-suggestions

refactor(spec-tests): simplify fork-transition handling in the json-loader
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport benchmarks/amsterdam On merge, bot cherry-picks the squash commit to `benchmarks/amsterdam` and opens a PR. Squash only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants