Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/filling_tests/transition_tool_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ The following transition tools are supported by the framework:
| [ethereum/go-ethereum](https://github.com/ethereum/go-ethereum) | [`evm t8n`](https://github.com/ethereum/go-ethereum/tree/master/cmd/evm) | Yes |
| [besu-eth/besu](https://github.com/besu-eth/besu/tree/main/ethereum/evmtool) | [`evmtool t8n-server`](https://github.com/besu-eth/besu/tree/main/ethereum/evmtool) | Yes |
| [status-im/nimbus-eth1](https://github.com/status-im/nimbus-eth1) | [`t8n`](https://github.com/status-im/nimbus-eth1/blob/master/tools/t8n/readme.md) | Yes |

## Fork transitions

The filler resolves every block of a transition test to the fork whose rules apply to it and passes that fork's name in `--state.fork` (or the server request's `state.fork` field). External tools therefore see `BPO2` for the blocks before the boundary of `BPO2ToAmsterdamAtTime15k` and `Amsterdam` from the boundary on; they never see the transition name and cannot tell the fork block from a later one.

The in-process EELS t8n receives the transition fork itself and executes each block with the rules of the side it falls on. It mirrors the spec's `is_fork_block` with the transition schedule: the block activates its fork when the schedule places its parent, by number and `parentTimestamp`, on the other side of the boundary. The `ethereum-spec-evm t8n` command accepts the transition name directly, for example `--state.fork=BPO2ToAmsterdamAtTime15k`, and derives the same activation from the environment. No separate activation flag exists.

A fixture whose genesis is already in the fork has no fork block: a plain fork has no boundary, so nothing activates. Only a fixture that crosses the boundary runs the EIP-8253 nonce update, on the first block at or after the activation timestamp whose parent is before it.
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def evaluate(
del slow_request # not meaningful for the engine path
env = transition_tool_data.env
txs = transition_tool_data.txs
block_fork = transition_tool_data.fork
block_fork = transition_tool_data.active_fork

payload_attributes = self._payload_attributes(env, block_fork)
parent_block_hash = Hash(env.parent_hash or 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
ForkBlobSchedule,
)
from execution_testing.exceptions import ExceptionMapper
from execution_testing.forks import Fork
from execution_testing.forks import Fork, TransitionFork
from execution_testing.forks.helpers import (
get_development_forks,
get_forks,
Expand Down Expand Up @@ -325,34 +325,53 @@ class TransitionToolData:
alloc: Alloc | LazyAlloc
txs: List[Transaction]
env: Environment
fork: Fork
fork: Fork | TransitionFork
chain_id: int
reward: int
blob_schedule: BlobSchedule | None
state_test: bool = False

@property
def active_fork(self) -> Fork:
"""
Return the fork whose rules apply to this block.

`fork` may be a transition fork; the block's number and
timestamp pick the side of the boundary it falls on.
"""
return self.fork.fork_at(
block_number=int(self.env.number),
timestamp=int(self.env.timestamp),
)

@property
def fork_name(self) -> str:
"""Return the fork name."""
return self.fork.transition_tool_name()
"""
Return the name of the block's fork for external tools.

External tools always get the resolved fork, never the
transition name. The in-process EELS t8n reads `fork` itself
and derives fork-block activation from the transition schedule.
"""
return self.active_fork.transition_tool_name()

@property
def fork_name_if_supports_blob_params(self) -> str:
"""Return the fork name."""
fork = self.fork()
fork = self.active_fork

# For tools that support blob_params, return base fork for BPO
# forks.
if fork.bpo_fork():
return fork.non_bpo_ancestor().transition_tool_name()
else:
return self.fork.transition_tool_name()
return self.fork_name

@property
def blob_params(self) -> ForkBlobSchedule | None:
"""Return the blob parameters for the current fork."""
if self.blob_schedule:
fork_name = self.fork.name()
fork_name = self.active_fork.name()
# Only return blob params if this fork has them
if fork_name in self.blob_schedule.root:
return self.blob_schedule[fork_name]
Expand Down
52 changes: 41 additions & 11 deletions packages/testing/src/execution_testing/evm_tools/t8n/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
TransitionTool,
)
from execution_testing.exceptions import ExceptionMapper
from execution_testing.forks import Fork, TransitionFork
from execution_testing.test_types import (
Environment as TestingEnvironment,
)
Expand Down Expand Up @@ -139,6 +140,7 @@ class T8N(Load):
body: Bytes
state_test: bool
state_reward: int
fork_schedule: "Fork | TransitionFork"
exception_mapper: Optional["ExceptionMapper"]
_block_exception: Optional[str]

Expand All @@ -152,14 +154,15 @@ def __init__(
tracers: Optional[GroupTracer] = None,
exception_mapper: Optional["ExceptionMapper"] = None,
) -> None:
# ``resolve_fork`` only maps the testing fork name to a spec
# ``Hardfork`` module — CLI exception aliases like
# ``HomesteadToDaoAt5`` are unfolded by ``find_fork`` in
# :mod:`.cli` before the testing ``Fork`` is constructed. For
# those transition-fork tests the CLI also reports the block
# number at which the resolved fork activates via
# ``fork_block``; the in-process path leaves it ``None``.
fork_module = resolve_fork(t8n_data.fork_name)
# ``t8n_data.fork`` may be a transition fork: resolve the spec
# module from the side of the boundary this block falls on, and
# keep the schedule so ``is_fork_block`` can tell the first block
# of the target fork from the later ones. Legacy CLI aliases such
# as ``HomesteadToDaoAt5`` are unfolded by ``find_fork`` in
# :mod:`.cli`, which reports the activation block number via
# ``fork_block`` instead.
active_fork = t8n_data.active_fork
fork_module = resolve_fork(active_fork.transition_tool_name())
fork_criteria: Optional[ByBlockNumber] = None
if fork_block is not None and fork_block != 0:
fork_criteria = ByBlockNumber(fork_block)
Expand All @@ -181,8 +184,8 @@ def __init__(
base_fee_update_fraction: Optional[Uint] = None
if (
t8n_data.blob_params is not None
and t8n_data.fork.bpo_fork()
and t8n_data.fork != t8n_data.fork.non_bpo_ancestor()
and active_fork.bpo_fork()
and active_fork != active_fork.non_bpo_ancestor()
):
target_blobs_per_block = U64(
int(t8n_data.blob_params.target_blobs_per_block)
Expand Down Expand Up @@ -212,6 +215,7 @@ def __init__(
self.chain_id = U64(t8n_data.chain_id)
self.state_test = t8n_data.state_test
self.state_reward = t8n_data.reward
self.fork_schedule = t8n_data.fork
self.exception_mapper = exception_mapper

from execution_testing.client_clis.cli_types import LazyAlloc
Expand All @@ -226,7 +230,7 @@ def __init__(
if isinstance(input_alloc, LazyAlloc):
input_alloc = input_alloc.materialize()
self.alloc = input_alloc.model_copy(deep=True)
self.alloc.migrate_state_commitment(t8n_data.fork.state_commitment())
self.alloc.migrate_state_commitment(active_fork.state_commitment())
self.env = t8n_data.env
self.txs = list(t8n_data.txs)
self.ommers = list(ommers)
Expand Down Expand Up @@ -369,7 +373,33 @@ def run_state_test(self) -> None:
self.rejected_transactions,
)

@property
def is_fork_block(self) -> bool:
"""
Check whether this block is the first block of its fork.

Mirror the spec's ``is_fork_block`` with the testing fork
schedule: the block falls on a different side of the transition
than its parent. A plain fork has no boundary, so a chain that
starts inside the fork has no fork block.
"""
if self.env.number == 0 or self.env.parent_timestamp is None:
return False
parent_fork = self.fork_schedule.fork_at(
block_number=int(self.env.number) - 1,
timestamp=int(self.env.parent_timestamp),
)
return parent_fork != self.fork_schedule.fork_at(
block_number=int(self.env.number),
timestamp=int(self.env.timestamp),
)

def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None:
# EIP-8253: the fork block bumps the nonce of the zero-nonce
# storage accounts before any pre-execution system call.
if self.fork.has_zero_nonce_storage_accounts and self.is_fork_block:
self.fork.bump_zero_nonce_storage_accounts(block_env)

# Same order as the fork's `apply_body`: beacon roots, then history.
if self.fork.has_beacon_roots_address:
self.fork.process_unchecked_system_transaction(
Expand Down
42 changes: 37 additions & 5 deletions packages/testing/src/execution_testing/evm_tools/t8n/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
from ethereum_rlp import rlp
from ethereum_spec_tools.forks import Hardfork
from ethereum_spec_tools.loaders.fork_loader import ForkLoad
from ethereum_spec_tools.utils import FatalError, find_fork, parse_hex_or_int
from ethereum_spec_tools.utils import (
EXCEPTION_MAPS,
FatalError,
find_fork,
parse_hex_or_int,
resolve_fork,
)
from ethereum_types.bytes import Bytes
from ethereum_types.numeric import U64

Expand Down Expand Up @@ -314,6 +320,7 @@ def build_t8n_from_cli_options(
"""
from execution_testing.base_types.composite_types import BlobSchedule
from execution_testing.client_clis.transition_tool import TransitionTool
from execution_testing.forks import get_transition_forks
from execution_testing.test_types import (
Alloc as TestingAlloc,
)
Expand All @@ -336,16 +343,41 @@ def build_t8n_from_cli_options(
else:
stdin = None

fork_module, fork_block = find_fork(forks, options, stdin)
testing_fork = _testing_fork_from_spec_hardfork(fork_module)

raw_alloc_json = _read_json_input(options.input_alloc, stdin, "alloc")
raw_env_json = _read_json_input(options.input_env, stdin, "env")
raw_txs_json = _read_json_input(options.input_txs, stdin, "txs")
blob_params = _parse_blob_params_from_options(options, stdin)

alloc = TestingAlloc.model_validate(raw_alloc_json)
env = TestingEnvironment.model_validate(raw_env_json)
# Legacy aliases such as ``HomesteadToDaoAt5`` keep their
# ``find_fork`` unfolding; any other transition fork name resolves
# through the testing framework's schedule.
transition = (
None
if options.state_fork in EXCEPTION_MAPS

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.

I don't like that EXCEPTION_MAPS has to exist in the first place, but that's outside the scope of this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let's raise an issue for this. I do not have sufficient context to raise it, I quickly checked, but I also feel that I do not like this.

else next(
(
fork
for fork in get_transition_forks()
if fork.name() == options.state_fork
),
None,
)
)
if transition is not None:
testing_fork = transition
active_fork = transition.fork_at(
block_number=int(env.number), timestamp=int(env.timestamp)
)
fork_module = resolve_fork(active_fork.transition_tool_name())
# ``T8N`` derives the activation criteria from the transition
# fork itself; ``fork_block`` only serves the legacy aliases.
fork_block = None
else:
fork_module, fork_block = find_fork(forks, options, stdin)
testing_fork = _testing_fork_from_spec_hardfork(fork_module)
active_fork = testing_fork
txs, _body = _parse_txs_json_to_testing(
raw_txs_json, fork_module, TestingTransaction
)
Expand All @@ -356,7 +388,7 @@ def build_t8n_from_cli_options(
blob_schedule: Any = None
if blob_params is not None:
blob_schedule = BlobSchedule()
blob_schedule.append(fork=testing_fork.name(), schedule=blob_params)
blob_schedule.append(fork=active_fork.name(), schedule=blob_params)

t8n_data = TransitionTool.TransitionToolData(
alloc=alloc,
Expand Down
Loading
Loading