ethereum / ethereum/execution-specs

fill-stateful: replace debug-namespace chain rewind with an engine API anchor-block rewind

Open
#3,134 1 comment 0 reactions 0 assignees View on GitHub
stale
Dominant language
Python
Stars
1.2k
Forks
505
Avg merge
2d 14h
Merged PRs (30d)
116

Description

## Problem

The per-test chain rewind in `fill_stateful` relies on the non-standard `debug` namespace (`debug_setHead` on geth, `debug_resetHead` on e.g. Nethermind, see: #3127), falling back to no rewind at all for clients with neither. In the fallback (and `resetHead`) modes, `latest` stays at the previous test's tip until the next test submits its first block, so any state read during test setup (deploy checks, funding checks, nonces) can return state from the abandoned fork.

## Proposal

Use the anchor-block technique from [NethermindEth/gas-benchmarks](https://github.com/NethermindEth/gas-benchmarks): rewind with plain `engine_forkchoiceUpdated` (FCU) and drop the `debug` namespace entirely.

During session pre-run, insert two distinct empty sibling blocks on top of the start block: an **anchor block** `A` (rewind target, never built on) and a **build-base block** `B` (parent of every test's first block).

```text
start_block ──┬── A (anchor: FCU target between tests)
└── B (build base: tests build here)
```

Per-test cycle:

```text
during test N: after test N (rewind):

start_block ──┬── A start_block ──┬── A ◄── FCU head
└── B ── t1 ── t2 └── B ── t1 ── t2
▲ (side chain, abandoned)
head
```

Both `A` and `B` are empty, so after the FCU to `A` the head state equals the start-block state and `latest` is immediately correct.

The two-block construction is what makes the rewind legal: clients reject an FCU to a canonical **ancestor** of the current head. The target must be a fork off some parent. Since `A` and `B` are siblings, `A` is never an ancestor of a test chain built on `B`, so the FCU to `A` is always an ordinary side-chain reorg, which every client supports. `A` and `B` must have distinct hashes (e.g. differing extraData). (Most/All) clients will reject FCU to `B`.

## Notes

- Tests now build on `B`, so the pre-run fixture must record `A` and `B` for fixture consumers to replay. (We might be able to get rid of recording `A`?)
- Keep the teardown verification (FCU returns `VALID`, `latest == A`) to catch live-chain reorgs.

Contributor guide

Open the contributing guide

Research direction

Start in the fill_stateful pre-run fixture and per-test teardown flow; trace how debug_setHead and debug_resetHead are currently selected and how fixture consumers receive block data. Replace that path with engine_forkchoiceUpdated using distinct anchor and build-base blocks, then verify teardown returns VALID and latest equals A while setup reads the start-block state.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, testing
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.