diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index e17cde49005..b26eff8e2bb 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 @@ -18,7 +17,11 @@ from ethereum.exceptions import EthereumException, InvalidBlock from ethereum.fork_criteria import ByBlockNumber, ByTimestamp, Unscheduled from ethereum.merkle_patricia_trie import copy_trie -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 ( @@ -34,7 +37,6 @@ from .t8n_types import Alloc, Result, Txs T = TypeVar("T") -ForkCriteriaArgument = ByBlockNumber | ByTimestamp | Unscheduled | None def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: @@ -92,95 +94,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 = {} @@ -207,7 +126,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, @@ -226,19 +145,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 cf29086b53b..e32433c115b 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: @@ -204,16 +291,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 @@ -221,6 +299,9 @@ def clone( """ from .new_fork.builder import ForkBuilder + if overrides is None: + overrides = ForkOverrides() + maybe_directory: TemporaryDirectory | None = TemporaryDirectory() try: @@ -240,33 +321,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..261fbe8999a 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: @@ -141,7 +148,7 @@ def test_fork_cache_returns_template_for_identical_overrides( ByBlockNumber | ByTimestamp | Unscheduled, ) - def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: + def clone(template: Hardfork, overrides: ForkOverrides) -> DummyTemporaryFork: pytest.fail("Hardfork.clone() should not run for identical overrides") monkeypatch.setattr(Hardfork, "clone", clone) @@ -164,8 +171,11 @@ def test_fork_cache_clones_when_fork_criteria_changes_template( cloned = DummyTemporaryFork() seen: dict[str, Any] = {} - def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: - seen.update(kwargs) + def clone( + template: Hardfork, overrides: ForkOverrides, + ) -> DummyTemporaryFork: + seen["template"] = template + seen["overrides"] = overrides return cloned monkeypatch.setattr(Hardfork, "clone", clone) @@ -182,7 +192,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) @@ -194,7 +204,7 @@ def test_fork_cache_returns_template_for_each_identical_blob_override( template = _template() value = _override_defaults(template)[field] - def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: + def clone(template: Hardfork, overrides: ForkOverrides) -> DummyTemporaryFork: pytest.fail("Hardfork.clone() should not run for identical overrides") monkeypatch.setattr(Hardfork, "clone", clone) @@ -217,8 +227,11 @@ def test_fork_cache_clones_for_each_changed_blob_override( cloned = DummyTemporaryFork() seen: dict[str, Any] = {} - def clone(*args: Any, **kwargs: Any) -> DummyTemporaryFork: - seen.update(kwargs) + def clone( + template: Hardfork, overrides: ForkOverrides, + ) -> DummyTemporaryFork: + seen["template"] = template + seen["overrides"] = overrides return cloned monkeypatch.setattr(Hardfork, "clone", clone) @@ -228,7 +241,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(