Skip to content

fix(specs): BPO3 inherits BPO2, not Amsterdam; run BPO transition pyspec fixtures#12312

Draft
Marchhill wants to merge 22 commits into
sparse-blob-pool-rebasedfrom
fix/bpo-fork-lineage
Draft

fix(specs): BPO3 inherits BPO2, not Amsterdam; run BPO transition pyspec fixtures#12312
Marchhill wants to merge 22 commits into
sparse-blob-pool-rebasedfrom
fix/bpo-fork-lineage

Conversation

@Marchhill

@Marchhill Marchhill commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Reparent BPO3 from Amsterdam.Instance to BPO2.Instance. BPO forks are blob-parameter-only forks chained BPO1→BPO2→BPO3→BPO4→BPO5, with Amsterdam a parallel branch (matching the execution-spec-tests fork lineage). Inheriting Amsterdam wrongly enabled EIP-7928 (and every other Amsterdam EIP) on BPO3/BPO4/BPO5, so BPO2→BPO3 transition blocks demanded a BAL hash and failed with MissingBlockLevelAccessListHash.
  • Add regression test ForkTests.Bpo_forks_are_blob_only_and_do_not_inherit_Amsterdam asserting the BPO parent chain and that EIP-7928 stays Amsterdam-only.
  • Add the missing Pyspec fixture classes Bpo2ToBpo3AtTime15k/Bpo3ToBpo4AtTime15k (blockchain + engine variants) to Ethereum.Blockchain.Pyspec.Test/Tests.cs. tests-glamsterdam-devnet@v6.1.1 ships for_bpo2tobpo3attime15k and for_bpo3tobpo4attime15k under both blockchain_tests/ and blockchain_tests_engine/, but Tests.cs stopped at Bpo2ToAmsterdamAtTime15k, so these transition fixtures were never run in CI — which is how the fork-lineage bug went unnoticed.
  • Add TangerineWhistle/SpuriousDragon blockchain + state fixture classes: an audit of the archive found for_tangerinewhistle and for_spuriousdragon under blockchain_tests/ and state_tests/ also had no fixture classes. Every for_* directory in the archive now maps to a fixture class across all five fixture trees (blockchain, engine, sync, state, transaction).

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

  • ForkTests pass, including the new lineage regression test.
  • New fixture classes verified locally: 96/96 pass (FullyQualifiedName~Bpo2ToBpo3AtTime15k|FullyQualifiedName~Bpo3ToBpo4AtTime15k); without the reparent these fail with MissingBlockLevelAccessListHash.
  • TangerineWhistle/SpuriousDragon classes verified locally: 232/232 pass.
  • CI Pyspec chunking needs no changes: default chunks shard per-test by stable-name hash and only exclude the AmsterdamParallel/AmsterdamBatchRead variants by name.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

The same BPO3(Amsterdam.Instance) bug exists on master, but the fixture classes must not be added there: master pins tests-bal@v7.2.0, which has no for_bpo2tobpo3attime15k/for_bpo3tobpo4attime15k dirs. The spec fix alone can be cherry-picked to master separately.

🤖 Generated with Claude Code

Marchhill and others added 17 commits July 6, 2026 16:19
Per review agreement on #12118, the intrinsic must not read world state:
replace the draft's tiered recipient/value costs (dead-account, code,
access-list probes) with the spec's flat model, and drop the worldState
parameter from IntrinsicGasCalculator and IGasPolicy. Also compress
comments across the branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: update pyspec fixtures to glamsterdam-devnet v6.0.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: ignore ZkEVM stateless suite (fixtures target bal-devnet-7)

The ZkEvmBlockchainTests stateless suite runs Nethermind.Stateless.Executor
against the pinned tests-zkevm@v0.4.1 archive, which is generated against
bal-devnet-7 (tests-bal@v7.2.0). Its EIP-7928 block-access-list semantics differ
from the devnet-6 (tests-glamsterdam-devnet@v6.0.0) rules this stack implements,
so stateless execution rejects every fixture block on a BAL mismatch
(StatelessValidationResult.IsSuccess=0) — the full Pyspec [Sequential]/engine
shards each failed only on the *_stateless_block_N cases.

Mark the suite [Ignore] until the client tracks bal-devnet-7 (or the archive is
repinned to devnet-6). No production code is affected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: bump Glamsterdam devnet Pyspec fixtures to v6.1.0

Run the Ethereum.Blockchain.Pyspec.Test suite against the
tests-glamsterdam-devnet@v6.1.0 fixture release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: re-enable zkEVM stateless fixtures on tests-zkevm@v0.5.0

The zkVM stateless suite was disabled because the pinned archive
(tests-zkevm@v0.4.1) was generated against bal-devnet-7, whose EIP-7928
BAL semantics differ from the devnet-6 rules this stack implements.

tests-zkevm@v0.5.0 is generated against tests-glamsterdam-devnet@v6.1.0
(the devnet-6 fixtures this stack tracks), so the mismatch is resolved.
Bump the archive version and drop the [Ignore].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: update Pyspec fixtures to Glamsterdam devnet-6 (v6.1.0) and zkEVM v0.5.0

Point the default Pyspec fixture archive at tests-glamsterdam-devnet@v6.1.0
and the zkEVM stateless preview suite at tests-zkevm@v0.5.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: implement EIP-8246 (Remove SELFDESTRUCT Burn)

Removes the residual ETH-burn cases left by EIP-6780:

- A self-targeting SELFDESTRUCT now moves no ETH and emits no log,
  regardless of whether the account was created in the same transaction.
- At transaction finalization, accounts marked for destruction keep their
  balance: storage and code are cleared and the nonce is reset to 0. A
  resulting zero-balance account is still removed as empty per EIP-161,
  matching the spec and keeping CREATE2 redeployment unblocked.

Wires a new IsEip8246Enabled flag through IReleaseSpec / ReleaseSpec /
decorators / chainspec (Eip8246TransitionTimestamp) and enables it in the
Amsterdam fork. Adds Eip8246Tests covering same-transaction self-destruct
to self/other, post-destruct funding, the zero-balance empty-account case,
and the unchanged not-in-same-tx no-op, each pinned with EIP on and off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover deferred finalization path; address review feedback

- Add an EIP-8037 + EIP-7708 fixture dimension so every EIP-8246 scenario
  also runs through the deferred FinalizeDestroyedAccount path (as in
  Amsterdam), covering the balance-preservation logic there.
- Add a CREATE2 redeployment test: a factory re-creates a self-destructed
  child at the same address across transactions, verifying the nonce reset
  keeps redeployment unblocked and the preserved balance accumulates.
- Report the destroy refund to the tracer in the deferred path, matching
  the inline path.
- Document the pre-existing Burn-vs-SelfDestruct log distinction between the
  post-fee and pre-fee finalization paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: do not enable EIP-8246 in Amsterdam fork

EIP-8246 is a Draft and is not part of the EEST `for_amsterdam` conformance
fixtures. Enabling it in the Amsterdam fork changed SELFDESTRUCT behaviour
for that fork, diverging from the reference post-states and failing the
Pyspec tests across all shards.

Keep the full implementation and spec/chainspec plumbing so the EIP can be
activated via the Eip8246Transition chainspec parameter once it is scheduled,
but leave it disabled in the named forks. Unit tests toggle the flag directly
via OverridableReleaseSpec, so coverage is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: assert storage clearing in EIP-8246 self-destruct tests

Address PR review comments:
- The self-destruct-to-self contract now writes storage slot 0 before
  SELFDESTRUCT so AssertBalanceOnly can verify storage is wiped on the
  surviving balance-only account.
- Clarify the self-targeting SELFDESTRUCT short-circuit comment to
  distinguish the EIP-6780 pure no-op case from the EIP-8246 burn-removal
  case (account in destroy list, finalization still clears code/storage).

Note: the deferred finalization path also gained a tracer.ReportRefund
call to match the inline path; that is an incidental tracer fix unrelated
to EIP-8246 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: tidy EIP-8246 self-destruct test per review

- Rename the self-destruct-to-self fixtures to _sstoreThenSelfDestructToSelf
  so the name reflects the SSTORE-then-SELFDESTRUCT bytecode.
- Wrap AssertBalanceOnly in Assert.EnterMultipleScope so every mismatch is
  surfaced in one run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(test-build): cast ulong DestroyRefund for tracer refund (master #11937)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: port EIP-8246 test BlockNumber override to ulong (master #11937)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: bump EF fixtures to tests-glamsterdam-devnet@v6.1.1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: map InvalidTxSignature to INVALID_SIGNATURE_VRS for v6.1.1 test_bad_v_r_s

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: map InvalidTxChainId/EIP-155 sig failure to INVALID_CHAINID (v6.1.1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: decode invalid-tx state tests from txbytes (v6.1.1 expectException)

v6.1.1 state tests carry expectException plus the actual signed tx in txbytes.
The template tx is re-signed pre-EIP-155, which cannot reproduce signature-level
invalidity (test_invalid_chain_id executes as a valid tx and diverges from the
expected post state). Decode the fixture's raw tx for such entries so validation
rejects it; undecodable txbytes fall back to the template.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: key opcode dispatch tables by spec instance, not on the spec object

The per-spec opcode tables were cached in IReleaseSpec.EvmInstructionsNoTrace/
Traced slots. OverridableReleaseSpec's getter fell back to the wrapped spec and
ReleaseSpecDecorator's setter wrote through to it, so wrappers with different
EVM flags (7708/8037/8246 test variants) shared one table with fork singletons —
first writer won, giving wrong opcode handlers (missing transfer logs, wrong
state-gas charges) depending on test order. Port master's fix (#12146): cache
tables in a ConcurrentDictionary keyed by spec instance (plain statics for the
single-fork zkEVM guest) and drop the IReleaseSpec slots.

Full Evm.Test suite: 10/10 clean (was ~5 of 6 runs failing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: tighten EIP-8246 comments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: move the v6.1.1 pyspec fixes off the EIP-8246 branch

The fixture bump and its harness fixes (INVALID_SIGNATURE_VRS/INVALID_CHAINID
mappings, txbytes decode for expectException state tests) are unrelated to
EIP-8246; they move to fix/zkevm-stateless-v050-v2 where the fixture/format
alignment lives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: move the opcode-table cache fix to the stateless branch; group 8246 with Amsterdam EIPs

The spec-instance opcode-table keying is test-infrastructure hardening
unrelated to EIP-8246; it moves to fix/zkevm-stateless-v050-v2. IsEip8246Enabled
now sits with the other Amsterdam EIP flags in the spec surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop the Amsterdam EIP-8246 activation note

The flag is enabled one PR up (repricings, #12214) where the note is already gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: enable EIP-8246 in Amsterdam

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tent already present) into repricings-and-updates-v2
- Enforce the EIP-2780 => EIP-7708 co-activation invariant in
  ChainSpecBasedSpecProvider (with regression test).
- Restore the MaxCallDepth capacity hint on the VM state stack.
- Route the inlined net-spill expressions through GetUnrefundedStateGasSpill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 3m 36s —— View job


Review: BPO3 fork lineage fix

  • Read the PR diff
  • Examined BPO fork lineage (BPO1–BPO5, Amsterdam) and NamedReleaseSpec replay mechanism
  • Verified BPO3 reparent correctness & Apply chain
  • Checked regression test & Pyspec fixture additions
  • Verified fixture directory convention + fixture version pin

Verdict: LGTM — correct, well-scoped bugfix. No Critical/High/Medium issues.

Why the reparent is correct

NamedReleaseSpec builds each spec by replaying the parent chain root→leaf (ReplayAncestors in NamedReleaseSpec.cs:69), so with the old BPO3(Amsterdam.Instance) the replay included Amsterdam.Apply, cumulatively enabling every Amsterdam EIP (7928 BAL, 7954 max-code-size, 2780, the engine-API V5/V6 bumps, etc.) on BPO3/4/5. Reparenting to BPO2.Instance restores the intended blob-only chain Osaka → BPO1 → BPO2 → {BPO3 → BPO4 → BPO5}, with Amsterdam(BPO2) as a parallel branch — matching the EEST fork tree. This is confirmed empirically: the for_bpo2tobpo3attime15k fixtures carry no BAL hash, so they fail with MissingBlockLevelAccessListHash under the old lineage and pass under the new one.

Blast radius is contained to the test path ✅

I checked that the reparent does not affect real-network consensus. Live specs are assembled cumulatively-by-timestamp through ChainSpecBasedSpecProvider (from ChainSpec.Parameters transition timestamps), not from these *.Instance singletons. The NamedReleaseSpec singletons are consumed only via SpecNameParser (EF/pyspec fixtures). No SpecProvider references BPO3/4/5.Instance, so no mainnet/devnet activation path regresses.

Fixture additions verified ✅

  • Naming follows the established convention: Bpo2ToBpo3AtTime15kBlockchainTestsfor_bpo2tobpo3attime15k (via PyspecTestFixture.cs:129, LoadPyspecTestsStrategy handles the for_ prefix). Consistent with the existing Bpo2ToAmsterdam entry.
  • The branch pins tests-glamsterdam-devnet@v6.1.1 (Constants.cs:9), which the PR states ships these dirs under both blockchain_tests/ and blockchain_tests_engine/.
  • Regression test Bpo_forks_are_blob_only_and_do_not_inherit_Amsterdam asserts both the parent chain and that EIP-7928 stays Amsterdam-only — a clean guard against reintroducing the bug.

Low / informational (non-blocking)

  • There are intentionally two different fork orderings in play, which is worth a note for future readers: the NamedReleaseSpec lineage treats BPO3–5 as parallel to Amsterdam (EEST test-generation model), whereas the real devnet-6 chronological schedule in GethGenesisLoader._blobScheduleForks places Amsterdam(5) before BPO3(6)…BPO5(8). These don't conflict because the live path is cumulative-by-timestamp (so Amsterdam EIPs remain active at BPO3 time on a real chain), but the divergence between the "test lineage" and "wire schedule" is subtle. No action needed — just flagging so the distinction is understood.

Nice catch on the missing CI coverage that let this slip through.

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

No comments needed

Marchhill and others added 5 commits July 6, 2026 20:05
BPO forks are blob-parameter-only forks chained off BPO2, parallel to
Amsterdam (matching execution-spec-tests fork lineage). Inheriting
Amsterdam wrongly enabled EIP-7928 (and the rest of Amsterdam) on
BPO3/BPO4/BPO5, so BPO2->BPO3 transition blocks demanded a BAL hash
(MissingBlockLevelAccessListHash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests-glamsterdam-devnet@v6.1.1 ships for_bpo2tobpo3attime15k and
for_bpo3tobpo4attime15k under both blockchain_tests and
blockchain_tests_engine, but Tests.cs stopped at Bpo2ToAmsterdam, so
these transition fixtures were never run in CI - which is how the BPO3
fork-lineage bug went unnoticed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests-glamsterdam-devnet ships for_tangerinewhistle and
for_spuriousdragon under blockchain_tests and state_tests, but Tests.cs
had no fixture classes for them, so they never ran. Completes the
class-per-directory mapping: every for_* dir in the archive now has a
matching fixture class across all five fixture trees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Marchhill Marchhill force-pushed the fix/bpo-fork-lineage branch from 92fb8fb to f50396d Compare July 6, 2026 19:08
@Marchhill Marchhill changed the base branch from fix/zkevm-stateless-v050-v2 to sparse-blob-pool-rebased July 6, 2026 19:09
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

EVM Opcode Benchmark Diff

Aggregated runs: base=3, pr=3
Noisy rerun opcodes: DUP4

No significant regressions or improvements detected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants