From 4550e690d832052aa1075c9ecc891bbb44a50dd0 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 1 Apr 2026 22:05:19 +0200 Subject: [PATCH 1/2] feat(test-forks): Add EIP classes (#2571) * feat(forks): Add EIP-7928 * fix(forks): EIPs use BaseFork base class * fix(forks): Move new payload + get payload versions to the EIP # Conflicts: # packages/testing/src/execution_testing/forks/tests/test_forks.py * refactor(forks): Split Prague and Osaka * refactor(forks): Abstract methods * refactor(forks): Simplify blob constant methods * refactor(forks): Cancun, Shanghai * refactor(test-forks): All EIPs (except EIP-2) * refactor(forks): Add Homestead, Spurious Dragon EIPs * fix(forks): Fix import * fix(forks): Hasher match * fix(forks): Make `deployed` property * feat(test-forks): Allow validity markers to specify EIPs * feat(test-forks): Try to fix validity markers * fix(test-forks): bug * fix(test-forks): tox * fix(test-forks): improve eips module exported objects --- packages/testing/pyproject.toml | 8 +- .../pytest_commands/plugins/forks/forks.py | 67 +- .../plugins/forks/tests/test_markers.py | 35 + .../src/execution_testing/forks/__init__.py | 2 + .../src/execution_testing/forks/base_fork.py | 351 +++- .../forks/forks/eips/__init__.py | 35 + .../forks/forks/eips/amsterdam/__init__.py | 1 + .../forks/forks/eips/amsterdam/eip_7928.py | 61 + .../forks/forks/eips/berlin/__init__.py | 1 + .../forks/forks/eips/berlin/eip_2930.py | 65 + .../forks/forks/eips/bogota/__init__.py | 1 + .../forks/forks/eips/byzantium/__init__.py | 1 + .../forks/forks/eips/byzantium/eip_140.py | 37 + .../forks/forks/eips/byzantium/eip_196.py | 35 + .../forks/forks/eips/byzantium/eip_197.py | 34 + .../forks/forks/eips/byzantium/eip_198.py | 24 + .../forks/forks/eips/byzantium/eip_211.py | 40 + .../forks/forks/eips/byzantium/eip_214.py | 45 + .../forks/forks/eips/byzantium/eip_649.py | 18 + .../forks/forks/eips/cancun/__init__.py | 1 + .../forks/forks/eips/cancun/eip_1153.py | 39 + .../forks/forks/eips/cancun/eip_4788.py | 47 + .../forks/forks/eips/cancun/eip_4844.py | 216 ++ .../forks/forks/eips/cancun/eip_5656.py | 40 + .../forks/forks/eips/cancun/eip_7516.py | 40 + .../forks/eips/constantinople/__init__.py | 1 + .../forks/eips/constantinople/eip_1014.py | 61 + .../forks/eips/constantinople/eip_1052.py | 37 + .../forks/eips/constantinople/eip_1234.py | 18 + .../forks/eips/constantinople/eip_145.py | 40 + .../forks/forks/eips/homestead/__init__.py | 1 + .../forks/forks/eips/homestead/eip_2.py | 49 + .../forks/forks/eips/homestead/eip_7.py | 46 + .../forks/forks/eips/istanbul/__init__.py | 1 + .../forks/forks/eips/istanbul/eip_1108.py | 25 + .../forks/forks/eips/istanbul/eip_1344.py | 32 + .../forks/forks/eips/istanbul/eip_152.py | 32 + .../forks/forks/eips/istanbul/eip_1884.py | 33 + .../forks/forks/eips/istanbul/eip_2028.py | 24 + .../forks/forks/eips/london/__init__.py | 1 + .../forks/forks/eips/london/eip_1559.py | 171 ++ .../forks/forks/eips/london/eip_3198.py | 32 + .../forks/forks/eips/london/eip_3529.py | 18 + .../forks/forks/eips/osaka/__init__.py | 1 + .../forks/forks/eips/osaka/eip_7594.py | 26 + .../forks/forks/eips/osaka/eip_7825.py | 19 + .../forks/forks/eips/osaka/eip_7918.py | 87 + .../forks/forks/eips/osaka/eip_7934.py | 21 + .../forks/forks/eips/osaka/eip_7939.py | 36 + .../forks/forks/eips/osaka/eip_7951.py | 39 + .../forks/forks/eips/paris/__init__.py | 1 + .../forks/forks/eips/paris/eip_3675.py | 34 + .../forks/forks/eips/prague/__init__.py | 1 + .../contracts/consolidation_request.bin | Bin .../prague}/contracts/deposit_contract.bin | Bin .../prague}/contracts/history_contract.bin | Bin .../prague}/contracts/withdrawal_request.bin | Bin .../forks/forks/eips/prague/eip_2537.py | 58 + .../forks/forks/eips/prague/eip_2935.py | 46 + .../forks/forks/eips/prague/eip_6110.py | 58 + .../forks/forks/eips/prague/eip_7002.py | 48 + .../forks/forks/eips/prague/eip_7251.py | 47 + .../forks/forks/eips/prague/eip_7623.py | 110 + .../forks/forks/eips/prague/eip_7685.py | 39 + .../forks/forks/eips/prague/eip_7691.py | 23 + .../forks/forks/eips/prague/eip_7702.py | 97 + .../forks/forks/eips/shanghai/__init__.py | 1 + .../forks/forks/eips/shanghai/eip_3855.py | 35 + .../forks/forks/eips/shanghai/eip_3860.py | 60 + .../forks/forks/eips/shanghai/eip_4895.py | 24 + .../forks/eips/spurious_dragon/__init__.py | 1 + .../forks/eips/spurious_dragon/eip_155.py | 18 + .../forks/eips/spurious_dragon/eip_161.py | 35 + .../forks/eips/spurious_dragon/eip_170.py | 16 + .../execution_testing/forks/forks/forks.py | 1813 ++--------------- .../src/execution_testing/forks/helpers.py | 40 +- .../forks/tests/test_forks.py | 48 + .../forks/transition_base_fork.py | 6 +- .../src/execution_testing/specs/blockchain.py | 5 +- .../test_types/blob_types.py | 45 +- .../eip4895_withdrawals/test_withdrawals.py | 1 + 81 files changed, 3083 insertions(+), 1722 deletions(-) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/berlin/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/bogota/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_140.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_198.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_214.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_649.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4788.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_7516.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/constantinople/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1014.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1052.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1234.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/homestead/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_7.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1344.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1884.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/london/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/london/eip_1559.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/london/eip_3198.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/london/eip_3529.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7594.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7825.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7918.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7934.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/paris/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/paris/eip_3675.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/__init__.py rename packages/testing/src/execution_testing/forks/forks/{ => eips/prague}/contracts/consolidation_request.bin (100%) rename packages/testing/src/execution_testing/forks/forks/{ => eips/prague}/contracts/deposit_contract.bin (100%) rename packages/testing/src/execution_testing/forks/forks/{ => eips/prague}/contracts/history_contract.bin (100%) rename packages/testing/src/execution_testing/forks/forks/{ => eips/prague}/contracts/withdrawal_request.bin (100%) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2935.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_6110.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7002.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7251.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7685.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7691.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/shanghai/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3855.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_4895.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/__init__.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_155.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_161.py create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_170.py diff --git a/packages/testing/pyproject.toml b/packages/testing/pyproject.toml index bde2463e157..40fee87eb1d 100644 --- a/packages/testing/pyproject.toml +++ b/packages/testing/pyproject.toml @@ -12,7 +12,7 @@ description = "Ethereum execution layer client test generation and runner framew readme = "README.md" requires-python = ">=3.11" license = "MIT" -license-files = [ "LICENSE" ] +license-files = ["LICENSE"] keywords = ["ethereum", "testing", "blockchain"] classifiers = [ "Programming Language :: Python :: 3.11", @@ -71,8 +71,8 @@ lint = [ "types-requests>=2.31,<2.33", ] dev = [ - {include-group = "test"}, - {include-group = "lint"}, + { include-group = "test" }, + { include-group = "lint" }, ] [project.scripts] @@ -106,7 +106,7 @@ where = ["src"] exclude = ["*tests*"] [tool.setuptools.package-data] -"execution_testing.forks" = ["forks/contracts/*.bin"] +"execution_testing.forks" = ["forks/eips/**/*.bin"] "execution_testing.cli.pytest_commands.plugins.execute" = ["eth_config/networks.yml"] "execution_testing.cli.eest.make" = ["templates/*.j2"] "execution_testing.cli.pytest_commands" = ["pytest_ini_files/*.ini"] diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 743741de7e4..996e064abc0 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -29,6 +29,7 @@ ALL_FORKS, ALL_FORKS_WITH_TRANSITIONS, Fork, + ForkEIPSetAdapter, ForkSetAdapter, InvalidForkError, TransitionFork, @@ -712,6 +713,13 @@ class ValidityMarker(ABC): mark: Mark | None + class ValidityMarkerCombinationError(Exception): + """ + Combination of two validity markers generates an empty fork range. + """ + + pass + def __init_subclass__( cls, marker_name: str | None = None, @@ -749,12 +757,21 @@ def process_fork_arguments( self, *fork_args: str ) -> Set[Fork | TransitionFork]: """Process the fork arguments.""" - fork_set = ForkSetAdapter.validate_python(fork_args) - if len(fork_set) != len(fork_args): + fork_eips_set = ForkEIPSetAdapter.validate_python(fork_args) + if len(fork_eips_set) != len(fork_args): raise Exception( f"Duplicate argument specified in '{self.marker_name}'" ) - return fork_set + forks_set: Set[Fork | TransitionFork] = set() + for fork_eip in fork_eips_set: + if fork_eip.is_transition_fork: + forks_set.add(fork_eip) + else: + if not fork_eip.is_eip(): + forks_set.add(fork_eip) + else: + forks_set |= fork_eip.enabling_forks() + return forks_set @staticmethod def get_all_validity_markers( @@ -855,7 +872,14 @@ def process( ) if self.flag: return forks - fork_set - return forks & fork_set + if not fork_set: + # Test is marked for an EIP that is not yet enabled in any + # fork. + return fork_set + resulting_set = forks & fork_set + if not resulting_set: + raise ValidityMarker.ValidityMarkerCombinationError() + return resulting_set @abstractmethod def _process_with_marker_args( @@ -1125,17 +1149,39 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: test_fork_set = ValidityMarker.get_test_fork_set_from_metafunc( metafunc ) - except Exception as e: - pytest.fail(f"Error generating tests for {test_name}: {e}") - - if not test_fork_set: + except ValidityMarker.ValidityMarkerCombinationError: + markers = ValidityMarker.get_all_validity_markers( + metafunc.definition.iter_markers() + ) + marker_names = [ + f"@pytest.mark.{marker.marker_name}" for marker in markers + ] pytest.fail( "The test function's " f"'{test_name}' fork validity markers generate " "an empty fork range. Please check the arguments to its " - f"markers: @pytest.mark.valid_from and " - f"@pytest.mark.valid_until." + f"markers: {', '.join(marker_names)}." ) + except Exception as e: + pytest.fail(f"Error generating tests for {test_name}: {e}") + + pytest_params: List[Any] + if not test_fork_set: + if metafunc.config.getoption("verbose") >= 2: + pytest_params = [ + pytest.param( + None, + marks=[ + pytest.mark.skip( + reason=( + f"{test_name} is not enabled for any fork." + ) + ) + ], + ) + ] + metafunc.parametrize("fork", pytest_params, scope="function") + return # Get the intersection between the test's validity marker and the current # filling parameters. @@ -1146,7 +1192,6 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: if "fork" not in metafunc.fixturenames: return - pytest_params: List[Any] unsupported_forks: Set[Fork | TransitionFork] = ( metafunc.config.unsupported_forks # type: ignore ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py index 37afa40d44f..dc219f6f87a 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py @@ -45,6 +45,14 @@ def test_case(state_test): {"passed": 4, "failed": 0, "skipped": 0, "errors": 0}, id="valid_from", ), + pytest.param( + generate_test( + valid_from='"EIP3675"', + ), + ["--until=Prague"], + {"passed": 4, "failed": 0, "skipped": 0, "errors": 0}, + id="valid_from_eip", + ), pytest.param( generate_test( valid_from='"Paris"', @@ -54,6 +62,33 @@ def test_case(state_test): {"passed": 3, "failed": 0, "skipped": 0, "errors": 0}, id="valid_from_until", ), + pytest.param( + generate_test( + valid_from='"EIP3675"', + valid_until='"EIP4844"', + ), + [], + {"passed": 3, "failed": 0, "skipped": 0, "errors": 0}, + id="valid_from_eip_until_eip", + ), + pytest.param( + generate_test( + valid_from='"Paris"', + valid_until='"EIP4844"', + ), + [], + {"passed": 3, "failed": 0, "skipped": 0, "errors": 0}, + id="valid_from_fork_until_eip", + ), + pytest.param( + generate_test( + valid_from='"EIP3675"', + valid_until='"Cancun"', + ), + [], + {"passed": 3, "failed": 0, "skipped": 0, "errors": 0}, + id="valid_from_eip_until_fork", + ), pytest.param( generate_test( valid_from='"Paris"', diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 9c7dcb7a395..9dd5f0d4334 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -45,6 +45,7 @@ ALL_TRANSITION_FORKS, Fork, ForkAdapter, + ForkEIPSetAdapter, ForkOrNoneAdapter, ForkRangeDescriptor, ForkSet, @@ -79,6 +80,7 @@ "ALL_TRANSITION_FORKS", "Fork", "ForkAdapter", + "ForkEIPSetAdapter", "ForkOrNoneAdapter", "ForkSet", "ForkSetAdapter", diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 269640e2985..d03e4c740db 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -8,14 +8,12 @@ ClassVar, Dict, List, - Literal, Mapping, Optional, Protocol, Set, Sized, Type, - Union, ) if TYPE_CHECKING: @@ -247,14 +245,16 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _children: ClassVar[Set[Type["BaseFork"]]] = set() _ruleset_name: ClassVar[Optional[str]] = None _fork_by_timestamp: ClassVar[bool] = False + _blob_constants: ClassVar[Dict[str, int]] = {} + _deployed: ClassVar[bool] = True + _enabled_eips: ClassVar[Set[int]] = set() + _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() - # make mypy happy - BLOB_CONSTANTS: ClassVar[Dict[str, Union[int, Literal["big"]]]] = {} - - @classmethod - def get_blob_constant(cls, name: str) -> int | Literal["big"]: - """Return value of requested blob constant.""" - raise NotImplementedError + # Method version bumps + _engine_new_payload_version_bump: ClassVar[bool] = False + _engine_forkchoice_updated_version_bump: ClassVar[bool] = False + _engine_get_payload_version_bump: ClassVar[bool] = False + _engine_get_blobs_version_bump: ClassVar[bool] = False def __init_subclass__( cls, @@ -265,6 +265,12 @@ def __init_subclass__( bpo_fork: bool = False, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, + deployed: Optional[bool] = None, + update_blob_constants: Optional[Dict[str, int]] = None, + engine_new_payload_version_bump: Optional[bool] = None, + engine_forkchoice_updated_version_bump: Optional[bool] = None, + engine_get_payload_version_bump: Optional[bool] = None, + engine_get_blobs_version_bump: Optional[bool] = None, ) -> None: """ Initialize new fork with values that don't carry over to subclass @@ -283,10 +289,78 @@ def __init_subclass__( ) else: cls._fork_by_timestamp = fork_by_timestamp - base_class = cls.__bases__[0] - assert issubclass(base_class, BaseFork) - if base_class != BaseFork: - base_class._children.add(cls) + base_fork_class = None + for base_class in cls.__bases__: + if issubclass(base_class, BaseFork) and not base_class.is_eip(): + base_fork_class = base_class + break + assert base_fork_class is not None + cls._enabled_eips = set() + if base_fork_class != BaseFork: + base_fork_class._children.add(cls) + cls._enabled_eips |= base_fork_class._enabled_eips + eip_bases = [ + base_class + for base_class in cls.__bases__ + if issubclass(base_class, BaseFork) and base_class.is_eip() + ] + cls._enabling_forks = set() + for eip_base in eip_bases: + cls._enabled_eips.add(eip_base.eip()) + eip_base._enabling_forks.add(cls) + # Bump the versions if any of the EIPs bump the version + if engine_new_payload_version_bump is not None: + cls._engine_new_payload_version_bump = ( + engine_new_payload_version_bump + ) + else: + cls._engine_new_payload_version_bump = any( + eip_base._engine_new_payload_version_bump + for eip_base in eip_bases + ) + + if engine_forkchoice_updated_version_bump is not None: + cls._engine_forkchoice_updated_version_bump = ( + engine_forkchoice_updated_version_bump + ) + else: + cls._engine_forkchoice_updated_version_bump = any( + eip_base._engine_forkchoice_updated_version_bump + for eip_base in eip_bases + ) + + if engine_get_payload_version_bump is not None: + cls._engine_get_payload_version_bump = ( + engine_get_payload_version_bump + ) + else: + cls._engine_get_payload_version_bump = any( + eip_base._engine_get_payload_version_bump + for eip_base in eip_bases + ) + + if engine_get_blobs_version_bump is not None: + cls._engine_get_blobs_version_bump = engine_get_blobs_version_bump + else: + cls._engine_get_blobs_version_bump = any( + eip_base._engine_get_blobs_version_bump + for eip_base in eip_bases + ) + + # Calculate blob constants + cls._blob_constants = base_fork_class._blob_constants.copy() + if update_blob_constants is not None: + cls._blob_constants |= update_blob_constants + else: + for eip_base in eip_bases: + cls._blob_constants |= eip_base._blob_constants + + # Determine fork deployment status + if deployed is not None: + cls._deployed = deployed + else: + if base_fork_class is not BaseFork: + cls._deployed = base_fork_class._deployed # Header information abstract methods @classmethod @@ -377,6 +451,98 @@ def opcode_gas_map( """ pass + # Gas calculation helpers + @classmethod + @abstractmethod + def _calculate_sstore_refund( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """Calculate SSTORE gas refund based on metadata.""" + pass + + @classmethod + @abstractmethod + def _calculate_sstore_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """Calculate SSTORE gas cost based on metadata.""" + pass + + @classmethod + @abstractmethod + def _calculate_call_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate CALL/DELEGATECALL/STATICCALL gas cost based on metadata. + """ + pass + + @classmethod + @abstractmethod + def _calculate_create_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """CREATE gas is constant at Frontier.""" + pass + + @classmethod + @abstractmethod + def _calculate_create2_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate CREATE2 gas cost including initcode cost. + """ + pass + + @classmethod + @abstractmethod + def _calculate_return_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """Calculate RETURN gas cost based on metadata.""" + pass + + @classmethod + @abstractmethod + def _calculate_selfdestruct_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """Calculate SELFDESTRUCT gas cost based on metadata.""" + pass + + @classmethod + @abstractmethod + def _with_memory_expansion( + cls, + base_gas: int | Callable[[OpcodeBase], int], + memory_expansion_gas_calculator: MemoryExpansionGasCalculator, + ) -> Callable[[OpcodeBase], int]: + """Wrap a gas cost calculator to include memory expansion cost.""" + pass + + @classmethod + @abstractmethod + def _with_account_access( + cls, + base_gas: int | Callable[[OpcodeBase], int], + gas_costs: "GasCosts", + ) -> Callable[[OpcodeBase], int]: + """Wrap a gas cost calculator to include account access cost.""" + pass + + @classmethod + @abstractmethod + def _with_data_copy( + cls, + base_gas: int | Callable[[OpcodeBase], int], + gas_costs: "GasCosts", + ) -> Callable[[OpcodeBase], int]: + """Wrap a gas cost calculator to include data copy cost.""" + pass + + # Gas calculators @classmethod @abstractmethod def memory_expansion_gas_calculator(cls) -> MemoryExpansionGasCalculator: @@ -412,6 +578,7 @@ def base_fee_change_calculator(cls) -> BaseFeeChangeCalculator: """ pass + # Fee helpers @classmethod @abstractmethod def base_fee_max_change_denominator(cls) -> int: @@ -430,6 +597,7 @@ def max_refund_quotient(cls) -> int: """Return the max refund quotient at a given fork.""" pass + # Transaction cost calculators @classmethod @abstractmethod def transaction_data_floor_cost_calculator( @@ -469,47 +637,62 @@ def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: """ pass + # Blob constants methods @classmethod @abstractmethod + def supports_blobs(cls) -> bool: + """Return whether the given fork supports blobs or not.""" + pass + + @classmethod + def get_blob_constant(cls, name: str) -> int: + """Return blob constant if it exists.""" + assert cls.supports_blobs(), ( + "Requested a blob constant in a fork that does not support blobs" + ) + retrieved_constant = cls._blob_constants.get(name) + assert retrieved_constant is not None, ( + f"You tried to retrieve the blob constant {name} but it does " + "not exist!" + ) + return retrieved_constant + + @classmethod def min_base_fee_per_blob_gas(cls) -> int: """Return the minimum base fee per blob gas at a given fork.""" - pass + return cls.get_blob_constant("MIN_BASE_FEE_PER_BLOB_GAS") @classmethod - @abstractmethod def blob_gas_per_blob(cls) -> int: """Return the amount of blob gas used per blob at a given fork.""" - pass + return cls.get_blob_constant("BLOB_GAS_PER_BLOB") @classmethod - @abstractmethod def blob_base_fee_update_fraction(cls) -> int: """Return the blob base fee update fraction at a given fork.""" - pass + return cls.get_blob_constant("BLOB_BASE_FEE_UPDATE_FRACTION") @classmethod - @abstractmethod - def supports_blobs(cls) -> bool: - """Return whether the given fork supports blobs or not.""" - pass - - @classmethod - @abstractmethod def target_blobs_per_block(cls) -> int: """Return the target blobs per block at a given fork.""" - pass + return cls.get_blob_constant("TARGET_BLOBS_PER_BLOCK") @classmethod - @abstractmethod def max_blobs_per_tx(cls) -> int: """Return the max blobs per transaction at a given fork.""" - pass + if "MAX_BLOBS_PER_TX" in cls._blob_constants: + return cls._blob_constants["MAX_BLOBS_PER_TX"] + return cls.get_blob_constant("MAX_BLOBS_PER_BLOCK") @classmethod - @abstractmethod def max_blobs_per_block(cls) -> int: """Return the max blobs per block at a given fork.""" - pass + return cls.get_blob_constant("MAX_BLOBS_PER_BLOCK") + + @classmethod + def blob_base_cost(cls) -> int: + """Return the base cost of a blob at a given fork.""" + return cls.get_blob_constant("BLOB_BASE_COST") @classmethod @abstractmethod @@ -520,12 +703,6 @@ def blob_reserve_price_active(cls) -> bool: """ pass - @classmethod - @abstractmethod - def blob_base_cost(cls) -> int: - """Return the base cost of a blob at a given fork.""" - pass - @classmethod @abstractmethod def full_blob_tx_wrapper_version(cls) -> int | None: @@ -634,15 +811,6 @@ def pre_allocation_blockchain(cls) -> Mapping: pass # Engine API information abstract methods - @classmethod - @abstractmethod - def engine_new_payload_version(cls) -> Optional[int]: - """ - Return `None` if this fork's payloads cannot be sent over the engine - API, or the payload version if it can. - """ - pass - @classmethod @abstractmethod def engine_new_payload_blob_hashes(cls) -> bool: @@ -705,32 +873,62 @@ def engine_payload_attribute_max_blobs_per_block(cls) -> bool: """ pass + # Engine API method versions + @classmethod + def engine_new_payload_version(cls) -> Optional[int]: + """ + Return `None` if this fork's payloads cannot be sent over the engine + API, or the payload version if it can. + """ + current_version = 0 + current_cls: Type["BaseFork"] | None = cls + while current_cls is not None: + if current_cls._engine_new_payload_version_bump: + current_version += 1 + current_cls = current_cls.parent() + return current_version if current_version > 0 else None + @classmethod - @abstractmethod def engine_forkchoice_updated_version(cls) -> Optional[int]: """ Return `None` if the forks canonical chain cannot be set using the forkchoice method. """ - pass + current_version = 0 + current_cls: Type["BaseFork"] | None = cls + while current_cls is not None: + if current_cls._engine_forkchoice_updated_version_bump: + current_version += 1 + current_cls = current_cls.parent() + return current_version if current_version > 0 else None @classmethod - @abstractmethod def engine_get_payload_version(cls) -> Optional[int]: """ Return `None` if the forks canonical chain cannot build a payload using the engine API. """ - pass + current_version = 0 + current_cls: Type["BaseFork"] | None = cls + while current_cls is not None: + if current_cls._engine_get_payload_version_bump: + current_version += 1 + current_cls = current_cls.parent() + return current_version if current_version > 0 else None @classmethod - @abstractmethod def engine_get_blobs_version(cls) -> Optional[int]: """ Return `None` if the fork does not support the engine get blobs version. """ - pass + current_version = 0 + current_cls: Type["BaseFork"] | None = cls + while current_cls is not None: + if current_cls._engine_get_blobs_version_bump: + current_version += 1 + current_cls = current_cls.parent() + return current_version if current_version > 0 else None # EVM information abstract methods @classmethod @@ -833,11 +1031,8 @@ def solc_name(cls) -> str: def is_deployed(cls) -> bool: """ Return whether the fork has been deployed to mainnet, or not. - - Must be overridden and return False for forks that are still under - development. """ - return True + return cls._deployed @classmethod def ignore(cls) -> bool: @@ -849,14 +1044,52 @@ def bpo_fork(cls) -> bool: """Return whether the fork is a BPO fork.""" return cls._bpo_fork + @classmethod + def is_eip(cls) -> bool: + """Return whether this class is an EIP.""" + return cls.__name__.startswith("EIP") and cls.__name__[-1].isdigit() + + @classmethod + def eip(cls) -> int: + """Return the number of this EIP class.""" + if not cls.is_eip(): + raise Exception(f"Class {cls.__name__} is not an EIP.") + return int(cls.__name__[3:]) + + @classmethod + def is_eip_enabled(cls, *, eip_number: int) -> bool: + """Return whether this class has an EIP enabled.""" + return eip_number in cls._enabled_eips + + @classmethod + def enabling_forks(cls) -> Set[Type["BaseFork"]]: + """Return the forks that enable this EIP.""" + if not cls.is_eip(): + raise Exception(f"Class {cls.__name__} is not an EIP.") + return cls._enabling_forks + @classmethod def parent(cls) -> Type["BaseFork"] | None: """Return the parent fork.""" - base_class = cls.__bases__[0] - assert issubclass(base_class, BaseFork) - if base_class == BaseFork: + base_fork_class = None + for base_class in cls.__bases__: + if issubclass(base_class, BaseFork) and not base_class.is_eip(): + base_fork_class = base_class + break + assert base_fork_class is not None + if base_fork_class == BaseFork: return None - return base_class + return base_fork_class + + @classmethod + def parent_or_fail(cls) -> Type["BaseFork"]: + """Return the parent fork or raise if the class has no parent.""" + parent = cls.parent() + if parent is None: + raise Exception( + f"Expected class {cls.__name__} to have a parent fork." + ) + return parent @classmethod def non_bpo_ancestor(cls) -> Type["BaseFork"]: diff --git a/packages/testing/src/execution_testing/forks/forks/eips/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/__init__.py new file mode 100644 index 00000000000..9c401623780 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/__init__.py @@ -0,0 +1,35 @@ +"""Listings of all EIPs, current and upcoming.""" + +import importlib +import inspect +import pkgutil +import re +from typing import Any + +__all__ = [] + +_prefix = __name__ + "." + +ALL_EIPS = [] + +for _importer, _modname, _ispkg in pkgutil.walk_packages( + __path__, prefix=_prefix +): + if not re.search(r"\.eip_\d+$", _modname): + continue + + _module = importlib.import_module(_modname) + + for _name, _obj in inspect.getmembers(_module, inspect.isclass): + if re.match(r"^EIP\d+$", _name) and _obj.__module__ == _modname: + globals()[_name] = _obj + ALL_EIPS.append(_obj) + __all__.append(_name) + + +def __getattr__(name: str) -> Any: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# Clean up module namespace +del _importer, _modname, _ispkg, _module, _name, _obj, _prefix diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/__init__.py new file mode 100644 index 00000000000..eea9ab260e0 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Amsterdam fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py new file mode 100644 index 00000000000..5e46e0ccb95 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py @@ -0,0 +1,61 @@ +""" +EIP-7928: Block-Level Access Lists. + +Enforced block access lists with state locations and post-transaction state +diffs. + +https://eips.ethereum.org/EIPS/eip-7928 +""" + +from dataclasses import replace + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP7928( + BaseFork, + # Engine API method version bumps + # New field `blockAccessList` in ExecutionPayload + engine_new_payload_version_bump=True, + engine_get_payload_version_bump=True, +): + """EIP-7928 class.""" + + @classmethod + def header_bal_hash_required(cls) -> bool: + """ + Header must contain block access list hash (EIP-7928). + """ + return True + + @classmethod + def gas_costs(cls) -> GasCosts: + """ + The cost per block access list item is introduced in EIP-7928. + """ + return replace( + super(EIP7928, cls).gas_costs(), + GAS_BLOCK_ACCESS_LIST_ITEM=2000, + ) + + @classmethod + def empty_block_bal_item_count(cls) -> int: + """ + Return the BAL item count for an empty EIP-7928 block. + + Four system contracts produce 15 items: + EIP-4788 beacon roots: 1 address + 1 write + 1 read = 3 + EIP-2935 history storage: 1 address + 1 write = 2 + EIP-7002 withdrawal requests: 1 address + 4 reads = 5 + EIP-7251 consolidation requests: 1 address + 4 reads = 5 + """ + return 15 + + @classmethod + def engine_execution_payload_block_access_list(cls) -> bool: + """ + From EIP-7928, engine execution payload includes `block_access_list` + as a parameter. + """ + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/berlin/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/berlin/__init__.py new file mode 100644 index 00000000000..2048dbaea74 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/berlin/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Berlin fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py b/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py new file mode 100644 index 00000000000..7544ae1580a --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py @@ -0,0 +1,65 @@ +""" +EIP-2930: Optional access lists. + +Add a transaction type which contains an access list, a list of addresses +and storage keys that the transaction plans to access. + +https://eips.ethereum.org/EIPS/eip-2930 +""" + +from typing import List, Sized + +from execution_testing.base_types import AccessList +from execution_testing.base_types.conversions import BytesConvertible + +from ....base_fork import BaseFork, TransactionIntrinsicCostCalculator + + +class EIP2930(BaseFork): + """EIP-2930 class.""" + + @classmethod + def tx_types(cls) -> List[int]: + """Access list transactions are introduced.""" + return [1] + super(EIP2930, cls).tx_types() + + @classmethod + def contract_creating_tx_types(cls) -> List[int]: + """Access list transactions can create contracts.""" + return [1] + super(EIP2930, cls).contract_creating_tx_types() + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + Transaction intrinsic cost includes access list cost. + """ + super_fn = super(EIP2930, cls).transaction_intrinsic_cost_calculator() + gas_costs = cls.gas_costs() + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + ) -> int: + del return_cost_deducted_prior_execution + + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + authorization_list_or_count=authorization_list_or_count, + ) + if access_list is not None: + for access in access_list: + intrinsic_cost += gas_costs.GAS_TX_ACCESS_LIST_ADDRESS + for _ in access.storage_keys: + intrinsic_cost += ( + gas_costs.GAS_TX_ACCESS_LIST_STORAGE_KEY + ) + return intrinsic_cost + + return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/bogota/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/bogota/__init__.py new file mode 100644 index 00000000000..3579c6578a2 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/bogota/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Bogotá fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/__init__.py new file mode 100644 index 00000000000..11880dc2696 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Byzantium fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_140.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_140.py new file mode 100644 index 00000000000..f3eab6af239 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_140.py @@ -0,0 +1,37 @@ +""" +EIP-140: REVERT instruction. + +Provide a way to stop execution and revert state changes without consuming +all provided gas. + +https://eips.ethereum.org/EIPS/eip-140 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP140(BaseFork): + """EIP-140 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add REVERT opcode gas cost.""" + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP140, cls).opcode_gas_map() + return { + **base_map, + Opcodes.REVERT: cls._with_memory_expansion( + 0, memory_expansion_calculator + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add REVERT to valid opcodes.""" + return [Opcodes.REVERT] + super(EIP140, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py new file mode 100644 index 00000000000..2d7178efdca --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py @@ -0,0 +1,35 @@ +""" +EIP-196: Precompiled contracts for addition and scalar multiplication on +the elliptic curve alt_bn128. + +https://eips.ethereum.org/EIPS/eip-196 +""" + +from dataclasses import replace +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP196(BaseFork): + """EIP-196 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """Add BN254 addition and scalar multiplication precompiles.""" + return [ + Address(6, label="BN254_ADD"), + Address(7, label="BN254_MUL"), + ] + super(EIP196, cls).precompiles() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Set gas costs for BN254 addition and multiplication.""" + return replace( + super(EIP196, cls).gas_costs(), + GAS_PRECOMPILE_ECADD=500, + GAS_PRECOMPILE_ECMUL=40_000, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py new file mode 100644 index 00000000000..55a4e60c34c --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py @@ -0,0 +1,34 @@ +""" +EIP-197: Precompiled contracts for optimal ate pairing check on the +elliptic curve alt_bn128. + +https://eips.ethereum.org/EIPS/eip-197 +""" + +from dataclasses import replace +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP197(BaseFork): + """EIP-197 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """Add BN254 pairing check precompile.""" + return [ + Address(8, label="BN254_PAIRING"), + ] + super(EIP197, cls).precompiles() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Set gas costs for BN254 pairing check.""" + return replace( + super(EIP197, cls).gas_costs(), + GAS_PRECOMPILE_ECPAIRING_BASE=100_000, + GAS_PRECOMPILE_ECPAIRING_PER_POINT=80_000, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_198.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_198.py new file mode 100644 index 00000000000..30a8cb530d9 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_198.py @@ -0,0 +1,24 @@ +""" +EIP-198: Big integer modular exponentiation. + +Precompile for modular exponentiation. + +https://eips.ethereum.org/EIPS/eip-198 +""" + +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + + +class EIP198(BaseFork): + """EIP-198 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """Add modular exponentiation precompile.""" + return [ + Address(5, label="MODEXP"), + ] + super(EIP198, cls).precompiles() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py new file mode 100644 index 00000000000..51b958f741e --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py @@ -0,0 +1,40 @@ +""" +EIP-211: New opcodes: RETURNDATASIZE and RETURNDATACOPY. + +https://eips.ethereum.org/EIPS/eip-211 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP211(BaseFork): + """EIP-211 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add RETURNDATASIZE and RETURNDATACOPY opcode gas costs.""" + gas_costs = cls.gas_costs() + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP211, cls).opcode_gas_map() + return { + **base_map, + Opcodes.RETURNDATASIZE: gas_costs.GAS_BASE, + Opcodes.RETURNDATACOPY: cls._with_memory_expansion( + cls._with_data_copy(gas_costs.GAS_VERY_LOW, gas_costs), + memory_expansion_calculator, + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add RETURNDATASIZE and RETURNDATACOPY to valid opcodes.""" + return [ + Opcodes.RETURNDATASIZE, + Opcodes.RETURNDATACOPY, + ] + super(EIP211, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_214.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_214.py new file mode 100644 index 00000000000..f0db5f83a4f --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_214.py @@ -0,0 +1,45 @@ +""" +EIP-214: New opcode STATICCALL. + +https://eips.ethereum.org/EIPS/eip-214 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP214(BaseFork): + """EIP-214 class.""" + + @classmethod + def call_opcodes(cls) -> List[Opcodes]: + """Add STATICCALL opcode.""" + return [ + Opcodes.STATICCALL, + ] + super(EIP214, cls).call_opcodes() + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add STATICCALL opcode gas cost.""" + gas_costs = cls.gas_costs() + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP214, cls).opcode_gas_map() + return { + **base_map, + Opcodes.STATICCALL: cls._with_memory_expansion( + lambda op: cls._calculate_call_gas(op, gas_costs), + memory_expansion_calculator, + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add STATICCALL to valid opcodes.""" + return [ + Opcodes.STATICCALL, + ] + super(EIP214, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_649.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_649.py new file mode 100644 index 00000000000..f9fa22e5d7d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_649.py @@ -0,0 +1,18 @@ +""" +EIP-649: Metropolis difficulty bomb delay and block reward reduction. + +Delay the difficulty bomb and reduce the block reward to 3 ETH. + +https://eips.ethereum.org/EIPS/eip-649 +""" + +from ....base_fork import BaseFork + + +class EIP649(BaseFork): + """EIP-649 class.""" + + @classmethod + def get_reward(cls) -> int: + """Block reward is reduced to 3 ETH.""" + return 3_000_000_000_000_000_000 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/__init__.py new file mode 100644 index 00000000000..c22e0072e2f --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Cancun fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py new file mode 100644 index 00000000000..0481a794842 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py @@ -0,0 +1,39 @@ +""" +EIP-1153: Transient storage opcodes. + +Add opcodes for manipulating state that behaves identically to storage +but is discarded after every transaction. + +https://eips.ethereum.org/EIPS/eip-1153 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP1153(BaseFork): + """EIP-1153 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add TLOAD and TSTORE opcode gas costs.""" + gas_costs = cls.gas_costs() + base_map = super(EIP1153, cls).opcode_gas_map() + return { + **base_map, + Opcodes.TLOAD: gas_costs.GAS_WARM_SLOAD, + Opcodes.TSTORE: gas_costs.GAS_WARM_SLOAD, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add TLOAD and TSTORE to valid opcodes.""" + return [ + Opcodes.TLOAD, + Opcodes.TSTORE, + ] + super(EIP1153, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4788.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4788.py new file mode 100644 index 00000000000..29db5a9b93d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4788.py @@ -0,0 +1,47 @@ +""" +EIP-4788: Beacon block root in the EVM. + +Expose beacon chain roots in the EVM. + +https://eips.ethereum.org/EIPS/eip-4788 +""" + +from typing import List, Mapping + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + +BEACON_ROOTS_ADDRESS = 0x000F3DF6D732807EF1319FB7B8BB8522D0BEAC02 + + +class EIP4788(BaseFork): + """EIP-4788 class.""" + + @classmethod + def header_beacon_root_required(cls) -> bool: + """Parent beacon block root is required.""" + return True + + @classmethod + def system_contracts(cls) -> List[Address]: + """Add the beacon roots system contract.""" + return [Address(BEACON_ROOTS_ADDRESS, label="BEACON_ROOTS_ADDRESS")] + + @classmethod + def pre_allocation_blockchain(cls) -> Mapping: + """Pre-allocate the beacon root contract.""" + return { + BEACON_ROOTS_ADDRESS: { + "nonce": 1, + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d" + "57602036146024575f5ffd5b5f35801560495762001fff810690" + "815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd" + "5b62001fff42064281555f359062001fff015500", + } + } | super(EIP4788, cls).pre_allocation_blockchain() # type: ignore + + @classmethod + def engine_new_payload_beacon_root(cls) -> bool: + """Payloads must have a parent beacon block root.""" + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py new file mode 100644 index 00000000000..ac6733817e6 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py @@ -0,0 +1,216 @@ +""" +EIP-4844: Shard Blob Transactions. + +Shard Blob Transactions scale data-availability of Ethereum in a simple, +forwards-compatible manner. + +https://eips.ethereum.org/EIPS/eip-4844 +""" + +from dataclasses import replace +from typing import Callable, Dict, List + +from execution_testing.base_types import ( + Address, + BlobSchedule, + ForkBlobSchedule, +) +from execution_testing.vm import ( + OpcodeBase, + Opcodes, +) + +from ....base_fork import ( + BaseFork, + BlobGasPriceCalculator, + ExcessBlobGasCalculator, +) +from ....gas_costs import GasCosts +from ...helpers import fake_exponential + + +class EIP4844( + BaseFork, + update_blob_constants={ + "FIELD_ELEMENTS_PER_BLOB": 4096, + "BYTES_PER_FIELD_ELEMENT": 32, + "CELL_LENGTH": 2048, + # EIP-2537: Main subgroup order = q, due to this BLS_MODULUS + # every blob byte (uint256) must be smaller than 116 + "BLS_MODULUS": ( + 0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 + ), + # https://github.com/ethereum/consensus-specs/blob/ + # cc6996c22692d70e41b7a453d925172ee4b719ad/specs/deneb/ + # polynomial-commitments.md?plain=1#L78 + "BYTES_PER_PROOF": 48, + "BYTES_PER_COMMITMENT": 48, + "AMOUNT_CELL_PROOFS": 0, + "BLOB_GAS_PER_BLOB": 2**17, + "MAX_BLOBS_PER_BLOCK": 6, + "TARGET_BLOBS_PER_BLOCK": 3, + "BLOB_BASE_FEE_UPDATE_FRACTION": 3338477, + "MIN_BASE_FEE_PER_BLOB_GAS": 1, + }, + # Engine API method version bumps + engine_new_payload_version_bump=True, + engine_forkchoice_updated_version_bump=True, + engine_get_payload_version_bump=True, + engine_get_blobs_version_bump=True, +): + """EIP-4844 class.""" + + @classmethod + def header_excess_blob_gas_required(cls) -> bool: + """Excess blob gas is required starting from Cancun.""" + return True + + @classmethod + def header_blob_gas_used_required(cls) -> bool: + """Blob gas used is required starting from Cancun.""" + return True + + @classmethod + def blob_gas_price_calculator(cls) -> BlobGasPriceCalculator: + """Return a callable that calculates the blob gas price at Cancun.""" + min_base_fee_per_blob_gas = cls.min_base_fee_per_blob_gas() + blob_base_fee_update_fraction = cls.blob_base_fee_update_fraction() + + def fn(*, excess_blob_gas: int) -> int: + return fake_exponential( + min_base_fee_per_blob_gas, + excess_blob_gas, + blob_base_fee_update_fraction, + ) + + return fn + + @classmethod + def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: + """ + Return a callable that calculates the excess blob gas for a block at + Cancun. + """ + target_blobs_per_block = cls.target_blobs_per_block() + blob_gas_per_blob = cls.blob_gas_per_blob() + blob_target_gas_per_block = target_blobs_per_block * blob_gas_per_blob + + def fn( + *, + parent_excess_blob_gas: int | None = None, + parent_excess_blobs: int | None = None, + parent_blob_gas_used: int | None = None, + parent_blob_count: int | None = None, + # Required for Osaka as using this as base + parent_base_fee_per_gas: int, + ) -> int: + del parent_base_fee_per_gas + + if parent_excess_blob_gas is None: + assert parent_excess_blobs is not None, ( + "Parent excess blobs are required" + ) + parent_excess_blob_gas = ( + parent_excess_blobs * blob_gas_per_blob + ) + if parent_blob_gas_used is None: + assert parent_blob_count is not None, ( + "Parent blob count is required" + ) + parent_blob_gas_used = parent_blob_count * blob_gas_per_blob + if ( + parent_excess_blob_gas + parent_blob_gas_used + < blob_target_gas_per_block + ): + return 0 + else: + return ( + parent_excess_blob_gas + + parent_blob_gas_used + - blob_target_gas_per_block + ) + + return fn + + @classmethod + def supports_blobs(cls) -> bool: + """At Cancun, blobs support is enabled.""" + return True + + @classmethod + def blob_reserve_price_active(cls) -> bool: + """Blob reserve price is not supported in Cancun.""" + return False + + @classmethod + def full_blob_tx_wrapper_version(cls) -> int | None: + """ + Pre-Osaka forks don't use tx wrapper versions for full blob + transactions. + """ + return None + + @classmethod + def blob_schedule(cls) -> BlobSchedule | None: + """ + At Cancun, the fork object runs this routine to get the updated blob + schedule. + """ + parent_fork = cls.parent() + assert parent_fork is not None, "Parent fork must be defined" + blob_schedule = parent_fork.blob_schedule() or BlobSchedule() + current_blob_schedule = ForkBlobSchedule( + target_blobs_per_block=cls.target_blobs_per_block(), + max_blobs_per_block=cls.max_blobs_per_block(), + base_fee_update_fraction=cls.blob_base_fee_update_fraction(), + ) + blob_schedule.append(fork=cls.name(), schedule=current_blob_schedule) + return blob_schedule + + @classmethod + def tx_types(cls) -> List[int]: + """At Cancun, blob type transactions are introduced.""" + return [3] + super(EIP4844, cls).tx_types() + + @classmethod + def precompiles(cls) -> List[Address]: + """At Cancun, a precompile for kzg point evaluation is introduced.""" + return [ + Address(10, label="KZG_POINT_EVALUATION"), + ] + super(EIP4844, cls).precompiles() + + @classmethod + def engine_new_payload_blob_hashes(cls) -> bool: + """From Cancun, payloads must have blob hashes.""" + return True + + @classmethod + def gas_costs(cls) -> GasCosts: + """On Cancun, the point evaluation precompile gas cost is set.""" + return replace( + super(EIP4844, cls).gas_costs(), + GAS_PRECOMPILE_POINT_EVALUATION=50_000, + ) + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """ + Return a mapping of opcodes to their gas costs for Cancun. + + Adds Cancun-specific opcodes: BLOBHASH, BLOBBASEFEE, TLOAD, TSTORE, + MCOPY. + """ + gas_costs = cls.gas_costs() + + # Get parent fork's opcode gas map + base_map = super(EIP4844, cls).opcode_gas_map() + + # Add Cancun-specific opcodes + return {**base_map, Opcodes.BLOBHASH: gas_costs.GAS_VERY_LOW} + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Return list of Opcodes that are valid to work on this fork.""" + return [Opcodes.BLOBHASH] + super(EIP4844, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py new file mode 100644 index 00000000000..a3d0190c6f3 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py @@ -0,0 +1,40 @@ +""" +EIP-5656: MCOPY - Memory copying instruction. + +An efficient EVM instruction for copying memory areas. + +https://eips.ethereum.org/EIPS/eip-5656 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP5656(BaseFork): + """EIP-5656 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add MCOPY opcode gas cost.""" + gas_costs = cls.gas_costs() + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP5656, cls).opcode_gas_map() + return { + **base_map, + Opcodes.MCOPY: cls._with_memory_expansion( + cls._with_data_copy(gas_costs.GAS_VERY_LOW, gas_costs), + memory_expansion_calculator, + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add MCOPY to valid opcodes.""" + return [ + Opcodes.MCOPY, + ] + super(EIP5656, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_7516.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_7516.py new file mode 100644 index 00000000000..87fde9fc426 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_7516.py @@ -0,0 +1,40 @@ +""" +EIP-7516: BLOBBASEFEE instruction. + +Instruction that returns the current data-blob base-fee. + +https://eips.ethereum.org/EIPS/eip-7516 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP7516(BaseFork): + """EIP-7516 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add BLOBBASEFEE opcode gas cost.""" + gas_costs = cls.gas_costs() + + # Get parent fork's opcode gas map + base_map = super(EIP7516, cls).opcode_gas_map() + + # Add Cancun-specific opcodes + return { + **base_map, + Opcodes.BLOBBASEFEE: gas_costs.GAS_BASE, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add BLOBBASEFEE to valid opcodes.""" + return [ + Opcodes.BLOBBASEFEE, + ] + super(EIP7516, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/__init__.py new file mode 100644 index 00000000000..d49a1167aa1 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Constantinople fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1014.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1014.py new file mode 100644 index 00000000000..5dacb5b54cd --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1014.py @@ -0,0 +1,61 @@ +""" +EIP-1014: Skinny CREATE2. + +Add a new CREATE2 opcode that uses keccak256 for address derivation. + +https://eips.ethereum.org/EIPS/eip-1014 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP1014(BaseFork): + """EIP-1014 class.""" + + @classmethod + def _calculate_create2_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """Calculate CREATE2 gas cost based on metadata.""" + metadata = opcode.metadata + + init_code_size = metadata["init_code_size"] + init_code_words = (init_code_size + 31) // 32 + hash_gas = gas_costs.GAS_KECCAK256_PER_WORD * init_code_words + + return gas_costs.GAS_CREATE + hash_gas + + @classmethod + def create_opcodes(cls) -> List[Opcodes]: + """Add CREATE2 opcode.""" + return [ + Opcodes.CREATE2, + ] + super(EIP1014, cls).create_opcodes() + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add CREATE2 opcode gas cost.""" + gas_costs = cls.gas_costs() + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP1014, cls).opcode_gas_map() + return { + **base_map, + Opcodes.CREATE2: cls._with_memory_expansion( + lambda op: cls._calculate_create2_gas(op, gas_costs), + memory_expansion_calculator, + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add CREATE2 to valid opcodes.""" + return [ + Opcodes.CREATE2, + ] + super(EIP1014, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1052.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1052.py new file mode 100644 index 00000000000..5845bf12c99 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1052.py @@ -0,0 +1,37 @@ +""" +EIP-1052: EXTCODEHASH opcode. + +Provide a new opcode that returns the keccak256 hash of a contract's +code. + +https://eips.ethereum.org/EIPS/eip-1052 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP1052(BaseFork): + """EIP-1052 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add EXTCODEHASH opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP1052, cls).opcode_gas_map() + return { + **base_map, + Opcodes.EXTCODEHASH: cls._with_account_access(0, gas_costs), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add EXTCODEHASH to valid opcodes.""" + return [ + Opcodes.EXTCODEHASH, + ] + super(EIP1052, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1234.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1234.py new file mode 100644 index 00000000000..c4473d1d9dd --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_1234.py @@ -0,0 +1,18 @@ +""" +EIP-1234: Constantinople difficulty bomb delay and block reward adjustment. + +Delay the difficulty bomb and reduce the block reward to 2 ETH. + +https://eips.ethereum.org/EIPS/eip-1234 +""" + +from ....base_fork import BaseFork + + +class EIP1234(BaseFork): + """EIP-1234 class.""" + + @classmethod + def get_reward(cls) -> int: + """Block reward is reduced to 2 ETH.""" + return 2_000_000_000_000_000_000 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py new file mode 100644 index 00000000000..2ca9ca345c6 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py @@ -0,0 +1,40 @@ +""" +EIP-145: Bitwise shifting instructions in EVM. + +Add SHL, SHR, and SAR instructions to the EVM. + +https://eips.ethereum.org/EIPS/eip-145 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP145(BaseFork): + """EIP-145 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add SHL, SHR, and SAR opcode gas costs.""" + gas_costs = cls.gas_costs() + base_map = super(EIP145, cls).opcode_gas_map() + return { + **base_map, + Opcodes.SHL: gas_costs.GAS_VERY_LOW, + Opcodes.SHR: gas_costs.GAS_VERY_LOW, + Opcodes.SAR: gas_costs.GAS_VERY_LOW, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add SHL, SHR, and SAR to valid opcodes.""" + return [ + Opcodes.SHL, + Opcodes.SHR, + Opcodes.SAR, + ] + super(EIP145, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/homestead/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/homestead/__init__.py new file mode 100644 index 00000000000..d7747aebca2 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/homestead/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Homestead fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py new file mode 100644 index 00000000000..29007094e3d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py @@ -0,0 +1,49 @@ +""" +EIP-2: Homestead Hard-fork Changes. + +https://eips.ethereum.org/EIPS/eip-2 +""" + +from typing import List, Sized + +from execution_testing.base_types import AccessList +from execution_testing.base_types.conversions import BytesConvertible + +from ....base_fork import BaseFork, TransactionIntrinsicCostCalculator + + +class EIP2(BaseFork): + """EIP-2 class.""" + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + The transaction intrinsic cost needs to take contract creation into + account. + """ + super_fn = super(EIP2, cls).transaction_intrinsic_cost_calculator() + gas_costs = cls.gas_costs() + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + ) -> int: + del return_cost_deducted_prior_execution + + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + authorization_list_or_count=authorization_list_or_count, + ) + if contract_creation: + intrinsic_cost += gas_costs.GAS_TX_CREATE + return intrinsic_cost + + return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_7.py b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_7.py new file mode 100644 index 00000000000..3008b67d419 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_7.py @@ -0,0 +1,46 @@ +""" +EIP-7: DELEGATECALL. + +A new opcode that is similar to CALLCODE but propagates the sender and +value from the parent scope. + +https://eips.ethereum.org/EIPS/eip-7 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP7(BaseFork): + """EIP-7 class.""" + + @classmethod + def call_opcodes(cls) -> List[Opcodes]: + """Add DELEGATECALL opcode.""" + return [Opcodes.DELEGATECALL] + super(EIP7, cls).call_opcodes() + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add DELEGATECALL opcode gas cost.""" + gas_costs = cls.gas_costs() + memory_expansion_calculator = cls.memory_expansion_gas_calculator() + base_map = super(EIP7, cls).opcode_gas_map() + return { + **base_map, + Opcodes.DELEGATECALL: cls._with_memory_expansion( + lambda op: cls._calculate_call_gas(op, gas_costs), + memory_expansion_calculator, + ), + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add DELEGATECALL to valid opcodes.""" + return [ + Opcodes.DELEGATECALL, + ] + super(EIP7, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/__init__.py new file mode 100644 index 00000000000..d58c86d9d1b --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Istanbul fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py new file mode 100644 index 00000000000..6d45947dc1f --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py @@ -0,0 +1,25 @@ +""" +EIP-1108: Reduce alt_bn128 precompile gas costs. + +https://eips.ethereum.org/EIPS/eip-1108 +""" + +from dataclasses import replace + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP1108(BaseFork): + """EIP-1108 class.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """Reduce BN254 precompile gas costs.""" + return replace( + super(EIP1108, cls).gas_costs(), + GAS_PRECOMPILE_ECADD=150, + GAS_PRECOMPILE_ECMUL=6000, + GAS_PRECOMPILE_ECPAIRING_BASE=45_000, + GAS_PRECOMPILE_ECPAIRING_PER_POINT=34_000, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1344.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1344.py new file mode 100644 index 00000000000..89a7272f497 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1344.py @@ -0,0 +1,32 @@ +""" +EIP-1344: ChainID opcode. + +Add a new opcode that returns the current chain's EIP-155 unique +identifier. + +https://eips.ethereum.org/EIPS/eip-1344 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP1344(BaseFork): + """EIP-1344 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add CHAINID opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP1344, cls).opcode_gas_map() + return {**base_map, Opcodes.CHAINID: gas_costs.GAS_BASE} + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add CHAINID to valid opcodes.""" + return [Opcodes.CHAINID] + super(EIP1344, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py new file mode 100644 index 00000000000..9979cc4076d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py @@ -0,0 +1,32 @@ +""" +EIP-152: Add BLAKE2 compression function F precompile. + +https://eips.ethereum.org/EIPS/eip-152 +""" + +from dataclasses import replace +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP152(BaseFork): + """EIP-152 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """Add BLAKE2 compression function precompile.""" + return [ + Address(9, label="BLAKE2F"), + ] + super(EIP152, cls).precompiles() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Set BLAKE2F per-round gas cost.""" + return replace( + super(EIP152, cls).gas_costs(), + GAS_PRECOMPILE_BLAKE2F_PER_ROUND=1, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1884.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1884.py new file mode 100644 index 00000000000..f434b90a56e --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1884.py @@ -0,0 +1,33 @@ +""" +EIP-1884: Repricing for trie-size-dependent opcodes. + +Introduces SELFBALANCE opcode. + +https://eips.ethereum.org/EIPS/eip-1884 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP1884(BaseFork): + """EIP-1884 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add SELFBALANCE opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP1884, cls).opcode_gas_map() + return {**base_map, Opcodes.SELFBALANCE: gas_costs.GAS_LOW} + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add SELFBALANCE to valid opcodes.""" + return [ + Opcodes.SELFBALANCE, + ] + super(EIP1884, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py new file mode 100644 index 00000000000..6dbb2446b43 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py @@ -0,0 +1,24 @@ +""" +EIP-2028: Transaction data gas cost reduction. + +Reduce the gas cost of non-zero transaction data bytes to 16. + +https://eips.ethereum.org/EIPS/eip-2028 +""" + +from dataclasses import replace + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP2028(BaseFork): + """EIP-2028 class.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """Reduce non-zero calldata byte gas cost to 16.""" + return replace( + super(EIP2028, cls).gas_costs(), + GAS_TX_DATA_PER_NON_ZERO=16, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/london/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/london/__init__.py new file mode 100644 index 00000000000..7b2d7e3e28b --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/london/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for London fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/london/eip_1559.py b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_1559.py new file mode 100644 index 00000000000..4bfd4bf5809 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_1559.py @@ -0,0 +1,171 @@ +""" +EIP-1559: Fee market change for ETH 1.0 chain. + +A transaction pricing mechanism that includes fixed-per-block network fee +that is burned and dynamically expands/contracts block sizes to deal with +transient congestion. + +https://eips.ethereum.org/EIPS/eip-1559 +""" + +from typing import List + +from ....base_fork import ( + BaseFeeChangeCalculator, + BaseFeePerGasCalculator, + BaseFork, +) + + +class EIP1559(BaseFork): + """EIP-1559 class.""" + + @classmethod + def header_base_fee_required(cls) -> bool: + """Header must contain the base fee.""" + return True + + @classmethod + def tx_types(cls) -> List[int]: + """Dynamic fee transactions are introduced.""" + return [2] + super(EIP1559, cls).tx_types() + + @classmethod + def contract_creating_tx_types(cls) -> List[int]: + """Dynamic fee transactions can create contracts.""" + return [2] + super(EIP1559, cls).contract_creating_tx_types() + + @classmethod + def base_fee_max_change_denominator(cls) -> int: + """Return the base fee max change denominator.""" + return 8 + + @classmethod + def base_fee_elasticity_multiplier(cls) -> int: + """Return the base fee elasticity multiplier.""" + return 2 + + @classmethod + def base_fee_per_gas_calculator(cls) -> BaseFeePerGasCalculator: + """ + Return a callable that calculates the base fee per gas. + + EIP-1559 block validation pseudo code: + + if INITIAL_FORK_BLOCK_NUMBER == block.number: + expected_base_fee_per_gas = INITIAL_BASE_FEE + elif parent_gas_used == parent_gas_target: + expected_base_fee_per_gas = parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + base_fee_per_gas_delta = max( parent_base_fee_per_gas + * gas_used_delta // parent_gas_target // + BASE_FEE_MAX_CHANGE_DENOMINATOR, 1, ) + expected_base_fee_per_gas = parent_base_fee_per_gas + + base_fee_per_gas_delta + else: + gas_used_delta = parent_gas_target - parent_gas_used + base_fee_per_gas_delta = ( + parent_base_fee_per_gas * gas_used_delta // + parent_gas_target // + BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + expected_base_fee_per_gas = parent_base_fee_per_gas - + base_fee_per_gas_delta + """ + base_fee_max_change_denominator = cls.base_fee_max_change_denominator() + elasticity_multiplier = cls.base_fee_elasticity_multiplier() + + def fn( + *, + parent_base_fee_per_gas: int, + parent_gas_used: int, + parent_gas_limit: int, + ) -> int: + parent_gas_target = parent_gas_limit // elasticity_multiplier + if parent_gas_used == parent_gas_target: + return parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + base_fee_per_gas_delta = max( + parent_base_fee_per_gas + * gas_used_delta + // parent_gas_target + // base_fee_max_change_denominator, + 1, + ) + return parent_base_fee_per_gas + base_fee_per_gas_delta + else: + gas_used_delta = parent_gas_target - parent_gas_used + base_fee_per_gas_delta = ( + parent_base_fee_per_gas + * gas_used_delta + // parent_gas_target + // base_fee_max_change_denominator + ) + return parent_base_fee_per_gas - base_fee_per_gas_delta + + return fn + + @classmethod + def base_fee_change_calculator(cls) -> BaseFeeChangeCalculator: + """ + Return a callable that calculates the gas that needs to be used + to change the base fee. + """ + base_fee_max_change_denominator = cls.base_fee_max_change_denominator() + elasticity_multiplier = cls.base_fee_elasticity_multiplier() + base_fee_per_gas_calculator = cls.base_fee_per_gas_calculator() + + def fn( + *, + parent_base_fee_per_gas: int, + parent_gas_limit: int, + required_base_fee_per_gas: int, + ) -> int: + parent_gas_target = parent_gas_limit // elasticity_multiplier + + if parent_base_fee_per_gas == required_base_fee_per_gas: + return parent_gas_target + elif required_base_fee_per_gas > parent_base_fee_per_gas: + base_fee_per_gas_delta = ( + required_base_fee_per_gas - parent_base_fee_per_gas + ) + parent_gas_used = ( + ( + base_fee_per_gas_delta + * base_fee_max_change_denominator + * parent_gas_target + ) + // parent_base_fee_per_gas + ) + parent_gas_target + elif required_base_fee_per_gas < parent_base_fee_per_gas: + base_fee_per_gas_delta = ( + parent_base_fee_per_gas - required_base_fee_per_gas + ) + + parent_gas_used = ( + parent_gas_target + - ( + ( + base_fee_per_gas_delta + * base_fee_max_change_denominator + * parent_gas_target + ) + // parent_base_fee_per_gas + ) + - 1 + ) + + assert ( + base_fee_per_gas_calculator( + parent_base_fee_per_gas=parent_base_fee_per_gas, + parent_gas_used=parent_gas_used, + parent_gas_limit=parent_gas_limit, + ) + == required_base_fee_per_gas + ) + + return parent_gas_used + + return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3198.py b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3198.py new file mode 100644 index 00000000000..5be886c92f5 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3198.py @@ -0,0 +1,32 @@ +""" +EIP-3198: BASEFEE opcode. + +Add an opcode that returns the value of the base fee of the current +block. + +https://eips.ethereum.org/EIPS/eip-3198 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP3198(BaseFork): + """EIP-3198 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add BASEFEE opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP3198, cls).opcode_gas_map() + return {**base_map, Opcodes.BASEFEE: gas_costs.GAS_BASE} + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add BASEFEE to valid opcodes.""" + return [Opcodes.BASEFEE] + super(EIP3198, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3529.py b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3529.py new file mode 100644 index 00000000000..beb12140d94 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/london/eip_3529.py @@ -0,0 +1,18 @@ +""" +EIP-3529: Reduction in refunds. + +Remove gas refunds for SELFDESTRUCT and reduce refunds for SSTORE. + +https://eips.ethereum.org/EIPS/eip-3529 +""" + +from ....base_fork import BaseFork + + +class EIP3529(BaseFork): + """EIP-3529 class.""" + + @classmethod + def max_refund_quotient(cls) -> int: + """Max refund quotient is increased to 5 (reducing refunds).""" + return 5 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/__init__.py new file mode 100644 index 00000000000..963ef55d3f6 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Osaka fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7594.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7594.py new file mode 100644 index 00000000000..2c6a5e03216 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7594.py @@ -0,0 +1,26 @@ +""" +EIP-7594: PeerDAS - Peer Data Availability Sampling. + +Introducing simple DAS utilizing gossip distribution and peer requests. + +https://eips.ethereum.org/EIPS/eip-7594 +""" + +from ....base_fork import BaseFork + + +class EIP7594( + BaseFork, + engine_get_payload_version_bump=True, + engine_get_blobs_version_bump=True, + update_blob_constants={ + "AMOUNT_CELL_PROOFS": 128, + "MAX_BLOBS_PER_TX": 6, + }, +): + """EIP-7594 class.""" + + @classmethod + def full_blob_tx_wrapper_version(cls) -> int | None: + """Full blob transaction wrapper version is defined.""" + return 1 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7825.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7825.py new file mode 100644 index 00000000000..e36ce4b459d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7825.py @@ -0,0 +1,19 @@ +""" +EIP-7825: Transaction gas limit cap. + +Introduce a protocol-level cap on the maximum gas used by a transaction to +16,777,216 (2^24). + +https://eips.ethereum.org/EIPS/eip-7825 +""" + +from ....base_fork import BaseFork + + +class EIP7825(BaseFork): + """EIP-7825 class.""" + + @classmethod + def transaction_gas_limit_cap(cls) -> int | None: + """Transaction gas limit is capped at 16 million (2**24).""" + return 16_777_216 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7918.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7918.py new file mode 100644 index 00000000000..192788a881f --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7918.py @@ -0,0 +1,87 @@ +""" +EIP-7918: Blob base fee bounded by execution cost. + +Imposes that the price of GAS_PER_BLOB blob gas is greater than the price +of BLOB_BASE_COST execution gas. + +https://eips.ethereum.org/EIPS/eip-7918 +""" + +from ....base_fork import BaseFork, ExcessBlobGasCalculator + + +class EIP7918( + BaseFork, + update_blob_constants={ + "BLOB_BASE_COST": 2**13, + }, +): + """EIP-7918 class.""" + + @classmethod + def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: + """ + Return a callable that calculates the excess blob gas for a block. + """ + target_blobs_per_block = cls.target_blobs_per_block() + blob_gas_per_blob = cls.blob_gas_per_blob() + blob_target_gas_per_block = target_blobs_per_block * blob_gas_per_blob + max_blobs_per_block = cls.max_blobs_per_block() + blob_base_cost = cls.blob_base_cost() + + def fn( + *, + parent_excess_blob_gas: int | None = None, + parent_excess_blobs: int | None = None, + parent_blob_gas_used: int | None = None, + parent_blob_count: int | None = None, + parent_base_fee_per_gas: int, + ) -> int: + if parent_excess_blob_gas is None: + assert parent_excess_blobs is not None, ( + "Parent excess blobs are required" + ) + parent_excess_blob_gas = ( + parent_excess_blobs * blob_gas_per_blob + ) + if parent_blob_gas_used is None: + assert parent_blob_count is not None, ( + "Parent blob count is required" + ) + parent_blob_gas_used = parent_blob_count * blob_gas_per_blob + if ( + parent_excess_blob_gas + parent_blob_gas_used + < blob_target_gas_per_block + ): + return 0 + + # EIP-7918: Apply reserve price when execution costs dominate + # blob costs + current_blob_base_fee = cls.blob_gas_price_calculator()( + excess_blob_gas=parent_excess_blob_gas + ) + reserve_price_active = ( + blob_base_cost * parent_base_fee_per_gas + > blob_gas_per_blob * current_blob_base_fee + ) + if reserve_price_active: + blob_excess_adjustment = ( + parent_blob_gas_used + * (max_blobs_per_block - target_blobs_per_block) + // max_blobs_per_block + ) + return parent_excess_blob_gas + blob_excess_adjustment + + # Original EIP-4844 calculation + return ( + parent_excess_blob_gas + + parent_blob_gas_used + - blob_target_gas_per_block + ) + + return fn + + @classmethod + def blob_reserve_price_active(cls) -> bool: + """Blob reserve price is supported.""" + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7934.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7934.py new file mode 100644 index 00000000000..ad3bbde62e3 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7934.py @@ -0,0 +1,21 @@ +""" +EIP-7934: RLP encoded block size limit. + +Introduce a protocol-level cap on the maximum RLP-encoded block size to 10 MiB, +including a 2 MiB margin for beacon block size. + +https://eips.ethereum.org/EIPS/eip-7934 +""" + +from ....base_fork import BaseFork + + +class EIP7934(BaseFork): + """EIP-7934 class.""" + + @classmethod + def block_rlp_size_limit(cls) -> int | None: + """Block RLP size is limited.""" + max_block_size = 10_485_760 + safety_margin = 2_097_152 + return max_block_size - safety_margin diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py new file mode 100644 index 00000000000..88b5a23e783 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py @@ -0,0 +1,36 @@ +""" +EIP-7939: CLZ (Count Leading Zeros) EVM opcode. + +Opcode to count the number of leading zero bits in a 256-bit word. + +https://eips.ethereum.org/EIPS/eip-7939 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP7939(BaseFork): + """EIP-7939 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add CLZ opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP7939, cls).opcode_gas_map() + return { + **base_map, + Opcodes.CLZ: gas_costs.GAS_LOW, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add CLZ to valid opcodes.""" + return [ + Opcodes.CLZ, + ] + super(EIP7939, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py new file mode 100644 index 00000000000..1729515f158 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py @@ -0,0 +1,39 @@ +""" +EIP-7951: Precompile for secp256r1 curve support. + +Add precompiled contract for secp256r1 ECDSA signature verification with proper +security checks. + +https://eips.ethereum.org/EIPS/eip-7951 +""" + +from dataclasses import replace +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP7951(BaseFork): + """EIP-7951 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """ + Add a precompile for P256 signature verification. + + P256VERIFY = 0x100 + """ + return [ + Address(0x100, label="P256VERIFY"), + ] + super(EIP7951, cls).precompiles() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Set the P256VERIFY precompile gas cost.""" + return replace( + super(EIP7951, cls).gas_costs(), + GAS_PRECOMPILE_P256VERIFY=6_900, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/paris/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/paris/__init__.py new file mode 100644 index 00000000000..7f3611f0d3d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/paris/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Paris fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/paris/eip_3675.py b/packages/testing/src/execution_testing/forks/forks/eips/paris/eip_3675.py new file mode 100644 index 00000000000..e82350170ed --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/paris/eip_3675.py @@ -0,0 +1,34 @@ +""" +EIP-3675: Upgrade consensus to Proof-of-Stake. + +Deprecate Proof-of-Work and upgrade the consensus mechanism to +Proof-of-Stake. + +https://eips.ethereum.org/EIPS/eip-3675 +""" + +from ....base_fork import BaseFork + + +class EIP3675( + BaseFork, + engine_new_payload_version_bump=True, + engine_forkchoice_updated_version_bump=True, + engine_get_payload_version_bump=True, +): + """EIP-3675 class.""" + + @classmethod + def header_prev_randao_required(cls) -> bool: + """Prev Randao is required.""" + return True + + @classmethod + def header_zero_difficulty_required(cls) -> bool: + """Zero difficulty is required.""" + return True + + @classmethod + def get_reward(cls) -> int: + """Block reward is removed.""" + return 0 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/__init__.py new file mode 100644 index 00000000000..1f35d8755ce --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Prague fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/contracts/consolidation_request.bin b/packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/consolidation_request.bin similarity index 100% rename from packages/testing/src/execution_testing/forks/forks/contracts/consolidation_request.bin rename to packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/consolidation_request.bin diff --git a/packages/testing/src/execution_testing/forks/forks/contracts/deposit_contract.bin b/packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/deposit_contract.bin similarity index 100% rename from packages/testing/src/execution_testing/forks/forks/contracts/deposit_contract.bin rename to packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/deposit_contract.bin diff --git a/packages/testing/src/execution_testing/forks/forks/contracts/history_contract.bin b/packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/history_contract.bin similarity index 100% rename from packages/testing/src/execution_testing/forks/forks/contracts/history_contract.bin rename to packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/history_contract.bin diff --git a/packages/testing/src/execution_testing/forks/forks/contracts/withdrawal_request.bin b/packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/withdrawal_request.bin similarity index 100% rename from packages/testing/src/execution_testing/forks/forks/contracts/withdrawal_request.bin rename to packages/testing/src/execution_testing/forks/forks/eips/prague/contracts/withdrawal_request.bin diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py new file mode 100644 index 00000000000..92702858ed6 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py @@ -0,0 +1,58 @@ +""" +EIP-2537: Precompile for BLS12-381 curve operations. + +Adds operations on BLS12-381 curve as precompiles in a set necessary to +efficiently perform operations such as BLS signature verification. + +https://eips.ethereum.org/EIPS/eip-2537 +""" + +from dataclasses import replace +from typing import List + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP2537(BaseFork): + """EIP-2537 class.""" + + @classmethod + def precompiles(cls) -> List[Address]: + """ + Add precompiles for BLS12-381 curve operations. + + BLS12_G1ADD = 0x0B + BLS12_G1MSM = 0x0C + BLS12_G2ADD = 0x0D + BLS12_G2MSM = 0x0E + BLS12_PAIRING_CHECK = 0x0F + BLS12_MAP_FP_TO_G1 = 0x10 + BLS12_MAP_FP2_TO_G2 = 0x11 + """ + return [ + Address(11, label="BLS12_G1ADD"), + Address(12, label="BLS12_G1MSM"), + Address(13, label="BLS12_G2ADD"), + Address(14, label="BLS12_G2MSM"), + Address(15, label="BLS12_PAIRING_CHECK"), + Address(16, label="BLS12_MAP_FP_TO_G1"), + Address(17, label="BLS12_MAP_FP2_TO_G2"), + ] + super(EIP2537, cls).precompiles() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Add gas costs for BLS12-381 precompiles.""" + return replace( + super(EIP2537, cls).gas_costs(), + GAS_PRECOMPILE_BLS_G1ADD=375, + GAS_PRECOMPILE_BLS_G1MUL=12_000, + GAS_PRECOMPILE_BLS_G1MAP=5_500, + GAS_PRECOMPILE_BLS_G2ADD=600, + GAS_PRECOMPILE_BLS_G2MUL=22_500, + GAS_PRECOMPILE_BLS_G2MAP=23_800, + GAS_PRECOMPILE_BLS_PAIRING_BASE=37_700, + GAS_PRECOMPILE_BLS_PAIRING_PER_PAIR=32_600, + ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2935.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2935.py new file mode 100644 index 00000000000..e9c996fff24 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2935.py @@ -0,0 +1,46 @@ +""" +EIP-2935: Serve historical block hashes from state. + +Store and serve last 8191 block hashes as storage slots of a system contract +to allow for stateless execution. + +https://eips.ethereum.org/EIPS/eip-2935 +""" + +from os.path import realpath +from pathlib import Path +from typing import List, Mapping + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + +BYTECODE_FILE = ( + Path(realpath(__file__)).parent / "contracts" / "history_contract.bin" +) +HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53A10CB7A02335B175320002935 +HISTORY_STORAGE_BYTECODE = BYTECODE_FILE.read_bytes() + + +class EIP2935(BaseFork): + """EIP-2935 class.""" + + @classmethod + def system_contracts(cls) -> List[Address]: + """Add the history storage contract.""" + return [ + Address( + HISTORY_STORAGE_ADDRESS, + label="HISTORY_STORAGE_ADDRESS", + ), + ] + super(EIP2935, cls).system_contracts() + + @classmethod + def pre_allocation_blockchain(cls) -> Mapping: + """Pre-allocate the history storage contract.""" + return { + HISTORY_STORAGE_ADDRESS: { + "nonce": 1, + "code": HISTORY_STORAGE_BYTECODE, + } + } | super(EIP2935, cls).pre_allocation_blockchain() # type: ignore diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_6110.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_6110.py new file mode 100644 index 00000000000..e84a0ac2b1a --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_6110.py @@ -0,0 +1,58 @@ +""" +EIP-6110: Supply validator deposits on chain. + +Provides validator deposits as a list of deposit operations added to the +Execution Layer block. + +https://eips.ethereum.org/EIPS/eip-6110 +""" + +from hashlib import sha256 +from os.path import realpath +from pathlib import Path +from typing import List, Mapping + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + +BYTECODE_FILE = ( + Path(realpath(__file__)).parent / "contracts" / "deposit_contract.bin" +) +DEPOSIT_CONTRACT_ADDRESS = 0x00000000219AB540356CBB839CBE05303D7705FA +DEPOSIT_CONTRACT_BYTECODE = BYTECODE_FILE.read_bytes() + + +class EIP6110(BaseFork): + """EIP-6110 class.""" + + @classmethod + def system_contracts(cls) -> List[Address]: + """Add the beacon chain deposit contract.""" + return [ + Address( + DEPOSIT_CONTRACT_ADDRESS, + label="DEPOSIT_CONTRACT_ADDRESS", + ), + ] + super(EIP6110, cls).system_contracts() + + @classmethod + def pre_allocation_blockchain(cls) -> Mapping: + """Pre-allocate the beacon chain deposit contract.""" + deposit_contract_tree_depth = 32 + storage = {} + next_hash = sha256(b"\x00" * 64).digest() + for i in range( + deposit_contract_tree_depth + 2, + deposit_contract_tree_depth * 2 + 1, + ): + storage[i] = next_hash + next_hash = sha256(next_hash + next_hash).digest() + + return { + DEPOSIT_CONTRACT_ADDRESS: { + "nonce": 1, + "code": DEPOSIT_CONTRACT_BYTECODE, + "storage": storage, + } + } | super(EIP6110, cls).pre_allocation_blockchain() # type: ignore diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7002.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7002.py new file mode 100644 index 00000000000..92118d50a1b --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7002.py @@ -0,0 +1,48 @@ +""" +EIP-7002: Execution layer triggerable withdrawals. + +Allow validators to trigger exits and partial withdrawals via their execution +layer (0x01) withdrawal credentials. + +https://eips.ethereum.org/EIPS/eip-7002 +""" + +from os.path import realpath +from pathlib import Path +from typing import List, Mapping + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + +BYTECODE_FILE = ( + Path(realpath(__file__)).parent / "contracts" / "withdrawal_request.bin" +) +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = ( + 0x00000961EF480EB55E80D19AD83579A64C007002 +) +WITHDRAWAL_REQUEST_PREDEPLOY_BYTECODE = BYTECODE_FILE.read_bytes() + + +class EIP7002(BaseFork): + """EIP-7002 class.""" + + @classmethod + def system_contracts(cls) -> List[Address]: + """Add the withdrawal request predeploy contract.""" + return [ + Address( + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + label="WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS", + ), + ] + super(EIP7002, cls).system_contracts() + + @classmethod + def pre_allocation_blockchain(cls) -> Mapping: + """Pre-allocate the withdrawal request contract.""" + return { + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS: { + "nonce": 1, + "code": WITHDRAWAL_REQUEST_PREDEPLOY_BYTECODE, + }, + } | super(EIP7002, cls).pre_allocation_blockchain() # type: ignore diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7251.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7251.py new file mode 100644 index 00000000000..56605c5073c --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7251.py @@ -0,0 +1,47 @@ +""" +EIP-7251: Increase the MAX_EFFECTIVE_BALANCE. + +Allow validators to consolidate via execution layer requests. + +https://eips.ethereum.org/EIPS/eip-7251 +""" + +from os.path import realpath +from pathlib import Path +from typing import List, Mapping + +from execution_testing.base_types import Address + +from ....base_fork import BaseFork + +BYTECODE_FILE = ( + Path(realpath(__file__)).parent / "contracts" / "consolidation_request.bin" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = ( + 0x0000BBDDC7CE488642FB579F8B00F3A590007251 +) +CONSOLIDATION_REQUEST_PREDEPLOY_BYTECODE = BYTECODE_FILE.read_bytes() + + +class EIP7251(BaseFork): + """EIP-7251 class.""" + + @classmethod + def system_contracts(cls) -> List[Address]: + """Add the consolidation request predeploy contract.""" + return [ + Address( + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + label="CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS", + ), + ] + super(EIP7251, cls).system_contracts() + + @classmethod + def pre_allocation_blockchain(cls) -> Mapping: + """Pre-allocate the consolidation request contract.""" + return { + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS: { + "nonce": 1, + "code": CONSOLIDATION_REQUEST_PREDEPLOY_BYTECODE, + }, + } | super(EIP7251, cls).pre_allocation_blockchain() # type: ignore diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py new file mode 100644 index 00000000000..894ac85b433 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py @@ -0,0 +1,110 @@ +""" +EIP-7623: Increase calldata cost. + +Increase calldata cost to reduce maximum block size. + +https://eips.ethereum.org/EIPS/eip-7623 +""" + +from dataclasses import replace +from typing import List, Sized + +from execution_testing.base_types import AccessList, Bytes +from execution_testing.base_types.conversions import BytesConvertible + +from ....base_fork import ( + BaseFork, + CalldataGasCalculator, + TransactionDataFloorCostCalculator, + TransactionIntrinsicCostCalculator, +) +from ....gas_costs import GasCosts + + +class EIP7623(BaseFork): + """EIP-7623 class.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """Add standard and floor token costs for calldata.""" + return replace( + super(EIP7623, cls).gas_costs(), + GAS_TX_DATA_TOKEN_STANDARD=4, + GAS_TX_DATA_TOKEN_FLOOR=10, + ) + + @classmethod + def calldata_gas_calculator(cls) -> CalldataGasCalculator: + """ + Return a callable that calculates the transaction gas cost for its + calldata depending on its contents. + """ + gas_costs = cls.gas_costs() + + def fn(*, data: BytesConvertible, floor: bool = False) -> int: + raw = Bytes(data) + num_zeros = raw.count(0) + num_non_zeros = len(raw) - num_zeros + tokens = num_zeros + num_non_zeros * 4 + if floor: + return tokens * gas_costs.GAS_TX_DATA_TOKEN_FLOOR + return tokens * gas_costs.GAS_TX_DATA_TOKEN_STANDARD + + return fn + + @classmethod + def transaction_data_floor_cost_calculator( + cls, + ) -> TransactionDataFloorCostCalculator: + """ + Transaction data floor cost is introduced. + """ + calldata_gas_calculator = cls.calldata_gas_calculator() + gas_costs = cls.gas_costs() + + def fn(*, data: BytesConvertible) -> int: + return ( + calldata_gas_calculator(data=data, floor=True) + + gas_costs.GAS_TX_BASE + ) + + return fn + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + Transaction intrinsic cost wraps the parent calculator with a floor + cost. + """ + super_fn = super(EIP7623, cls).transaction_intrinsic_cost_calculator() + transaction_data_floor_cost_calculator = ( + cls.transaction_data_floor_cost_calculator() + ) + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + authorization_list_or_count=authorization_list_or_count, + return_cost_deducted_prior_execution=False, + ) + + if return_cost_deducted_prior_execution: + return intrinsic_cost + + transaction_floor_data_cost = ( + transaction_data_floor_cost_calculator(data=calldata) + ) + return max(intrinsic_cost, transaction_floor_data_cost) + + return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7685.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7685.py new file mode 100644 index 00000000000..97c3a4e23ea --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7685.py @@ -0,0 +1,39 @@ +""" +EIP-7685: General purpose execution layer requests. + +A general purpose bus for sharing EL triggered requests with the CL. + +https://eips.ethereum.org/EIPS/eip-7685 +""" + +from ....base_fork import BaseFork + + +class EIP7685( + BaseFork, + engine_new_payload_version_bump=True, + engine_get_payload_version_bump=True, +): + """EIP-7685 class.""" + + @classmethod + def max_request_type(cls) -> int: + """ + Three request types are introduced: deposits, withdrawal requests, + and consolidation requests. + """ + return 2 + + @classmethod + def header_requests_required(cls) -> bool: + """ + Header must contain the beacon chain requests hash. + """ + return True + + @classmethod + def engine_new_payload_requests(cls) -> bool: + """ + New payloads include the requests hash as a parameter. + """ + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7691.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7691.py new file mode 100644 index 00000000000..babb9eb325e --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7691.py @@ -0,0 +1,23 @@ +""" +EIP-7691: Blob throughput increase. + +Increase the number of blobs to reach a new target and max of 6 and 9 +blobs per block respectively. + +https://eips.ethereum.org/EIPS/eip-7691 +""" + +from ....base_fork import BaseFork + + +class EIP7691( + BaseFork, + update_blob_constants={ + "MAX_BLOBS_PER_BLOCK": 9, + "TARGET_BLOBS_PER_BLOCK": 6, + "BLOB_BASE_FEE_UPDATE_FRACTION": 5007716, + }, +): + """EIP-7691 class.""" + + pass diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py new file mode 100644 index 00000000000..1904b135f6a --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py @@ -0,0 +1,97 @@ +""" +EIP-7702: Set EOA account code. + +Add a new tx type that permanently sets the code for an EOA. + +https://eips.ethereum.org/EIPS/eip-7702 +""" + +from dataclasses import replace +from typing import List, Sized + +from execution_testing.base_types import AccessList +from execution_testing.base_types.conversions import BytesConvertible +from execution_testing.vm import OpcodeBase + +from ....base_fork import ( + BaseFork, + TransactionIntrinsicCostCalculator, +) +from ....gas_costs import GasCosts + + +class EIP7702(BaseFork): + """EIP-7702 class.""" + + @classmethod + def tx_types(cls) -> List[int]: + """Set-code type transactions are introduced.""" + return [4] + super(EIP7702, cls).tx_types() + + @classmethod + def gas_costs(cls) -> GasCosts: + """Add gas costs for authorization operations.""" + return replace( + super(EIP7702, cls).gas_costs(), + GAS_AUTH_PER_EMPTY_ACCOUNT=25_000, + REFUND_AUTH_PER_EXISTING_ACCOUNT=12_500, + ) + + @classmethod + def _calculate_call_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Call gas cost needs to take the authorization into account. + """ + metadata = opcode.metadata + + base_cost = super(EIP7702, cls)._calculate_call_gas(opcode, gas_costs) + + if metadata["delegated_address"] or metadata["delegated_address_warm"]: + if metadata["delegated_address_warm"]: + base_cost += gas_costs.GAS_WARM_ACCESS + else: + base_cost += gas_costs.GAS_COLD_ACCOUNT_ACCESS + + return base_cost + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + Transaction intrinsic cost includes authorization list cost. + """ + super_fn = super(EIP7702, cls).transaction_intrinsic_cost_calculator() + gas_costs = cls.gas_costs() + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + return_cost_deducted_prior_execution=( + return_cost_deducted_prior_execution + ), + ) + if authorization_list_or_count is not None: + if isinstance(authorization_list_or_count, Sized): + authorization_list_or_count = len( + authorization_list_or_count + ) + intrinsic_cost += ( + authorization_list_or_count + * gas_costs.GAS_AUTH_PER_EMPTY_ACCOUNT + ) + + return intrinsic_cost + + return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/__init__.py new file mode 100644 index 00000000000..65e3b09f6c8 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Shanghai fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3855.py b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3855.py new file mode 100644 index 00000000000..cf734cb3f3d --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3855.py @@ -0,0 +1,35 @@ +""" +EIP-3855: PUSH0 instruction. + +Introduce a new instruction which pushes the constant value 0 onto the +stack. + +https://eips.ethereum.org/EIPS/eip-3855 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP3855(BaseFork): + """EIP-3855 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add PUSH0 opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP3855, cls).opcode_gas_map() + return { + **base_map, + Opcodes.PUSH0: gas_costs.GAS_BASE, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add PUSH0 to valid opcodes.""" + return [Opcodes.PUSH0] + super(EIP3855, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py new file mode 100644 index 00000000000..c5e52708bce --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py @@ -0,0 +1,60 @@ +""" +EIP-3860: Limit and meter initcode. + +Limit the maximum size of initcode to 49152 and apply extra gas cost of 2 for +every 32-byte chunk of initcode. + +https://eips.ethereum.org/EIPS/eip-3860 +""" + +from execution_testing.vm import OpcodeBase + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP3860(BaseFork): + """EIP-3860 class.""" + + @classmethod + def max_initcode_size(cls) -> int: + """Initcode size is limited.""" + return 0xC000 + + @classmethod + def _calculate_create_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate CREATE gas cost including initcode cost. + """ + metadata = opcode.metadata + + base_cost = super(EIP3860, cls)._calculate_create_gas( + opcode, gas_costs + ) + + init_code_size = metadata["init_code_size"] + init_code_words = (init_code_size + 31) // 32 + init_code_gas = gas_costs.GAS_CODE_INIT_PER_WORD * init_code_words + + return base_cost + init_code_gas + + @classmethod + def _calculate_create2_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate CREATE2 gas cost including initcode cost. + """ + metadata = opcode.metadata + + base_cost = super(EIP3860, cls)._calculate_create2_gas( + opcode, gas_costs + ) + + init_code_size = metadata["init_code_size"] + init_code_words = (init_code_size + 31) // 32 + init_code_gas = gas_costs.GAS_CODE_INIT_PER_WORD * init_code_words + + return base_cost + init_code_gas diff --git a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_4895.py b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_4895.py new file mode 100644 index 00000000000..9577621e400 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_4895.py @@ -0,0 +1,24 @@ +""" +EIP-4895: Beacon chain push withdrawals as operations. + +Support validator withdrawals from the beacon chain to the EVM via a new +"system-level" operation type. + +https://eips.ethereum.org/EIPS/eip-4895 +""" + +from ....base_fork import BaseFork + + +class EIP4895( + BaseFork, + engine_new_payload_version_bump=True, + engine_forkchoice_updated_version_bump=True, + engine_get_payload_version_bump=True, +): + """EIP-4895 class.""" + + @classmethod + def header_withdrawals_required(cls) -> bool: + """Withdrawals are required.""" + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/__init__.py b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/__init__.py new file mode 100644 index 00000000000..79d5de50890 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/__init__.py @@ -0,0 +1 @@ +"""Listings of all EIPs for Spurious Dragon fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_155.py b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_155.py new file mode 100644 index 00000000000..e754e7a2fa8 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_155.py @@ -0,0 +1,18 @@ +""" +EIP-155: Simple replay attack protection. + +https://eips.ethereum.org/EIPS/eip-155 +""" + +from ....base_fork import BaseFork + + +class EIP155(BaseFork): + """EIP-155 class.""" + + @classmethod + def supports_protected_txs(cls) -> bool: + """ + Enables support for protected transactions. + """ + return True diff --git a/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_161.py b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_161.py new file mode 100644 index 00000000000..6a5b61d919c --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_161.py @@ -0,0 +1,35 @@ +""" +EIP-161: State trie clearing (invariant-preserving alternative). + +https://eips.ethereum.org/EIPS/eip-161 +""" + +from execution_testing.vm import OpcodeBase + +from ....base_fork import BaseFork, GasCosts + + +class EIP161(BaseFork): + """EIP-161 class.""" + + @classmethod + def _calculate_call_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + The call gas cost needs to take the value transfer + and account new into account. + """ + base_cost = super(EIP161, cls)._calculate_call_gas(opcode, gas_costs) + + # Additional costs for value transfer, does not apply to STATICCALL + metadata = opcode.metadata + if "value_transfer" in metadata: + if metadata["value_transfer"]: + base_cost += gas_costs.GAS_CALL_VALUE + if metadata["account_new"]: + base_cost += gas_costs.GAS_NEW_ACCOUNT + elif metadata["account_new"]: + raise ValueError("Account new requires value transfer") + + return base_cost diff --git a/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_170.py b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_170.py new file mode 100644 index 00000000000..0a0f9ff24ae --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/spurious_dragon/eip_170.py @@ -0,0 +1,16 @@ +""" +EIP-170: Contract code size limit. + +https://eips.ethereum.org/EIPS/eip-170 +""" + +from ....base_fork import BaseFork + + +class EIP170(BaseFork): + """EIP-170 class.""" + + @classmethod + def max_code_size(cls) -> int: + """Upper bound is introduced for max contract code size.""" + return 0x6000 diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index b74c52d071a..637e104b9b3 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -2,20 +2,7 @@ from __future__ import annotations -from dataclasses import replace -from hashlib import sha256 -from os.path import realpath -from pathlib import Path -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - List, - Literal, - Mapping, - Optional, - Sized, -) +from typing import TYPE_CHECKING, Callable, Dict, List, Mapping, Sized if TYPE_CHECKING: from execution_testing.fixtures.blockchain import FixtureHeader @@ -25,7 +12,6 @@ Address, BlobSchedule, Bytes, - ForkBlobSchedule, ZeroPaddedHexNumber, ) from execution_testing.base_types.conversions import BytesConvertible @@ -47,14 +33,15 @@ TransactionIntrinsicCostCalculator, ) from ..gas_costs import GasCosts -from .helpers import ceiling_division, fake_exponential - -CURRENT_FILE = Path(realpath(__file__)) -CURRENT_FOLDER = CURRENT_FILE.parent +from . import eips +from .helpers import ceiling_division # All forks must be listed here !!! in the order they were introduced !!! -class Frontier(BaseFork, solc_name="homestead"): +class Frontier( + BaseFork, + solc_name="homestead", +): """Frontier fork.""" @classmethod @@ -651,6 +638,17 @@ def _calculate_create_gas( del opcode return gas_costs.GAS_CREATE + @classmethod + def _calculate_create2_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate CREATE2 gas cost including initcode cost. + """ + raise NotImplementedError( + f"CREATE2 opcode is not supported in {cls.name()}" + ) + @classmethod def _calculate_return_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts @@ -827,44 +825,11 @@ def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: f"Excess blob gas calculator is not supported in {cls.name()}" ) - @classmethod - def min_base_fee_per_blob_gas(cls) -> int: - """Return the amount of blob gas used per blob at a given fork.""" - raise NotImplementedError( - f"Base fee per blob gas is not supported in {cls.name()}" - ) - - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction at a given fork.""" - raise NotImplementedError( - f"Blob base fee update fraction is not supported in {cls.name()}" - ) - - @classmethod - def blob_gas_per_blob(cls) -> int: - """Return the amount of blob gas used per blob at a given fork.""" - return 0 - @classmethod def supports_blobs(cls) -> bool: """Blobs are not supported at Frontier.""" return False - @classmethod - def target_blobs_per_block(cls) -> int: - """Return the target number of blobs per block at a given fork.""" - raise NotImplementedError( - f"Target blobs per block is not supported in {cls.name()}" - ) - - @classmethod - def max_blobs_per_block(cls) -> int: - """Return the max number of blobs per block at a given fork.""" - raise NotImplementedError( - f"Max blobs per block is not supported in {cls.name()}" - ) - @classmethod def blob_reserve_price_active(cls) -> bool: """ @@ -875,13 +840,6 @@ def blob_reserve_price_active(cls) -> bool: f"Blob reserve price is not supported in {cls.name()}" ) - @classmethod - def blob_base_cost(cls) -> int: - """Return the base cost of a blob at a given fork.""" - raise NotImplementedError( - f"Blob base cost is not supported in {cls.name()}" - ) - @classmethod def full_blob_tx_wrapper_version(cls) -> int | None: """Return the version of the full blob transaction wrapper.""" @@ -890,13 +848,6 @@ def full_blob_tx_wrapper_version(cls) -> int | None: f"{cls.name()}" ) - @classmethod - def max_blobs_per_tx(cls) -> int: - """Return the max number of blobs per tx at a given fork.""" - raise NotImplementedError( - f"Max blobs per tx is not supported in {cls.name()}" - ) - @classmethod def blob_schedule(cls) -> BlobSchedule | None: """At genesis, no blob schedule is used.""" @@ -917,11 +868,6 @@ def empty_block_bal_item_count(cls) -> int: """Pre-Amsterdam forks have no block access list.""" return 0 - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """At genesis, payloads cannot be sent through the engine API.""" - return None - @classmethod def header_beacon_root_required(cls) -> bool: """At genesis, header must not contain parent beacon block root.""" @@ -967,23 +913,6 @@ def engine_payload_attribute_max_blobs_per_block(cls) -> bool: """ return False - @classmethod - def engine_forkchoice_updated_version(cls) -> Optional[int]: - """ - At genesis, forkchoice updates cannot be sent through the engine API. - """ - return cls.engine_new_payload_version() - - @classmethod - def engine_get_payload_version(cls) -> Optional[int]: - """At genesis, payloads cannot be retrieved through the engine API.""" - return cls.engine_new_payload_version() - - @classmethod - def engine_get_blobs_version(cls) -> Optional[int]: - """At genesis, blobs cannot be retrieved through the engine API.""" - return None - @classmethod def get_reward(cls) -> int: """ @@ -1019,8 +948,16 @@ def block_rlp_size_limit(cls) -> int | None: @classmethod def precompiles(cls) -> List[Address]: - """At Genesis, no precompiles are present.""" - return [] + """ + At Genesis, EC-recover, SHA256, RIPEMD160, and Identity precompiles + are introduced. + """ + return [ + Address(1, label="ECREC"), + Address(2, label="SHA256"), + Address(3, label="RIPEMD160"), + Address(4, label="ID"), + ] @classmethod def system_contracts(cls) -> List[Address]: @@ -1278,279 +1215,74 @@ def build_default_block_header( return FixtureHeader(**defaults) -class Homestead(Frontier): +class Homestead( + eips.EIP7, + eips.EIP2, + Frontier, +): """Homestead fork.""" - @classmethod - def precompiles(cls) -> List[Address]: - """ - At Homestead, EC-recover, SHA256, RIPEMD160, and Identity precompiles - are introduced. - """ - return [ - Address(1, label="ECREC"), - Address(2, label="SHA256"), - Address(3, label="RIPEMD160"), - Address(4, label="ID"), - ] + super(Homestead, cls).precompiles() - - @classmethod - def call_opcodes(cls) -> List[Opcodes]: - """At Homestead, DELEGATECALL opcode was introduced.""" - return [Opcodes.DELEGATECALL] + super(Homestead, cls).call_opcodes() - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add DELEGATECALL opcode gas cost for Homestead.""" - gas_costs = cls.gas_costs() - memory_expansion_calculator = cls.memory_expansion_gas_calculator() - base_map = super(Homestead, cls).opcode_gas_map() - return { - **base_map, - Opcodes.DELEGATECALL: cls._with_memory_expansion( - lambda op: cls._calculate_call_gas(op, gas_costs), - memory_expansion_calculator, - ), - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return the list of Opcodes that are valid to work on this fork.""" - return [Opcodes.DELEGATECALL] + super(Homestead, cls).valid_opcodes() - - @classmethod - def transaction_intrinsic_cost_calculator( - cls, - ) -> TransactionIntrinsicCostCalculator: - """ - At Homestead, the transaction intrinsic cost needs to take contract - creation into account. - """ - super_fn = super( - Homestead, cls - ).transaction_intrinsic_cost_calculator() - gas_costs = cls.gas_costs() - - def fn( - *, - calldata: BytesConvertible = b"", - contract_creation: bool = False, - access_list: List[AccessList] | None = None, - authorization_list_or_count: Sized | int | None = None, - return_cost_deducted_prior_execution: bool = False, - ) -> int: - del return_cost_deducted_prior_execution - - intrinsic_cost: int = super_fn( - calldata=calldata, - contract_creation=contract_creation, - access_list=access_list, - authorization_list_or_count=authorization_list_or_count, - ) - if contract_creation: - intrinsic_cost += gas_costs.GAS_TX_CREATE - return intrinsic_cost - - return fn + pass -class DAOFork(Homestead, ignore=True, ruleset_name=""): +class DAOFork( + Homestead, + ignore=True, + ruleset_name="", +): """DAO fork.""" pass -class TangerineWhistle(DAOFork, ignore=True, ruleset_name="TANGERINE"): +class TangerineWhistle( + DAOFork, + ignore=True, + ruleset_name="TANGERINE", +): """TangerineWhistle fork (EIP-150).""" pass -class SpuriousDragon(TangerineWhistle, ignore=True, ruleset_name="SPURIOUS"): +class SpuriousDragon( + eips.EIP170, + eips.EIP161, + eips.EIP155, + TangerineWhistle, + ignore=True, + ruleset_name="SPURIOUS", +): """SpuriousDragon fork.""" - @classmethod - def _calculate_call_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - At Spurious Dragon, the call gas cost needs to take the value transfer - and account new into account. - """ - base_cost = super(SpuriousDragon, cls)._calculate_call_gas( - opcode, gas_costs - ) - - # Additional costs for value transfer, does not apply to STATICCALL - metadata = opcode.metadata - if "value_transfer" in metadata: - if metadata["value_transfer"]: - base_cost += gas_costs.GAS_CALL_VALUE - if metadata["account_new"]: - base_cost += gas_costs.GAS_NEW_ACCOUNT - elif metadata["account_new"]: - raise ValueError("Account new requires value transfer") - - return base_cost - - @classmethod - def supports_protected_txs(cls) -> bool: - """ - At Genesis, supports EIP-155 protected transactions. - """ - return True + pass -class Byzantium(SpuriousDragon): +class Byzantium( + eips.EIP649, + eips.EIP214, + eips.EIP211, + eips.EIP140, + eips.EIP198, + eips.EIP196, + eips.EIP197, + SpuriousDragon, +): """Byzantium fork.""" - @classmethod - def get_reward(cls) -> int: - """ - At Byzantium, the block reward is reduced to 3_000_000_000_000_000_000 - wei. - """ - return 3_000_000_000_000_000_000 - - @classmethod - def precompiles(cls) -> List[Address]: - """ - At Byzantium, precompiles for bigint modular exponentiation, addition - and scalar multiplication on elliptic curve alt_bn128, and optimal ate - pairing check on elliptic curve alt_bn128 are introduced. - """ - return [ - Address(5, label="MODEXP"), - Address(6, label="BN254_ADD"), - Address(7, label="BN254_MUL"), - Address(8, label="BN254_PAIRING"), - ] + super(Byzantium, cls).precompiles() - - @classmethod - def max_code_size(cls) -> int: - # NOTE: Move this to Spurious Dragon once this fork is introduced. See - # EIP-170. - """ - At Spurious Dragon, an upper bound was introduced for max contract code - size. - """ - return 0x6000 - - @classmethod - def call_opcodes(cls) -> List[Opcodes]: - """At Byzantium, STATICCALL opcode was introduced.""" - return [Opcodes.STATICCALL] + super(Byzantium, cls).call_opcodes() - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add Byzantium opcodes gas costs.""" - gas_costs = cls.gas_costs() - memory_expansion_calculator = cls.memory_expansion_gas_calculator() - base_map = super(Byzantium, cls).opcode_gas_map() - return { - **base_map, - Opcodes.RETURNDATASIZE: gas_costs.GAS_BASE, - Opcodes.RETURNDATACOPY: cls._with_memory_expansion( - cls._with_data_copy(gas_costs.GAS_VERY_LOW, gas_costs), - memory_expansion_calculator, - ), - Opcodes.STATICCALL: cls._with_memory_expansion( - lambda op: cls._calculate_call_gas(op, gas_costs), - memory_expansion_calculator, - ), - Opcodes.REVERT: cls._with_memory_expansion( - 0, memory_expansion_calculator - ), - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [ - Opcodes.REVERT, - Opcodes.RETURNDATASIZE, - Opcodes.RETURNDATACOPY, - Opcodes.STATICCALL, - ] + super(Byzantium, cls).valid_opcodes() - - @classmethod - def gas_costs(cls) -> GasCosts: - """ - On Byzantium, precompiled contract gas costs are introduced. - """ - return replace( - super(Byzantium, cls).gas_costs(), - GAS_PRECOMPILE_ECADD=500, - GAS_PRECOMPILE_ECMUL=40_000, - GAS_PRECOMPILE_ECPAIRING_BASE=100_000, - GAS_PRECOMPILE_ECPAIRING_PER_POINT=80_000, - ) + pass -class Constantinople(Byzantium): +class Constantinople( + eips.EIP1234, + eips.EIP1052, + eips.EIP1014, + eips.EIP145, + Byzantium, +): """Constantinople fork.""" - @classmethod - def get_reward(cls) -> int: - """ - At Constantinople, the block reward is reduced to - 2_000_000_000_000_000_000 wei. - """ - return 2_000_000_000_000_000_000 - - @classmethod - def _calculate_create2_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """Calculate CREATE2 gas cost based on metadata.""" - metadata = opcode.metadata - - # Keccak256 hashing cost - init_code_size = metadata["init_code_size"] - init_code_words = (init_code_size + 31) // 32 - hash_gas = gas_costs.GAS_KECCAK256_PER_WORD * init_code_words - - return gas_costs.GAS_CREATE + hash_gas - - @classmethod - def create_opcodes(cls) -> List[Opcodes]: - """At Constantinople, `CREATE2` opcode is added.""" - return [Opcodes.CREATE2] + super(Constantinople, cls).create_opcodes() - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add Constantinople opcodes gas costs.""" - gas_costs = cls.gas_costs() - memory_expansion_calculator = cls.memory_expansion_gas_calculator() - base_map = super(Constantinople, cls).opcode_gas_map() - return { - **base_map, - Opcodes.SHL: gas_costs.GAS_VERY_LOW, - Opcodes.SHR: gas_costs.GAS_VERY_LOW, - Opcodes.SAR: gas_costs.GAS_VERY_LOW, - Opcodes.EXTCODEHASH: cls._with_account_access(0, gas_costs), - Opcodes.CREATE2: cls._with_memory_expansion( - lambda op: cls._calculate_create2_gas(op, gas_costs), - memory_expansion_calculator, - ), - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [ - Opcodes.SHL, - Opcodes.SHR, - Opcodes.SAR, - Opcodes.EXTCODEHASH, - Opcodes.CREATE2, - ] + super(Constantinople, cls).valid_opcodes() + pass class ConstantinopleFix( @@ -1563,1269 +1295,206 @@ class ConstantinopleFix( pass -class Istanbul(ConstantinopleFix): +class Istanbul( + eips.EIP2028, + eips.EIP1884, + eips.EIP1344, + eips.EIP1108, + eips.EIP152, + ConstantinopleFix, +): """Istanbul fork.""" - @classmethod - def precompiles(cls) -> List[Address]: - """At Istanbul, a precompile for blake2 compression is introduced.""" - return [ - Address(9, label="BLAKE2F"), - ] + super(Istanbul, cls).precompiles() - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add Istanbul opcodes gas costs.""" - gas_costs = cls.gas_costs() - base_map = super(Istanbul, cls).opcode_gas_map() - return { - **base_map, - Opcodes.CHAINID: gas_costs.GAS_BASE, - Opcodes.SELFBALANCE: gas_costs.GAS_LOW, - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [Opcodes.CHAINID, Opcodes.SELFBALANCE] + super( - Istanbul, cls - ).valid_opcodes() - - @classmethod - def gas_costs(cls) -> GasCosts: - """ - On Istanbul, the non-zero transaction data byte cost is reduced to 16 - due to EIP-2028. - """ - return replace( - super(Istanbul, cls).gas_costs(), - GAS_TX_DATA_PER_NON_ZERO=16, # https://eips.ethereum.org/EIPS/eip-2028 - # https://eips.ethereum.org/EIPS/eip-1108 - GAS_PRECOMPILE_ECADD=150, - GAS_PRECOMPILE_ECMUL=6000, - GAS_PRECOMPILE_ECPAIRING_BASE=45_000, - GAS_PRECOMPILE_ECPAIRING_PER_POINT=34_000, - GAS_PRECOMPILE_BLAKE2F_PER_ROUND=1, - ) + pass # Glacier forks skipped, unless explicitly specified -class MuirGlacier(Istanbul, solc_name="istanbul", ignore=True): +class MuirGlacier( + Istanbul, + solc_name="istanbul", + ignore=True, +): """Muir Glacier fork.""" pass -class Berlin(Istanbul): +class Berlin( + eips.EIP2930, + Istanbul, +): """Berlin fork.""" - @classmethod - def tx_types(cls) -> List[int]: - """At Berlin, access list transactions are introduced.""" - return [1] + super(Berlin, cls).tx_types() + pass - @classmethod - def contract_creating_tx_types(cls) -> List[int]: - """At Berlin, access list transactions are introduced.""" - return [1] + super(Berlin, cls).contract_creating_tx_types() - @classmethod - def transaction_intrinsic_cost_calculator( - cls, - ) -> TransactionIntrinsicCostCalculator: - """ - At Berlin, the transaction intrinsic cost needs to take the access list - into account. - """ - super_fn = super(Berlin, cls).transaction_intrinsic_cost_calculator() - gas_costs = cls.gas_costs() +class London( + eips.EIP3529, + eips.EIP3198, + eips.EIP1559, + Berlin, +): + """London fork.""" - def fn( - *, - calldata: BytesConvertible = b"", - contract_creation: bool = False, - access_list: List[AccessList] | None = None, - authorization_list_or_count: Sized | int | None = None, - return_cost_deducted_prior_execution: bool = False, - ) -> int: - del return_cost_deducted_prior_execution - - intrinsic_cost: int = super_fn( - calldata=calldata, - contract_creation=contract_creation, - authorization_list_or_count=authorization_list_or_count, - ) - if access_list is not None: - for access in access_list: - intrinsic_cost += gas_costs.GAS_TX_ACCESS_LIST_ADDRESS - for _ in access.storage_keys: - intrinsic_cost += ( - gas_costs.GAS_TX_ACCESS_LIST_STORAGE_KEY - ) - return intrinsic_cost - - return fn - - -class London(Berlin): - """London fork.""" - - @classmethod - def header_base_fee_required(cls) -> bool: - """Header must contain the Base Fee starting from London.""" - return True - - @classmethod - def tx_types(cls) -> List[int]: - """At London, dynamic fee transactions are introduced.""" - return [2] + super(London, cls).tx_types() - - @classmethod - def contract_creating_tx_types(cls) -> List[int]: - """At London, dynamic fee transactions are introduced.""" - return [2] + super(London, cls).contract_creating_tx_types() - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add London opcodes gas costs.""" - gas_costs = cls.gas_costs() - base_map = super(London, cls).opcode_gas_map() - return { - **base_map, - Opcodes.BASEFEE: gas_costs.GAS_BASE, - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [Opcodes.BASEFEE] + super(London, cls).valid_opcodes() - - @classmethod - def max_refund_quotient(cls) -> int: - """Return the max refund quotient at London.""" - return 5 - - @classmethod - def base_fee_max_change_denominator(cls) -> int: - """Return the base fee max change denominator at London.""" - return 8 - - @classmethod - def base_fee_elasticity_multiplier(cls) -> int: - """Return the base fee elasticity multiplier at London.""" - return 2 - - @classmethod - def base_fee_per_gas_calculator(cls) -> BaseFeePerGasCalculator: - """ - Return a callable that calculates the base fee per gas at London. - - EIP-1559 block validation pseudo code: - - if INITIAL_FORK_BLOCK_NUMBER == block.number: - expected_base_fee_per_gas = INITIAL_BASE_FEE - elif parent_gas_used == parent_gas_target: - expected_base_fee_per_gas = parent_base_fee_per_gas - elif parent_gas_used > parent_gas_target: - gas_used_delta = parent_gas_used - parent_gas_target - base_fee_per_gas_delta = max( parent_base_fee_per_gas - * gas_used_delta // parent_gas_target // - BASE_FEE_MAX_CHANGE_DENOMINATOR, 1, ) - expected_base_fee_per_gas = parent_base_fee_per_gas + - base_fee_per_gas_delta - else: - gas_used_delta = parent_gas_target - parent_gas_used - base_fee_per_gas_delta = ( - parent_base_fee_per_gas * gas_used_delta // - parent_gas_target // - BASE_FEE_MAX_CHANGE_DENOMINATOR - ) - expected_base_fee_per_gas = parent_base_fee_per_gas - - base_fee_per_gas_delta - """ - base_fee_max_change_denominator = cls.base_fee_max_change_denominator() - elasticity_multiplier = cls.base_fee_elasticity_multiplier() - - def fn( - *, - parent_base_fee_per_gas: int, - parent_gas_used: int, - parent_gas_limit: int, - ) -> int: - parent_gas_target = parent_gas_limit // elasticity_multiplier - if parent_gas_used == parent_gas_target: - return parent_base_fee_per_gas - elif parent_gas_used > parent_gas_target: - gas_used_delta = parent_gas_used - parent_gas_target - base_fee_per_gas_delta = max( - parent_base_fee_per_gas - * gas_used_delta - // parent_gas_target - // base_fee_max_change_denominator, - 1, - ) - return parent_base_fee_per_gas + base_fee_per_gas_delta - else: - gas_used_delta = parent_gas_target - parent_gas_used - base_fee_per_gas_delta = ( - parent_base_fee_per_gas - * gas_used_delta - // parent_gas_target - // base_fee_max_change_denominator - ) - return parent_base_fee_per_gas - base_fee_per_gas_delta - - return fn - - @classmethod - def base_fee_change_calculator(cls) -> BaseFeeChangeCalculator: - """ - Return a callable that calculates the gas that needs to be used to - change the base fee. - """ - base_fee_max_change_denominator = cls.base_fee_max_change_denominator() - elasticity_multiplier = cls.base_fee_elasticity_multiplier() - base_fee_per_gas_calculator = cls.base_fee_per_gas_calculator() - - def fn( - *, - parent_base_fee_per_gas: int, - parent_gas_limit: int, - required_base_fee_per_gas: int, - ) -> int: - parent_gas_target = parent_gas_limit // elasticity_multiplier - - if parent_base_fee_per_gas == required_base_fee_per_gas: - return parent_gas_target - elif required_base_fee_per_gas > parent_base_fee_per_gas: - # Base fee needs to go up, so we need to use more than target - base_fee_per_gas_delta = ( - required_base_fee_per_gas - parent_base_fee_per_gas - ) - parent_gas_used = ( - ( - base_fee_per_gas_delta - * base_fee_max_change_denominator - * parent_gas_target - ) - // parent_base_fee_per_gas - ) + parent_gas_target - elif required_base_fee_per_gas < parent_base_fee_per_gas: - # Base fee needs to go down, so we need to use less than target - base_fee_per_gas_delta = ( - parent_base_fee_per_gas - required_base_fee_per_gas - ) - - parent_gas_used = ( - parent_gas_target - - ( - ( - base_fee_per_gas_delta - * base_fee_max_change_denominator - * parent_gas_target - ) - // parent_base_fee_per_gas - ) - - 1 - ) - - assert ( - base_fee_per_gas_calculator( - parent_base_fee_per_gas=parent_base_fee_per_gas, - parent_gas_used=parent_gas_used, - parent_gas_limit=parent_gas_limit, - ) - == required_base_fee_per_gas - ) - - return parent_gas_used - - return fn - - -# Glacier forks skipped, unless explicitly specified -class ArrowGlacier(London, solc_name="london", ignore=True): - """Arrow Glacier fork.""" - - pass - - -class GrayGlacier(ArrowGlacier, solc_name="london", ignore=True): - """Gray Glacier fork.""" - - pass - - -class Paris( - London, - transition_tool_name="Merge", - ruleset_name="MERGE", -): - """Paris (Merge) fork.""" - - @classmethod - def header_prev_randao_required(cls) -> bool: - """Prev Randao is required starting from Paris.""" - return True - - @classmethod - def header_zero_difficulty_required(cls) -> bool: - """Zero difficulty is required starting from Paris.""" - return True - - @classmethod - def get_reward(cls) -> int: - """Paris updates the reward to 0.""" - return 0 - - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """From Paris, payloads can be sent through the engine API.""" - return 1 - - -class Shanghai(Paris, fork_by_timestamp=True): - """Shanghai fork.""" - - @classmethod - def header_withdrawals_required(cls) -> bool: - """Withdrawals are required starting from Shanghai.""" - return True - - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """From Shanghai, new payload calls must use version 2.""" - return 2 - - @classmethod - def max_initcode_size(cls) -> int: - """From Shanghai, the initcode size is now limited. See EIP-3860.""" - return 0xC000 - - @classmethod - def _calculate_create_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - Calculate CREATE gas cost based on metadata (from Shanghai, includes - initcode cost). - """ - metadata = opcode.metadata - - # Get base cost from parent fork - base_cost = super(Shanghai, cls)._calculate_create_gas( - opcode, gas_costs - ) - - # Add initcode cost (EIP-3860) - init_code_size = metadata["init_code_size"] - init_code_words = (init_code_size + 31) // 32 - init_code_gas = gas_costs.GAS_CODE_INIT_PER_WORD * init_code_words - - return base_cost + init_code_gas - - @classmethod - def _calculate_create2_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - Calculate CREATE2 gas cost based on metadata (from Shanghai, - includes initcode cost). - """ - metadata = opcode.metadata - - # Get base cost from parent fork (includes keccak hash cost) - base_cost = super(Shanghai, cls)._calculate_create2_gas( - opcode, gas_costs - ) - - # Add initcode cost (EIP-3860) - init_code_size = metadata["init_code_size"] - init_code_words = (init_code_size + 31) // 32 - init_code_gas = gas_costs.GAS_CODE_INIT_PER_WORD * init_code_words - - return base_cost + init_code_gas - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add Shanghai opcodes gas costs.""" - gas_costs = cls.gas_costs() - base_map = super(Shanghai, cls).opcode_gas_map() - return { - **base_map, - Opcodes.PUSH0: gas_costs.GAS_BASE, - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [Opcodes.PUSH0] + super(Shanghai, cls).valid_opcodes() - - -class Cancun(Shanghai): - """Cancun fork.""" - - BLOB_CONSTANTS = { # every value is an int or a Literal - "FIELD_ELEMENTS_PER_BLOB": 4096, - "BYTES_PER_FIELD_ELEMENT": 32, - "CELL_LENGTH": 2048, - # EIP-2537: Main subgroup order = q, due to this BLS_MODULUS - # every blob byte (uint256) must be smaller than 116 - "BLS_MODULUS": ( - 0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 - ), - # https://github.com/ethereum/consensus-specs/blob/ - # cc6996c22692d70e41b7a453d925172ee4b719ad/specs/deneb/ - # polynomial-commitments.md?plain=1#L78 - "BYTES_PER_PROOF": 48, - "BYTES_PER_COMMITMENT": 48, - "KZG_ENDIANNESS": "big", - "AMOUNT_CELL_PROOFS": 0, - } - - @classmethod - def get_blob_constant(cls, name: str) -> int | Literal["big"]: - """Return blob constant if it exists.""" - retrieved_constant = cls.BLOB_CONSTANTS.get(name) - assert retrieved_constant is not None, ( - f"You tried to retrieve the blob constant {name} but it does " - "not exist!" - ) - return retrieved_constant - - @classmethod - def header_excess_blob_gas_required(cls) -> bool: - """Excess blob gas is required starting from Cancun.""" - return True - - @classmethod - def header_blob_gas_used_required(cls) -> bool: - """Blob gas used is required starting from Cancun.""" - return True - - @classmethod - def header_beacon_root_required(cls) -> bool: - """Parent beacon block root is required starting from Cancun.""" - return True - - @classmethod - def blob_gas_price_calculator(cls) -> BlobGasPriceCalculator: - """Return a callable that calculates the blob gas price at Cancun.""" - min_base_fee_per_blob_gas = cls.min_base_fee_per_blob_gas() - blob_base_fee_update_fraction = cls.blob_base_fee_update_fraction() - - def fn(*, excess_blob_gas: int) -> int: - return fake_exponential( - min_base_fee_per_blob_gas, - excess_blob_gas, - blob_base_fee_update_fraction, - ) - - return fn - - @classmethod - def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: - """ - Return a callable that calculates the excess blob gas for a block at - Cancun. - """ - target_blobs_per_block = cls.target_blobs_per_block() - blob_gas_per_blob = cls.blob_gas_per_blob() - blob_target_gas_per_block = target_blobs_per_block * blob_gas_per_blob - - def fn( - *, - parent_excess_blob_gas: int | None = None, - parent_excess_blobs: int | None = None, - parent_blob_gas_used: int | None = None, - parent_blob_count: int | None = None, - # Required for Osaka as using this as base - parent_base_fee_per_gas: int, - ) -> int: - del parent_base_fee_per_gas - - if parent_excess_blob_gas is None: - assert parent_excess_blobs is not None, ( - "Parent excess blobs are required" - ) - parent_excess_blob_gas = ( - parent_excess_blobs * blob_gas_per_blob - ) - if parent_blob_gas_used is None: - assert parent_blob_count is not None, ( - "Parent blob count is required" - ) - parent_blob_gas_used = parent_blob_count * blob_gas_per_blob - if ( - parent_excess_blob_gas + parent_blob_gas_used - < blob_target_gas_per_block - ): - return 0 - else: - return ( - parent_excess_blob_gas - + parent_blob_gas_used - - blob_target_gas_per_block - ) - - return fn - - @classmethod - def min_base_fee_per_blob_gas(cls) -> int: - """Return the minimum base fee per blob gas for Cancun.""" - return 1 - - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for Cancun.""" - return 3338477 - - @classmethod - def blob_gas_per_blob(cls) -> int: - """Blobs are enabled starting from Cancun.""" - return 2**17 - - @classmethod - def supports_blobs(cls) -> bool: - """At Cancun, blobs support is enabled.""" - return True - - @classmethod - def target_blobs_per_block(cls) -> int: - """ - Blobs are enabled starting from Cancun, with a static target of 3 blobs - per block. - """ - return 3 - - @classmethod - def max_blobs_per_block(cls) -> int: - """ - Blobs are enabled starting from Cancun, with a static max of 6 blobs - per block. - """ - return 6 - - @classmethod - def blob_reserve_price_active(cls) -> bool: - """Blob reserve price is not supported in Cancun.""" - return False - - @classmethod - def full_blob_tx_wrapper_version(cls) -> int | None: - """ - Pre-Osaka forks don't use tx wrapper versions for full blob - transactions. - """ - return None - - @classmethod - def max_blobs_per_tx(cls) -> int: - """ - Blobs are enabled starting from Cancun, with a static max equal to the - max per block. - """ - return cls.max_blobs_per_block() - - @classmethod - def blob_schedule(cls) -> BlobSchedule | None: - """ - At Cancun, the fork object runs this routine to get the updated blob - schedule. - """ - parent_fork = cls.parent() - assert parent_fork is not None, "Parent fork must be defined" - blob_schedule = parent_fork.blob_schedule() or BlobSchedule() - current_blob_schedule = ForkBlobSchedule( - target_blobs_per_block=cls.target_blobs_per_block(), - max_blobs_per_block=cls.max_blobs_per_block(), - base_fee_update_fraction=cls.blob_base_fee_update_fraction(), - ) - blob_schedule.append(fork=cls.name(), schedule=current_blob_schedule) - return blob_schedule - - @classmethod - def tx_types(cls) -> List[int]: - """At Cancun, blob type transactions are introduced.""" - return [3] + super(Cancun, cls).tx_types() - - @classmethod - def precompiles(cls) -> List[Address]: - """At Cancun, a precompile for kzg point evaluation is introduced.""" - return [ - Address(10, label="KZG_POINT_EVALUATION"), - ] + super(Cancun, cls).precompiles() - - @classmethod - def system_contracts(cls) -> List[Address]: - """Cancun introduces the system contract for EIP-4788.""" - return [ - Address( - 0x000F3DF6D732807EF1319FB7B8BB8522D0BEAC02, - label="BEACON_ROOTS_ADDRESS", - ) - ] - - @classmethod - def pre_allocation_blockchain(cls) -> Mapping: - """ - Cancun requires pre-allocation of the beacon root contract for EIP-4788 - on blockchain type tests. - """ - new_allocation = { - 0x000F3DF6D732807EF1319FB7B8BB8522D0BEAC02: { - "nonce": 1, - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d" - "57602036146024575f5ffd5b5f35801560495762001fff810690" - "815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd" - "5b62001fff42064281555f359062001fff015500", - } - } - return new_allocation | super(Cancun, cls).pre_allocation_blockchain() # type: ignore - - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """From Cancun, new payload calls must use version 3.""" - return 3 - - @classmethod - def engine_get_blobs_version(cls) -> Optional[int]: - """At Cancun, the engine get blobs version is 1.""" - return 1 - - @classmethod - def engine_new_payload_blob_hashes(cls) -> bool: - """From Cancun, payloads must have blob hashes.""" - return True - - @classmethod - def engine_new_payload_beacon_root(cls) -> bool: - """From Cancun, payloads must have a parent beacon block root.""" - return True - - @classmethod - def gas_costs(cls) -> GasCosts: - """On Cancun, the point evaluation precompile gas cost is set.""" - return replace( - super(Cancun, cls).gas_costs(), - GAS_PRECOMPILE_POINT_EVALUATION=50_000, - ) - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """ - Return a mapping of opcodes to their gas costs for Cancun. - - Adds Cancun-specific opcodes: BLOBHASH, BLOBBASEFEE, TLOAD, TSTORE, - MCOPY. - """ - gas_costs = cls.gas_costs() - memory_expansion_calculator = cls.memory_expansion_gas_calculator() - - # Get parent fork's opcode gas map - base_map = super(Cancun, cls).opcode_gas_map() - - # Add Cancun-specific opcodes - return { - **base_map, - # EIP-4844: Shard Blob Transactions - Opcodes.BLOBHASH: gas_costs.GAS_VERY_LOW, - # EIP-7516: BLOBBASEFEE opcode - Opcodes.BLOBBASEFEE: gas_costs.GAS_BASE, - # EIP-1153: Transient storage opcodes - Opcodes.TLOAD: gas_costs.GAS_WARM_SLOAD, - Opcodes.TSTORE: gas_costs.GAS_WARM_SLOAD, - # EIP-5656: MCOPY - Memory copying instruction - Opcodes.MCOPY: cls._with_memory_expansion( - cls._with_data_copy(gas_costs.GAS_VERY_LOW, gas_costs), - memory_expansion_calculator, - ), - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [ - Opcodes.BLOBHASH, - Opcodes.BLOBBASEFEE, - Opcodes.TLOAD, - Opcodes.TSTORE, - Opcodes.MCOPY, - ] + super(Cancun, cls).valid_opcodes() - - -class Prague(Cancun): - """Prague fork.""" - - # update some blob constants - BLOB_CONSTANTS = { - **Cancun.BLOB_CONSTANTS, # same base constants as cancun - "MAX_BLOBS_PER_BLOCK": 9, # but overwrite or add these - "TARGET_BLOBS_PER_BLOCK": 6, - "MAX_BLOB_GAS_PER_BLOCK": 1179648, - "BLOB_TARGET_GAS_PER_BLOCK": 786432, - "BLOB_BASE_FEE_UPDATE_FRACTION": 5007716, - } - - @classmethod - def precompiles(cls) -> List[Address]: - """ - At Prague, precompiles for BLS operations are added. - - BLS12_G1ADD = 0x0B - BLS12_G1MSM = 0x0C - BLS12_G2ADD = 0x0D - BLS12_G2MSM = 0x0E - BLS12_PAIRING_CHECK = 0x0F - BLS12_MAP_FP_TO_G1 = 0x10 - BLS12_MAP_FP2_TO_G2 = 0x11 - """ - return [ - Address(11, label="BLS12_G1ADD"), - Address(12, label="BLS12_G1MSM"), - Address(13, label="BLS12_G2ADD"), - Address(14, label="BLS12_G2MSM"), - Address(15, label="BLS12_PAIRING_CHECK"), - Address(16, label="BLS12_MAP_FP_TO_G1"), - Address(17, label="BLS12_MAP_FP2_TO_G2"), - ] + super(Prague, cls).precompiles() - - @classmethod - def tx_types(cls) -> List[int]: - """At Prague, set-code type transactions are introduced.""" - return [4] + super(Prague, cls).tx_types() - - @classmethod - def gas_costs(cls) -> GasCosts: - """ - On Prague, the standard token cost and the floor token costs are - introduced due to EIP-7623. - """ - return replace( - super(Prague, cls).gas_costs(), - GAS_TX_DATA_TOKEN_STANDARD=4, # https://eips.ethereum.org/EIPS/eip-7623 - GAS_TX_DATA_TOKEN_FLOOR=10, - GAS_AUTH_PER_EMPTY_ACCOUNT=25_000, - REFUND_AUTH_PER_EXISTING_ACCOUNT=12_500, - GAS_PRECOMPILE_BLS_G1ADD=375, - GAS_PRECOMPILE_BLS_G1MUL=12_000, - GAS_PRECOMPILE_BLS_G1MAP=5_500, - GAS_PRECOMPILE_BLS_G2ADD=600, - GAS_PRECOMPILE_BLS_G2MUL=22_500, - GAS_PRECOMPILE_BLS_G2MAP=23_800, - GAS_PRECOMPILE_BLS_PAIRING_BASE=37_700, - GAS_PRECOMPILE_BLS_PAIRING_PER_PAIR=32_600, - ) - - @classmethod - def system_contracts(cls) -> List[Address]: - """ - Prague introduces the system contracts for EIP-6110, EIP-7002, EIP-7251 - and EIP-2935. - """ - return [ - Address( - 0x00000000219AB540356CBB839CBE05303D7705FA, - label="DEPOSIT_CONTRACT_ADDRESS", - ), - Address( - 0x00000961EF480EB55E80D19AD83579A64C007002, - label="WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS", - ), - Address( - 0x0000BBDDC7CE488642FB579F8B00F3A590007251, - label="CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS", - ), - Address( - 0x0000F90827F1C53A10CB7A02335B175320002935, - label="HISTORY_STORAGE_ADDRESS", - ), - ] + super(Prague, cls).system_contracts() - - @classmethod - def max_request_type(cls) -> int: - """ - At Prague, three request types are introduced, hence the max request - type is 2. - """ - return 2 - - @classmethod - def calldata_gas_calculator(cls) -> CalldataGasCalculator: - """ - Return a callable that calculates the transaction gas cost for its - calldata depending on its contents. - """ - gas_costs = cls.gas_costs() - - def fn(*, data: BytesConvertible, floor: bool = False) -> int: - raw = Bytes(data) - num_zeros = raw.count(0) - num_non_zeros = len(raw) - num_zeros - tokens = num_zeros + num_non_zeros * 4 - if floor: - return tokens * gas_costs.GAS_TX_DATA_TOKEN_FLOOR - return tokens * gas_costs.GAS_TX_DATA_TOKEN_STANDARD - - return fn - - @classmethod - def _calculate_call_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - At Prague, the call gas cost needs to take the authorization into - account. - """ - metadata = opcode.metadata - - base_cost = super(Prague, cls)._calculate_call_gas(opcode, gas_costs) - - if metadata["delegated_address"] or metadata["delegated_address_warm"]: - if metadata["delegated_address_warm"]: - base_cost += gas_costs.GAS_WARM_ACCESS - else: - base_cost += gas_costs.GAS_COLD_ACCOUNT_ACCESS - - return base_cost - - @classmethod - def transaction_data_floor_cost_calculator( - cls, - ) -> TransactionDataFloorCostCalculator: - """ - On Prague, due to EIP-7623, the transaction data floor cost is - introduced. - """ - calldata_gas_calculator = cls.calldata_gas_calculator() - gas_costs = cls.gas_costs() - - def fn(*, data: BytesConvertible) -> int: - return ( - calldata_gas_calculator(data=data, floor=True) - + gas_costs.GAS_TX_BASE - ) + pass - return fn - @classmethod - def transaction_intrinsic_cost_calculator( - cls, - ) -> TransactionIntrinsicCostCalculator: - """ - At Prague, the transaction intrinsic cost needs to take the - authorizations into account. - """ - super_fn = super(Prague, cls).transaction_intrinsic_cost_calculator() - gas_costs = cls.gas_costs() - transaction_data_floor_cost_calculator = ( - cls.transaction_data_floor_cost_calculator() - ) +# Glacier forks skipped, unless explicitly specified +class ArrowGlacier( + London, + solc_name="london", + ignore=True, +): + """Arrow Glacier fork.""" - def fn( - *, - calldata: BytesConvertible = b"", - contract_creation: bool = False, - access_list: List[AccessList] | None = None, - authorization_list_or_count: Sized | int | None = None, - return_cost_deducted_prior_execution: bool = False, - ) -> int: - intrinsic_cost: int = super_fn( - calldata=calldata, - contract_creation=contract_creation, - access_list=access_list, - return_cost_deducted_prior_execution=False, - ) - if authorization_list_or_count is not None: - if isinstance(authorization_list_or_count, Sized): - authorization_list_or_count = len( - authorization_list_or_count - ) - intrinsic_cost += ( - authorization_list_or_count - * gas_costs.GAS_AUTH_PER_EMPTY_ACCOUNT - ) + pass - if return_cost_deducted_prior_execution: - return intrinsic_cost - transaction_floor_data_cost = ( - transaction_data_floor_cost_calculator(data=calldata) - ) - return max(intrinsic_cost, transaction_floor_data_cost) +class GrayGlacier( + ArrowGlacier, + solc_name="london", + ignore=True, +): + """Gray Glacier fork.""" - return fn + pass - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for Prague.""" - return 5007716 - @classmethod - def target_blobs_per_block(cls) -> int: - """Blobs in Prague, have a static target of 6 blobs per block.""" - return 6 +class Paris( + eips.EIP3675, + London, + transition_tool_name="Merge", + ruleset_name="MERGE", +): + """Paris (Merge) fork.""" - @classmethod - def max_blobs_per_block(cls) -> int: - """Blobs in Prague, have a static max of 9 blobs per block.""" - return 9 + pass - @classmethod - def pre_allocation_blockchain(cls) -> Mapping: - """ - Prague requires pre-allocation of the beacon chain deposit contract for - EIP-6110, the exits contract for EIP-7002, and the history storage - contract for EIP-2935. - """ - new_allocation = {} - - # Add the beacon chain deposit contract - deposit_contract_tree_depth = 32 - storage = {} - next_hash = sha256(b"\x00" * 64).digest() - for i in range( - deposit_contract_tree_depth + 2, - deposit_contract_tree_depth * 2 + 1, - ): - storage[i] = next_hash - next_hash = sha256(next_hash + next_hash).digest() - - with open( - CURRENT_FOLDER / "contracts" / "deposit_contract.bin", mode="rb" - ) as f: - new_allocation.update( - { - 0x00000000219AB540356CBB839CBE05303D7705FA: { - "nonce": 1, - "code": f.read(), - "storage": storage, - } - } - ) - # EIP-7002: Add the withdrawal request contract - with open( - CURRENT_FOLDER / "contracts" / "withdrawal_request.bin", mode="rb" - ) as f: - new_allocation.update( - { - 0x00000961EF480EB55E80D19AD83579A64C007002: { - "nonce": 1, - "code": f.read(), - }, - } - ) +class Shanghai( + eips.EIP3855, + eips.EIP3860, + eips.EIP4895, + Paris, + fork_by_timestamp=True, +): + """Shanghai fork.""" - # EIP-7251: Add the consolidation request contract - with open( - CURRENT_FOLDER / "contracts" / "consolidation_request.bin", - mode="rb", - ) as f: - new_allocation.update( - { - 0x0000BBDDC7CE488642FB579F8B00F3A590007251: { - "nonce": 1, - "code": f.read(), - }, - } - ) + pass - # EIP-2935: Add the history storage contract - with open( - CURRENT_FOLDER / "contracts" / "history_contract.bin", mode="rb" - ) as f: - new_allocation.update( - { - 0x0000F90827F1C53A10CB7A02335B175320002935: { - "nonce": 1, - "code": f.read(), - } - } - ) - return new_allocation | super(Prague, cls).pre_allocation_blockchain() # type: ignore +class Cancun( + eips.EIP5656, + eips.EIP1153, + eips.EIP4788, + eips.EIP4844, + eips.EIP7516, + Shanghai, +): + """Cancun fork.""" - @classmethod - def header_requests_required(cls) -> bool: - """ - Prague requires that the execution layer header contains the beacon - chain requests hash. - """ - return True + pass - @classmethod - def engine_new_payload_requests(cls) -> bool: - """ - From Prague, new payloads include the requests hash as a parameter. - """ - return True - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """From Prague, new payload calls must use version 4.""" - return 4 +class Prague( + eips.EIP7691, + eips.EIP7685, + eips.EIP2935, + eips.EIP7251, + eips.EIP7002, + eips.EIP6110, + eips.EIP7623, + eips.EIP7702, + eips.EIP2537, + Cancun, +): + """Prague fork.""" - @classmethod - def engine_forkchoice_updated_version(cls) -> Optional[int]: - """ - At Prague, version number of NewPayload and ForkchoiceUpdated diverge. - """ - return 3 + pass -class Osaka(Prague, solc_name="cancun"): +class Osaka( + eips.EIP7939, + eips.EIP7934, + eips.EIP7825, + eips.EIP7918, + eips.EIP7594, + eips.EIP7951, + Prague, + solc_name="cancun", +): """Osaka fork.""" - # update some blob constants - BLOB_CONSTANTS = { - **Prague.BLOB_CONSTANTS, # same base constants as prague - "AMOUNT_CELL_PROOFS": 128, - } - - @classmethod - def engine_get_payload_version(cls) -> Optional[int]: - """From Osaka, get payload calls must use version 5.""" - return 5 - - @classmethod - def engine_get_blobs_version(cls) -> Optional[int]: - """At Osaka, the engine get blobs version is 2.""" - return 2 - - @classmethod - def full_blob_tx_wrapper_version(cls) -> int | None: - """At Osaka, the full blob transaction wrapper version is defined.""" - return 1 - - @classmethod - def transaction_gas_limit_cap(cls) -> int | None: - """At Osaka, transaction gas limit is capped at 16 million (2**24).""" - return 16_777_216 - - @classmethod - def block_rlp_size_limit(cls) -> int | None: - """From Osaka, block RLP size is limited as specified in EIP-7934.""" - max_block_size = 10_485_760 - safety_margin = 2_097_152 - return max_block_size - safety_margin - - @classmethod - def opcode_gas_map( - cls, - ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: - """Add Osaka opcodes gas costs.""" - gas_costs = cls.gas_costs() - base_map = super(Osaka, cls).opcode_gas_map() - return { - **base_map, - Opcodes.CLZ: gas_costs.GAS_LOW, - } - - @classmethod - def valid_opcodes(cls) -> List[Opcodes]: - """Return list of Opcodes that are valid to work on this fork.""" - return [ - Opcodes.CLZ, - ] + super(Prague, cls).valid_opcodes() - - @classmethod - def precompiles(cls) -> List[Address]: - """ - At Osaka, a precompile for p256verify operation is added. - - P256VERIFY = 0x100 - """ - return [ - Address(0x100, label="P256VERIFY"), - ] + super(Osaka, cls).precompiles() - - @classmethod - def gas_costs(cls) -> GasCosts: - """On Osaka, the P256VERIFY precompile gas cost is set.""" - return replace( - super(Osaka, cls).gas_costs(), - GAS_PRECOMPILE_P256VERIFY=6_900, - ) - - @classmethod - def excess_blob_gas_calculator(cls) -> ExcessBlobGasCalculator: - """ - Return a callable that calculates the excess blob gas for a block. - """ - target_blobs_per_block = cls.target_blobs_per_block() - blob_gas_per_blob = cls.blob_gas_per_blob() - blob_target_gas_per_block = target_blobs_per_block * blob_gas_per_blob - max_blobs_per_block = cls.max_blobs_per_block() - blob_base_cost = 2**13 # EIP-7918 new parameter - - def fn( - *, - parent_excess_blob_gas: int | None = None, - parent_excess_blobs: int | None = None, - parent_blob_gas_used: int | None = None, - parent_blob_count: int | None = None, - parent_base_fee_per_gas: int, # EIP-7918 additional parameter - ) -> int: - if parent_excess_blob_gas is None: - assert parent_excess_blobs is not None, ( - "Parent excess blobs are required" - ) - parent_excess_blob_gas = ( - parent_excess_blobs * blob_gas_per_blob - ) - if parent_blob_gas_used is None: - assert parent_blob_count is not None, ( - "Parent blob count is required" - ) - parent_blob_gas_used = parent_blob_count * blob_gas_per_blob - if ( - parent_excess_blob_gas + parent_blob_gas_used - < blob_target_gas_per_block - ): - return 0 - - # EIP-7918: Apply reserve price when execution costs dominate blob - # costs - current_blob_base_fee = cls.blob_gas_price_calculator()( - excess_blob_gas=parent_excess_blob_gas - ) - reserve_price_active = ( - blob_base_cost * parent_base_fee_per_gas - > blob_gas_per_blob * current_blob_base_fee - ) - if reserve_price_active: - blob_excess_adjustment = ( - parent_blob_gas_used - * (max_blobs_per_block - target_blobs_per_block) - // max_blobs_per_block - ) - return parent_excess_blob_gas + blob_excess_adjustment - - # Original EIP-4844 calculation - return ( - parent_excess_blob_gas - + parent_blob_gas_used - - blob_target_gas_per_block - ) - - return fn - - @classmethod - def max_blobs_per_tx(cls) -> int: - """ - Blobs in Osaka, have a static max of 6 blobs per tx. Differs from the - max per block. - """ - return 6 - - @classmethod - def blob_reserve_price_active(cls) -> bool: - """Blob reserve price is supported in Osaka.""" - return True - - @classmethod - def blob_base_cost(cls) -> int: - """Return the base cost of a blob at a given fork.""" - return 2**13 # EIP-7918 new parameter + pass -class BPO1(Osaka, bpo_fork=True): +class BPO1( + Osaka, + bpo_fork=True, + update_blob_constants={ + "BLOB_BASE_FEE_UPDATE_FRACTION": 8346193, + "TARGET_BLOBS_PER_BLOCK": 10, + "MAX_BLOBS_PER_BLOCK": 15, + }, +): """Mainnet BPO1 fork - Blob Parameter Only fork 1.""" - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for BPO1.""" - return 8346193 - - @classmethod - def target_blobs_per_block(cls) -> int: - """Blobs in BPO1 have a target of 10 blobs per block.""" - return 10 - - @classmethod - def max_blobs_per_block(cls) -> int: - """Blobs in BPO1 have a max of 15 blobs per block.""" - return 15 + pass -class BPO2(BPO1, bpo_fork=True): +class BPO2( + BPO1, + bpo_fork=True, + update_blob_constants={ + "BLOB_BASE_FEE_UPDATE_FRACTION": 11684671, + "TARGET_BLOBS_PER_BLOCK": 14, + "MAX_BLOBS_PER_BLOCK": 21, + }, +): """Mainnet BPO2 fork - Blob Parameter Only fork 2.""" - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for BPO2.""" - return 11684671 - - @classmethod - def target_blobs_per_block(cls) -> int: - """Blobs in BPO2 have a target of 14 blobs per block.""" - return 14 - - @classmethod - def max_blobs_per_block(cls) -> int: - """Blobs in BPO2 have a max of 21 blobs per block.""" - return 21 + pass -class BPO3(BPO2, bpo_fork=True): +class BPO3( + BPO2, + bpo_fork=True, + deployed=False, + update_blob_constants={ + "BLOB_BASE_FEE_UPDATE_FRACTION": 20609697, + "TARGET_BLOBS_PER_BLOCK": 21, + "MAX_BLOBS_PER_BLOCK": 32, + }, +): """ Pseudo BPO3 fork - Blob Parameter Only fork 3. For testing purposes only. """ - @classmethod - def is_deployed(cls) -> bool: - """BPO3 is a pseudo fork for testing, not deployed to mainnet.""" - return False - - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for BPO3.""" - return 20609697 - - @classmethod - def target_blobs_per_block(cls) -> int: - """Blobs in BPO3 have a target of 21 blobs per block.""" - return 21 - - @classmethod - def max_blobs_per_block(cls) -> int: - """Blobs in BPO3 have a max of 32 blobs per block.""" - return 32 + pass -class BPO4(BPO3, bpo_fork=True): +class BPO4( + BPO3, + bpo_fork=True, + update_blob_constants={ + "BLOB_BASE_FEE_UPDATE_FRACTION": 13739630, + "TARGET_BLOBS_PER_BLOCK": 14, + "MAX_BLOBS_PER_BLOCK": 21, + }, +): """ Pseudo BPO4 fork - Blob Parameter Only fork 4. For testing purposes only. Testing a decrease in values from BPO3. """ - @classmethod - def blob_base_fee_update_fraction(cls) -> int: - """Return the blob base fee update fraction for BPO4.""" - return 13739630 - - @classmethod - def target_blobs_per_block(cls) -> int: - """Blobs in BPO4 have a target of 14 blobs per block.""" - return 14 - - @classmethod - def max_blobs_per_block(cls) -> int: - """Blobs in BPO4 have a max of 21 blobs per block.""" - return 21 + pass -class BPO5(BPO4, bpo_fork=True): +class BPO5( + BPO4, + bpo_fork=True, +): """ Pseudo BPO5 fork - Blob Parameter Only fork 5. For testing purposes only. Required to parse Fusaka devnet genesis files. @@ -2834,63 +1503,15 @@ class BPO5(BPO4, bpo_fork=True): pass -class Amsterdam(BPO2): +class Amsterdam( + eips.EIP7928, + BPO2, + deployed=False, +): """Amsterdam fork.""" # TODO: We may need to adjust which BPO Amsterdam inherits from as the # related Amsterdam specs change over time, and before Amsterdam is # live on mainnet. - @classmethod - def header_bal_hash_required(cls) -> bool: - """ - From Amsterdam, header must contain block access list hash (EIP-7928). - """ - return True - - @classmethod - def gas_costs(cls) -> GasCosts: - """ - On Amsterdam, the cost per block access list item is introduced - in EIP-7928. - """ - return replace( - super(Amsterdam, cls).gas_costs(), - GAS_BLOCK_ACCESS_LIST_ITEM=2000, - ) - - @classmethod - def empty_block_bal_item_count(cls) -> int: - """ - Return the BAL item count for an empty Amsterdam block. - - Four system contracts produce 15 items: - EIP-4788 beacon roots: 1 address + 1 write + 1 read = 3 - EIP-2935 history storage: 1 address + 1 write = 2 - EIP-7002 withdrawal requests: 1 address + 4 reads = 5 - EIP-7251 consolidation requests: 1 address + 4 reads = 5 - """ - return 15 - - @classmethod - def is_deployed(cls) -> bool: - """Return True if this fork is deployed.""" - return False - - @classmethod - def engine_new_payload_version(cls) -> Optional[int]: - """From Amsterdam, new payload calls must use version 5.""" - return 5 - - @classmethod - def engine_get_payload_version(cls) -> Optional[int]: - """From Amsterdam, get payload calls must use version 6.""" - return 6 - - @classmethod - def engine_execution_payload_block_access_list(cls) -> bool: - """ - From Amsterdam, engine execution payload includes `block_access_list` - as a parameter. - """ - return True + pass diff --git a/packages/testing/src/execution_testing/forks/helpers.py b/packages/testing/src/execution_testing/forks/helpers.py index 4ce107f27c5..9a08b50ea52 100644 --- a/packages/testing/src/execution_testing/forks/helpers.py +++ b/packages/testing/src/execution_testing/forks/helpers.py @@ -24,7 +24,7 @@ ) from .base_fork import BaseFork -from .forks import forks, transition +from .forks import eips, forks, transition from .transition_base_fork import TransitionBaseClass @@ -44,9 +44,15 @@ def __init__(self, message: str) -> None: fork = forks.__dict__[fork_name] if not isinstance(fork, type): continue - if issubclass(fork, BaseFork) and fork is not BaseFork: + if ( + issubclass(fork, BaseFork) + and fork is not BaseFork + and not fork.is_eip() + ): all_forks.append(fork) +all_eips: List[Type[BaseFork]] = eips.ALL_EIPS + transition_forks: List[Type[TransitionBaseClass]] = [] for fork_name in transition.__dict__: @@ -99,14 +105,6 @@ def get_development_forks() -> List[Type[BaseFork]]: return [fork for fork in get_forks() if not fork.is_deployed()] -def get_parent_fork(fork: Type[BaseFork]) -> Type[BaseFork]: - """Return parent fork of the specified fork.""" - parent_fork = fork.__base__ - if not parent_fork: - raise InvalidForkError(f"Parent fork of {fork} not found.") - return parent_fork - - def get_closest_fork(fork: Type[BaseFork]) -> Optional[Type[BaseFork]]: """Return None if BaseFork is passed, otherwise return the fork itself.""" if fork is BaseFork: @@ -284,14 +282,14 @@ def forks_from_until( Return specified fork and all forks after it until and including the second specified fork. """ - prev_fork = fork_until + prev_fork: Type[BaseFork] | None = fork_until forks: List[Type[BaseFork]] = [] - while prev_fork != BaseFork and prev_fork != fork_from: + while prev_fork is not None and prev_fork != fork_from: forks.insert(0, prev_fork) - prev_fork = get_parent_fork(prev_fork) + prev_fork = prev_fork.parent() if prev_fork == BaseFork: return [] @@ -461,6 +459,22 @@ def set_before_validator(value: Any) -> Any: BeforeValidator(set_before_validator), ] ForkSetAdapter: TypeAdapter = TypeAdapter(ForkSet) + +ForkEIP = Annotated[ + Type[BaseFork], + PlainSerializer(str), + PlainValidator( + fork_validator_generator( + BaseFork, all_forks + all_eips + transition_forks + ) + ), +] +ForkEIPSet = Annotated[ + Set[ForkEIP], + BeforeValidator(set_before_validator), +] +ForkEIPSetAdapter: TypeAdapter = TypeAdapter(ForkEIPSet) + TransitionFork = Annotated[ Type[TransitionBaseClass], PlainSerializer(str), diff --git a/packages/testing/src/execution_testing/forks/tests/test_forks.py b/packages/testing/src/execution_testing/forks/tests/test_forks.py index 25183a2a222..7dadc3667c9 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_forks.py +++ b/packages/testing/src/execution_testing/forks/tests/test_forks.py @@ -7,6 +7,7 @@ from execution_testing.base_types import BlobSchedule +from ..forks.eips.paris.eip_3675 import EIP3675 from ..forks.forks import ( BPO1, BPO2, @@ -680,3 +681,50 @@ def test_transition_from_normal_until(self) -> None: assert OsakaToBPO1AtTime15k in result assert BPO1ToBPO2AtTime15k in result assert BPO2ToAmsterdamAtTime15k not in result + + +def test_blob_constants() -> None: # noqa: D103 + assert Osaka.get_blob_constant("AMOUNT_CELL_PROOFS") == 128 + + +def test_method_versions() -> None: # noqa: D103 + assert London.engine_get_blobs_version() is None + assert London.engine_get_payload_version() is None + assert London.engine_new_payload_version() is None + assert London.engine_forkchoice_updated_version() is None + + assert Paris.engine_get_blobs_version() is None + assert Paris.engine_get_payload_version() == 1 + assert Paris.engine_new_payload_version() == 1 + assert Paris.engine_forkchoice_updated_version() == 1 + + assert Shanghai.engine_get_blobs_version() is None + assert Shanghai.engine_get_payload_version() == 2 + assert Shanghai.engine_new_payload_version() == 2 + assert Shanghai.engine_forkchoice_updated_version() == 2 + + assert Cancun.engine_get_blobs_version() == 1 + assert Cancun.engine_get_payload_version() == 3 + assert Cancun.engine_new_payload_version() == 3 + assert Cancun.engine_forkchoice_updated_version() == 3 + + assert Prague.engine_get_blobs_version() == 1 + assert Prague.engine_get_payload_version() == 4 + assert Prague.engine_new_payload_version() == 4 + assert Prague.engine_forkchoice_updated_version() == 3 + + assert Osaka.engine_get_blobs_version() == 2 + assert Osaka.engine_get_payload_version() == 5 + assert Osaka.engine_new_payload_version() == 4 + assert Osaka.engine_forkchoice_updated_version() == 3 + + assert Amsterdam.engine_get_payload_version() == 6 + assert Amsterdam.engine_new_payload_version() == 5 + + +def test_eips() -> None: # noqa: D103 + assert EIP3675.enabling_forks() == {Paris} + assert Paris.is_eip_enabled(eip_number=3675) + assert Shanghai.is_eip_enabled(eip_number=3675) + assert not Paris.is_eip_enabled(eip_number=3855) + assert Shanghai.is_eip_enabled(eip_number=3855) diff --git a/packages/testing/src/execution_testing/forks/transition_base_fork.py b/packages/testing/src/execution_testing/forks/transition_base_fork.py index f67b5005bd8..3dca0cb1d31 100644 --- a/packages/testing/src/execution_testing/forks/transition_base_fork.py +++ b/packages/testing/src/execution_testing/forks/transition_base_fork.py @@ -94,10 +94,8 @@ def ignore(cls) -> bool: @classmethod def is_deployed(cls) -> bool: """ - Return whether the fork has been deployed to mainnet, or not. - - Must be overridden and return False for forks that are still under - development. + Return whether the transitions-to fork has been deployed to mainnet, + or not. """ return cls.transitions_to().is_deployed() diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 12816e470ec..ae467bbfa8c 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -658,8 +658,9 @@ def generate_block_data( # executing the block by simply counting the type-3 txs, we need to set # the correct value by default. blob_gas_used: int | None = None - if (blob_gas_per_blob := fork.blob_gas_per_blob()) > 0: - blob_gas_used = blob_gas_per_blob * count_blobs(txs) + if fork.supports_blobs(): + if (blob_gas_per_blob := fork.blob_gas_per_blob()) > 0: + blob_gas_used = blob_gas_per_blob * count_blobs(txs) header = FixtureHeader( **( diff --git a/packages/testing/src/execution_testing/test_types/blob_types.py b/packages/testing/src/execution_testing/test_types/blob_types.py index b4dfc58a9bb..3c7b6b7b5a2 100644 --- a/packages/testing/src/execution_testing/test_types/blob_types.py +++ b/packages/testing/src/execution_testing/test_types/blob_types.py @@ -5,7 +5,7 @@ from hashlib import sha256 from os.path import realpath from pathlib import Path -from typing import Any, ClassVar, List, Literal, cast +from typing import Any, ClassVar, List import ckzg # type: ignore import platformdirs @@ -81,9 +81,7 @@ def get_filename(fork: Fork, seed: int) -> str: """ Return filename this blob would have as string (with .json extension). """ - amount_cell_proofs: int = cast( - int, fork.get_blob_constant("AMOUNT_CELL_PROOFS") - ) + amount_cell_proofs = fork.get_blob_constant("AMOUNT_CELL_PROOFS") return ( "blob_" + str(seed) @@ -118,23 +116,16 @@ def generate_blob_data(rng_seed: int = 0) -> Bytes: # generate blob ints: list[int] = [ - rng.randrange(cast(int, fork.get_blob_constant("BLS_MODULUS"))) + rng.randrange(fork.get_blob_constant("BLS_MODULUS")) for _ in range( - cast( - int, fork.get_blob_constant("FIELD_ELEMENTS_PER_BLOB") - ) + fork.get_blob_constant("FIELD_ELEMENTS_PER_BLOB") ) ] encoded: list[bytes] = [ i.to_bytes( - cast( - int, fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT") - ), - cast( - Literal["big"], - fork.get_blob_constant("KZG_ENDIANNESS"), - ), + fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT"), + "big", ) for i in ints ] @@ -153,12 +144,8 @@ def get_commitment(data: Bytes) -> Bytes: Note: Each cell holds the exact same copy of this commitment. """ # sanity check - field_elements: int = cast( - int, fork.get_blob_constant("FIELD_ELEMENTS_PER_BLOB") - ) - bytes_per_field: int = cast( - int, fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT") - ) + field_elements = fork.get_blob_constant("FIELD_ELEMENTS_PER_BLOB") + bytes_per_field = fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT") assert len(data) == field_elements * bytes_per_field, ( f"Expected blob of length " f"{field_elements * bytes_per_field} but got blob of length " @@ -190,9 +177,7 @@ def get_proof(fork: Fork, data: Bytes) -> List[Bytes] | Bytes: # 7a1d5962cd3dfb5f7b3e41aab728c55/tests/core/pyspec/eth2spec/ # test/utils/kzg_tests.py#L58-L66) z_valid_size: bytes = z.to_bytes( - cast( - int, fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT") - ), + fork.get_blob_constant("BYTES_PER_FIELD_ELEMENT"), byteorder="big", ) proof, _ = ckzg.compute_kzg_proof( @@ -344,9 +329,7 @@ def verify_cell_kzg_proof_batch(self, cell_indices: list) -> bool: Check whether all cell proofs are valid and returns True only if that is the case. """ - amount_cell_proofs: int = cast( - int, self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") - ) + amount_cell_proofs = self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") assert amount_cell_proofs > 0, ( f"verify_cell_kzg_proof_batch() is not available for your fork: " @@ -389,9 +372,7 @@ def delete_cells_then_recover_them( missing cells. If no assertion is triggered the reconstruction was successful. """ - amount_cell_proofs: int = cast( - int, self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") - ) + amount_cell_proofs = self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") assert amount_cell_proofs > 0, ( f"delete_cells_then_recover_them() is not available for fork: " @@ -483,9 +464,7 @@ def corrupt_byte(b: bytes) -> Bytes: return Bytes(bytes([b[0] ^ 0xFF])) # >=osaka - amount_cell_proofs: int = cast( - int, self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") - ) + amount_cell_proofs = self.fork.get_blob_constant("AMOUNT_CELL_PROOFS") if amount_cell_proofs > 0: assert isinstance(self.proof, list), ( "proof was expected to be a list but it isn't" diff --git a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py index dccd875e502..f789f349919 100644 --- a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py +++ b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py @@ -312,6 +312,7 @@ class TestMultipleWithdrawalsSameAddress: @pytest.fixture def addresses(self, fork: Fork) -> List[Address]: # noqa: D102 addresses = [Address(p) for p in fork.precompiles()] + addresses.sort() return addresses + [Address(2**160 - 1)] @pytest.fixture From 26c98b9c6a713135285e351efe6634e8c364ee3f Mon Sep 17 00:00:00 2001 From: kclowes Date: Wed, 1 Apr 2026 14:17:56 -0600 Subject: [PATCH 2/2] feat(cli): Add --until option for checklist generation (#2593) * Add --until option for checklist generation * Make development forks build checklist by default --- .../cli/pytest_commands/checklist.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/checklist.py b/packages/testing/src/execution_testing/cli/pytest_commands/checklist.py index e51f3cc593a..dda4fbad103 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/checklist.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/checklist.py @@ -4,9 +4,16 @@ import click +from ...forks import get_development_forks from .fill import FillCommand +def _last_development_fork() -> str | None: + """Return the name of the last development fork, if any.""" + dev_forks = get_development_forks() + return dev_forks[-1].name() if dev_forks else None + + @click.command() @click.option( "--output", @@ -22,13 +29,25 @@ multiple=True, help="Generate checklist only for specific EIP(s)", ) -def checklist(output: str, eip: tuple[int, ...], **kwargs: Any) -> None: +@click.option( + "--until", + "-u", + type=str, + default=None, + help="Include upcoming forks up to and including this fork", +) +def checklist( + output: str, eip: tuple[int, ...], until: str | None, **kwargs: Any +) -> None: """ Generate EIP test checklists based on pytest.mark.eip_checklist markers. This command scans test files for eip_checklist markers and generates filled checklists showing which checklist items have been implemented. + By default, includes all development forks so that checklists for + upcoming EIPs are generated without needing --until. + Examples: # Generate checklists for all EIPs uv run checklist @@ -39,6 +58,9 @@ def checklist(output: str, eip: tuple[int, ...], **kwargs: Any) -> None: # Generate checklists for specific test path uv run checklist tests/prague/eip7702* + # Limit to a specific fork + uv run checklist --until Prague + # Specify output directory uv run checklist --output ./my-checklists @@ -52,6 +74,13 @@ def checklist(output: str, eip: tuple[int, ...], **kwargs: Any) -> None: for eip_num in eip: args.extend(["--checklist-eip", str(eip_num)]) + # Default --until to the last development fork so checklists for + # upcoming EIPs are generated without requiring the flag explicitly. + if until is None: + until = _last_development_fork() + if until: + args.extend(["--until", until]) + command = FillCommand( plugins=[ "execution_testing.cli.pytest_commands.plugins.filler.eip_checklist"