From 8f44fd4db7fec2945b8d0e412b287b8b6ce2aabd Mon Sep 17 00:00:00 2001 From: JackCC Date: Tue, 28 Apr 2026 11:48:22 +0800 Subject: [PATCH] refactor(forks): pass fork overrides to clone --- .../evm_tools/t8n/__init__.py | 109 +------------ src/ethereum_spec_tools/forks.py | 151 ++++++++++++++---- tests/evm_tools/test_fork_cache.py | 13 +- tests/json_loader/test_tools_new_fork.py | 92 ++++++++++- 4 files changed, 227 insertions(+), 138 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 23781ad7b94..49f8c08850b 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -7,7 +7,6 @@ import json import os from contextlib import AbstractContextManager -from dataclasses import astuple, dataclass from typing import Any, Final, TextIO, Type, TypeVar from ethereum_rlp import rlp @@ -25,7 +24,11 @@ BlockAccessListBuilder, validate_block_access_list_gas_limit, ) -from ethereum_spec_tools.forks import Hardfork, TemporaryHardfork +from ethereum_spec_tools.forks import ( + ForkOverrides, + Hardfork, + TemporaryHardfork, +) from ..loaders.fixture_loader import Load from ..utils import ( @@ -41,7 +44,6 @@ from .t8n_types import Alloc, Result, Txs T = TypeVar("T") -ForkCriteriaArgument = ByBlockNumber | ByTimestamp | Unscheduled | None def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: @@ -99,95 +101,12 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: t8n_parser.add_argument("--state-test", action="store_true") -@dataclass(frozen=True) -class _ForkOverrides: - """Store temporary hardfork override values.""" - - fork_criteria: ForkCriteriaArgument = None - blob_target_gas_per_block: U64 | None = None - gas_per_blob: U64 | None = None - blob_min_gasprice: Uint | None = None - blob_base_fee_update_fraction: Uint | None = None - max_blob_gas_per_block: U64 | None = None - blob_schedule_target: U64 | None = None - blob_schedule_max: U64 | None = None - - def is_empty(self) -> bool: - """Return true when all override values are unset.""" - return all(value is None for value in astuple(self)) - - @staticmethod - def _matches_field(override: object | None, on: object, name: str) -> bool: - if override is None: - return True - - try: - default = getattr(on, name) - except AttributeError: - return False - - return override == default - - def matches_template( - self, - template: Hardfork, - ) -> bool: - """Return true when the requested overrides match the template.""" - if self.is_empty(): - return True - - if ( - self.fork_criteria is not None - and self.fork_criteria != template.criteria - ): - return False - - fork_mod = template.module("fork") - gas_costs = template.module("vm.gas").GasCosts - - checks = ( - ( - self.max_blob_gas_per_block, - fork_mod, - "MAX_BLOB_GAS_PER_BLOCK", - ), - ( - self.blob_target_gas_per_block, - gas_costs, - "BLOB_TARGET_GAS_PER_BLOCK", - ), - (self.gas_per_blob, gas_costs, "PER_BLOB"), - ( - self.blob_min_gasprice, - gas_costs, - "BLOB_MIN_GASPRICE", - ), - ( - self.blob_base_fee_update_fraction, - gas_costs, - "BLOB_BASE_FEE_UPDATE_FRACTION", - ), - ( - self.blob_schedule_target, - gas_costs, - "BLOB_SCHEDULE_TARGET", - ), - ( - self.blob_schedule_max, - gas_costs, - "BLOB_SCHEDULE_MAX", - ), - ) - - return all(self._matches_field(*x) for x in checks) - - class ForkCache(AbstractContextManager): """ Stores references to temporary hardforks and cleans them up when exited. """ - _cache: Final[dict[tuple[str, _ForkOverrides], TemporaryHardfork]] + _cache: Final[dict[tuple[str, ForkOverrides], TemporaryHardfork]] def __init__(self) -> None: self._cache = {} @@ -214,7 +133,7 @@ def get( Search the cache for a matching hardfork, or create one if it doesn't exist. """ - overrides = _ForkOverrides( + overrides = ForkOverrides( fork_criteria=fork_criteria, blob_target_gas_per_block=blob_target_gas_per_block, gas_per_blob=gas_per_blob, @@ -233,19 +152,7 @@ def get( except KeyError: pass - clone = Hardfork.clone( - template=template, - fork_criteria=overrides.fork_criteria, - blob_target_gas_per_block=overrides.blob_target_gas_per_block, - gas_per_blob=overrides.gas_per_blob, - blob_min_gasprice=overrides.blob_min_gasprice, - blob_base_fee_update_fraction=( - overrides.blob_base_fee_update_fraction - ), - max_blob_gas_per_block=overrides.max_blob_gas_per_block, - blob_schedule_target=overrides.blob_schedule_target, - blob_schedule_max=overrides.blob_schedule_max, - ) + clone = Hardfork.clone(template=template, overrides=overrides) self._cache[cache_key] = clone return clone diff --git a/src/ethereum_spec_tools/forks.py b/src/ethereum_spec_tools/forks.py index 112da3ad592..38a1f192a8e 100644 --- a/src/ethereum_spec_tools/forks.py +++ b/src/ethereum_spec_tools/forks.py @@ -11,6 +11,7 @@ import random import sys from contextlib import AbstractContextManager +from dataclasses import astuple, dataclass from enum import Enum, auto from importlib.machinery import ModuleSpec, PathFinder from pathlib import Path @@ -26,20 +27,16 @@ Optional, Type, TypeVar, - Union, cast, ) from ethereum_types.numeric import U64, U256, Uint from typing_extensions import override +from ethereum.fork_criteria import ByBlockNumber, ByTimestamp, Unscheduled + if TYPE_CHECKING: - from ethereum.fork_criteria import ( - ByBlockNumber, - ByTimestamp, - ForkCriteria, - Unscheduled, - ) + from ethereum.fork_criteria import ForkCriteria class ConsensusType(Enum): @@ -64,6 +61,96 @@ def is_pos(self) -> bool: H = TypeVar("H", bound="Hardfork") +ForkCriteriaArgument = ByBlockNumber | ByTimestamp | Unscheduled | None + + +@dataclass(frozen=True) +class ForkOverrides: + """ + Temporary hardfork override values. + """ + + fork_criteria: ForkCriteriaArgument = None + blob_target_gas_per_block: U64 | None = None + gas_per_blob: U64 | None = None + blob_min_gasprice: Uint | None = None + blob_base_fee_update_fraction: Uint | None = None + max_blob_gas_per_block: U64 | None = None + blob_schedule_target: U64 | None = None + blob_schedule_max: U64 | None = None + + def is_empty(self) -> bool: + """ + Return true when all override values are unset. + """ + return all(value is None for value in astuple(self)) + + @staticmethod + def _matches_field(override: object | None, on: object, name: str) -> bool: + if override is None: + return True + + try: + default = getattr(on, name) + except AttributeError: + return False + + return override == default + + def matches_template( + self, + template: "Hardfork", + ) -> bool: + """ + Return true when the requested overrides match the template. + """ + if self.is_empty(): + return True + + if ( + self.fork_criteria is not None + and self.fork_criteria != template.criteria + ): + return False + + fork_mod = template.module("fork") + gas_costs = template.module("vm.gas").GasCosts + + checks = ( + ( + self.max_blob_gas_per_block, + fork_mod, + "MAX_BLOB_GAS_PER_BLOCK", + ), + ( + self.blob_target_gas_per_block, + gas_costs, + "BLOB_TARGET_GAS_PER_BLOCK", + ), + (self.gas_per_blob, gas_costs, "PER_BLOB"), + ( + self.blob_min_gasprice, + gas_costs, + "BLOB_MIN_GASPRICE", + ), + ( + self.blob_base_fee_update_fraction, + gas_costs, + "BLOB_BASE_FEE_UPDATE_FRACTION", + ), + ( + self.blob_schedule_target, + gas_costs, + "BLOB_SCHEDULE_TARGET", + ), + ( + self.blob_schedule_max, + gas_costs, + "BLOB_SCHEDULE_MAX", + ), + ) + + return all(self._matches_field(*x) for x in checks) class Hardfork: @@ -193,16 +280,7 @@ def load_from_json(cls: Type[H], json: Any) -> List[H]: @staticmethod def clone( template: H | str, - fork_criteria: Union[ - "ByBlockNumber", "ByTimestamp", "Unscheduled", None - ] = None, - blob_target_gas_per_block: U64 | None = None, - gas_per_blob: U64 | None = None, - blob_min_gasprice: Uint | None = None, - blob_base_fee_update_fraction: Uint | None = None, - max_blob_gas_per_block: U64 | None = None, - blob_schedule_target: U64 | None = None, - blob_schedule_max: U64 | None = None, + overrides: ForkOverrides | None = None, ) -> "TemporaryHardfork": """ Create a temporary clone of an existing fork, optionally tweaking its @@ -210,6 +288,9 @@ def clone( """ from .new_fork.builder import ForkBuilder + if overrides is None: + overrides = ForkOverrides() + maybe_directory: TemporaryDirectory | None = TemporaryDirectory() try: @@ -229,33 +310,37 @@ def clone( builder.output = Path(directory.name) - if fork_criteria is not None: - builder.fork_criteria = fork_criteria + if overrides.fork_criteria is not None: + builder.fork_criteria = overrides.fork_criteria - if blob_target_gas_per_block is not None: + if overrides.blob_target_gas_per_block is not None: builder.modify_target_blob_gas_per_block( - blob_target_gas_per_block + overrides.blob_target_gas_per_block ) - if gas_per_blob is not None: - builder.modify_gas_per_blob(gas_per_blob) + if overrides.gas_per_blob is not None: + builder.modify_gas_per_blob(overrides.gas_per_blob) - if blob_min_gasprice is not None: - builder.modify_min_blob_gasprice(blob_min_gasprice) + if overrides.blob_min_gasprice is not None: + builder.modify_min_blob_gasprice(overrides.blob_min_gasprice) - if blob_base_fee_update_fraction is not None: + if overrides.blob_base_fee_update_fraction is not None: builder.modify_blob_base_fee_update_fraction( - blob_base_fee_update_fraction + overrides.blob_base_fee_update_fraction ) - if max_blob_gas_per_block is not None: - builder.modify_max_blob_gas_per_block(max_blob_gas_per_block) + if overrides.max_blob_gas_per_block is not None: + builder.modify_max_blob_gas_per_block( + overrides.max_blob_gas_per_block + ) - if blob_schedule_target is not None: - builder.modify_blob_schedule_target(blob_schedule_target) + if overrides.blob_schedule_target is not None: + builder.modify_blob_schedule_target( + overrides.blob_schedule_target + ) - if blob_schedule_max is not None: - builder.modify_blob_schedule_max(blob_schedule_max) + if overrides.blob_schedule_max is not None: + builder.modify_blob_schedule_max(overrides.blob_schedule_max) builder.build() diff --git a/tests/evm_tools/test_fork_cache.py b/tests/evm_tools/test_fork_cache.py index b2b67f2bdf0..4d74f4647ab 100644 --- a/tests/evm_tools/test_fork_cache.py +++ b/tests/evm_tools/test_fork_cache.py @@ -13,7 +13,7 @@ Unscheduled, ) from ethereum_spec_tools.evm_tools.t8n import ForkCache -from ethereum_spec_tools.forks import Hardfork +from ethereum_spec_tools.forks import ForkOverrides, Hardfork pytestmark = pytest.mark.evm_tools @@ -40,6 +40,13 @@ def _template() -> Hardfork: return Hardfork(importlib.import_module("ethereum.forks.amsterdam")) +def _seen_overrides(seen: dict[str, Any]) -> ForkOverrides: + """Return the ForkOverrides passed to Hardfork.clone.""" + overrides = seen["overrides"] + assert isinstance(overrides, ForkOverrides) + return overrides + + def _different_fork_criteria( criteria: ByBlockNumber | ByTimestamp | Unscheduled, ) -> ByBlockNumber | ByTimestamp | Unscheduled: @@ -182,7 +189,7 @@ def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: assert fork is cloned assert seen["template"] is template - assert seen["fork_criteria"] == changed_fork_criteria + assert _seen_overrides(seen).fork_criteria == changed_fork_criteria @pytest.mark.parametrize("field", OVERRIDE_FIELDS) @@ -228,7 +235,7 @@ def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: assert fork is cloned assert seen["template"] is template - assert seen[field] == changed_value + assert getattr(_seen_overrides(seen), field) == changed_value def test_fork_cache_reuses_cached_clone_for_identical_changed_request( diff --git a/tests/json_loader/test_tools_new_fork.py b/tests/json_loader/test_tools_new_fork.py index 0f009afeac5..064df4fc87d 100644 --- a/tests/json_loader/test_tools_new_fork.py +++ b/tests/json_loader/test_tools_new_fork.py @@ -4,12 +4,17 @@ from pathlib import Path from tempfile import TemporaryDirectory +from types import ModuleType +from typing import Any import libcst as cst import pytest +from ethereum_types.numeric import U64, Uint from libcst.codemod import CodemodContext -from ethereum_spec_tools.forks import Hardfork +from ethereum.fork_criteria import ByTimestamp +from ethereum_spec_tools.forks import ForkOverrides, Hardfork +from ethereum_spec_tools.new_fork import builder as builder_module from ethereum_spec_tools.new_fork.cli import main as new_fork from ethereum_spec_tools.new_fork.codemod.remove_docstring import ( RemoveDocstringCommand, @@ -100,6 +105,91 @@ def test_end_to_end(template_fork: str) -> None: assert not (fork_dir / "trie.py").exists() +def test_hardfork_clone_applies_overrides( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """ + Apply fork override values through `Hardfork.clone`. + """ + seen: dict[str, Any] = {} + + class DummyForkBuilder: + fork_criteria: ByTimestamp | None + output: Path | None + + def __init__(self, template_name: str, clone_name: str) -> None: + self.fork_criteria = None + self.output = None + seen["template_name"] = template_name + seen["clone_name"] = clone_name + + def modify_target_blob_gas_per_block(self, value: U64) -> None: + seen["blob_target_gas_per_block"] = value + + def modify_gas_per_blob(self, value: U64) -> None: + seen["gas_per_blob"] = value + + def modify_min_blob_gasprice(self, value: Uint) -> None: + seen["blob_min_gasprice"] = value + + def modify_blob_base_fee_update_fraction( + self, + value: Uint, + ) -> None: + seen["blob_base_fee_update_fraction"] = value + + def modify_max_blob_gas_per_block(self, value: U64) -> None: + seen["max_blob_gas_per_block"] = value + + def modify_blob_schedule_target(self, value: U64) -> None: + seen["blob_schedule_target"] = value + + def modify_blob_schedule_max(self, value: U64) -> None: + seen["blob_schedule_max"] = value + + def build(self) -> None: + seen["fork_criteria"] = self.fork_criteria + seen["output"] = self.output + + def discover( + submodule_search_locations: None | list[str] = None, + ) -> list[Hardfork]: + seen["submodule_search_locations"] = submodule_search_locations + mod = ModuleType(f"ethereum.forks.{seen['clone_name']}") + return [Hardfork(mod)] + + monkeypatch.setattr(builder_module, "ForkBuilder", DummyForkBuilder) + monkeypatch.setattr(Hardfork, "discover", staticmethod(discover)) + + overrides = ForkOverrides( + fork_criteria=ByTimestamp(7), + blob_target_gas_per_block=U64(199), + gas_per_blob=U64(1), + blob_min_gasprice=Uint(2), + blob_base_fee_update_fraction=Uint(750), + max_blob_gas_per_block=U64(99), + blob_schedule_target=U64(88), + blob_schedule_max=U64(77), + ) + + fork = Hardfork.clone("osaka", overrides=overrides) + try: + assert seen["template_name"] == "osaka" + assert seen["fork_criteria"] == overrides.fork_criteria + assert seen["blob_target_gas_per_block"] == U64(199) + assert seen["gas_per_blob"] == U64(1) + assert seen["blob_min_gasprice"] == Uint(2) + assert seen["blob_base_fee_update_fraction"] == Uint(750) + assert seen["max_blob_gas_per_block"] == U64(99) + assert seen["blob_schedule_target"] == U64(88) + assert seen["blob_schedule_max"] == U64(77) + assert isinstance(seen["output"], Path) + assert seen["submodule_search_locations"] == [str(seen["output"])] + assert fork.short_name == seen["clone_name"] + finally: + fork.__exit__(None, None, None) + + def has_module_docstring(file_path: Path) -> bool: """Return True if the file starts with a module-level doc-string.""" tree = cst.parse_module(file_path.read_text())