From a2e59a6cfbd2de55ae3427673b2cd7db5a42ba38 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:34:45 +0200 Subject: [PATCH 01/55] refactor(spec-tools): use testing pydantic models in t8n (#2924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(tests): make Alloc implement the PreState protocol Add a CONSTRUCTION/LIVE/FROZEN lifecycle to the testing-side Alloc so it directly satisfies ethereum.state.PreState. The first PreState read transitions the alloc to LIVE and rejects further __setitem__/ __delitem__; apply_diff(BlockDiff) is the sole mutation entry point in LIVE, and freeze() locks the allocation for assertion use. Groundwork for the t8n refactor: with Alloc directly usable as a PreState, fork.BlockState(pre_state=alloc) works without an adapter, and t8n can drop its bespoke Alloc/Env/Result/Txs JSON types. * refactor(spec-tools): rewrite T8N to consume testing pydantic types T8N now wires the testing-package types end-to-end: * __init__ accepts an optional ``t8n_data`` and otherwise parses the JSON inputs into testing ``Alloc``/``Environment``/``Transaction`` via ``model_validate``. The CLI ingress is structured so the future in-process lift only needs to swap the caller, not the constructor. * ``env.py`` drops the bespoke ``Env`` class in favour of ``build_block_environment(fork, env, pre_state, chain_id, ommers, state_test)`` plus a handful of ``_resolve_*`` helpers. ``Ommer`` stays as a small dataclass for the pre-PoS reward path. * ``convert_transaction`` routes through ``TransactionLoad`` rather than ``rlp.decode_to`` so contract-creating typed txs (Blob / SetCode with ``to=null``) construct successfully and let ``check_transaction`` raise the canonical ``TransactionTypeContractCreationError``. * The bespoke ``Result`` / ``Txs`` / ``Alloc`` classes are gone; ``build_result`` and ``get_receipts_from_output`` produce a ``cli_types.Result`` directly, and ``T8N.run()`` emits the ``TransitionToolOutput``-shaped JSON. * The per-tx ``backup_state`` / ``restore_state`` pattern disappears with the snapshot-based State: a failed ``process_transaction`` no longer reaches ``incorporate_tx_into_block``, so ``BlockState`` is untouched without explicit rollback. After execution the block diff is applied in-place via ``Alloc.apply_diff``. JSON ingress smooths two boundary mismatches: ``yParity`` on auth tuples (duplicated by the testing serializer, rejected by the validator) and ``secretKey`` left on already-signed txs (rejected by ``InvalidSignaturePrivateKeyError``). Unsigned txs with only a ``secretKey`` are signed post-validation; pre-Spurious-Dragon forks get ``protected=False`` so the v-value stays in {27, 28}. * refactor(testing): drive ExecutionSpecsTransitionTool's T8N in-process The testing-side EELS caller no longer marshals the input through a JSON ``StringIO`` and back. ``_evaluate`` now hands the testing ``TransitionToolInput`` directly to ``T8N`` via the existing ``t8n_data`` kwarg and assembles the ``TransitionToolOutput`` from ``T8N``'s in-memory ``alloc``/``result``/``body``. To make the in-process path symmetric with the CLI path, ``T8N``: * pulls ``blob_params`` from ``t8n_data.blob_params`` (camelCase dump matches the existing parse), so BPO-fork blob schedules don't have to be re-serialized through ``--input.blobParams=stdin``; * refactor(spec-tools): make T8N JSON-free; CLI wrapper in t8n.cli T8N now takes a testing ``TransitionTool.TransitionToolData`` and nothing else from the JSON/CLI surface. The CLI plumbing (``argparse`` namespace, ``--input.*``/``--output.*`` flags, stdin, file paths, tracer construction from CLI flags) lives in a new ``t8n.cli`` module: * ``build_t8n_from_cli_options(options, in_file, cache) -> T8N`` reads the JSON inputs (stdin / files), validates each piece into testing pydantic types, resolves the fork, bundles everything into a ``TransitionToolData``, builds tracers from the CLI flags, and hands them to ``T8N``. * ``write_t8n_outputs(t8n, output, options, out_file)`` serialises the t8n output + opcode counts per ``--output.*``. * ``run_t8n_cli(options, out_file, in_file, cache) -> int`` chains the two for the CLI entry point. ``T8N`` internally calls ``resolve_fork(t8n_data.fork_name, t8n_data.env)`` to translate the testing-side fork name into a spec ``Hardfork`` + optional ``ByBlockNumber`` criteria (handles both canonical names and CLI exception aliases like ``Paris``, ``ConstantinopleFix``, ``HomesteadToDaoAt5``). ``T8N.run()`` returns the ``TransitionToolOutput`` directly — no more out_file writing. Callers updated: * ``evm_tools.__init__.main`` now calls ``run_t8n_cli``. * ``statetest`` and ``tests/json_loader`` use ``build_t8n_from_cli_options``. * ``tests/evm_tools/test_count_opcodes`` uses ``run_t8n_cli``. * ``ExecutionSpecsTransitionTool._evaluate`` hands its ``transition_tool_data`` straight to ``T8N`` — no argparse dance. The CLI ↔ testing fork-name mapping is title-case + a one-entry override for ``DAOFork`` (testing's irregular capitalisation). ``state_reward=None`` is resolved to the fork's ``BLOCK_REWARD`` (or ``-1`` for PoS forks) in the wrapper before constructing ``TransitionToolData.reward: int``. * refactor(testing): drop duplicate State/trie in test_types ``Alloc`` used to maintain its own parallel ``State`` dataclass plus ``set_account``/``set_storage``/``state_root``/``storage_root`` free functions to compute its root. Now that ``Alloc`` implements the ``PreState`` protocol, ``state_root()`` can route through ``_materialize_state()`` and ``ethereum.state.state_root``, so the in-package trie machinery is redundant. * ``Alloc.state_root()`` reduced to a one-liner over ``spec_state.state_root(self._materialize_state())``. The materialize call doesn't transition the alloc out of ``CONSTRUCTION``, so existing callers that compute a genesis root and then keep mutating the alloc are unaffected. * Local ``State`` dataclass + trie helpers (``set_account``, ``set_storage``, ``storage_root``, ``state_root``) removed; they had no consumers outside the deleted ``Alloc.state_root`` body. * ``test_types/trie.py`` deleted along with its now-tautological ``test_eest_trie_keccak256_matches_eels`` keccak-dispatch check (the module just re-exported ``ethereum.crypto.hash.keccak256``). * refactor(spec-tools): final clean up * refactor(spec-tools): post review update * fix(spec-tools): load txs that carry no signature material The CLI parser builds testing `Transaction` objects and RLP-encodes them for the returned body. A tx with neither `v`/`r`/`s` nor `secretKey` made `Transaction.rlp` auto-sign a key-less tx and die on `assert signing_key is not None`, failing every json_loader case that replays such a fixture (136 in CI, all `test_bad_v_r_s`). Default the missing signature components to zero in `_normalize_tx_json`, matching the previous parser (`t8n_types.Txs.parse_json_tx`): the tx then executes with an invalid signature and the fork rejects it, which is exactly what these fixtures assert via `expectException`. Verified against locally filled `bad_v_r_s` fixtures for Homestead and Prague. * chore: fix-up docstring * chore: fix-up docstring formatting for ruff * chore: just one more docstring fix * refactor(test-clis): Refactor LazyAlloc * refactor(test-clis): Update LazyAlloc * refactor(test-clis): Refactor LazyAlloc * post review updates --------- Co-authored-by: danceratopz Co-authored-by: Mario Vega --- .../base_types/tests/test_keccak_dispatch.py | 10 - .../client_clis/cli_types.py | 113 +++- .../client_clis/clis/besu.py | 2 +- .../client_clis/clis/execution_specs.py | 117 ++-- .../client_clis/file_utils.py | 28 +- .../client_clis/tests/test_execution_specs.py | 2 +- .../client_clis/tests/test_transition_tool.py | 28 +- .../client_clis/transition_tool.py | 4 +- .../src/execution_testing/specs/blockchain.py | 12 +- .../src/execution_testing/specs/state.py | 6 +- .../test_types/account_types.py | 376 ++++++++--- .../test_types/tests/test_alloc_prestate.py | 277 ++++++++ .../src/execution_testing/test_types/trie.py | 401 ----------- src/ethereum_spec_tools/evm_tools/__init__.py | 6 +- .../evm_tools/loaders/fork_loader.py | 18 - .../evm_tools/statetest/__init__.py | 25 +- .../evm_tools/t8n/__init__.py | 631 ++++++++---------- .../evm_tools/t8n/block_environment.py | 233 +++++++ src/ethereum_spec_tools/evm_tools/t8n/cli.py | 483 ++++++++++++++ src/ethereum_spec_tools/evm_tools/t8n/env.py | 333 --------- .../evm_tools/t8n/result.py | 149 +++++ .../evm_tools/t8n/t8n_types.py | 443 ------------ src/ethereum_spec_tools/evm_tools/utils.py | 67 +- tests/evm_tools/test_count_opcodes.py | 8 +- tests/json_loader/helpers/load_state_tests.py | 12 +- vulture_whitelist.py | 10 +- 26 files changed, 1960 insertions(+), 1834 deletions(-) create mode 100644 packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py delete mode 100644 packages/testing/src/execution_testing/test_types/trie.py create mode 100644 src/ethereum_spec_tools/evm_tools/t8n/block_environment.py create mode 100644 src/ethereum_spec_tools/evm_tools/t8n/cli.py delete mode 100644 src/ethereum_spec_tools/evm_tools/t8n/env.py create mode 100644 src/ethereum_spec_tools/evm_tools/t8n/result.py delete mode 100644 src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py diff --git a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py index 95bd341ba6d..af3fb835d21 100644 --- a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py +++ b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py @@ -162,13 +162,3 @@ def test_eest_bytes_keccak256_matches_eels() -> None: from_eest = bytes(Bytes(buffer).keccak256()) from_eels = bytes(keccak256(buffer)) assert from_eest == from_eels - - -def test_eest_trie_keccak256_matches_eels() -> None: - """`trie.keccak256` and EELS `keccak256` return identical digests.""" - from ethereum.crypto.hash import keccak256 as eels - - from ...test_types.trie import keccak256 as trie - - for buffer in (b"", b"hashme", bytes(range(256))): - assert bytes(trie(buffer)) == bytes(eels(buffer)) diff --git a/packages/testing/src/execution_testing/client_clis/cli_types.py b/packages/testing/src/execution_testing/client_clis/cli_types.py index 769ccfec632..fbb47bfaf9b 100644 --- a/packages/testing/src/execution_testing/client_clis/cli_types.py +++ b/packages/testing/src/execution_testing/client_clis/cli_types.py @@ -428,8 +428,8 @@ def validate(self) -> Alloc: """Validate the alloc.""" raise NotImplementedError("validate method not implemented.") - def get(self) -> Alloc: - """Model validate the allocation and return it.""" + def materialize(self) -> Alloc: + """Materialize the allocation, validating it on first access.""" if self.alloc is None: self.alloc = self.validate() return self.alloc @@ -438,6 +438,28 @@ def state_root(self) -> Hash: """Return state root of the allocation.""" return self._state_root + def serialize(self, **model_dump_config: Any) -> str: + """ + Serialize the allocation to a JSON string. + + The default materializes the ``Alloc`` and dumps it. Subclasses + backed by already-serialized data override this to return their + cache directly and skip the round trip through ``Alloc``. + """ + return self.materialize().model_dump_json(**model_dump_config) + + def serialize_to_file( + self, file_path: Path, **model_dump_config: Any + ) -> None: + """ + Serialize the allocation to ``file_path`` as JSON. + + Writes whatever :meth:`serialize` produces. ``LazyAllocFile`` + overrides this with a byte-for-byte copy that avoids building + the JSON string at all. + """ + file_path.write_text(self.serialize(**model_dump_config)) + JSONDict = Dict[str, Any] @@ -453,6 +475,19 @@ def validate(self) -> Alloc: """Validate the alloc.""" return Alloc.model_validate(self.raw) + def serialize(self, **model_dump_config: Any) -> str: + """ + Dump the cached JSON dict without round-tripping through ``Alloc``. + + Only ``indent`` applies; the dict is already-serialized data, so + pydantic options such as ``by_alias`` / ``exclude_none`` are moot. + """ + return json.dumps( + self.raw, + ensure_ascii=True, + indent=model_dump_config.get("indent"), + ) + class LazyAllocStr(LazyAlloc[str]): """ @@ -465,6 +500,11 @@ def validate(self) -> Alloc: """Validate the alloc.""" return Alloc.model_validate_json(self.raw) + def serialize(self, **model_dump_config: Any) -> str: + """Return the cached JSON string verbatim (no re-serialization).""" + del model_dump_config # raw already encodes its own formatting + return self.raw + @dataclass(kw_only=True) class LazyAllocFile(LazyAlloc[Path]): @@ -484,7 +524,7 @@ class LazyAllocFile(LazyAlloc[Path]): LazyAllocFile is dropped. That lets a chained next-block t8n call consume the alloc directly from disk (via ``--input.alloc=`` for geth, or ``shutil.copyfile`` for filesystem t8ns) without round-tripping - through ``Alloc.get().model_dump_json()`` in Python. + through ``LazyAlloc.materialize().model_dump_json()`` in Python. """ _keepalive: Optional[tempfile.TemporaryDirectory] = field(default=None) @@ -514,6 +554,46 @@ def validate(self) -> Alloc: ) return Alloc.model_validate(accumulated) + def serialize_to_file( + self, file_path: Path, **model_dump_config: Any + ) -> None: + """ + Copy the backing file byte-for-byte, avoiding a parse/dump cycle. + + If the backing temp dir was already cleaned up (e.g. a + chained-block t8n consumed it on the next block), fall back to + dumping the cached ``Alloc`` so debug output still captures the + input. + """ + if Path(self.raw).exists(): + shutil.copyfile(self.raw, file_path) + else: + super().serialize_to_file(file_path, **model_dump_config) + + +@dataclass(kw_only=True) +class MaterializedAlloc(LazyAlloc[None]): + """ + Allocation already materialized in memory; ``get()`` is a no-op. + + Used by in-process transition tools (EELS) whose ``Alloc`` never + exists in a serialized form — hence ``raw`` is ``None``. The + ``alloc`` field must be provided at construction, so ``get()`` + always short-circuits and ``validate()`` is unreachable. + """ + + raw: None = None + + def __post_init__(self) -> None: + """Require the materialized alloc at construction.""" + assert self.alloc is not None, ( + "MaterializedAlloc requires `alloc` at construction" + ) + + def validate(self) -> Alloc: + """Unreachable: ``alloc`` is always set at construction.""" + raise AssertionError("unreachable: alloc is set at construction") + @dataclass class TransitionToolInput: @@ -534,16 +614,15 @@ def to_files( For ``LazyAllocFile`` inputs whose backing file is still on disk (chained-block handoff: previous t8n call's temp dir is pinned via the keepalive field), the alloc is copied byte-for-byte rather than - round-tripped through ``Alloc.get().model_dump_json()``. + round-tripped through ``LazyAlloc.materialize().model_dump_json()``. """ alloc_path = directory_path / "alloc.json" - if ( - isinstance(self.alloc, LazyAllocFile) - and Path(self.alloc.raw).exists() - ): - shutil.copyfile(self.alloc.raw, alloc_path) + if isinstance(self.alloc, LazyAlloc): + self.alloc.serialize_to_file(alloc_path, **model_dump_config) else: - alloc_path.write_text(self._serialize_alloc(**model_dump_config)) + alloc_path.write_text( + self.alloc.model_dump_json(**model_dump_config) + ) env_contents = self.env.model_dump_json(**model_dump_config) txs_contents = ( @@ -570,13 +649,9 @@ def to_files( def _serialize_alloc(self, **model_dump_config: Any) -> str: """Serialize ``self.alloc`` to a JSON string.""" - if isinstance(self.alloc, Alloc): - return self.alloc.model_dump_json(**model_dump_config) - if isinstance(self.alloc, LazyAllocStr): - return self.alloc.raw - if isinstance(self.alloc, LazyAllocFile): - return self.alloc.get().model_dump_json(**model_dump_config) - raise Exception(f"Invalid alloc type: {type(self.alloc)}") + if isinstance(self.alloc, LazyAlloc): + return self.alloc.serialize(**model_dump_config) + return self.alloc.model_dump_json(**model_dump_config) def model_dump_json( self, *, exclude_alloc: bool = False, **model_dump_config: Any @@ -623,7 +698,7 @@ def model_dump(self, mode: str, **model_dump_config: Any) -> Any: elif isinstance(self.alloc, LazyAllocJson): alloc_contents = self.alloc.raw elif isinstance(self.alloc, LazyAllocFile): - alloc_contents = self.alloc.get().model_dump( + alloc_contents = self.alloc.materialize().model_dump( mode=mode, **model_dump_config ) else: @@ -681,7 +756,7 @@ def model_validate_files( different JSON file. `alloc.json` is referenced by path and parsed incrementally on - `.get()` via `LazyAllocFile`, so the full file is never held in + `.materialize()` via `LazyAllocFile`, so the full file is never held in memory alongside the validated `Alloc`. """ result_data = (directory_path / "result.json").read_text() diff --git a/packages/testing/src/execution_testing/client_clis/clis/besu.py b/packages/testing/src/execution_testing/client_clis/clis/besu.py index c4c17101d5d..f4d5114579a 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/besu.py +++ b/packages/testing/src/execution_testing/client_clis/clis/besu.py @@ -278,7 +278,7 @@ def _evaluate( dump_files_to_directory( debug_output_path, { - "output/alloc.json": output.alloc.raw, + "output/alloc.json": output.alloc, "output/result.json": output.result.model_dump( mode="json", **model_dump_config ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py index f0be44460e6..4d5d7d81865 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py +++ b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py @@ -2,15 +2,16 @@ Ethereum Specs EVM Transition Tool Interface. """ -import json import tempfile -from io import StringIO from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional from typing_extensions import override -from execution_testing.client_clis.cli_types import TransitionToolOutput +from execution_testing.client_clis.cli_types import ( + OpcodeCount, + TransitionToolOutput, +) from execution_testing.client_clis.file_utils import ( dump_files_to_directory, ) @@ -92,84 +93,76 @@ def _evaluate( profiler: Profiler, ) -> TransitionToolOutput: """ - Evaluate using the EELS T8N entry point. + Evaluate using the EELS T8N entry point in-process. + + ``transition_tool_data`` is handed to ``T8N`` as-is — fork, + chain_id, reward, state_test, blob_schedule all flow through + — and ``T8N.run()`` returns the ``TransitionToolOutput`` + directly. """ - from ethereum_spec_tools.evm_tools import create_parser from ethereum_spec_tools.evm_tools.t8n import T8N + from ethereum_spec_tools.evm_tools.t8n.evm_trace.count import ( + CountTracer, + ) + from ethereum_spec_tools.evm_tools.t8n.evm_trace.eip3155 import ( + Eip3155Tracer, + ) + from ethereum_spec_tools.evm_tools.t8n.evm_trace.group import ( + GroupTracer, + ) del slow_request, profiler - request_data = transition_tool_data.get_request_data() - request_data_json = request_data.model_dump( - mode="json", **model_dump_config - ) temp_dir = tempfile.TemporaryDirectory() - t8n_args = [ - "t8n", - "--input.alloc=stdin", - "--input.env=stdin", - "--input.txs=stdin", - "--output.result=stdout", - "--output.body=stdout", - "--output.alloc=stdout", - f"--output.basedir={temp_dir.name}", - f"--state.fork={request_data_json['state']['fork']}", - f"--state.chainid={request_data_json['state']['chainid']}", - f"--state.reward={request_data_json['state']['reward']}", - ] - - if transition_tool_data.state_test: - t8n_args.append("--state-test") - - if transition_tool_data.blob_params: - fork = transition_tool_data.fork - if fork.bpo_fork() and fork != fork.non_bpo_ancestor(): - # Only send this information for BPO forks. - # TODO: This should be optimized by the t8n tool instead. - t8n_args.append("--input.blobParams=stdin") - - if self.supports_opcode_count: - t8n_args.append("--opcode.count=stdout") + tracers = None if self.trace: - t8n_args.extend( - [ - "--trace", - "--trace.memory", - "--trace.returndata", - ] + # TODO: Eip3155 traces still round-trip through tempfile + # JSON — the tracer writes one ``trace-.jsonl`` per tx + # to ``output_basedir`` and ``collect_traces`` reads them + # back. Same JSON round-trip we eliminated for alloc / + # result / body; a follow-up should wire the tracer + # output through memory like the rest of the in-process + # path. + tracers = GroupTracer() + tracers.add( + Eip3155Tracer( + trace_memory=True, + trace_stack=True, + trace_return_data=True, + output_basedir=temp_dir.name, + ) ) - parser = create_parser() - t8n_options = parser.parse_args(t8n_args) - - out_stream = StringIO() - - in_stream = StringIO(json.dumps(request_data_json["input"])) - - t8n = T8N(t8n_options, out_stream, in_stream, self.fork_cache) - t8n.run() - - output_dict = json.loads(out_stream.getvalue()) + count_tracer = None + if self.supports_opcode_count: + count_tracer = CountTracer() + if tracers is None: + tracers = GroupTracer() + tracers.add(count_tracer) + + t8n = T8N( + transition_tool_data, + cache=self.fork_cache, + tracers=tracers, + exception_mapper=self.exception_mapper, + ) + output = t8n.run() - if "opcodeCount" in output_dict and "result" in output_dict: - output_dict["result"]["opcodeCount"] = output_dict.pop( - "opcodeCount" + if count_tracer is not None: + output.result.opcode_count = OpcodeCount.model_validate( + count_tracer.results() ) - output: TransitionToolOutput = TransitionToolOutput.model_validate( - output_dict, context={"exception_mapper": self.exception_mapper} - ) - if debug_output_path: dump_files_to_directory( debug_output_path, { - "input/alloc.json": request_data.input.alloc, - "input/env.json": request_data.input.env, + "input/alloc.json": transition_tool_data.alloc, + "input/env.json": transition_tool_data.env, "input/txs.json": [ tx.model_dump(mode="json", **model_dump_config) - for tx in request_data.input.txs + for tx in transition_tool_data.txs ], }, ) diff --git a/packages/testing/src/execution_testing/client_clis/file_utils.py b/packages/testing/src/execution_testing/client_clis/file_utils.py index 47c1232dfbc..190700b1d58 100644 --- a/packages/testing/src/execution_testing/client_clis/file_utils.py +++ b/packages/testing/src/execution_testing/client_clis/file_utils.py @@ -1,7 +1,6 @@ """Methods to work with the filesystem and json.""" import os -import shutil import stat from json import dump from pathlib import Path @@ -10,9 +9,7 @@ from pydantic import BaseModel, RootModel from execution_testing.client_clis.cli_types import ( - LazyAllocFile, - LazyAllocJson, - LazyAllocStr, + LazyAlloc, TransitionToolInput, ) @@ -30,28 +27,13 @@ def dump_files_to_directory(output_path: Path, files: Dict[str, Any]) -> None: if rel_path: os.makedirs(output_path / rel_path, exist_ok=True) file_path = output_path / file_rel_path - if ( - isinstance(file_contents, LazyAllocFile) - and Path(file_contents.raw).exists() - ): - shutil.copyfile(file_contents.raw, file_path) - elif isinstance(file_contents, LazyAllocFile): - # Backing temp dir was cleaned up after a previous `.get()` - # (e.g. chained-block t8n on the next block); fall back to - # the cached Alloc so debug dumps still capture the input. - file_path.write_text( - file_contents.get().model_dump_json( - indent=4, exclude_none=True, by_alias=True - ) + if isinstance(file_contents, LazyAlloc): + file_contents.serialize_to_file( + file_path, indent=4, exclude_none=True, by_alias=True ) else: with open(file_path, "w") as f: - if isinstance(file_contents, (LazyAllocStr, LazyAllocJson)): - if isinstance(file_contents, LazyAllocJson): - dump(file_contents.raw, f, ensure_ascii=True, indent=4) - else: - f.write(file_contents.raw) - elif isinstance( + if isinstance( file_contents, (BaseModel, RootModel, TransitionToolInput) ): f.write( diff --git a/packages/testing/src/execution_testing/client_clis/tests/test_execution_specs.py b/packages/testing/src/execution_testing/client_clis/tests/test_execution_specs.py index a013da0548f..2d88da5e8c1 100644 --- a/packages/testing/src/execution_testing/client_clis/tests/test_execution_specs.py +++ b/packages/testing/src/execution_testing/client_clis/tests/test_execution_specs.py @@ -183,7 +183,7 @@ def test_evm_t8n( blob_schedule=Berlin.blob_schedule(), ), ) - assert to_json(t8n_output.alloc.get()) == expected.get("alloc") + assert to_json(t8n_output.alloc.materialize()) == expected.get("alloc") t8n_result = to_json(t8n_output.result) if isinstance(default_t8n, ExecutionSpecsTransitionTool): # The expected output was generated with geth, instead of deleting diff --git a/packages/testing/src/execution_testing/client_clis/tests/test_transition_tool.py b/packages/testing/src/execution_testing/client_clis/tests/test_transition_tool.py index 3c7c1b281df..4e7a7e82ff3 100644 --- a/packages/testing/src/execution_testing/client_clis/tests/test_transition_tool.py +++ b/packages/testing/src/execution_testing/client_clis/tests/test_transition_tool.py @@ -133,7 +133,7 @@ def test_unknown_binary_path() -> None: def test_lazy_alloc(ty: Type[LazyAlloc], raw: Any) -> None: """Test LazyAlloc types.""" lazy_instance = ty(raw=raw, _state_root=TEST_ALLOC_STATE_ROOT) - assert lazy_instance.get() == TEST_ALLOC + assert lazy_instance.materialize() == TEST_ALLOC assert lazy_instance.state_root() == TEST_ALLOC_STATE_ROOT @@ -144,7 +144,7 @@ def test_lazy_alloc_file(tmp_path: Path) -> None: lazy_instance = LazyAllocFile( raw=alloc_path, _state_root=TEST_ALLOC_STATE_ROOT ) - assert lazy_instance.get() == TEST_ALLOC + assert lazy_instance.materialize() == TEST_ALLOC assert lazy_instance.state_root() == TEST_ALLOC_STATE_ROOT @@ -169,7 +169,7 @@ def test_lazy_alloc_file_handles_mixed_entries(tmp_path: Path) -> None: alloc_path = tmp_path / "alloc.json" alloc_path.write_text(alloc.model_dump_json()) lazy_instance = LazyAllocFile(raw=alloc_path, _state_root=state_root) - assert lazy_instance.get() == alloc + assert lazy_instance.materialize() == alloc assert lazy_instance.state_root() == state_root @@ -202,7 +202,7 @@ def test_model_validate_files_uses_lazy_alloc_file(tmp_path: Path) -> None: assert isinstance(output.alloc, LazyAllocFile) assert output.alloc.raw == alloc_path - assert output.alloc.get() == TEST_ALLOC + assert output.alloc.materialize() == TEST_ALLOC def test_transition_tool_input_serializes_lazy_alloc_file( @@ -241,7 +241,7 @@ def test_to_files_copies_chained_lazy_alloc_file_without_serialize( """ Chained-block handoff: `to_files` should copy the backing alloc file byte-for-byte rather than round-tripping through - `LazyAllocFile.get().model_dump_json()`. Verified by populating the + `LazyAllocFile.materialize().model_dump_json()`. Verified by populating the file with bytes that don't match what pydantic would re-emit and asserting those exact bytes survive the dump. """ @@ -313,7 +313,7 @@ def test_lazy_alloc_file_keepalive_pins_temp_dir() -> None: # Releasing our handle leaves the file alive via the keepalive on lazy. del keep assert alloc_path.exists() - assert lazy.get() == TEST_ALLOC + assert lazy.materialize() == TEST_ALLOC # Dropping the LazyAllocFile drops the keepalive; TemporaryDirectory's # finalizer wipes the directory. PyPy doesn't refcount, so trigger GC @@ -351,10 +351,10 @@ def test_dump_files_to_directory_lazy_alloc_file_after_backing_removed( ) -> None: """ On chained blocks, the previous block's t8n temp dir is cleaned up after - its alloc is materialized via ``.get()``. The resulting ``LazyAllocFile`` - still carries a now-stale ``.raw`` path. Debug dumps must fall back to - re-serializing the cached ``Alloc`` instead of attempting to copy the - missing backing file. + its alloc is materialized via ``.materialize()``. The resulting + ``LazyAllocFile`` still carries a now-stale ``.raw`` path. Debug dumps must + fall back to re-serializing the cached ``Alloc`` instead of attempting to + copy the missing backing file. """ from execution_testing.client_clis.file_utils import ( dump_files_to_directory, @@ -363,7 +363,7 @@ def test_dump_files_to_directory_lazy_alloc_file_after_backing_removed( source = tmp_path / "source_alloc.json" source.write_text(TEST_ALLOC.model_dump_json()) lazy = LazyAllocFile(raw=source, _state_root=TEST_ALLOC_STATE_ROOT) - lazy.get() + lazy.materialize() source.unlink() dump_dir = tmp_path / "dump" @@ -397,7 +397,7 @@ def test_lazy_alloc_file_malformed_json_raises( lazy = LazyAllocFile(raw=alloc_path, _state_root=TEST_ALLOC_STATE_ROOT) with pytest.raises(ijson.common.IncompleteJSONError): - lazy.get() + lazy.materialize() @pytest.mark.parametrize( @@ -422,7 +422,7 @@ def test_lazy_alloc_file_non_object_top_level_raises( lazy = LazyAllocFile(raw=alloc_path, _state_root=TEST_ALLOC_STATE_ROOT) with pytest.raises(ValueError, match="Expected JSON object"): - lazy.get() + lazy.materialize() def test_lazy_alloc_file_empty_object_yields_empty_alloc( @@ -436,7 +436,7 @@ def test_lazy_alloc_file_empty_object_yields_empty_alloc( alloc_path.write_bytes(b"{}") lazy = LazyAllocFile(raw=alloc_path, _state_root=TEST_ALLOC_STATE_ROOT) - assert lazy.get() == Alloc.model_validate({}) + assert lazy.materialize() == Alloc.model_validate({}) def _output_with_opcode_count(counts: dict) -> TransitionToolOutput: diff --git a/packages/testing/src/execution_testing/client_clis/transition_tool.py b/packages/testing/src/execution_testing/client_clis/transition_tool.py index c69e53cf382..4417d8f7682 100644 --- a/packages/testing/src/execution_testing/client_clis/transition_tool.py +++ b/packages/testing/src/execution_testing/client_clis/transition_tool.py @@ -169,7 +169,7 @@ def set(self, subkey: int, value: TransitionToolOutput) -> None: # Without this, every cached subcall would retain its own # `output/alloc.json` on disk for the test's lifetime - O(N) for # an N-block chained test. - alloc.get() + alloc.materialize() alloc._keepalive = None self._cache[subkey] = value @@ -671,7 +671,7 @@ def _evaluate_server( dump_files_to_directory( debug_output_path, { - "output/alloc.json": output.alloc.raw, + "output/alloc.json": output.alloc, "output/result.json": output.result, "output/txs.rlp": str(output.body), "response_info.txt": response_info, diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 6eea651a26a..09913d04b9b 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -450,7 +450,7 @@ class BuiltBlock(CamelModel): header: FixtureHeader env: Environment - alloc: LazyAlloc + alloc: LazyAlloc | Alloc state_root: Hash txs: List[Transaction] ommers: List[FixtureHeader] @@ -1084,7 +1084,7 @@ def generate_block_data( print_traces(t8n.get_traces()) pprint(transition_tool_output.result) pprint(previous_alloc) - pprint(transition_tool_output.alloc.get()) + pprint(transition_tool_output.alloc.materialize()) raise e if len(rejected_txs) > 0 and block.exception is None: @@ -1179,13 +1179,13 @@ def make_fixture( if block.expected_post_state: self.verify_post_state( t8n, - t8n_state=alloc.get() + t8n_state=alloc.materialize() if isinstance(alloc, LazyAlloc) else alloc, expected_state=block.expected_post_state, ) self.check_exception_test(exception=invalid_blocks > 0) - alloc = alloc.get() if isinstance(alloc, LazyAlloc) else alloc + alloc = alloc.materialize() if isinstance(alloc, LazyAlloc) else alloc self.verify_post_state(t8n, t8n_state=alloc) fixture = BlockchainFixture( fork=self.fork, @@ -1269,7 +1269,7 @@ def make_hive_fixture( if block.expected_post_state: self.verify_post_state( t8n, - t8n_state=alloc.get() + t8n_state=alloc.materialize() if isinstance(alloc, LazyAlloc) else alloc, expected_state=block.expected_post_state, @@ -1283,7 +1283,7 @@ def make_hive_fixture( " The framework should never try to execute this test case." ) - alloc = alloc.get() if isinstance(alloc, LazyAlloc) else alloc + alloc = alloc.materialize() if isinstance(alloc, LazyAlloc) else alloc self.verify_post_state(t8n, t8n_state=alloc) # Create base fixture data, common to all fixture formats diff --git a/packages/testing/src/execution_testing/specs/state.py b/packages/testing/src/execution_testing/specs/state.py index 6e13e07480a..ac050f0c9ba 100644 --- a/packages/testing/src/execution_testing/specs/state.py +++ b/packages/testing/src/execution_testing/specs/state.py @@ -167,7 +167,7 @@ def verify_modified_gas_limit( f"Traces are not equivalent (gas_limit={current_gas_limit})" ) return False - modified_tool_alloc = modified_tool_output.alloc.get() + modified_tool_alloc = modified_tool_output.alloc.materialize() try: self.post.verify_post_alloc(modified_tool_alloc) except Exception as e: @@ -378,7 +378,7 @@ def make_state_test_fixture( ), slow_request=self.is_tx_gas_heavy_test, ) - output_alloc = transition_tool_output.alloc.get() + output_alloc = transition_tool_output.alloc.materialize() try: self.post.verify_post_alloc(output_alloc) @@ -408,7 +408,7 @@ def make_state_test_fixture( self.operation_mode == OpMode.OPTIMIZE_GAS_POST_PROCESSING ) base_tool_output = transition_tool_output - base_tool_alloc = base_tool_output.alloc.get() + base_tool_alloc = base_tool_output.alloc.materialize() base_tool_result = base_tool_output.result assert base_tool_result.traces is not None, "Traces not found." diff --git a/packages/testing/src/execution_testing/test_types/account_types.py b/packages/testing/src/execution_testing/test_types/account_types.py index 349a8a715e0..f0c51791e51 100644 --- a/packages/testing/src/execution_testing/test_types/account_types.py +++ b/packages/testing/src/execution_testing/test_types/account_types.py @@ -1,9 +1,10 @@ """Account-related types for Ethereum tests.""" import json -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum, auto from typing import ( + AbstractSet, Any, Dict, ItemsView, @@ -15,14 +16,20 @@ Tuple, ) -from ethereum_types.bytes import Bytes20 +import ethereum.state as spec_state +from ethereum.crypto.hash import Hash32 +from ethereum.crypto.hash import keccak256 as spec_keccak256 +from ethereum.merkle_patricia_trie import InternalNode +from ethereum_types.bytes import Bytes, Bytes20 from ethereum_types.numeric import U256, Bytes32, Uint +from pydantic import PrivateAttr from spec256k1 import PrivateKey from execution_testing.base_types import ( Account, Address, Hash, + HashInt, Number, Storage, StorageRootType, @@ -34,82 +41,22 @@ NumberConvertible, ) -from .trie import ( - EMPTY_TRIE_ROOT, - FrontierAccount, - Trie, - root, - trie_get, - trie_set, -) from .utils import keccak256 -FrontierAddress = Bytes20 - - -@dataclass -class State: - """Contains all information that is preserved between transactions.""" - - _main_trie: Trie[Bytes20, Optional[FrontierAccount]] = field( - default_factory=lambda: Trie(secured=True, default=None) - ) - _storage_tries: Dict[Bytes20, Trie[Bytes32, U256]] = field( - default_factory=dict - ) - _snapshots: List[ - Tuple[ - Trie[Bytes20, Optional[FrontierAccount]], - Dict[Bytes20, Trie[Bytes32, U256]], - ] - ] = field(default_factory=list) - -def set_account( - state: State, address: Bytes20, account: Optional[FrontierAccount] -) -> None: +class _Phase(Enum): """ - Set the `Account` object at an address. Setting to `None` deletes the - account (but not its storage, see `destroy_account()`). - """ - trie_set(state._main_trie, address, account) - + Lifecycle phase of an `Alloc` instance used as a `PreState`. -def set_storage( - state: State, address: Bytes20, key: Bytes32, value: U256 -) -> None: - """ - Set a value at a storage key on an account. Setting to `U256(0)` deletes - the key. + See `Alloc` for the rules each phase enforces. """ - assert trie_get(state._main_trie, address) is not None - - trie = state._storage_tries.get(address) - if trie is None: - trie = Trie(secured=True, default=U256(0)) - state._storage_tries[address] = trie - trie_set(trie, key, value) - if trie._data == {}: - del state._storage_tries[address] - - -def storage_root(state: State, address: Bytes20) -> Bytes32: - """Calculate the storage root of an account.""" - assert not state._snapshots - if address in state._storage_tries: - return root(state._storage_tries[address]) - else: - return EMPTY_TRIE_ROOT - -def state_root(state: State) -> Bytes32: - """Calculate the state root.""" - assert not state._snapshots - - def get_storage_root(address: Bytes20) -> Bytes32: - return storage_root(state, address) - - return root(state._main_trie, get_storage_root=get_storage_root) + CONSTRUCTION = auto() + """Free mutations on `self.root` are allowed; no cache exists.""" + LIVE = auto() + """Cache built; only `apply_diff` may mutate.""" + FROZEN = auto() + """No mutations are allowed.""" class EOA(Address): @@ -161,7 +108,20 @@ def copy(self) -> Self: class Alloc(BaseAlloc): - """Allocation of accounts in the state, pre and post test execution.""" + """ + Allocation of accounts in the state, pre and post test execution. + + Doubles as a `PreState` provider for the spec's state transition: once + any `PreState` method is called the instance transitions from + `CONSTRUCTION` to `LIVE` (a code-hash → bytes cache is built once) and + further free mutations via `__setitem__`/`__delitem__` are rejected. + The only mutation entry point in `LIVE` is `apply_diff`, which patches + `self.root` and updates the cache in lockstep. `freeze` locks the + allocation for read-only assertion use. + """ + + _phase: _Phase = PrivateAttr(default=_Phase.CONSTRUCTION) + _code_store: Dict[Hash32, Bytes] = PrivateAttr(default_factory=dict) @dataclass(kw_only=True) class UnexpectedAccountError(Exception): @@ -298,6 +258,7 @@ def __setitem__( account: Account | None, ) -> None: """Set account associated with an address.""" + self._require_construction("__setitem__") if not isinstance(address, Address): address = Address(address) self.root[address] = account @@ -306,6 +267,7 @@ def __delitem__( self, address: Address | FixedSizeBytesConvertible ) -> None: """Delete account associated with an address.""" + self._require_construction("__delitem__") if not isinstance(address, Address): address = Address(address) self.root.pop(address, None) @@ -339,34 +301,7 @@ def empty_accounts(self) -> List[Address]: def state_root(self) -> Hash: """Return state root of the allocation.""" - state = State() - for address, account in self.root.items(): - if account is None: - continue - set_account( - state=state, - address=FrontierAddress(address), - account=FrontierAccount( - nonce=Uint(account.nonce) - if account.nonce is not None - else Uint(0), - balance=( - U256(account.balance) - if account.balance is not None - else U256(0) - ), - code=account.code if account.code is not None else b"", - ), - ) - if account.storage is not None: - for key, value in account.storage.root.items(): - set_storage( - state=state, - address=FrontierAddress(address), - key=Bytes32(Hash(key)), - value=U256(value), - ) - return Hash(state_root(state)) + return Hash(spec_state.state_root(self._materialize_state())) def verify_post_alloc(self, got_alloc: "Alloc") -> None: """ @@ -393,6 +328,247 @@ def verify_post_alloc(self, got_alloc: "Alloc") -> None: else: raise Alloc.MissingAccountError(address=address) + # ------------------------------------------------------------------ + # PreState protocol implementation + # ------------------------------------------------------------------ + + def _require_construction(self, operation: str) -> None: + """Reject mutations once the allocation has left construction.""" + if self._phase is not _Phase.CONSTRUCTION: + raise RuntimeError( + f"{operation} not allowed: Alloc is in phase " + f"{self._phase.name}. Mutate via apply_diff during LIVE, " + f"or call freeze() to lock the allocation." + ) + + def _build_cache(self) -> None: + """Populate the code-hash → bytes cache from `self.root`.""" + self._code_store = {spec_state.EMPTY_CODE_HASH: Bytes(b"")} + for account in self.root.values(): + if account is None: + continue + code = bytes(account.code) if account.code else b"" + if not code: + continue + self._code_store[spec_keccak256(code)] = Bytes(code) + + def _ensure_live(self) -> None: + """Transition from `CONSTRUCTION` to `LIVE`, building the cache.""" + if self._phase is _Phase.CONSTRUCTION: + self._build_cache() + self._phase = _Phase.LIVE + + def _materialize_state(self) -> spec_state.State: + """ + Build an in-memory `ethereum.state.State` mirror of `self.root`. + + Used as the trie-backed delegate for + `compute_state_root_and_trie_changes` (a cold, once-per-block call). + The materialized state is not retained. + """ + state = spec_state.State() + for address, account in self.root.items(): + if account is None: + continue + addr = Bytes20(address) + code = bytes(account.code) if account.code else b"" + code_hash = ( + spec_keccak256(code) if code else spec_state.EMPTY_CODE_HASH + ) + spec_state.set_account( + state, + addr, + spec_state.Account( + nonce=Uint(int(account.nonce)), + balance=U256(int(account.balance)), + code_hash=code_hash, + ), + ) + for key_hi, value_hi in account.storage.root.items(): + value_int = int(value_hi) + if value_int == 0: + continue + spec_state.set_storage( + state, + addr, + Bytes32(int(key_hi).to_bytes(32, "big")), + U256(value_int), + ) + state._code_store.update(self._code_store) + return state + + def get_account_optional( + self, address: Bytes20 + ) -> Optional[spec_state.Account]: + """ + Return the spec-side `Account` at `address`, or `None`. + + Conforms to `ethereum.state.PreState.get_account_optional`. + """ + self._ensure_live() + account = self.root.get(Address(address)) + if account is None: + return None + code = bytes(account.code) if account.code else b"" + code_hash = ( + spec_keccak256(code) if code else spec_state.EMPTY_CODE_HASH + ) + return spec_state.Account( + nonce=Uint(int(account.nonce)), + balance=U256(int(account.balance)), + code_hash=code_hash, + ) + + def get_storage(self, address: Bytes20, key: Bytes32) -> U256: + """ + Return the storage value at `key` for `address`, or `U256(0)`. + + Conforms to `ethereum.state.PreState.get_storage`. + """ + self._ensure_live() + account = self.root.get(Address(address)) + if account is None: + return U256(0) + key_int = int.from_bytes(bytes(key), "big") + value_hi = account.storage.root.get(HashInt(key_int)) + if value_hi is None: + return U256(0) + return U256(int(value_hi)) + + def get_code(self, code_hash: Hash32) -> Bytes: + """ + Return the bytecode for `code_hash`. + + Conforms to `ethereum.state.PreState.get_code`. + """ + self._ensure_live() + if code_hash == spec_state.EMPTY_CODE_HASH: + return Bytes(b"") + return self._code_store[code_hash] + + def account_has_storage(self, address: Bytes20) -> bool: + """ + Return whether the account at `address` has any storage slots set. + + Conforms to `ethereum.state.PreState.account_has_storage`. + """ + self._ensure_live() + account = self.root.get(Address(address)) + return account is not None and bool(account.storage.root) + + def compute_state_root_and_trie_changes( + self, + account_changes: Dict[Bytes20, Optional[spec_state.Account]], + storage_changes: Dict[Bytes20, Dict[Bytes32, U256]], + storage_clears: AbstractSet[Bytes20] = frozenset(), + ) -> Tuple[Hash32, List["InternalNode"]]: + """ + Compute the state root after applying `*_changes` to the pre-state. + + Conforms to + `ethereum.state.PreState.compute_state_root_and_trie_changes`. + Builds the trie inline; `Alloc` does not cache `Trie` instances. + """ + self._ensure_live() + state = self._materialize_state() + return state.compute_state_root_and_trie_changes( + account_changes, storage_changes, storage_clears + ) + + # ------------------------------------------------------------------ + # Lifecycle: apply_diff and freeze + # ------------------------------------------------------------------ + + def apply_diff(self, diff: spec_state.BlockDiff) -> None: + """ + Apply a `BlockDiff` to mutate the allocation in place. + + The only mutation entry point in the `LIVE` phase. Writes bypass + `__setitem__` intentionally — `_code_store` is updated additively + in lockstep with `self.root`. + """ + if self._phase is _Phase.FROZEN: + raise RuntimeError("apply_diff not allowed: Alloc is FROZEN") + if self._phase is _Phase.CONSTRUCTION: + raise RuntimeError( + "apply_diff not allowed in CONSTRUCTION: the allocation " + "has not been used as a PreState yet, so its cache is not " + "built. Trigger a PreState method (or hand it to a " + "BlockState) before calling apply_diff." + ) + + for code_hash, code in diff.code_changes.items(): + self._code_store[Hash32(code_hash)] = Bytes(code) + + for address in diff.storage_clears: + addr = Address(address) + current = self.root.get(addr) + if current is not None and current.storage.root: + self.root[addr] = current.model_copy( + update={"storage": Storage(root={})} + ) + + for address, spec_account in diff.account_changes.items(): + addr = Address(address) + if spec_account is None: + self.root.pop(addr, None) + continue + code_hash = Hash32(spec_account.code_hash) + if code_hash == spec_state.EMPTY_CODE_HASH: + code = Bytes(b"") + else: + code = self._code_store[code_hash] + existing = self.root.get(addr) + existing_storage = ( + existing.storage if existing is not None else Storage(root={}) + ) + self.root[addr] = Account( + nonce=int(spec_account.nonce), + balance=int(spec_account.balance), + code=code, + storage=existing_storage, + ) + + for address, slots in diff.storage_changes.items(): + addr = Address(address) + account = self.root.get(addr) + if account is None: + continue + merged: Dict[HashInt, HashInt] = dict(account.storage.root) + for key, value in slots.items(): + key_int = HashInt(int.from_bytes(bytes(key), "big")) + value_int = int(value) + if value_int == 0: + merged.pop(key_int, None) + else: + merged[key_int] = HashInt(value_int) + self.root[addr] = account.model_copy( + update={"storage": Storage(root=merged)} + ) + + # Drop zero-valued storage entries from every account. Ethereum + # treats an absent slot as zero, so a literal ``{0x00: 0x00}`` + # pair carried over untouched from the pre-state JSON would + # otherwise survive into the post-state dump and produce noise + # the spec-state-backed pipeline never had (the spec's + # ``set_storage`` drops zeros on insert). + for addr, account in list(self.root.items()): + if account is None or not account.storage.root: + continue + cleaned = { + key: value + for key, value in account.storage.root.items() + if int(value) != 0 + } + if len(cleaned) != len(account.storage.root): + self.root[addr] = account.model_copy( + update={"storage": Storage(root=cleaned)} + ) + + def freeze(self) -> None: + """Lock the allocation: no further mutations allowed.""" + self._phase = _Phase.FROZEN + def deterministic_deploy_contract( self, *, diff --git a/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py b/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py new file mode 100644 index 00000000000..30cb39af3d6 --- /dev/null +++ b/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py @@ -0,0 +1,277 @@ +""" +Unit tests for `Alloc` acting as a `PreState` provider. + +Covers four invariants of the lifecycle phase machinery: + 1. The code-hash → bytes cache is built correctly when the alloc goes + LIVE, and the PreState read methods agree with the source dict. + 2. `compute_state_root_and_trie_changes` on `Alloc` matches the same + call on a freshly built `ethereum.state.State` over the same data. + 3. Mutating an `Alloc` via `__setitem__`/`__delitem__` is rejected + after it has been used as a PreState. + 4. Building alloc B by `apply_diff`ing a diff onto alloc A produces a + post-state whose root matches an alloc independently constructed + to look like the post-state. +""" + +from typing import Dict, Optional + +import ethereum.state as spec_state +import pytest +from ethereum.crypto.hash import keccak256 +from ethereum_types.bytes import Bytes20, Bytes32 +from ethereum_types.numeric import U256, Uint + +from execution_testing.base_types import Account +from execution_testing.test_types import Alloc +from execution_testing.test_types.account_types import _Phase + + +def _b20(hex_str: str) -> Bytes20: + """Build a `Bytes20` address from a 40-char hex string (no `0x`).""" + return Bytes20(bytes.fromhex(hex_str)) + + +# A small, fixed set of addresses for ergonomic reuse in tests. They are +# `Bytes20` so they satisfy the `PreState` protocol's address parameter +# type, and pydantic re-validates them into `Address` when used as `Alloc` +# keys. +ADDR_A = _b20("000000000000000000000000000000000000aaaa") +ADDR_B = _b20("000000000000000000000000000000000000bbbb") +ADDR_C = _b20("000000000000000000000000000000000000cccc") +ADDR_MISSING = _b20("dead000000000000000000000000000000000000") +CODE = bytes.fromhex("60016002") # PUSH1 1 PUSH1 2 + + +def _fixture_alloc() -> Alloc: + """Build a small alloc with one EOA, one contract, and one empty acct.""" + return Alloc.model_validate( + { + ADDR_A: {"balance": 100, "nonce": 1}, + ADDR_B: { + "balance": 7, + "nonce": 3, + "code": "0x" + CODE.hex(), + "storage": {1: 0x42, 2: 0xCAFE}, + }, + ADDR_C: {"balance": 0, "nonce": 0}, + } + ) + + +def _state_from_alloc(alloc: Alloc) -> spec_state.State: + """Build a spec `State` mirroring `alloc` for parity comparisons.""" + state = spec_state.State() + for address, account in alloc.root.items(): + if account is None: + continue + addr = Bytes20(address) + code = bytes(account.code) if account.code else b"" + code_hash = keccak256(code) if code else spec_state.EMPTY_CODE_HASH + spec_state.set_account( + state, + addr, + spec_state.Account( + nonce=Uint(int(account.nonce)), + balance=U256(int(account.balance)), + code_hash=code_hash, + ), + ) + if code: + state._code_store[code_hash] = code + for key_hi, value_hi in account.storage.root.items(): + if int(value_hi) == 0: + continue + spec_state.set_storage( + state, + addr, + Bytes32(int(key_hi).to_bytes(32, "big")), + U256(int(value_hi)), + ) + return state + + +def test_cache_build_and_read_methods_agree_with_source() -> None: + """PreState reads on the alloc agree with the source dict.""" + alloc = _fixture_alloc() + assert alloc._phase is _Phase.CONSTRUCTION + + # The first PreState call must transition the alloc to LIVE. + acct_b = alloc.get_account_optional(ADDR_B) + assert alloc._phase is _Phase.LIVE + assert acct_b is not None + assert acct_b.nonce == Uint(3) + assert acct_b.balance == U256(7) + assert acct_b.code_hash == keccak256(CODE) + + # _code_store contains the empty hash and the only contract's code. + assert alloc._code_store[spec_state.EMPTY_CODE_HASH] == b"" + assert alloc._code_store[keccak256(CODE)] == CODE + # EOA + empty account contribute no code entries. + assert len(alloc._code_store) == 2 + + # Storage reads agree with the source for set and unset keys. + assert alloc.get_storage(ADDR_B, Bytes32(b"\x00" * 31 + b"\x01")) == U256( + 0x42 + ) + assert alloc.get_storage(ADDR_B, Bytes32(b"\x00" * 31 + b"\x02")) == U256( + 0xCAFE + ) + assert alloc.get_storage(ADDR_B, Bytes32(b"\x00" * 31 + b"\x03")) == U256( + 0 + ) + # Account with no storage returns zero for any key. + assert alloc.get_storage(ADDR_A, Bytes32(b"\x00" * 32)) == U256(0) + # Missing account returns zero. + assert alloc.get_storage(ADDR_MISSING, Bytes32(b"\x00" * 32)) == U256(0) + + # get_code round-trips, including the empty-code sentinel. + assert alloc.get_code(spec_state.EMPTY_CODE_HASH) == b"" + assert alloc.get_code(keccak256(CODE)) == CODE + + # account_has_storage distinguishes the contract from EOAs. + assert alloc.account_has_storage(ADDR_B) is True + assert alloc.account_has_storage(ADDR_A) is False + assert alloc.account_has_storage(ADDR_MISSING) is False + + # Missing accounts return None from get_account_optional. + assert alloc.get_account_optional(ADDR_MISSING) is None + + +def test_state_root_parity_against_spec_state() -> None: + """`Alloc.compute_state_root_and_trie_changes` matches spec `State`.""" + alloc = _fixture_alloc() + state = _state_from_alloc(alloc) + + alloc_root, _ = alloc.compute_state_root_and_trie_changes({}, {}) + spec_root, _ = state.compute_state_root_and_trie_changes({}, {}) + assert alloc_root == spec_root + + # Same parity under non-trivial change sets. + account_changes: Dict[Bytes20, Optional[spec_state.Account]] = { + ADDR_A: spec_state.Account( + nonce=Uint(2), balance=U256(200), code_hash=keccak256(CODE) + ), + } + storage_changes: Dict[Bytes20, Dict[Bytes32, U256]] = { + ADDR_B: {Bytes32(b"\x00" * 31 + b"\x01"): U256(0x99)}, + } + alloc_root_changed, _ = alloc.compute_state_root_and_trie_changes( + account_changes, storage_changes + ) + spec_root_changed, _ = state.compute_state_root_and_trie_changes( + account_changes, storage_changes + ) + assert alloc_root_changed == spec_root_changed + assert alloc_root_changed != alloc_root + + +def test_phase_guard_rejects_mutation_after_live() -> None: + """`__setitem__` and `__delitem__` raise once the alloc is LIVE.""" + alloc = _fixture_alloc() + # Still in CONSTRUCTION — mutations are allowed. + alloc[_b20("000000000000000000000000000000000000dddd")] = Account( + balance=1 + ) + + # Any PreState read transitions to LIVE. + _ = alloc.get_account_optional(ADDR_A) + assert alloc._phase is _Phase.LIVE + + with pytest.raises(RuntimeError, match="not allowed"): + alloc[_b20("000000000000000000000000000000000000eeee")] = Account( + balance=1 + ) + + with pytest.raises(RuntimeError, match="not allowed"): + del alloc[ADDR_A] + + # freeze() locks further mutation including apply_diff. + alloc.freeze() + with pytest.raises(RuntimeError, match="FROZEN"): + alloc.apply_diff( + spec_state.BlockDiff( + account_changes={}, storage_changes={}, code_changes={} + ) + ) + + +def test_apply_diff_round_trip_matches_independent_post_state() -> None: + """A.apply_diff(diff) reproduces an independently built post-state.""" + new_code = bytes.fromhex("6005600555") # arbitrary, distinct from CODE + new_code_hash = keccak256(new_code) + + alloc_pre = _fixture_alloc() + + # Independently build the expected post-state: + # - ADDR_A: nonce 1 → 2, balance 100 → 50 + # - ADDR_B: keeps account, storage slot 1 cleared, slot 3 added, + # slot 2 left alone + # - ADDR_C: deleted + # - new ADDR_NEW: brand-new contract with `new_code` and a slot set + addr_new = _b20("000000000000000000000000000000000000ffff") + alloc_post_expected = Alloc.model_validate( + { + ADDR_A: {"balance": 50, "nonce": 2}, + ADDR_B: { + "balance": 7, + "nonce": 3, + "code": "0x" + CODE.hex(), + "storage": {2: 0xCAFE, 3: 0x77}, + }, + addr_new: { + "balance": 1, + "nonce": 1, + "code": "0x" + new_code.hex(), + "storage": {0: 0x11}, + }, + } + ) + + # Build the diff that, applied to alloc_pre, should produce + # alloc_post_expected. + diff = spec_state.BlockDiff( + account_changes={ + ADDR_A: spec_state.Account( + nonce=Uint(2), + balance=U256(50), + code_hash=spec_state.EMPTY_CODE_HASH, + ), + ADDR_C: None, + addr_new: spec_state.Account( + nonce=Uint(1), + balance=U256(1), + code_hash=new_code_hash, + ), + }, + storage_changes={ + ADDR_B: { + Bytes32(b"\x00" * 31 + b"\x01"): U256(0), + Bytes32(b"\x00" * 31 + b"\x03"): U256(0x77), + }, + addr_new: {Bytes32(b"\x00" * 32): U256(0x11)}, + }, + code_changes={new_code_hash: new_code}, + ) + + # Force LIVE so apply_diff is allowed. + _ = alloc_pre.get_account_optional(ADDR_A) + alloc_pre.apply_diff(diff) + + # State roots should match. + pre_root, _ = alloc_pre.compute_state_root_and_trie_changes({}, {}) + expected_root, _ = alloc_post_expected.compute_state_root_and_trie_changes( + {}, {} + ) + assert pre_root == expected_root + + # Cache must be updated additively with the new code. + assert alloc_pre._code_store[new_code_hash] == new_code + # The contract's pre-existing code is still cached too. + assert alloc_pre._code_store[keccak256(CODE)] == CODE + + # apply_diff is still allowed (alloc stays LIVE) for the next block. + alloc_pre.apply_diff( + spec_state.BlockDiff( + account_changes={}, storage_changes={}, code_changes={} + ) + ) diff --git a/packages/testing/src/execution_testing/test_types/trie.py b/packages/testing/src/execution_testing/test_types/trie.py deleted file mode 100644 index aec7206697e..00000000000 --- a/packages/testing/src/execution_testing/test_types/trie.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -The state trie is the structure responsible for storing Ethereum state. -""" - -import copy -from dataclasses import dataclass, field -from typing import ( - Callable, - Dict, - Generic, - List, - Mapping, - MutableMapping, - Optional, - Sequence, - Tuple, - TypeVar, - cast, -) - -from ethereum_rlp import Extended, rlp -from ethereum_types.bytes import Bytes, Bytes20, Bytes32 -from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U256, Uint -from typing_extensions import assert_type - - -def keccak256(buffer: bytes | bytearray) -> Bytes32: - """ - Compute the keccak256 hash of ``buffer``. - - The spec implementation is imported lazily so that importing this module - does not import the ``ethereum`` package: on xdist workers that import - would otherwise happen before pytest-cov starts the worker's coverage - session, making coverage report ``ethereum`` as "module-not-measured". - """ - from ethereum.crypto.hash import keccak256 as _keccak256 - - return _keccak256(buffer) - - -@slotted_freezable -@dataclass -class FrontierAccount: - """State associated with an address.""" - - nonce: Uint - balance: U256 - code: Bytes - - -def encode_account( - raw_account_data: FrontierAccount, storage_root: Bytes -) -> Bytes: - """ - Encode `Account` dataclass. - - Storage is not stored in the `Account` dataclass, so `Accounts` cannot be - encoded without providing a storage root. - """ - return rlp.encode( - ( - raw_account_data.nonce, - raw_account_data.balance, - storage_root, - keccak256(raw_account_data.code), - ) - ) - - -# note: an empty trie (regardless of whether it is secured) has root: -# keccak256(RLP(b'')) == -# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 -# also: -# keccak256(RLP(())) == -# 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 -# which is the sha3Uncles hash in block header with no uncles -EMPTY_TRIE_ROOT = Bytes32( - bytes.fromhex( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ) -) - -Node = FrontierAccount | Bytes | Uint | U256 | None -K = TypeVar("K", bound=Bytes) -V = TypeVar( - "V", - Optional[FrontierAccount], - Bytes, - Uint, - U256, -) - - -@slotted_freezable -@dataclass -class LeafNode: - """Leaf node in the Merkle Trie.""" - - rest_of_key: Bytes - value: Extended - - -@slotted_freezable -@dataclass -class ExtensionNode: - """Extension node in the Merkle Trie.""" - - key_segment: Bytes - subnode: Extended - - -BranchSubnodes = Tuple[ - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, - Extended, -] - - -@slotted_freezable -@dataclass -class BranchNode: - """Branch node in the Merkle Trie.""" - - subnodes: BranchSubnodes - value: Extended - - -InternalNode = LeafNode | ExtensionNode | BranchNode - - -def encode_internal_node(node: Optional[InternalNode]) -> Extended: - """ - Encode a Merkle Trie node into its RLP form. - - The RLP will then be serialized into a `Bytes` and hashed unless it is less - that 32 bytes when serialized. - - This function also accepts `None`, representing the absence of a node, - which is encoded to `b""`. - """ - unencoded: Extended - match node: - case None: - unencoded = b"" - case LeafNode(): - unencoded = ( - nibble_list_to_compact(node.rest_of_key, True), - node.value, - ) - case ExtensionNode(): - unencoded = ( - nibble_list_to_compact(node.key_segment, False), - node.subnode, - ) - case BranchNode(): - unencoded = list(node.subnodes) + [node.value] - case _: - raise AssertionError(f"Invalid internal node type {type(node)}!") - - encoded = rlp.encode(unencoded) - if len(encoded) < 32: - return unencoded - else: - return keccak256(encoded) - - -def encode_node(node: Node, storage_root: Optional[Bytes] = None) -> Bytes: - """ - Encode a Node for storage in the Merkle Trie. - - Currently mostly an unimplemented stub. - """ - match node: - case FrontierAccount(): - assert storage_root is not None - return encode_account(node, storage_root) - case U256(): - return rlp.encode(node) - case Bytes(): - return node - case _: - raise AssertionError( - f"encoding for {type(node)} is not currently implemented" - ) - - -@dataclass(slots=True) -class Trie(Generic[K, V]): - """The Merkle Trie.""" - - secured: bool - default: V - _data: Dict[K, V] = field(default_factory=dict) - - -def copy_trie(trie: Trie[K, V]) -> Trie[K, V]: - """ - Create a copy of `trie`. Since only frozen objects may be stored in tries, - the contents are reused. - """ - return Trie(trie.secured, trie.default, copy.copy(trie._data)) - - -def trie_set(trie: Trie[K, V], key: K, value: V) -> None: - """ - Store an item in a Merkle Trie. - - This method deletes the key if `value == trie.default`, because the Merkle - Trie represents the default value by omitting it from the trie. - """ - if value == trie.default: - if key in trie._data: - del trie._data[key] - else: - trie._data[key] = value - - -def trie_get(trie: Trie[K, V], key: K) -> V: - """ - Get an item from the Merkle Trie. - - This method returns `trie.default` if the key is missing. - """ - return trie._data.get(key, trie.default) - - -def common_prefix_length(a: Sequence, b: Sequence) -> int: - """Find the longest common prefix of two sequences.""" - for i in range(len(a)): - if i >= len(b) or a[i] != b[i]: - return i - return len(a) - - -def nibble_list_to_compact(x: Bytes, is_leaf: bool) -> Bytes: - """ - Compresses nibble-list into a standard byte array with a flag. - - A nibble-list is a list of byte values no greater than `15`. The flag is - encoded in high nibble of the highest byte. The flag nibble can be broken - down into two two-bit flags. - - Highest nibble:: - - +---+---+----------+--------+ - | _ | _ | is_leaf | parity | - +---+---+----------+--------+ - 3 2 1 0 - - The lowest bit of the nibble encodes the parity of the length of the - remaining nibbles -- `0` when even and `1` when odd. The second lowest bit - is used to distinguish leaf and extension nodes. The other two bits are not - used. - """ - compact = bytearray() - - if len(x) % 2 == 0: # ie even length - compact.append(16 * (2 * is_leaf)) - for i in range(0, len(x), 2): - compact.append(16 * x[i] + x[i + 1]) - else: - compact.append(16 * ((2 * is_leaf) + 1) + x[0]) - for i in range(1, len(x), 2): - compact.append(16 * x[i] + x[i + 1]) - - return Bytes(compact) - - -def bytes_to_nibble_list(bytes_: Bytes) -> Bytes: - """ - Convert a `Bytes` into to a sequence of nibbles (bytes with value < 16). - """ - nibble_list = bytearray(2 * len(bytes_)) - for byte_index, byte in enumerate(bytes_): - nibble_list[byte_index * 2] = (byte & 0xF0) >> 4 - nibble_list[byte_index * 2 + 1] = byte & 0x0F - return Bytes(nibble_list) - - -def _prepare_trie( - trie: Trie[K, V], - get_storage_root: Optional[Callable[[Bytes20], Bytes32]] = None, -) -> Mapping[Bytes, Bytes]: - """ - Prepare the trie for root calculation. Removes values that are empty, - hashes the keys (if `secured == True`) and encodes all the nodes. - """ - mapped: MutableMapping[Bytes, Bytes] = {} - - for preimage, value in trie._data.items(): - if isinstance(value, FrontierAccount): - assert get_storage_root is not None - address = Bytes20(preimage) - encoded_value = encode_node(value, get_storage_root(address)) - else: - encoded_value = encode_node(value) - if encoded_value == b"": - raise AssertionError - key: Bytes - if trie.secured: - # "secure" tries hash keys once before construction - key = keccak256(preimage) - else: - key = preimage - mapped[bytes_to_nibble_list(key)] = encoded_value - - return mapped - - -def root( - trie: Trie[K, V], - get_storage_root: Optional[Callable[[Bytes20], Bytes32]] = None, -) -> Bytes32: - """Compute the root of a modified merkle patricia trie (MPT).""" - obj = _prepare_trie(trie, get_storage_root) - - root_node = encode_internal_node(patricialize(obj, Uint(0))) - if len(rlp.encode(root_node)) < 32: - return keccak256(rlp.encode(root_node)) - else: - assert isinstance(root_node, Bytes) - return Bytes32(root_node) - - -def patricialize( - obj: Mapping[Bytes, Bytes], level: Uint -) -> Optional[InternalNode]: - """ - Structural composition function. - - Used to recursively patricialize and merkleize a dictionary. Includes - memoization of the tree structure and hashes. - """ - if len(obj) == 0: - return None - - arbitrary_key = next(iter(obj)) - - # if leaf node - if len(obj) == 1: - leaf = LeafNode(arbitrary_key[level:], obj[arbitrary_key]) - return leaf - - # prepare for extension node check by finding max j such that all keys in - # obj have the same key[i:j] - substring = arbitrary_key[level:] - prefix_length = len(substring) - for key in obj: - prefix_length = min( - prefix_length, common_prefix_length(substring, key[level:]) - ) - - # finished searching, found another key at the current level - if prefix_length == 0: - break - - # if extension node - if prefix_length > 0: - prefix = arbitrary_key[int(level) : int(level) + prefix_length] - return ExtensionNode( - prefix, - encode_internal_node( - patricialize(obj, level + Uint(prefix_length)) - ), - ) - - branches: List[MutableMapping[Bytes, Bytes]] = [] - for _ in range(16): - branches.append({}) - value = b"" - for key in obj: - if len(key) == level: - # shouldn't ever have an account or receipt in an internal node - if isinstance(obj[key], (FrontierAccount, Uint)): - raise AssertionError - value = obj[key] - else: - branches[key[level]][key] = obj[key] - - subnodes = tuple( - encode_internal_node(patricialize(branches[k], level + Uint(1))) - for k in range(16) - ) - return BranchNode( - cast(BranchSubnodes, assert_type(subnodes, Tuple[Extended, ...])), - value, - ) diff --git a/src/ethereum_spec_tools/evm_tools/__init__.py b/src/ethereum_spec_tools/evm_tools/__init__.py index 96be3137370..bf854c088cd 100644 --- a/src/ethereum_spec_tools/evm_tools/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/__init__.py @@ -14,7 +14,8 @@ from .b11r import B11R, b11r_arguments from .daemon import Daemon, daemon_arguments from .statetest import StateTest, state_test_arguments -from .t8n import T8N, ForkCache, t8n_arguments +from .t8n import ForkCache +from .t8n.cli import run_t8n_cli, t8n_arguments from .utils import get_supported_forks DESCRIPTION = """ @@ -112,8 +113,7 @@ def main( exit_stack.push(fork_cache) if options.evm_tool == "t8n": - t8n_tool = T8N(options, out_file, in_file, fork_cache) - return t8n_tool.run() + return run_t8n_cli(options, out_file, in_file, fork_cache) elif options.evm_tool == "b11r": b11r_tool = B11R(options, out_file, in_file) return b11r_tool.run() diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index f9ec92d6ded..fda1bac008b 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -134,11 +134,6 @@ def signing_hash_155(self) -> Any: """signing_hash_155 function of the fork.""" return self._module("transactions").signing_hash_155 - @property - def has_signing_hash_155(self) -> bool: - """Check if the fork has a `signing_hash_155` function.""" - return hasattr(self._module("transactions"), "signing_hash_155") - @property def build_block_access_list(self) -> Any: """build_block_access_list function of the fork.""" @@ -259,14 +254,6 @@ def LegacyTransaction(self) -> Any: """Legacytransaction class of the fork.""" return self._module("transactions").LegacyTransaction - @property - def has_legacy_transaction(self) -> bool: - """ - Return `True` if the fork has a `LegacyTransaction` class, or `False` - otherwise. - """ - return hasattr(self._module("transactions"), "LegacyTransaction") - @property def Access(self) -> Any: """Access class of the fork.""" @@ -316,11 +303,6 @@ def decode_transaction(self) -> Any: """decode_transaction function of the fork.""" return self._module("transactions").decode_transaction - @property - def has_decode_transaction(self) -> bool: - """Check if this fork has a `decode_transaction`.""" - return hasattr(self._module("transactions"), "decode_transaction") - @property def BlockState(self) -> Any: """BlockState class of the fork.""" diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py index 2f8246829c6..e9b74e62e65 100644 --- a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -9,14 +9,28 @@ from copy import deepcopy from dataclasses import dataclass from io import StringIO -from typing import Any, Dict, Generator, Iterable, List, Optional, TextIO +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + Iterable, + List, + Optional, + TextIO, +) from ethereum.utils.hexadecimal import hex_to_bytes -from ..t8n import T8N, ForkCache -from ..t8n.t8n_types import Result +from ..t8n import ForkCache +from ..t8n.cli import build_t8n_from_cli_options from ..utils import get_supported_forks +if TYPE_CHECKING: + from execution_testing.client_clis.cli_types import ( + Result as TestingResult, + ) + @dataclass class TestCase: @@ -87,7 +101,7 @@ def run_test_case( fork_cache: ForkCache, t8n_extra: Optional[List[str]] = None, output_basedir: Optional[str | TextIO] = None, -) -> Result: +) -> "TestingResult": """ Runs a single general state test. """ @@ -156,7 +170,8 @@ def run_test_case( if output_basedir is not None: t8n_options.output_basedir = output_basedir - t8n = T8N(t8n_options, out_stream, in_stream, fork_cache) + del out_stream # statetest reads ``t8n.result`` directly. + t8n = build_t8n_from_cli_options(t8n_options, in_stream, fork_cache) t8n.run_state_test() return t8n.result diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index cd449ad9f03..5d263f3fc34 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -1,22 +1,31 @@ """ Create a transition tool for the given fork. + +The ``T8N`` class consumes testing-package pydantic types directly; the +JSON CLI surface lives in :mod:`.cli`. """ -import argparse -import fnmatch -import json -import os from contextlib import AbstractContextManager -from typing import Any, Final, TextIO, Type, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Final, + List, + Optional, + Sequence, + Type, + TypeVar, +) from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from typing_extensions import override from ethereum import trace from ethereum.exceptions import EthereumException, InvalidBlock from ethereum.fork_criteria import ByBlockNumber, ByTimestamp, Unscheduled -from ethereum.merkle_patricia_trie import copy_trie from ethereum_spec_tools.forks import ( ForkOverrides, Hardfork, @@ -24,74 +33,30 @@ ) from ..loaders.fixture_loader import Load -from ..utils import ( - FatalError, - find_fork, - get_stream_logger, - parse_hex_or_int, -) -from .env import Env -from .evm_trace.count import CountTracer -from .evm_trace.eip3155 import Eip3155Tracer +from ..loaders.transaction_loader import TransactionLoad, UnsupportedTxError +from ..utils import get_stream_logger, resolve_fork +from .block_environment import Ommer, build_block_environment from .evm_trace.group import GroupTracer -from .t8n_types import Alloc, Result, Txs - -T = TypeVar("T") - - -def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: - """ - Adds the arguments for the t8n tool subparser. - """ - t8n_parser = subparsers.add_parser("t8n", help="This is the t8n tool.") +from .result import build_result, record_rejected_tx - t8n_parser.add_argument( - "--input.alloc", dest="input_alloc", type=str, default="alloc.json" +if TYPE_CHECKING: + from execution_testing.client_clis.cli_types import ( + TransitionToolOutput, ) - t8n_parser.add_argument( - "--input.env", dest="input_env", type=str, default="env.json" + from execution_testing.client_clis.transition_tool import ( + TransitionTool, ) - t8n_parser.add_argument( - "--input.txs", dest="input_txs", type=str, default="txs.json" + from execution_testing.exceptions import ExceptionMapper + from execution_testing.test_types import ( + Environment as TestingEnvironment, ) - t8n_parser.add_argument( - "--input.blobParams", - dest="blob_parameters", - type=str, - default=None, + from execution_testing.test_types import ( + Transaction as TestingTransaction, ) - t8n_parser.add_argument( - "--output.alloc", dest="output_alloc", type=str, default="alloc.json" - ) - t8n_parser.add_argument( - "--output.basedir", dest="output_basedir", type=str, default="." - ) - t8n_parser.add_argument("--output.body", dest="output_body", type=str) - t8n_parser.add_argument( - "--output.result", - dest="output_result", - type=str, - default="result.json", - ) - t8n_parser.add_argument( - "--state.chainid", dest="state_chainid", type=int, default=1 - ) - t8n_parser.add_argument( - "--state.fork", dest="state_fork", type=str, default="Frontier" - ) - t8n_parser.add_argument( - "--state.reward", dest="state_reward", type=int, default=None - ) - t8n_parser.add_argument("--trace", action="store_true") - t8n_parser.add_argument("--trace.memory", action="store_true") - t8n_parser.add_argument("--trace.nomemory", action="store_true") - t8n_parser.add_argument("--trace.noreturndata", action="store_true") - t8n_parser.add_argument("--trace.nostack", action="store_true") - t8n_parser.add_argument("--trace.returndata", action="store_true") - t8n_parser.add_argument("--opcode.count", dest="opcode_count", type=str) + TransitionToolData = TransitionTool.TransitionToolData - t8n_parser.add_argument("--state-test", action="store_true") +T = TypeVar("T") class ForkCache(AbstractContextManager): @@ -151,66 +116,80 @@ def get( class T8N(Load): - """The class that carries out the transition.""" + """ + Execute the transition function on already-parsed inputs. + + ``T8N`` is JSON-free: callers hand in a testing + ``TransitionTool.TransitionToolData`` (alloc / env / txs / + blob_schedule / fork / chain_id / reward / state_test) plus any + pre-PoS ommer data, and ``run()`` returns a + :class:`~execution_testing.client_clis.cli_types.TransitionToolOutput`. + See :mod:`.cli` for the JSON wrapper used by the + ``ethereum-spec-evm t8n`` entry point. + """ tracers: Final[GroupTracer | None] + alloc: Any + env: "TestingEnvironment" + txs: List["TestingTransaction"] + ommers: List[Ommer] + rejected_transactions: List[Any] + body: Bytes + state_test: bool + state_reward: int + exception_mapper: Optional["ExceptionMapper"] + _block_exception: Optional[str] def __init__( self, - options: Any, - out_file: TextIO, - in_file: TextIO, + t8n_data: "TransitionToolData", + *, cache: ForkCache, + fork_block: Optional[int] = None, + ommers: Sequence[Ommer] = (), + tracers: Optional[GroupTracer] = None, + exception_mapper: Optional["ExceptionMapper"] = None, ) -> None: - self.out_file = out_file - self.in_file = in_file - self.options = options - forks = Hardfork.discover() - - if "stdin" in ( - options.input_env, - options.input_alloc, - options.input_txs, - options.blob_parameters, + # ``resolve_fork`` only maps the testing fork name to a spec + # ``Hardfork`` module — CLI exception aliases like + # ``HomesteadToDaoAt5`` are unfolded by ``find_fork`` in + # :mod:`.cli` before the testing ``Fork`` is constructed. For + # those transition-fork tests the CLI also reports the block + # number at which the resolved fork activates via + # ``fork_block``; the in-process path leaves it ``None``. + fork_module = resolve_fork(t8n_data.fork_name) + fork_criteria: Optional[ByBlockNumber] = None + if fork_block is not None and fork_block != 0: + fork_criteria = ByBlockNumber(fork_block) + + # Translate ``t8n_data.blob_params`` (testing ``ForkBlobSchedule``) + # into the override arguments ``ForkCache.get`` consumes. + # + # Only forward overrides for BPO forks. BPO forks share their + # non-BPO ancestor's spec module and rely on the override to + # differentiate their blob schedule. Non-BPO forks (Cancun, + # Prague, Amsterdam, …) carry the correct schedule built into + # their spec module — overriding here would force ``ForkCache`` + # to clone the fork into a temporary directory whenever the + # override values don't byte-match the constants, attributing + # all opcode coverage to the clone's ``/tmp/...`` paths instead + # of the original ``src/ethereum/forks//`` source. + target_blobs_per_block: Optional[U64] = None + max_blobs_per_block: Optional[U64] = None + base_fee_update_fraction: Optional[Uint] = None + if ( + t8n_data.blob_params is not None + and t8n_data.fork.bpo_fork() + and t8n_data.fork != t8n_data.fork.non_bpo_ancestor() ): - stdin = json.load(in_file) - else: - stdin = None - - fork_module, self.fork_block = find_fork(forks, self.options, stdin) - - fork_criteria = None - if self.fork_block is not None and self.fork_block != 0: - # I can't find where `self.fork_block` is even used, and the vast - # majority of the time it's zero anyway. Not changing the fork - # criteria doesn't seem to break the tests, but changing it - # introduces cloning overhead, so... pretend it didn't happen. - fork_criteria = ByBlockNumber(self.fork_block) - - target_blobs_per_block = None - max_blobs_per_block = None - base_fee_update_fraction = None - - blob_parameters = None - if options.blob_parameters == "stdin": - assert stdin is not None - blob_parameters = stdin["blobParams"] - elif options.blob_parameters is not None: - with open(options.blob_parameters, "r") as f: - blob_parameters = json.load(f) - - if blob_parameters is not None: - target_blobs_per_block = parse_hex_or_int( - blob_parameters["target"], - U64, + target_blobs_per_block = U64( + int(t8n_data.blob_params.target_blobs_per_block) ) - max_blobs_per_block = parse_hex_or_int( - blob_parameters["max"], - U64, + max_blobs_per_block = U64( + int(t8n_data.blob_params.max_blobs_per_block) ) - base_fee_update_fraction = parse_hex_or_int( - blob_parameters["baseFeeUpdateFraction"], - Uint, + base_fee_update_fraction = Uint( + int(t8n_data.blob_params.base_fee_update_fraction) ) fork = cache.get( @@ -221,44 +200,35 @@ def __init__( blob_base_fee_update_fraction=base_fee_update_fraction, ) - tracers = GroupTracer() - - if self.options.trace: - trace_memory = getattr(self.options, "trace.memory", False) - trace_stack = not getattr(self.options, "trace.nostack", False) - trace_return_data = getattr(self.options, "trace.returndata") - tracers.add( - Eip3155Tracer( - trace_memory=trace_memory, - trace_stack=trace_stack, - trace_return_data=trace_return_data, - output_basedir=self.options.output_basedir, - ) - ) - - if self.options.opcode_count is not None: - tracers.add(CountTracer()) - - maybe_tracers: GroupTracer | None - if tracers.tracers: + if tracers is not None: trace.set_evm_trace(tracers) - maybe_tracers = tracers - else: - maybe_tracers = None - - self.tracers = maybe_tracers + self.tracers = tracers self.logger = get_stream_logger("T8N") - super().__init__(fork) - self.chain_id = parse_hex_or_int(self.options.state_chainid, U64) - self.alloc = Alloc(self, stdin) - self.env = Env(self, stdin) - self.txs = Txs(self, stdin) - self.result = Result( - self.env.block_difficulty, self.env.base_fee_per_gas - ) + self.chain_id = U64(t8n_data.chain_id) + self.state_test = t8n_data.state_test + self.state_reward = t8n_data.reward + self.exception_mapper = exception_mapper + + from execution_testing.client_clis.cli_types import LazyAlloc + + # Take a defensive copy of the input alloc so ``apply_diff`` + # (and any other in-place mutation T8N does) never escapes + # into the caller's Python object. Without this, multi-block + # tests that contain an invalid block would observe a mutated + # pre-state — the testing framework expects ``previous_alloc`` + # to remain unchanged when ``block.exception`` is set. + input_alloc = t8n_data.alloc + if isinstance(input_alloc, LazyAlloc): + input_alloc = input_alloc.materialize() + self.alloc = input_alloc.model_copy(deep=True) + self.env = t8n_data.env + self.txs = list(t8n_data.txs) + self.ommers = list(ommers) + self.body = Bytes(rlp.encode([tx.rlp() for tx in self.txs])) + self.rejected_transactions = [] def _tracer(self, type_: Type[T]) -> T: group = self.tracers @@ -271,70 +241,65 @@ def _tracer(self, type_: Type[T]) -> T: def block_environment(self) -> Any: """ - Create the environment for the transaction. The keyword - arguments are adjusted according to the fork. - """ - kw_arguments = { - "block_hashes": self.env.block_hashes, - "coinbase": self.env.coinbase, - "number": self.env.block_number, - "time": self.env.block_timestamp, - "block_gas_limit": self.env.block_gas_limit, - "chain_id": self.chain_id, - } - - block_state = self.fork.BlockState(pre_state=self.alloc.state) - kw_arguments["state"] = block_state - self._block_state = block_state - - block_environment = self.fork.BlockEnvironment - - if self.fork.has_calculate_base_fee_per_gas: - kw_arguments["base_fee_per_gas"] = self.env.base_fee_per_gas - - if self.fork.hardfork.consensus.is_pos(): - kw_arguments["prev_randao"] = self.env.prev_randao - else: - kw_arguments["difficulty"] = self.env.block_difficulty - - if self.fork.has_beacon_roots_address: - kw_arguments["parent_beacon_block_root"] = ( - self.env.parent_beacon_block_root - ) - kw_arguments["excess_blob_gas"] = self.env.excess_blob_gas + Build the fork's ``BlockEnvironment`` for the current block. - if self.fork.has_hash_block_access_list: - kw_arguments["block_access_list_builder"] = ( - self.fork.BlockAccessListBuilder() - ) - if self.fork.has_slot_number: - kw_arguments["slot_number"] = self.env.slot_number - - return block_environment(**kw_arguments) - - def backup_state(self) -> None: - """Back up the state in order to restore in case of an error.""" - state = self.alloc.state - main_trie = copy_trie(state._main_trie) - storage_tries = { - k: copy_trie(t) for (k, t) in state._storage_tries.items() - } - self.alloc.state_backup = ( - main_trie, - storage_tries, - dict(state._code_store), + Side effect: stores the resulting ``BlockState`` on ``self`` so + ``extract_block_diff`` can be called after execution. + """ + block_env = build_block_environment( + fork=self.fork, + env=self.env, + pre_state=self.alloc, + chain_id=self.chain_id, + state_test=self.state_test, ) + self._block_state = block_env.state + return block_env - def restore_state(self) -> None: - """Restore the state from the backup.""" - state = self.alloc.state - state._main_trie = self.alloc.state_backup[0] - state._storage_tries = self.alloc.state_backup[1] - state._code_store = self.alloc.state_backup[2] + def convert_transaction(self, tx: "TestingTransaction") -> Any: + """ + Convert a testing ``Transaction`` into the fork's tx object. + + TODO: Replace with ``self.fork.decode_transaction(tx.rlp())`` + once two pieces land in a follow-up PR: + + 1. Pre-Berlin forks gain a ``decode_transaction``. Pre-Berlin forks + predate typed txs and currently expose no decode entry + point — block decoding produces the legacy class directly. + 2. The testing exception_mapper learns to surface + ``DecodingError`` (raised when a contract-creating typed tx + like ``BlobTransaction`` (``to=None``) reaches + ``decode_transaction``) as the canonical + ``TransactionTypeContractCreationError``. Today + ``TransactionLoad`` constructs the tx object even when its + shape is illegal for the fork, so ``check_transaction`` + inside ``process_transaction`` raises the canonical error. + + Until both are in place, we go through ``TransactionLoad`` + (the JSON loader) which handles both concerns. + """ + raw: Dict[str, Any] = tx.model_dump( + mode="json", by_alias=True, exclude_none=True + ) + # Bridge testing-side aliases (geth-compatible) to the names + # ``TransactionLoad`` expects. + if "input" in raw: + raw.setdefault("data", raw["input"]) + if "gas" in raw: + raw.setdefault("gasLimit", raw["gas"]) + # ``to == None`` is dumped as JSON ``null``; ``TransactionLoad`` + # treats the empty string as the contract-creation sentinel. + if raw.get("to") in (None, "0x"): + raw["to"] = "" + # Ensure the ``type`` field is set so ``TransactionLoad`` + # dispatches to the right tx class (testing's dump uses ``ty`` + # which serializes to ``type`` only on some fork variants). + raw.setdefault("type", "0x" + format(int(tx.ty), "02x")) + return TransactionLoad(raw, self.fork).read() def pay_block_rewards(self, block_reward: U256, block_env: Any) -> None: """Apply the block rewards to the block coinbase.""" - ommer_count = U256(len(self.env.ommers)) + ommer_count = U256(len(self.ommers)) miner_reward = block_reward + ( ommer_count * (block_reward // U256(32)) ) @@ -343,42 +308,63 @@ def pay_block_rewards(self, block_reward: U256, block_env: Any) -> None: self.fork.create_ether(rewards_state, block_env.coinbase, miner_reward) - for ommer in self.env.ommers: - # Ommer age with respect to the current block. - ommer_age = U256(block_env.number - ommer.number) + for ommer in self.ommers: + # ``delta`` is the age of the ommer relative to the current block. + ommer_age = U256(int(ommer.delta, 16)) ommer_miner_reward = ( (U256(8) - ommer_age) * block_reward ) // U256(8) self.fork.create_ether( - rewards_state, ommer.coinbase, ommer_miner_reward + rewards_state, ommer.address, ommer_miner_reward ) self.fork.incorporate_tx_into_block(rewards_state) - def run_state_test(self) -> Any: + def _process_txs(self, block_env: Any, block_output: Any) -> None: + """Execute every transaction in ``self.txs`` against ``block_env``.""" + for tx_index, testing_tx in enumerate(self.txs): + try: + fork_tx = self.convert_transaction(testing_tx) + self.fork.process_transaction( + block_env, block_output, fork_tx, Uint(tx_index) + ) + except (EthereumException, UnsupportedTxError) as e: + # `UnsupportedTxError` covers ``convert_transaction`` + # failures when a typed tx is structurally malformed for + # this fork (e.g. a contract-creating BlobTransaction). + record_rejected_tx(self, tx_index, e) + self.logger.warning(f"Transaction {tx_index} failed: {e!r}") + + def run_state_test(self) -> None: """ Apply a single transaction on pre-state. No system operations are performed. """ - block_env = self.block_environment() - block_output = self.fork.BlockOutput() - self.backup_state() - if len(self.txs.transactions) > 0: - tx = self.txs.transactions[0] + self._block_env = self.block_environment() + self._block_output = self.fork.BlockOutput() + + if len(self.txs) > 0: + testing_tx = self.txs[0] try: + fork_tx = self.convert_transaction(testing_tx) self.fork.process_transaction( - block_env=block_env, - block_output=block_output, - tx=tx, + block_env=self._block_env, + block_output=self._block_output, + tx=fork_tx, index=Uint(0), ) - except EthereumException as e: - self.txs.rejected_txs[0] = f"Failed transaction: {e!r}" - self.restore_state() - self.logger.warning(f"Transaction {0} failed: {str(e)}") - - self.result.update(self, block_env, block_output) - self.result.rejected = self.txs.rejected_txs + except (EthereumException, UnsupportedTxError) as e: + record_rejected_tx(self, 0, e) + self.logger.warning(f"Transaction 0 failed: {e!r}") + + self._block_exception = None + self.result = build_result( + self, + self._block_env, + self._block_output, + self._block_exception, + self.rejected_transactions, + ) def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: if self.fork.has_compute_requests_hash: @@ -395,45 +381,35 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: data=block_env.parent_beacon_block_root, ) - for tx_index, (original_idx, tx) in enumerate( - zip( - self.txs.successfully_parsed, - self.txs.transactions, - strict=True, - ) - ): - self.backup_state() - try: - self.fork.process_transaction( - block_env, block_output, tx, Uint(tx_index) - ) - except EthereumException as e: - self.txs.rejected_txs[original_idx] = ( - f"Failed transaction: {e!r}" - ) - self.restore_state() - self.logger.warning( - f"Transaction {original_idx} failed: {e!r}" - ) + self._process_txs(block_env, block_output) # EIP-7928: Post-execution operations use index N+1 - num_txs = len(self.txs.transactions) if self.fork.has_hash_block_access_list: block_env.block_access_list_builder.block_access_index = ( - self.fork.BlockAccessIndex(Uint(num_txs) + Uint(1)) + self.fork.BlockAccessIndex(Uint(len(self.txs)) + Uint(1)) ) - if not self.fork.proof_of_stake: - if self.options.state_reward is None: - self.pay_block_rewards(self.fork.BLOCK_REWARD, block_env) - elif self.options.state_reward != -1: - self.pay_block_rewards( - U256(self.options.state_reward), block_env - ) + if not self.fork.proof_of_stake and self.state_reward != -1: + # ``-1`` is the sentinel for "skip block rewards entirely" + # (testing-side ``TransitionToolData.__post_init__`` sets + # this for genesis blocks; the CLI wrapper resolves a + # ``--state.reward=None`` to the fork's ``BLOCK_REWARD`` + # before constructing the data). + self.pay_block_rewards(U256(self.state_reward), block_env) if self.fork.has_withdrawal: + withdrawals = self.env.withdrawals or [] + fork_withdrawals = tuple( + self.fork.Withdrawal( + Uint(int(w.index)), + Uint(int(w.validator_index)), + self.fork.hex_to_address(w.address.hex()), + U256(int(w.amount)), + ) + for w in withdrawals + ) self.fork.process_withdrawals( - block_env, block_output, self.env.withdrawals + block_env, block_output, fork_withdrawals ) if self.fork.has_compute_requests_hash: @@ -454,102 +430,57 @@ def run_blockchain_test(self) -> None: """ Apply a block on the pre-state. Also includes system operations. """ - block_env = self.block_environment() - block_output = self.fork.BlockOutput() + self._block_env = self.block_environment() + self._block_output = self.fork.BlockOutput() + self._block_exception = None try: - self._run_blockchain_test(block_env, block_output) + self._run_blockchain_test(self._block_env, self._block_output) except InvalidBlock as e: - self.result.block_exception = f"{e}" - - self.result.update(self, block_env, block_output) - self.result.rejected = self.txs.rejected_txs - - def run(self) -> int: - """Run the transition and provide the relevant outputs.""" - # Clear files that may have been created in a previous - # run of the t8n tool. - # Define the specific files and pattern to delete - files_to_delete = [ - self.options.output_result, - self.options.output_alloc, - self.options.output_body, - ] - pattern_to_delete = "trace-*.jsonl" - - # Iterate through the directory - for file in os.listdir(self.options.output_basedir): - file_path = os.path.join(self.options.output_basedir, file) - - # Check if the file matches the specific names or the pattern - if file in files_to_delete or fnmatch.fnmatch( - file, pattern_to_delete - ): - os.remove(file_path) - - try: - if self.options.state_test: - self.run_state_test() - else: - self.run_blockchain_test() - except FatalError as e: - self.logger.error(str(e)) - return 1 - - json_state = self.alloc.to_json() - json_result = self.result.to_json() - - json_output: dict[str, object] = {} - - if self.options.output_body == "stdout": - txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() - json_output["body"] = txs_rlp - elif self.options.output_body is not None: - txs_rlp_path = os.path.join( - self.options.output_basedir, - self.options.output_body, - ) - txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() - with open(txs_rlp_path, "w") as f: - json.dump(txs_rlp, f) - self.logger.info(f"Wrote transaction rlp to {txs_rlp_path}") + self._block_exception = f"{e}" + + self.result = build_result( + self, + self._block_env, + self._block_output, + self._block_exception, + self.rejected_transactions, + ) - if self.options.output_alloc == "stdout": - json_output["alloc"] = json_state - else: - alloc_output_path = os.path.join( - self.options.output_basedir, - self.options.output_alloc, - ) - with open(alloc_output_path, "w") as f: - json.dump(json_state, f, indent=4) - self.logger.info(f"Wrote alloc to {alloc_output_path}") + def run(self) -> "TransitionToolOutput": + """ + Execute the transition; return the in-memory result. + + The returned ``TransitionToolOutput`` carries the post-state + ``Alloc`` as a ``MaterializedAlloc`` (already in memory, so + ``get()`` is a no-op), the ``Result`` (state root, receipts, + rejected txs, block exception, …), and the encoded transaction + body as raw RLP bytes. The JSON CLI surface lives in + :func:`.cli.write_t8n_outputs`. + """ + from execution_testing.base_types import Bytes as TestingBytes + from execution_testing.client_clis.cli_types import ( + MaterializedAlloc, + TransitionToolOutput, + ) - if self.options.output_result == "stdout": - json_output["result"] = json_result + if self.state_test: + self.run_state_test() else: - result_output_path = os.path.join( - self.options.output_basedir, - self.options.output_result, - ) - with open(result_output_path, "w") as f: - json.dump(json_result, f, indent=4) - self.logger.info(f"Wrote result to {result_output_path}") - - if self.options.opcode_count == "stdout": - opcode_count_results = self._tracer(CountTracer).results() - json_output["opcodeCount"] = opcode_count_results - elif self.options.opcode_count is not None: - opcode_count_results = self._tracer(CountTracer).results() - result_output_path = os.path.join( - self.options.output_basedir, - self.options.opcode_count, - ) - with open(result_output_path, "w") as f: - json.dump(opcode_count_results, f, indent=4) - self.logger.info(f"Wrote opcode counts to {result_output_path}") - - if json_output: - json.dump(json_output, self.out_file, indent=4) - - return 0 + self.run_blockchain_test() + + # Apply the block diff in place so ``self.alloc`` is the + # post-state when the caller reads it. Safe to do + # unconditionally — ``self.alloc`` is a defensive copy taken + # in ``__init__``, so mutating it never escapes to the caller. + diff = self.fork.extract_block_diff(self._block_state) + self.alloc.apply_diff(diff) + + return TransitionToolOutput( + alloc=MaterializedAlloc( + alloc=self.alloc, + _state_root=self.result.state_root, + ), + result=self.result, + body=TestingBytes(self.body), + ) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/block_environment.py b/src/ethereum_spec_tools/evm_tools/t8n/block_environment.py new file mode 100644 index 00000000000..aab52779c9d --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/t8n/block_environment.py @@ -0,0 +1,233 @@ +""" +Build the spec's per-fork ``BlockEnvironment`` from a testing-package +``Environment``. +""" + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, List, Optional + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes8, Bytes20, Bytes32, Bytes256 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +if TYPE_CHECKING: + from execution_testing.test_types import Environment as TestingEnvironment + + from ..loaders.fork_loader import ForkLoad + + +@dataclass +class Ommer: + """ + Pre-PoS ommer header summary consumed by `pay_block_rewards`. + + Carries the two fields needed for ommer-reward arithmetic + (`block_number - delta` and the ommer coinbase). The testing + `Environment.ommers` field is `List[Hash]` and cannot represent + these — the JSON CLI fallback populates this from the raw env JSON + instead, and the in-process path leaves it empty (PoS has no ommers). + """ + + delta: str + address: Bytes20 + + +def build_block_environment( + fork: "ForkLoad", + env: "TestingEnvironment", + pre_state: Any, + chain_id: U64, + state_test: bool = False, +) -> Any: + """ + Build the fork's `BlockEnvironment` from a testing `Environment`. + + `pre_state` must satisfy the spec's `PreState` protocol (in + practice, a testing `Alloc`). + """ + block_state = fork.BlockState(pre_state=pre_state) + + block_number = Uint(int(env.number)) + block_gas_limit = Uint(int(env.gas_limit)) + block_timestamp = U256(int(env.timestamp)) + coinbase = Bytes20(env.fee_recipient) + + base_fee_per_gas = _resolve_base_fee_per_gas(env, fork, block_gas_limit) + + kw_arguments: dict[str, Any] = { + "block_hashes": _resolve_block_hashes(env.block_hashes, block_number), + "coinbase": coinbase, + "number": block_number, + "time": block_timestamp, + "block_gas_limit": block_gas_limit, + "chain_id": chain_id, + "state": block_state, + } + + if fork.has_calculate_base_fee_per_gas: + assert base_fee_per_gas is not None + kw_arguments["base_fee_per_gas"] = base_fee_per_gas + + if fork.hardfork.consensus.is_pos(): + kw_arguments["prev_randao"] = _resolve_prev_randao(env) + else: + kw_arguments["difficulty"] = _resolve_block_difficulty( + env, fork, block_number, block_timestamp + ) + + if fork.has_beacon_roots_address: + kw_arguments["parent_beacon_block_root"] = ( + None if state_test else _resolve_parent_beacon_block_root(env) + ) + kw_arguments["excess_blob_gas"] = _resolve_excess_blob_gas(env, fork) + + if fork.has_hash_block_access_list: + kw_arguments["block_access_list_builder"] = ( + fork.BlockAccessListBuilder() + ) + + if fork.has_slot_number: + slot_number = env.slot_number + kw_arguments["slot_number"] = ( + U64(int(slot_number)) if slot_number is not None else None + ) + + return fork.BlockEnvironment(**kw_arguments) + + +def _resolve_base_fee_per_gas( + env: "TestingEnvironment", fork: "ForkLoad", block_gas_limit: Uint +) -> Optional[Uint]: + """Use ``currentBaseFee`` if present; otherwise derive from parent.""" + if not fork.has_calculate_base_fee_per_gas: + return None + if env.base_fee_per_gas is not None: + return Uint(int(env.base_fee_per_gas)) + assert env.parent_gas_limit is not None + assert env.parent_gas_used is not None + assert env.parent_base_fee_per_gas is not None + return fork.calculate_base_fee_per_gas( + block_gas_limit, + Uint(int(env.parent_gas_limit)), + Uint(int(env.parent_gas_used)), + Uint(int(env.parent_base_fee_per_gas)), + ) + + +def _resolve_excess_blob_gas( + env: "TestingEnvironment", + fork: "ForkLoad", +) -> Optional[U64]: + """Use ``currentExcessBlobGas`` if present; else derive from parent.""" + if env.excess_blob_gas is not None: + return U64(int(env.excess_blob_gas)) + + parent_blob_gas_used = U64( + int(env.parent_blob_gas_used) if env.parent_blob_gas_used else 0 + ) + parent_excess_blob_gas = U64( + int(env.parent_excess_blob_gas) if env.parent_excess_blob_gas else 0 + ) + # EIP-7918 reads ``parent.base_fee_per_gas`` from the parent header. + parent_base_fee_per_gas = Uint( + int(env.parent_base_fee_per_gas) + if env.parent_base_fee_per_gas is not None + else 0 + ) + + arguments: dict[str, Any] = { + "parent_hash": Hash32(b"\0" * 32), + "ommers_hash": Hash32(b"\0" * 32), + "coinbase": Bytes20(b"\0" * 20), + "state_root": Hash32(b"\0" * 32), + "transactions_root": Hash32(b"\0" * 32), + "receipt_root": Hash32(b"\0" * 32), + "bloom": Bytes256(b"\0" * 256), + "difficulty": Uint(0), + "number": Uint(0), + "gas_limit": Uint(0), + "gas_used": Uint(0), + "timestamp": U256(0), + "extra_data": b"", + "prev_randao": Bytes32(b"\0" * 32), + "nonce": Bytes8(b"\0" * 8), + "withdrawals_root": Hash32(b"\0" * 32), + "parent_beacon_block_root": Hash32(b"\0" * 32), + "base_fee_per_gas": parent_base_fee_per_gas, + "blob_gas_used": parent_blob_gas_used, + "excess_blob_gas": parent_excess_blob_gas, + } + if fork.has_compute_requests_hash: + arguments["requests_hash"] = Hash32(b"\0" * 32) + if fork.has_hash_block_access_list: + arguments["block_access_list_hash"] = Hash32(b"\0" * 32) + if fork.has_slot_number: + arguments["slot_number"] = U64(0) + + parent_header = fork.Header(**arguments) + return fork.calculate_excess_blob_gas(parent_header) + + +def _resolve_block_difficulty( + env: "TestingEnvironment", + fork: "ForkLoad", + block_number: Uint, + block_timestamp: U256, +) -> Optional[Uint]: + """Use ``currentDifficulty`` if present; otherwise derive from parent.""" + if env.difficulty is not None: + return Uint(int(env.difficulty)) + + assert env.parent_timestamp is not None + assert env.parent_difficulty is not None + args: List[Any] = [ + block_number, + block_timestamp, + U256(int(env.parent_timestamp)), + Uint(int(env.parent_difficulty)), + ] + if fork.calculate_block_difficulty_arity > 4: + empty_ommers_hash = keccak256(rlp.encode([])) + parent_ommers_hash = Hash32(env.parent_ommers_hash) + args.append(parent_ommers_hash != empty_ommers_hash) + return fork.calculate_block_difficulty(*args) + + +def _resolve_prev_randao(env: "TestingEnvironment") -> Bytes32: + """Pad the (numeric) ``prev_randao`` field to 32 bytes.""" + value = env.prev_randao + if value is None: + return Bytes32(b"\0" * 32) + return Bytes32(int(value).to_bytes(32, "big")) + + +def _resolve_block_hashes( + block_hashes: Any, block_number: Uint +) -> List[Optional[Hash32]]: + """ + Return up to the last 256 block hashes preceding ``block_number``. + + `block_hashes` is the testing `Environment.block_hashes` dict keyed by + block number; missing entries become `None` placeholders. + """ + result: List[Optional[Hash32]] = [] + if not block_hashes: + return result + normalized = {int(k): Hash32(v) for k, v in block_hashes.items()} + max_blockhash_count = min(Uint(256), block_number) + for number in range( + int(block_number) - int(max_blockhash_count), int(block_number) + ): + result.append(normalized.get(number)) + return result + + +def _resolve_parent_beacon_block_root( + env: "TestingEnvironment", +) -> Optional[Hash32]: + """Return the parent beacon block root, or ``None`` if absent.""" + if env.parent_beacon_block_root is None: + return None + return Hash32(env.parent_beacon_block_root) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/cli.py b/src/ethereum_spec_tools/evm_tools/t8n/cli.py new file mode 100644 index 00000000000..14fb0e737c3 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/t8n/cli.py @@ -0,0 +1,483 @@ +""" +CLI / JSON wrapper for the ``T8N`` transition tool. + +``T8N`` itself consumes a testing-package +``TransitionTool.TransitionToolData`` and knows nothing about argparse, +stdin/stdout, or JSON. This module provides the bridge used by the +``ethereum-spec-evm t8n`` entry point and by ``statetest``: + +* :func:`build_t8n_from_cli_options` reads the JSON inputs + (stdin / files), resolves the fork, parses everything into testing + pydantic types, bundles them into a ``TransitionToolData``, builds + the tracer group, and returns a constructed ``T8N``. +* :func:`write_t8n_outputs` serialises the t8n output + opcode-count + results to disk / stdout per ``--output.*`` flags. +* :func:`run_t8n_cli` chains the two for the CLI entry point. +""" + +import argparse +import fnmatch +import json +import os +from typing import Any, Dict, List, Optional, TextIO, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64 + +from ethereum_spec_tools.forks import Hardfork + +from ..loaders.fork_loader import ForkLoad +from ..utils import FatalError, find_fork, parse_hex_or_int +from . import T8N, ForkCache +from .block_environment import Ommer +from .evm_trace.count import CountTracer +from .evm_trace.eip3155 import Eip3155Tracer +from .evm_trace.group import GroupTracer + + +def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: + """ + Adds the arguments for the t8n tool subparser. + """ + t8n_parser = subparsers.add_parser("t8n", help="This is the t8n tool.") + + t8n_parser.add_argument( + "--input.alloc", dest="input_alloc", type=str, default="alloc.json" + ) + t8n_parser.add_argument( + "--input.env", dest="input_env", type=str, default="env.json" + ) + t8n_parser.add_argument( + "--input.txs", dest="input_txs", type=str, default="txs.json" + ) + t8n_parser.add_argument( + "--input.blobParams", + dest="blob_parameters", + type=str, + default=None, + ) + t8n_parser.add_argument( + "--output.alloc", dest="output_alloc", type=str, default="alloc.json" + ) + t8n_parser.add_argument( + "--output.basedir", dest="output_basedir", type=str, default="." + ) + t8n_parser.add_argument("--output.body", dest="output_body", type=str) + t8n_parser.add_argument( + "--output.result", + dest="output_result", + type=str, + default="result.json", + ) + t8n_parser.add_argument( + "--state.chainid", dest="state_chainid", type=int, default=1 + ) + t8n_parser.add_argument( + "--state.fork", dest="state_fork", type=str, default="Frontier" + ) + t8n_parser.add_argument( + "--state.reward", dest="state_reward", type=int, default=None + ) + t8n_parser.add_argument("--trace", action="store_true") + t8n_parser.add_argument("--trace.memory", action="store_true") + t8n_parser.add_argument("--trace.nomemory", action="store_true") + t8n_parser.add_argument("--trace.noreturndata", action="store_true") + t8n_parser.add_argument("--trace.nostack", action="store_true") + t8n_parser.add_argument("--trace.returndata", action="store_true") + + t8n_parser.add_argument("--opcode.count", dest="opcode_count", type=str) + + t8n_parser.add_argument("--state-test", action="store_true") + + +def _read_json_input( + path_or_stdin: str, stdin: Optional[Dict], key: str +) -> Any: + """Read one of the t8n JSON inputs (alloc / env / txs).""" + if path_or_stdin == "stdin": + assert stdin is not None + return stdin[key] + with open(path_or_stdin, "r") as f: + return json.load(f) + + +def _parse_ommers_from_env_json(env_json: Any, fork: Any) -> List[Ommer]: + """Parse the pre-PoS ``ommers`` block from a raw env JSON dict.""" + ommers: List[Ommer] = [] + for raw in env_json.get("ommers", []): + ommers.append( + Ommer( + delta=raw["delta"], + address=fork.hex_to_address(raw["address"]), + ) + ) + return ommers + + +def _normalize_tx_json(tx: Dict[str, Any]) -> Dict[str, Any]: + """ + Drop fields that the testing ``Transaction`` model rejects. + + Three boundary mismatches to smooth over: + + 1. ``yParity`` on authorization tuples. The testing + ``AuthorizationTuple`` serializer emits both ``v`` and + ``yParity`` (they are guaranteed equal — see the model's + ``duplicate_v_as_y_parity``), but its validator binds only + ``v`` and treats ``yParity`` as an extra-forbidden field. + 2. ``secretKey`` on an already-signed tx. The testing + ``Transaction`` retains the private key after auto-signing in + ``model_post_init``, so the dump still carries ``secretKey`` + alongside the populated ``v``/``r``/``s``. On re-validation + the model rejects the pair with + ``InvalidSignaturePrivateKeyError``. Strip ``secretKey`` + whenever ``v`` is set (i.e. the tx is already signed). + 3. A tx with no signature material at all. Filled state tests + store a tx whose signature is deliberately invalid without + ``v``/``r``/``s`` or ``secretKey`` (the fixture format cannot + express explicit signature values), expecting the fork to + reject it. Default the components to zero; leaving them unset + would make ``Transaction.rlp`` try to auto-sign a key-less tx + and die on an assertion. + """ + auth_list = tx.get("authorizationList") + if isinstance(auth_list, list): + tx["authorizationList"] = [ + {k: v for k, v in entry.items() if k != "yParity"} + if isinstance(entry, dict) + else entry + for entry in auth_list + ] + if "secretKey" in tx and tx.get("v") is not None: + tx = {k: v for k, v in tx.items() if k != "secretKey"} + if not any( + tx.get(key) is not None + for key in ("secretKey", "v", "yParity", "r", "s") + ): + tx["v"] = "0x00" + tx["r"] = "0x00" + tx["s"] = "0x00" + return tx + + +def _parse_txs_json_to_testing( + raw_txs_json: Any, + fork_module: Hardfork, + transaction_cls: Any, +) -> Tuple[List[Any], Bytes]: + """ + Parse a JSON tx array into signed testing ``Transaction`` objects. + + Unsigned txs carrying only ``secretKey`` are signed in place via + ``Transaction.sign``; pre-Spurious-Dragon forks get + ``protected=False`` so the ``v`` value stays in ``{27, 28}``. + + RLP-string input (a single hex string of an encoded tx list) is + rejected — this path only handles JSON arrays. + """ + if raw_txs_json is None: + return [], Bytes(b"") + if isinstance(raw_txs_json, str): + raise NotImplementedError( + "RLP-encoded `txs` input is not supported by the testing " + "T8N entry point; provide a JSON array instead." + ) + + fork_supports_eip155 = hasattr( + fork_module.module("transactions"), "signing_hash_155" + ) + + normalized = [_normalize_tx_json(dict(tx)) for tx in raw_txs_json] + txs: List[Any] = [] + for tx_dict in normalized: + tx = transaction_cls.model_validate(tx_dict) + if "v" not in tx.model_fields_set and tx.secret_key is not None: + if not fork_supports_eip155 and int(tx.ty) == 0: + tx.protected = False + tx.sign() + txs.append(tx) + body = Bytes(rlp.encode([tx.rlp() for tx in txs])) + return txs, body + + +def _parse_blob_params_from_options( + options: Any, stdin: Optional[Dict] +) -> Any: + """ + Load a testing ``ForkBlobSchedule`` from ``--input.blobParams``. + + Returns ``None`` when the flag is unset. Reads from ``stdin`` + (``"blobParams"`` key) or a file path depending on the flag value. + """ + # Function-scoped: see import-cycle note in ``build_t8n_from_cli_options``. + from execution_testing.base_types.composite_types import ( + ForkBlobSchedule, + ) + + if options.blob_parameters == "stdin": + assert stdin is not None + raw = stdin["blobParams"] + elif options.blob_parameters is not None: + with open(options.blob_parameters, "r") as f: + raw = json.load(f) + else: + return None + return ForkBlobSchedule.model_validate(raw) + + +def _build_tracers_from_options( + options: Any, +) -> Optional[GroupTracer]: + """ + Build the tracer group from CLI ``--trace*`` / ``--opcode.count`` + flags. Returns ``None`` if no tracer would be active. + """ + tracers = GroupTracer() + if options.trace: + trace_memory = getattr(options, "trace.memory", False) + trace_stack = not getattr(options, "trace.nostack", False) + trace_return_data = getattr(options, "trace.returndata") + tracers.add( + Eip3155Tracer( + trace_memory=trace_memory, + trace_stack=trace_stack, + trace_return_data=trace_return_data, + output_basedir=options.output_basedir, + ) + ) + if options.opcode_count is not None: + tracers.add(CountTracer()) + return tracers if tracers.tracers else None + + +# Spec ``Hardfork.title_case_name`` matches the testing-side +# ``Fork.name()`` after stripping spaces, except for a handful of +# legacy outliers where the testing class uses a different +# capitalisation convention. +_TESTING_FORK_NAME_OVERRIDES = { + "DaoFork": "DAOFork", +} + + +def _testing_fork_from_spec_hardfork(hardfork: Hardfork) -> Any: + """Map a spec ``Hardfork`` to the matching testing ``Fork`` class.""" + # Function-scoped: see import-cycle note in ``build_t8n_from_cli_options``. + from execution_testing.forks import get_fork_by_name + + name = hardfork.title_case_name.replace(" ", "") + name = _TESTING_FORK_NAME_OVERRIDES.get(name, name) + fork = get_fork_by_name(name) + if fork is None: + raise ValueError( + f"No testing.Fork class for spec hardfork " + f"{hardfork.short_name!r} (looked for {name!r})" + ) + return fork + + +def _resolve_state_reward( + state_reward: Optional[int], fork_module: Hardfork +) -> int: + """ + Resolve a CLI ``--state.reward`` value into the int that + ``TransitionToolData.reward`` expects. + + ``None`` means "use the fork's default ``BLOCK_REWARD``"; an + explicit ``-1`` means "skip block rewards entirely" (the testing + sentinel); any other int passes through unchanged. + """ + if state_reward is None: + fork_load = ForkLoad(fork_module) + if fork_load.proof_of_stake: + return -1 + return int(fork_load.BLOCK_REWARD) + return state_reward + + +def build_t8n_from_cli_options( + options: Any, + in_file: TextIO, + cache: ForkCache, +) -> T8N: + """ + Construct a ``T8N`` from CLI options + JSON stdin / file inputs. + + Reads ``--input.*`` files (or stdin), validates each piece into + testing pydantic types, bundles them into a ``TransitionToolData``, + builds the tracer group, and hands them to ``T8N``. + """ + # Function-scoped imports: ``execution_testing/__init__`` eagerly + # imports ``.specs`` which transitively imports ``client_clis``, + # which imports ``ExecutionSpecsTransitionTool`` — top-level imports + # from ``execution_testing`` would cycle back into spec-tools. + from execution_testing.base_types.composite_types import BlobSchedule + from execution_testing.client_clis.transition_tool import TransitionTool + from execution_testing.test_types import ( + Alloc as TestingAlloc, + ) + from execution_testing.test_types import ( + Environment as TestingEnvironment, + ) + from execution_testing.test_types import ( + Transaction as TestingTransaction, + ) + + forks = Hardfork.discover() + + if "stdin" in ( + options.input_env, + options.input_alloc, + options.input_txs, + options.blob_parameters, + ): + stdin = json.load(in_file) + else: + stdin = None + + fork_module, fork_block = find_fork(forks, options, stdin) + testing_fork = _testing_fork_from_spec_hardfork(fork_module) + + raw_alloc_json = _read_json_input(options.input_alloc, stdin, "alloc") + raw_env_json = _read_json_input(options.input_env, stdin, "env") + raw_txs_json = _read_json_input(options.input_txs, stdin, "txs") + blob_params = _parse_blob_params_from_options(options, stdin) + + alloc = TestingAlloc.model_validate(raw_alloc_json) + env = TestingEnvironment.model_validate(raw_env_json) + txs, _body = _parse_txs_json_to_testing( + raw_txs_json, fork_module, TestingTransaction + ) + + # Wrap the single per-fork blob schedule into a ``BlobSchedule`` + # collection keyed by fork name (the field TransitionToolData + # expects). + blob_schedule: Any = None + if blob_params is not None: + blob_schedule = BlobSchedule() + blob_schedule.append(fork=testing_fork.name(), schedule=blob_params) + + t8n_data = TransitionTool.TransitionToolData( + alloc=alloc, + env=env, + txs=txs, + fork=testing_fork, + chain_id=int(parse_hex_or_int(options.state_chainid, U64)), + reward=_resolve_state_reward(options.state_reward, fork_module), + blob_schedule=blob_schedule, + state_test=options.state_test, + ) + + # ``Ommer.address`` is parsed via the per-fork ``hex_to_address`` + # helper; construct a temporary ``ForkLoad`` from the resolved + # module just to get the conversion. + fork_load = ForkLoad(fork_module) + ommers = _parse_ommers_from_env_json(raw_env_json, fork_load) + + return T8N( + t8n_data, + cache=cache, + fork_block=fork_block, + ommers=ommers, + tracers=_build_tracers_from_options(options), + ) + + +def write_t8n_outputs( + t8n: T8N, + output: Any, + options: Any, + out_file: TextIO, +) -> None: + """Serialise the t8n output + opcode counts per ``--output.*``.""" + json_state = output.alloc.materialize().model_dump( + mode="json", by_alias=True + ) + json_result = output.result.model_dump( + mode="json", by_alias=True, exclude_none=True + ) + json_output: Dict[str, object] = {} + body_hex = "0x" + bytes(output.body or b"").hex() + + if options.output_body == "stdout": + json_output["body"] = body_hex + elif options.output_body is not None: + txs_rlp_path = os.path.join( + options.output_basedir, options.output_body + ) + with open(txs_rlp_path, "w") as f: + json.dump(body_hex, f) + t8n.logger.info(f"Wrote transaction rlp to {txs_rlp_path}") + + if options.output_alloc == "stdout": + json_output["alloc"] = json_state + else: + alloc_output_path = os.path.join( + options.output_basedir, options.output_alloc + ) + with open(alloc_output_path, "w") as f: + json.dump(json_state, f, indent=4) + t8n.logger.info(f"Wrote alloc to {alloc_output_path}") + + if options.output_result == "stdout": + json_output["result"] = json_result + else: + result_output_path = os.path.join( + options.output_basedir, options.output_result + ) + with open(result_output_path, "w") as f: + json.dump(json_result, f, indent=4) + t8n.logger.info(f"Wrote result to {result_output_path}") + + if options.opcode_count == "stdout": + json_output["opcodeCount"] = t8n._tracer(CountTracer).results() + elif options.opcode_count is not None: + result_output_path = os.path.join( + options.output_basedir, options.opcode_count + ) + with open(result_output_path, "w") as f: + json.dump(t8n._tracer(CountTracer).results(), f, indent=4) + t8n.logger.info(f"Wrote opcode counts to {result_output_path}") + + if json_output: + json.dump(json_output, out_file, indent=4) + + +def _clean_output_dir(options: Any) -> None: + """Remove prior output files matching ``--output.*`` from the basedir.""" + files_to_delete = [ + options.output_result, + options.output_alloc, + options.output_body, + ] + pattern_to_delete = "trace-*.jsonl" + for file in os.listdir(options.output_basedir): + file_path = os.path.join(options.output_basedir, file) + if file in files_to_delete or fnmatch.fnmatch(file, pattern_to_delete): + os.remove(file_path) + + +def run_t8n_cli( + options: Any, + out_file: TextIO, + in_file: TextIO, + cache: ForkCache, +) -> int: + """End-to-end CLI entry: read JSON, run ``T8N``, write JSON output.""" + _clean_output_dir(options) + t8n = build_t8n_from_cli_options(options, in_file, cache) + try: + output = t8n.run() + except FatalError as e: + t8n.logger.error(str(e)) + return 1 + write_t8n_outputs(t8n, output, options, out_file) + return 0 + + +__all__ = [ + "build_t8n_from_cli_options", + "run_t8n_cli", + "t8n_arguments", + "write_t8n_outputs", +] diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py deleted file mode 100644 index edf3763d573..00000000000 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ /dev/null @@ -1,333 +0,0 @@ -""" -Define t8n Env class. -""" - -import json -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes8, Bytes20, Bytes32, Bytes256 -from ethereum_types.numeric import U64, U256, Uint - -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.utils.byte import left_pad_zero_bytes -from ethereum.utils.hexadecimal import hex_to_bytes - -from ..utils import parse_hex_or_int - -if TYPE_CHECKING: - from ethereum_spec_tools.evm_tools.t8n import T8N - - -@dataclass -class Ommer: - """The Ommer type for the t8n tool.""" - - delta: str - address: Any - - -class Env: - """ - The environment for the transition tool. - """ - - coinbase: Any - block_gas_limit: Uint - block_number: Uint - block_timestamp: U256 - withdrawals: Any - block_difficulty: Optional[Uint] - prev_randao: Optional[Bytes32] - parent_difficulty: Optional[Uint] - parent_timestamp: Optional[U256] - base_fee_per_gas: Optional[Uint] - parent_gas_used: Optional[Uint] - parent_gas_limit: Optional[Uint] - parent_base_fee_per_gas: Optional[Uint] - block_hashes: Optional[List[Any]] - parent_ommers_hash: Optional[Hash32] - ommers: Any - parent_beacon_block_root: Optional[Hash32] - parent_excess_blob_gas: Optional[U64] - parent_blob_gas_used: Optional[U64] - excess_blob_gas: Optional[U64] - slot_number: Optional[U64] - requests: Any - - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): - if t8n.options.input_env == "stdin": - assert stdin is not None - data = stdin["env"] - else: - with open(t8n.options.input_env, "r") as f: - data = json.load(f) - - self.coinbase = t8n.fork.hex_to_address(data["currentCoinbase"]) - self.block_gas_limit = parse_hex_or_int(data["currentGasLimit"], Uint) - self.block_number = parse_hex_or_int(data["currentNumber"], Uint) - self.block_timestamp = parse_hex_or_int(data["currentTimestamp"], U256) - - self.read_block_difficulty(data, t8n) - self.read_base_fee_per_gas(data, t8n) - self.read_randao(data, t8n) - self.read_block_hashes(data) - self.read_ommers(data, t8n) - self.read_withdrawals(data, t8n) - - self.parent_beacon_block_root = None - if t8n.fork.has_beacon_roots_address: - if not t8n.options.state_test: - parent_beacon_block_root_hex = data["parentBeaconBlockRoot"] - self.parent_beacon_block_root = ( - Bytes32(hex_to_bytes(parent_beacon_block_root_hex)) - if parent_beacon_block_root_hex is not None - else None - ) - self.read_excess_blob_gas(data, t8n) - - self.read_slot_number(data, t8n) - - def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: - """ - Read the excess_blob_gas from the data. If the excess blob gas is - not present, it is calculated from the parent block parameters. - """ - self.parent_blob_gas_used = U64(0) - self.parent_excess_blob_gas = U64(0) - self.excess_blob_gas = None - - if not t8n.fork.has_beacon_roots_address: - return - - if "parentExcessBlobGas" in data: - self.parent_excess_blob_gas = parse_hex_or_int( - data["parentExcessBlobGas"], U64 - ) - - if "parentBlobGasUsed" in data: - self.parent_blob_gas_used = parse_hex_or_int( - data["parentBlobGasUsed"], U64 - ) - - if "currentExcessBlobGas" in data: - self.excess_blob_gas = parse_hex_or_int( - data["currentExcessBlobGas"], U64 - ) - return - - assert self.parent_excess_blob_gas is not None - assert self.parent_blob_gas_used is not None - - arguments = { - # Useless as far as calculate_excess_blob_gas is concerned. - "parent_hash": Hash32(b"\0" * 32), - "ommers_hash": Hash32(b"\0" * 32), - "coinbase": Bytes20(b"\0" * 20), - "state_root": Hash32(b"\0" * 32), - "transactions_root": Hash32(b"\0" * 32), - "receipt_root": Hash32(b"\0" * 32), - "bloom": Bytes256(b"\0" * 256), - "difficulty": Uint(0), - "number": Uint(0), - "gas_limit": Uint(0), - "gas_used": Uint(0), - "timestamp": U256(0), - "extra_data": b"", - "prev_randao": Bytes32(b"\0" * 32), - "nonce": Bytes8(b"\0" * 8), - "withdrawals_root": Hash32(b"\0" * 32), - "parent_beacon_block_root": Hash32(b"\0" * 32), - # Used for calculating excess_blob_gas. - "base_fee_per_gas": self.parent_base_fee_per_gas, - "blob_gas_used": self.parent_blob_gas_used, - "excess_blob_gas": self.parent_excess_blob_gas, - } - - if t8n.fork.has_compute_requests_hash: - arguments["requests_hash"] = Hash32(b"\0" * 32) - - if t8n.fork.has_hash_block_access_list: - arguments["block_access_list_hash"] = Hash32(b"\0" * 32) - if t8n.fork.has_slot_number: - arguments["slot_number"] = U64(0) - - parent_header = t8n.fork.Header(**arguments) - - self.excess_blob_gas = t8n.fork.calculate_excess_blob_gas( - parent_header - ) - - def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: - """ - Read the base_fee_per_gas from the data. If the base fee is - not present, it is calculated from the parent block parameters. - """ - self.parent_gas_used = None - self.parent_gas_limit = None - self.parent_base_fee_per_gas = None - self.base_fee_per_gas = None - - if t8n.fork.has_calculate_base_fee_per_gas: - if "currentBaseFee" in data: - self.base_fee_per_gas = parse_hex_or_int( - data["currentBaseFee"], Uint - ) - - if "parentGasUsed" in data: - self.parent_gas_used = parse_hex_or_int( - data["parentGasUsed"], Uint - ) - - if "parentGasLimit" in data: - self.parent_gas_limit = parse_hex_or_int( - data["parentGasLimit"], Uint - ) - - if "parentBaseFee" in data: - self.parent_base_fee_per_gas = parse_hex_or_int( - data["parentBaseFee"], Uint - ) - - if self.base_fee_per_gas is None: - assert self.parent_gas_limit is not None - assert self.parent_gas_used is not None - assert self.parent_base_fee_per_gas is not None - - parameters: List[object] = [ - self.block_gas_limit, - self.parent_gas_limit, - self.parent_gas_used, - self.parent_base_fee_per_gas, - ] - - self.base_fee_per_gas = t8n.fork.calculate_base_fee_per_gas( - *parameters - ) - - def read_randao(self, data: Any, t8n: "T8N") -> None: - """ - Read the randao from the data. - """ - self.prev_randao = None - if t8n.fork.proof_of_stake: - # tf tool might not always provide an - # even number of nibbles in the randao - # This could create issues in the - # hex_to_bytes function - current_random = data["currentRandom"] - if current_random.startswith("0x"): - current_random = current_random[2:] - - if len(current_random) % 2 == 1: - current_random = "0" + current_random - - self.prev_randao = Bytes32( - left_pad_zero_bytes(hex_to_bytes(current_random), 32) - ) - - def read_slot_number(self, data: Any, t8n: "T8N") -> None: - """ - Read the slot number from the data. - The slot number is provided by the consensus layer. - """ - self.slot_number = None - if t8n.fork.has_slot_number: - if "slotNumber" in data: - self.slot_number = parse_hex_or_int(data["slotNumber"], U64) - - def read_withdrawals(self, data: Any, t8n: "T8N") -> None: - """ - Read the withdrawals from the data. - """ - self.withdrawals = None - if t8n.fork.has_withdrawal: - self.withdrawals = tuple( - t8n.json_to_withdrawals(wd) for wd in data["withdrawals"] - ) - - def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: - """ - Read the block difficulty from the data. - If `currentDifficulty` is present, it is used. Otherwise, - the difficulty is calculated from the parent block. - """ - self.block_difficulty = None - self.parent_timestamp = None - self.parent_difficulty = None - self.parent_ommers_hash = None - if t8n.fork.proof_of_stake: - return - elif "currentDifficulty" in data: - self.block_difficulty = parse_hex_or_int( - data["currentDifficulty"], Uint - ) - else: - self.parent_timestamp = parse_hex_or_int( - data["parentTimestamp"], U256 - ) - self.parent_difficulty = parse_hex_or_int( - data["parentDifficulty"], Uint - ) - args: List[object] = [ - self.block_number, - self.block_timestamp, - self.parent_timestamp, - self.parent_difficulty, - ] - if t8n.fork.calculate_block_difficulty_arity > 4: - if "parentUncleHash" in data: - EMPTY_OMMER_HASH = keccak256(rlp.encode([])) # noqa N806 - self.parent_ommers_hash = Hash32( - hex_to_bytes(data["parentUncleHash"]) - ) - parent_has_ommers = ( - self.parent_ommers_hash != EMPTY_OMMER_HASH - ) - args.append(parent_has_ommers) - else: - args.append(False) - self.block_difficulty = t8n.fork.calculate_block_difficulty(*args) - - def read_block_hashes(self, data: Any) -> None: - """ - Read the block hashes. Returns a maximum of 256 block hashes. - """ - # Read the block hashes - block_hashes: List[Any] = [] - - # The hex key strings provided might not have standard formatting - clean_block_hashes: Dict[int, Hash32] = {} - if "blockHashes" in data: - for key, value in data["blockHashes"].items(): - int_key = int(key, 16) - clean_block_hashes[int_key] = Hash32(hex_to_bytes(value)) - - # Store a maximum of 256 block hashes. - max_blockhash_count = min(Uint(256), self.block_number) - for number in range( - self.block_number - max_blockhash_count, self.block_number - ): - if number in clean_block_hashes.keys(): - block_hashes.append(clean_block_hashes[number]) - else: - block_hashes.append(None) - - self.block_hashes = block_hashes - - def read_ommers(self, data: Any, t8n: "T8N") -> None: - """ - Read the ommers. The ommers data might not have all the details - needed to obtain the Header. - """ - ommers = [] - if "ommers" in data: - for ommer in data["ommers"]: - ommers.append( - Ommer( - ommer["delta"], - t8n.fork.hex_to_address(ommer["address"]), - ) - ) - self.ommers = ommers diff --git a/src/ethereum_spec_tools/evm_tools/t8n/result.py b/src/ethereum_spec_tools/evm_tools/t8n/result.py new file mode 100644 index 00000000000..8e434fdd412 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/t8n/result.py @@ -0,0 +1,149 @@ +""" +Build the testing-side ``Result`` from an executed block. + +All construction of ``Result`` and ``TransactionReceipt`` lives here +so the testing-package pydantic types stay isolated to one boundary +module. +""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from ethereum_rlp import rlp + +from ethereum.crypto.hash import keccak256 +from ethereum.merkle_patricia_trie import root, trie_get + +if TYPE_CHECKING: + from execution_testing.client_clis.cli_types import ( + Result as TestingResult, + ) + + from . import T8N + + +def get_receipts_from_output(t8n: "T8N", block_output: Any) -> List[Any]: + """Build testing-side `TransactionReceipt`s from the block output tries.""" + # Function-scoped: ``execution_testing/__init__`` eagerly imports + # ``.specs`` which transitively imports ``client_clis``, which + # imports ``ExecutionSpecsTransitionTool`` — top-level import would + # cycle back into ``t8n``. + from execution_testing.test_types.receipt_types import ( + TransactionLog, + TransactionReceipt, + ) + + receipts: List[Any] = [] + for key in block_output.receipt_keys: + tx = trie_get(block_output.transactions_trie, key) + receipt = trie_get(block_output.receipts_trie, key) + assert tx is not None + assert receipt is not None + + tx_hash = t8n.fork.get_transaction_hash(tx) + + if hasattr(t8n.fork, "decode_receipt"): + decoded_receipt = t8n.fork.decode_receipt(receipt) + else: + decoded_receipt = receipt + + receipt_kwargs: Dict[str, Any] = { + "transaction_hash": tx_hash, + "cumulative_gas_used": int(decoded_receipt.cumulative_gas_used), + "bloom": decoded_receipt.bloom, + "logs": [ + TransactionLog( + address=log.address, + topics=list(log.topics), + data=log.data, + ) + for log in decoded_receipt.logs + ], + } + if hasattr(decoded_receipt, "succeeded"): + receipt_kwargs["status"] = int(decoded_receipt.succeeded) + elif hasattr(decoded_receipt, "post_state"): + receipt_kwargs["post_state"] = decoded_receipt.post_state + receipts.append(TransactionReceipt(**receipt_kwargs)) + return receipts + + +def build_result( + t8n: "T8N", + block_env: Any, + block_output: Any, + block_exception: Optional[str], + rejected_transactions: List[Any], +) -> "TestingResult": + """Build the testing-side `Result` from the executed block.""" + # Function-scoped: see import-cycle note in ``get_receipts_from_output``. + from execution_testing.client_clis.cli_types import Result as TestingResult + + diff = t8n.fork.extract_block_diff(t8n._block_state) + state_root, _ = t8n.alloc.compute_state_root_and_trie_changes( + diff.account_changes, diff.storage_changes, diff.storage_clears + ) + + arguments: Dict[str, Any] = { + "state_root": state_root, + "transactions_trie": root(block_output.transactions_trie), + "receipts_root": root(block_output.receipts_trie), + "logs_hash": keccak256(rlp.encode(block_output.block_logs)), + "logs_bloom": t8n.fork.logs_bloom(block_output.block_logs), + "receipts": get_receipts_from_output(t8n, block_output), + "rejected_transactions": rejected_transactions, + "gas_used": int(block_output.block_gas_used), + } + if hasattr(block_output, "block_state_gas_used"): + if int(block_output.block_state_gas_used) > arguments["gas_used"]: + arguments["gas_used"] = int(block_output.block_state_gas_used) + if block_exception is not None: + arguments["block_exception"] = block_exception + if hasattr(block_env, "difficulty"): + arguments["difficulty"] = int(block_env.difficulty) + if hasattr(block_env, "base_fee_per_gas"): + arguments["base_fee_per_gas"] = int(block_env.base_fee_per_gas) + if hasattr(block_output, "withdrawals_trie"): + arguments["withdrawals_root"] = root(block_output.withdrawals_trie) + if hasattr(block_env, "excess_blob_gas"): + arguments["excess_blob_gas"] = int(block_env.excess_blob_gas) + arguments["blob_gas_used"] = int(block_output.blob_gas_used) + if hasattr(block_output, "requests"): + arguments["requests"] = list(block_output.requests) + arguments["requests_hash"] = t8n.fork.compute_requests_hash( + block_output.requests + ) + if hasattr(block_output, "block_access_list"): + arguments["block_access_list"] = rlp.encode( + block_output.block_access_list + ) + arguments["block_access_list_hash"] = t8n.fork.hash_block_access_list( + block_output.block_access_list + ) + + context: Optional[Dict[str, Any]] = None + if t8n.exception_mapper is not None: + context = {"exception_mapper": t8n.exception_mapper} + return TestingResult.model_validate(arguments, context=context) + + +def record_rejected_tx(t8n: "T8N", index: int, error: Exception) -> None: + """Append a ``RejectedTransaction`` to ``t8n.rejected_transactions``.""" + # Function-scoped: see import-cycle note in ``get_receipts_from_output``. + from execution_testing.client_clis.cli_types import RejectedTransaction + + context: Optional[Dict[str, Any]] = None + if t8n.exception_mapper is not None: + context = {"exception_mapper": t8n.exception_mapper} + t8n.rejected_transactions.append( + RejectedTransaction.model_validate( + {"index": index, "error": f"Failed transaction: {error!r}"}, + context=context, + ) + ) + + +__all__ = [ + "build_result", + "get_receipts_from_output", + "record_rejected_tx", +] diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py deleted file mode 100644 index 9a5a824e76b..00000000000 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ /dev/null @@ -1,443 +0,0 @@ -""" -Define the types used by the t8n tool. -""" - -import json -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from ethereum_rlp import Simple, rlp -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U64, U256, Uint - -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.merkle_patricia_trie import root, trie_get -from ethereum.state import EMPTY_CODE_HASH, apply_changes_to_state -from ethereum.utils.hexadecimal import hex_to_bytes, hex_to_u256, hex_to_uint - -from ..loaders.transaction_loader import TransactionLoad, UnsupportedTxError -from ..utils import FatalError, encode_to_hex, secp256k1_sign - -if TYPE_CHECKING: - from . import T8N - - -class Alloc: - """ - The alloc (state) type for the t8n tool. - """ - - state: Any - state_backup: Any - - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): - """Read the alloc file and return the state.""" - if t8n.options.input_alloc == "stdin": - assert stdin is not None - data = stdin["alloc"] - else: - with open(t8n.options.input_alloc, "r") as f: - data = json.load(f) - - # The json_to_state function expects the values to be hex - # strings, so we convert them here. - for address, account in data.items(): - for key, value in account.items(): - if key == "storage" or not value: - continue - elif not value.startswith("0x"): - data[address][key] = "0x" + hex(int(value)) - - state = t8n.json_to_state(data) - if t8n.fork.hardfork.short_name == "dao_fork": - t8n.fork.apply_dao(state) - - self.state = state - - def to_json(self) -> Any: - """Encode the state to JSON.""" - data = {} - for address, account in self.state._main_trie._data.items(): - account_data: Dict[str, Any] = {} - - if account.balance: - account_data["balance"] = hex(account.balance) - - if account.nonce: - account_data["nonce"] = hex(account.nonce) - - if account.code_hash != EMPTY_CODE_HASH: - code = self.state._code_store[account.code_hash] - account_data["code"] = "0x" + code.hex() - - if address in self.state._storage_tries: - account_data["storage"] = { - "0x" + k.hex(): hex(v) - for k, v in self.state._storage_tries[ - address - ]._data.items() - } - - data["0x" + address.hex()] = account_data - - return data - - -class Txs: - """ - Read the transactions file, sort out the valid transactions and - return a list of transactions. - """ - - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): - self.t8n = t8n - self.successfully_parsed: List[int] = [] - self.transactions: List[Any] = [] - self.rejected_txs = {} - self.rlp_input = False - self.all_txs = [] - - if t8n.options.input_txs == "stdin": - assert stdin is not None - data = stdin["txs"] - else: - with open(t8n.options.input_txs, "r") as f: - data = json.load(f) - - if data is None: - self.data: Simple = [] - elif isinstance(data, str): - self.rlp_input = True - self.data = rlp.decode(hex_to_bytes(data)) - else: - self.data = data - - for idx, raw_tx in enumerate(self.data): - try: - if self.rlp_input: - self.transactions.append(self.parse_rlp_tx(raw_tx)) - self.successfully_parsed.append(idx) - else: - self.transactions.append(self.parse_json_tx(raw_tx)) - self.successfully_parsed.append(idx) - except UnsupportedTxError as e: - self.t8n.logger.warning( - f"Unsupported transaction at index {idx}: " - f"{e.error_message}" - ) - self.rejected_txs[idx] = ( - f"Unsupported transaction type: {e.error_message}" - ) - if e.encoded_params is not None: - self.all_txs.append(e.encoded_params) - except Exception as e: - msg = f"Failed to parse transaction {idx}: {str(e)}" - self.t8n.logger.warning(msg, exc_info=e) - self.rejected_txs[idx] = msg - - def parse_rlp_tx(self, raw_tx: Any) -> Any: - """ - Read transactions from RLP. - """ - t8n = self.t8n - - tx_rlp = rlp.encode(raw_tx) - if t8n.fork.has_legacy_transaction: - if isinstance(raw_tx, Bytes): - transaction = t8n.fork.decode_transaction(raw_tx) - self.all_txs.append(raw_tx) - else: - transaction = rlp.decode_to(t8n.fork.LegacyTransaction, tx_rlp) - self.all_txs.append(transaction) - else: - transaction = rlp.decode_to(t8n.fork.Transaction, tx_rlp) - self.all_txs.append(transaction) - - return transaction - - def parse_json_tx(self, raw_tx: Any) -> Any: - """ - Read the transactions from json. - If a transaction is unsigned but has a `secretKey` field, the - transaction will be signed. - """ - t8n = self.t8n - - # for idx, json_tx in enumerate(self.data): - raw_tx["gasLimit"] = raw_tx["gas"] - raw_tx["data"] = raw_tx["input"] - if "to" not in raw_tx or raw_tx["to"] is None: - raw_tx["to"] = "" - - # tf tool might provide None instead of 0 - # for v, r, s - raw_tx["v"] = raw_tx.get("v") or raw_tx.get("y_parity") or "0x00" - raw_tx["r"] = raw_tx.get("r") or "0x00" - raw_tx["s"] = raw_tx.get("s") or "0x00" - - v = hex_to_u256(raw_tx["v"]) - r = hex_to_u256(raw_tx["r"]) - s = hex_to_u256(raw_tx["s"]) - - if "secretKey" in raw_tx and v == r == s == 0: - self.sign_transaction(raw_tx) - - tx = TransactionLoad(raw_tx, t8n.fork).read() - self.all_txs.append(tx) - - if t8n.fork.has_decode_transaction: - transaction = t8n.fork.decode_transaction(tx) - else: - transaction = tx - - return transaction - - def sign_transaction(self, json_tx: Any) -> None: - """ - Sign a transaction. This function will be invoked if a `secretKey` - is provided in the transaction. - Post spurious dragon, the transaction is signed according to EIP-155 - if the protected flag is missing or set to true. - """ - t8n = self.t8n - protected = json_tx.get("protected", True) - - tx = TransactionLoad(json_tx, t8n.fork).read() - - if isinstance(tx, bytes): - tx_decoded = t8n.fork.decode_transaction(tx) - else: - tx_decoded = tx - - secret_key = hex_to_uint(json_tx["secretKey"][2:]) - if t8n.fork.has_legacy_transaction: - Transaction = t8n.fork.LegacyTransaction # noqa N806 - else: - Transaction = t8n.fork.Transaction # noqa N806 - - v_addend: U256 - if isinstance(tx_decoded, Transaction): - if t8n.fork.has_signing_hash_155: - if protected: - signing_hash = t8n.fork.signing_hash_155( - tx_decoded, self.t8n.chain_id - ) - # EIP-155: CHAIN_ID * 2 + 35 - v_addend = U256(self.t8n.chain_id) * U256(2) + U256(35) - else: - signing_hash = t8n.fork.signing_hash_pre155(tx_decoded) - v_addend = U256(27) - else: - signing_hash = t8n.fork.signing_hash(tx_decoded) - v_addend = U256(27) - elif isinstance(tx_decoded, t8n.fork.AccessListTransaction): - signing_hash = t8n.fork.signing_hash_2930(tx_decoded) - v_addend = U256(0) - elif isinstance(tx_decoded, t8n.fork.FeeMarketTransaction): - signing_hash = t8n.fork.signing_hash_1559(tx_decoded) - v_addend = U256(0) - elif isinstance(tx_decoded, t8n.fork.BlobTransaction): - signing_hash = t8n.fork.signing_hash_4844(tx_decoded) - v_addend = U256(0) - elif isinstance(tx_decoded, t8n.fork.SetCodeTransaction): - signing_hash = t8n.fork.signing_hash_7702(tx_decoded) - v_addend = U256(0) - else: - raise FatalError("Unknown transaction type") - - r, s, y = secp256k1_sign(signing_hash, int(secret_key)) - json_tx["r"] = hex(r) - json_tx["s"] = hex(s) - json_tx["v"] = hex(y + v_addend) - - if v_addend == 0: - json_tx["y_parity"] = json_tx["v"] - - -@dataclass -class Result: - """Type that represents the result of a transition execution.""" - - difficulty: Any - base_fee: Any - state_root: Any = None - tx_root: Any = None - receipt_root: Any = None - withdrawals_root: Any = None - logs_hash: Any = None - bloom: Any = None - receipts: Any = None - rejected: Any = None - gas_used: Any = None - excess_blob_gas: Optional[U64] = None - blob_gas_used: Optional[Uint] = None - requests_hash: Optional[Hash32] = None - requests: Optional[List[Bytes]] = None - block_exception: Optional[str] = None - block_access_list: Optional[Any] = None - block_access_list_hash: Optional[Hash32] = None - - def get_receipts_from_output( - self, - t8n: Any, - block_output: Any, - ) -> List[Any]: - """ - Get receipts from the transaction and receipts tries. - """ - receipts: List[Any] = [] - for key in block_output.receipt_keys: - tx = trie_get(block_output.transactions_trie, key) - receipt = trie_get(block_output.receipts_trie, key) - - assert tx is not None - assert receipt is not None - - tx_hash = t8n.fork.get_transaction_hash(tx) - - if hasattr(t8n.fork, "decode_receipt"): - decoded_receipt = t8n.fork.decode_receipt(receipt) - else: - decoded_receipt = receipt - - receipts.append((tx_hash, decoded_receipt)) - - return receipts - - def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: - """ - Update the result after processing the inputs. - """ - self.gas_used = block_output.block_gas_used - if hasattr(block_output, "block_state_gas_used"): - if block_output.block_state_gas_used > self.gas_used: - self.gas_used = block_output.block_state_gas_used - self.tx_root = root(block_output.transactions_trie) - self.receipt_root = root(block_output.receipts_trie) - self.bloom = t8n.fork.logs_bloom(block_output.block_logs) - self.logs_hash = keccak256(rlp.encode(block_output.block_logs)) - block_diff = t8n.fork.extract_block_diff(t8n._block_state) - state_root_value, _ = ( - t8n.alloc.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) - ) - self.state_root = state_root_value - # Apply diffs to pre-state for alloc output - apply_changes_to_state(t8n.alloc.state, block_diff) - self.receipts = self.get_receipts_from_output(t8n, block_output) - - if hasattr(block_env, "base_fee_per_gas"): - self.base_fee = block_env.base_fee_per_gas - - if hasattr(block_output, "withdrawals_trie"): - self.withdrawals_root = root(block_output.withdrawals_trie) - - if hasattr(block_env, "excess_blob_gas"): - self.excess_blob_gas = block_env.excess_blob_gas - - if hasattr(block_output, "requests"): - self.requests = block_output.requests - self.requests_hash = t8n.fork.compute_requests_hash(self.requests) - - if hasattr(block_output, "block_access_list"): - self.block_access_list = block_output.block_access_list - self.block_access_list_hash = t8n.fork.hash_block_access_list( - block_output.block_access_list - ) - - def json_encode_receipts(self) -> Any: - """ - Encode receipts to JSON. - """ - receipts_json = [] - for tx_hash, receipt in self.receipts: - receipt_dict = {"transactionHash": "0x" + tx_hash.hex()} - - if hasattr(receipt, "succeeded"): - receipt_dict["succeeded"] = receipt.succeeded - else: - assert hasattr(receipt, "post_state") - receipt_dict["post_state"] = "0x" + receipt.post_state.hex() - - receipt_dict["cumulativeGasUsed"] = hex( - receipt.cumulative_gas_used - ) - receipt_dict["bloom"] = "0x" + receipt.bloom.hex() - - # Add logs to receipts - logs_json = [] - for log in receipt.logs: - log_dict = { - "address": "0x" + log.address.hex(), - "topics": ["0x" + topic.hex() for topic in log.topics], - "data": "0x" + log.data.hex(), - } - logs_json.append(log_dict) - receipt_dict["logs"] = logs_json - - receipts_json.append(receipt_dict) - - return receipts_json - - def to_json(self) -> Any: - """Encode the result to JSON.""" - data = {} - - data["stateRoot"] = "0x" + self.state_root.hex() - data["txRoot"] = "0x" + self.tx_root.hex() - data["receiptsRoot"] = "0x" + self.receipt_root.hex() - if self.withdrawals_root: - data["withdrawalsRoot"] = "0x" + self.withdrawals_root.hex() - data["logsHash"] = "0x" + self.logs_hash.hex() - data["logsBloom"] = "0x" + self.bloom.hex() - data["gasUsed"] = hex(self.gas_used) - if self.difficulty: - data["currentDifficulty"] = hex(self.difficulty) - else: - data["currentDifficulty"] = None - - if self.base_fee: - data["currentBaseFee"] = hex(self.base_fee) - else: - data["currentBaseFee"] = None - - if self.excess_blob_gas is not None: - data["currentExcessBlobGas"] = hex(self.excess_blob_gas) - - if self.blob_gas_used is not None: - data["blobGasUsed"] = hex(self.blob_gas_used) - - data["rejected"] = [ - {"index": idx, "error": error} - for idx, error in self.rejected.items() - ] - - data["receipts"] = self.json_encode_receipts() - - if self.requests_hash is not None: - assert self.requests is not None - - data["requestsHash"] = encode_to_hex(self.requests_hash) - # T8N doesn't consider the request type byte to be part of the - # request - data["requests"] = [encode_to_hex(req) for req in self.requests] - - if self.block_exception is not None: - data["blockException"] = self.block_exception - - if self.block_access_list is not None: - # Output BAL as RLP-encoded hex bytes; the testing framework - # handles JSON serialization. - data["blockAccessList"] = encode_to_hex( - rlp.encode(self.block_access_list) - ) - - if self.block_access_list_hash is not None: - data["blockAccessListHash"] = encode_to_hex( - self.block_access_list_hash - ) - - return data diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index 15aee92af71..7483c38b34f 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -15,13 +15,10 @@ Sequence, Tuple, TypeVar, - Union, ) -import spec256k1 from ethereum_types.numeric import U64, U256, Uint -from ethereum.crypto.hash import Hash32 from ethereum_spec_tools.forks import Hardfork W = TypeVar("W", Uint, U64, U256) @@ -132,6 +129,44 @@ def find_fork( sys.exit(f"Unsupported state fork: {options.state_fork}") +# Map testing ``Fork.transition_tool_name()`` → spec ``Hardfork.short_name`` +# for cases where CamelCase → snake_case does not produce the spec +# module name: +# * ``Paris`` reports itself as ``"Merge"`` to the t8n protocol. +# * ``DAOFork`` would snake-case to ``d_a_o_fork``. +# * ``ConstantinopleFix`` is a testing-side distinction that the spec +# folds into the ``constantinople`` module. +_SPEC_SHORT_NAME_OVERRIDES: Dict[str, str] = { + "Merge": "paris", + "DAOFork": "dao_fork", + "ConstantinopleFix": "constantinople", +} + + +def resolve_fork(fork_name: str) -> Hardfork: + """ + Resolve a testing ``Fork.transition_tool_name()`` to its matching + spec ``Hardfork``. + + CLI exception aliases like ``HomesteadToDaoAt5`` are resolved by + :func:`find_fork` before the testing ``Fork`` is built, so the name + reaching this function is always post-alias-resolution. + """ + short = _SPEC_SHORT_NAME_OVERRIDES.get(fork_name) + if short is None: + short = re.sub(r"(? List[str]: """ Get the supported forks. @@ -166,29 +201,3 @@ def get_stream_logger(name: str) -> Any: logger.addHandler(stream_handler) return logger - - -def secp256k1_sign(msg_hash: Hash32, secret_key: int) -> Tuple[U256, ...]: - """ - Returns the signature of a message hash given the secret key. - """ - private_key = spec256k1.PrivateKey(secret_key.to_bytes(32, "big")) - signature = private_key.sign_recoverable(msg_hash) - - return ( - U256.from_be_bytes(signature[0:32]), - U256.from_be_bytes(signature[32:64]), - U256(signature[64]), - ) - - -def encode_to_hex(data: Union[bytes, int]) -> str: - """ - Encode the data to a hex string. - """ - if isinstance(data, int): - return hex(data) - elif isinstance(data, bytes): - return "0x" + data.hex() - else: - raise Exception("Invalid data type") diff --git a/tests/evm_tools/test_count_opcodes.py b/tests/evm_tools/test_count_opcodes.py index 4220ffa6586..0ced37e51f5 100644 --- a/tests/evm_tools/test_count_opcodes.py +++ b/tests/evm_tools/test_count_opcodes.py @@ -11,7 +11,8 @@ import pytest from ethereum_spec_tools.evm_tools import create_parser -from ethereum_spec_tools.evm_tools.t8n import T8N, ForkCache +from ethereum_spec_tools.evm_tools.t8n import ForkCache +from ethereum_spec_tools.evm_tools.t8n.cli import run_t8n_cli parser = create_parser() @@ -41,10 +42,7 @@ def test_count_opcodes(root_relative: Callable[[str | Path], Path]) -> None: out_file = StringIO() with ForkCache() as fork_cache: - t8n_tool = T8N( - options, out_file=out_file, in_file=in_file, cache=fork_cache - ) - exit_code = t8n_tool.run() + exit_code = run_t8n_cli(options, out_file, in_file, fork_cache) assert 0 == exit_code results = json.loads(out_file.getvalue()) diff --git a/tests/json_loader/helpers/load_state_tests.py b/tests/json_loader/helpers/load_state_tests.py index 4cfe757d943..52a4557ff53 100644 --- a/tests/json_loader/helpers/load_state_tests.py +++ b/tests/json_loader/helpers/load_state_tests.py @@ -1,7 +1,6 @@ """Helper functions to load and run general state tests for Ethereum forks.""" import json -import sys from io import StringIO from typing import Any, Dict, Final, Iterable, List @@ -14,7 +13,8 @@ from ethereum.utils.hexadecimal import hex_to_bytes from ethereum_spec_tools.evm_tools import create_parser from ethereum_spec_tools.evm_tools.statetest import read_test_case -from ethereum_spec_tools.evm_tools.t8n import T8N, ForkCache +from ethereum_spec_tools.evm_tools.t8n import ForkCache +from ethereum_spec_tools.evm_tools.t8n.cli import build_t8n_from_cli_options from .. import FORKS from ..stash_keys import desired_forks_key, fork_cache_key @@ -144,14 +144,18 @@ def runtest(self) -> None: with ForkCache() as fork_cache: try: - t8n = T8N(t8n_options, sys.stdout, in_stream, fork_cache) + t8n = build_t8n_from_cli_options( + t8n_options, in_stream, fork_cache + ) except StateWithEmptyAccount as e: pytest.xfail(str(e)) t8n.run_state_test() if "expectException" in post: - assert 0 in t8n.txs.rejected_txs + assert any( + int(rej.index) == 0 for rej in t8n.rejected_transactions + ) return assert hex_to_bytes(post_hash) == t8n.result.state_root diff --git a/vulture_whitelist.py b/vulture_whitelist.py index fd1f6712690..e944b4e3f7c 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -19,7 +19,7 @@ from ethereum_spec_tools.evm_tools.loaders.transaction_loader import ( TransactionLoad, ) -from ethereum_spec_tools.evm_tools.t8n.env import Ommer +from ethereum_spec_tools.evm_tools.t8n.block_environment import Ommer from ethereum_spec_tools.evm_tools.t8n.evm_trace.eip3155 import ( FinalTrace, Trace, @@ -121,9 +121,15 @@ TransactionLoad.json_to_r TransactionLoad.json_to_s -# src/ethereum_spec_tools/evm_tools/t8n/env.py +# src/ethereum_spec_tools/evm_tools/t8n/block_environment.py Ommer.delta +# src/ethereum_spec_tools/evm_tools/t8n/__init__.py +# `protected` is a field on the testing-package `Transaction` model; +# T8N flips it to False for pre-EIP-155 forks before calling `sign()`. +_unused_protected_marker = None +_unused_protected_marker.protected # type: ignore[attr-defined] + # src/ethereum_spec_tools/evm_tools/t8n/evm_trace/eip3155.py Trace.gasCost Trace.memSize From 04e7b0daf64c5490cf0c4a7c132d2ac0c4f5419a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 24 Jul 2026 08:58:37 +0200 Subject: [PATCH 02/55] feat(tests): add EIP-2681 nonce-reaching-max regression tests (#3226) Add regression tests verifying that reaching the maximum account nonce (2**64-1) during execution is valid: per EIP-2681 only a transaction whose nonce is 2**64-1 is invalid, not one that merely increments an account to that value. Ported from ipsilon/evmone#1608: * top-level CALL from a sender at nonce 2**64-2 * top-level CREATE from a sender at nonce 2**64-2 (created-account nonce fork-gated per EIP-161) * EIP-7702 self-sponsored set-code tx whose authorization drives the sender to 2**64-1 --- .../eip2681_limit_account_nonce/__init__.py | 3 + .../eip2681_limit_account_nonce/spec.py | 23 +++ .../test_nonce_reaching_max.py | 136 ++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 tests/frontier/eip2681_limit_account_nonce/__init__.py create mode 100644 tests/frontier/eip2681_limit_account_nonce/spec.py create mode 100644 tests/frontier/eip2681_limit_account_nonce/test_nonce_reaching_max.py diff --git a/tests/frontier/eip2681_limit_account_nonce/__init__.py b/tests/frontier/eip2681_limit_account_nonce/__init__.py new file mode 100644 index 00000000000..65a94752f70 --- /dev/null +++ b/tests/frontier/eip2681_limit_account_nonce/__init__.py @@ -0,0 +1,3 @@ +""" +Tests [EIP-2681: Limit account nonce to 2^64-1](https://eips.ethereum.org/EIPS/eip-2681). +""" diff --git a/tests/frontier/eip2681_limit_account_nonce/spec.py b/tests/frontier/eip2681_limit_account_nonce/spec.py new file mode 100644 index 00000000000..24395fb3375 --- /dev/null +++ b/tests/frontier/eip2681_limit_account_nonce/spec.py @@ -0,0 +1,23 @@ +"""Defines EIP-2681 specification constants and functions.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +# EIP-2681 reference specification +ref_spec_2681 = ReferenceSpec( + "EIPS/eip-2681.md", "9e393a79d9937f579acbdcb234a67869259d5a96" +) + + +class Spec: + """Constants for the EIP-2681 account nonce limit tests.""" + + max_nonce = 2**64 - 1 diff --git a/tests/frontier/eip2681_limit_account_nonce/test_nonce_reaching_max.py b/tests/frontier/eip2681_limit_account_nonce/test_nonce_reaching_max.py new file mode 100644 index 00000000000..718c15879bd --- /dev/null +++ b/tests/frontier/eip2681_limit_account_nonce/test_nonce_reaching_max.py @@ -0,0 +1,136 @@ +""" +Tests that reaching the maximum account nonce (`2**64 - 1`) during execution +is valid. + +Per [EIP-2681](https://eips.ethereum.org/EIPS/eip-2681) only a transaction +whose nonce is `2**64 - 1` is invalid; merely incrementing an account to that +value while executing a transaction is permitted. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + AuthorizationTuple, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, + compute_create_address, +) +from execution_testing.forks import SpuriousDragon + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import Spec, ref_spec_2681 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2681.git_path +REFERENCE_SPEC_VERSION = ref_spec_2681.version + + +@pytest.mark.valid_from("Frontier") +@pytest.mark.pre_alloc_mutable +def test_tx_at_nonce_max_minus_one_call( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Test that a top-level CALL transaction from a sender at the highest usable + nonce (`2**64 - 2`) executes normally, bumping the sender to the maximum + nonce (`2**64 - 1`). + """ + sender = pre.fund_eoa(nonce=Spec.max_nonce - 1) + to = pre.fund_eoa(amount=0) + + tx = Transaction( + to=to, + nonce=Spec.max_nonce - 1, + sender=sender, + protected=False, + ) + + state_test(pre=pre, post={sender: Account(nonce=Spec.max_nonce)}, tx=tx) + + +@pytest.mark.valid_from("Frontier") +@pytest.mark.pre_alloc_mutable +def test_tx_at_nonce_max_minus_one_create( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Test that a top-level CREATE transaction from a sender at the highest + usable nonce (`2**64 - 2`) executes normally, creating a contract and + bumping the sender to the maximum nonce (`2**64 - 1`). + """ + sender = pre.fund_eoa(nonce=Spec.max_nonce - 1) + + tx = Transaction( + to=None, + nonce=Spec.max_nonce - 1, + sender=sender, + protected=False, + ) + + # EIP-161 (Spurious Dragon) initializes a new contract's nonce to 1. + created_nonce = 1 if fork >= SpuriousDragon else 0 + created = compute_create_address(address=sender, nonce=Spec.max_nonce - 1) + + state_test( + pre=pre, + post={ + sender: Account(nonce=Spec.max_nonce), + created: Account(nonce=created_nonce, code=b""), + }, + tx=tx, + ) + + +@pytest.mark.valid_from("Prague") +@pytest.mark.pre_alloc_mutable +def test_set_code_self_authorization_reaching_nonce_max( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Test a self-sponsored set-code transaction whose authorization bumps the + sender's nonce to the maximum value (`2**64 - 1`). + + The sender starts at nonce `2**64 - 3`. The transaction increments it to + `2**64 - 2`, then the self-signed authorization (nonce `2**64 - 2`) + applies and increments it to `2**64 - 1`. + """ + storage = Storage() + sender = pre.fund_eoa(nonce=Spec.max_nonce - 2) + delegate = pre.fund_eoa(amount=0) + + # The transaction targets this contract (not the sender), so its SSTORE + # proves the top-level call executed. + set_code_to_address = pre.deploy_contract( + code=Op.SSTORE(storage.store_next(sender), Op.ORIGIN), + ) + + tx = Transaction( + to=set_code_to_address, + authorization_list=[ + AuthorizationTuple( + address=delegate, + nonce=Spec.max_nonce - 1, + signer=sender, + ), + ], + sender=sender, + ) + + state_test( + pre=pre, + tx=tx, + post={ + set_code_to_address: Account(storage=storage), + sender: Account( + nonce=Spec.max_nonce, + code=Spec7702.delegation_designation(delegate), + ), + }, + ) From 1646cf550bfa2b713acb1442551bf47664186285 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 24 Jul 2026 08:04:46 +0100 Subject: [PATCH 03/55] refactor(test): Make max balance < 2^128 wei (#3227) --- tests/cancun/eip4844_blobs/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cancun/eip4844_blobs/conftest.py b/tests/cancun/eip4844_blobs/conftest.py index 6f93d162f88..d417b91c28c 100644 --- a/tests/cancun/eip4844_blobs/conftest.py +++ b/tests/cancun/eip4844_blobs/conftest.py @@ -315,7 +315,7 @@ def non_zero_blob_gas_used_genesis_block( f"with base_fee_per_gas {block_base_fee_per_gas}" ) - sender = pre.fund_eoa(10**42) + sender = pre.fund_eoa(10**36) empty_account_destination = pre.fund_eoa(0) blob_gas_price_calculator = block_fork.blob_gas_price_calculator() From 6463c0dc37939ca478dd19c5ccac6f5a5bda821b Mon Sep 17 00:00:00 2001 From: spencer Date: Fri, 24 Jul 2026 12:19:42 +0200 Subject: [PATCH 04/55] chore(tests): improve EIP-7708 coverage, checklist, and ref-spec pin (#3220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 蔡佳誠 Louis Tsai <72684086+LouisTsai-Csie@users.noreply.github.com> --- .../eip_checklist_not_applicable.txt | 1 + .../eip7708_eth_transfer_logs/spec.py | 3 +- .../test_eip_mainnet.py | 27 +++ .../test_fork_transition.py | 81 ++++++++ .../test_block_access_lists_eip7708.py | 188 ++++++++++++++++++ 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7708.py diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/eip_checklist_not_applicable.txt b/tests/amsterdam/eip7708_eth_transfer_logs/eip_checklist_not_applicable.txt index 48ce20d19f8..fd7d7d0a1f3 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/eip_checklist_not_applicable.txt +++ b/tests/amsterdam/eip7708_eth_transfer_logs/eip_checklist_not_applicable.txt @@ -12,3 +12,4 @@ execution_layer_request = EIP does not introduce an execution layer request new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint block_level_constraint = EIP does not introduce a block-level validation constraint +general/code_coverage/second_client = Optional diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py index 54088c5217c..56a42e2850a 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py @@ -14,7 +14,7 @@ class ReferenceSpec: ref_spec_7708 = ReferenceSpec( - "EIPS/eip-7708.md", "172188d7b090ed1afb876140f45e19ac00cba4bb" + "EIPS/eip-7708.md", "f7230c46a743313957d8f38a159bda934cc735b2" ) @@ -30,7 +30,6 @@ class Spec: TRANSFER_TOPIC: Hash = Hash( keccak256(b"Transfer(address,address,uint256)") ) - BURN_TOPIC: Hash = Hash(keccak256(b"Burn(address,uint256)")) def transfer_log( diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py b/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py index f14be0735dd..3d84ba109dc 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py @@ -13,6 +13,7 @@ StateTestFiller, Transaction, TransactionReceipt, + compute_create_address, ) from .spec import ref_spec_7708, transfer_log @@ -84,6 +85,32 @@ def test_call_with_value_mainnet( state_test(pre=pre, post=post, tx=tx) +def test_create_endowment_mainnet( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """Test that a CREATE endowment emits a transfer log on mainnet.""" + sender = pre.fund_eoa() + create_value = 1 + + contract = pre.deploy_contract( + Op.CREATE(value=create_value, offset=0, size=0), + balance=create_value, + ) + created = compute_create_address(address=contract, nonce=1) + + tx = Transaction( + sender=sender, + to=contract, + expected_receipt=TransactionReceipt( + logs=[transfer_log(contract, created, create_value)] + ), + ) + + post = {created: Account(balance=create_value)} + state_test(pre=pre, post=post, tx=tx) + + def test_selfdestruct_mainnet( state_test: StateTestFiller, pre: Alloc, diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/test_fork_transition.py b/tests/amsterdam/eip7708_eth_transfer_logs/test_fork_transition.py index 2d79177e1c4..f36b05ed9cd 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_fork_transition.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_fork_transition.py @@ -11,8 +11,10 @@ Alloc, Block, BlockchainTestFiller, + Op, Transaction, TransactionReceipt, + compute_create_address, ) from .spec import ref_spec_7708, transfer_log @@ -81,3 +83,82 @@ def test_transfer_log_fork_transition( recipient: Account(balance=300), }, ) + + +@pytest.mark.parametrize( + "emission_point", + [ + pytest.param("call", id="call"), + pytest.param("create", id="create"), + pytest.param("selfdestruct", id="selfdestruct"), + ], +) +@pytest.mark.valid_at_transition_to("EIP7708") +def test_emission_point_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + emission_point: str, +) -> None: + """ + Test the CALL, CREATE, and SELFDESTRUCT emission points at the fork + transition boundary. + + Clients gate each emission site in a separate code path, so every + site is checked at the transition independently of the + transaction-level log. + """ + sender = pre.fund_eoa() + value = 100 + recipient = pre.deploy_contract(Op.STOP) + + if emission_point == "call": + code = Op.CALL(address=recipient, value=Op.CALLVALUE) + elif emission_point == "create": + code = Op.CREATE(value=Op.CALLVALUE, offset=0, size=0) + else: + code = Op.SELFDESTRUCT(recipient) + contract = pre.deploy_contract(code) + + blocks = [] + for nonce, (timestamp, active) in enumerate( + [(14_999, False), (15_000, True), (15_001, True)], start=1 + ): + if emission_point == "create": + inner_recipient = compute_create_address( + address=contract, nonce=nonce + ) + else: + inner_recipient = recipient + logs = ( + [ + transfer_log(sender, contract, value), + transfer_log(contract, inner_recipient, value), + ] + if active + else [] + ) + blocks.append( + Block( + timestamp=timestamp, + txs=[ + Transaction( + to=contract, + sender=sender, + value=value, + expected_receipt=TransactionReceipt(logs=logs), + ) + ], + ) + ) + + if emission_point == "create": + post = { + compute_create_address(address=contract, nonce=nonce): Account( + balance=value + ) + for nonce in (1, 2, 3) + } + else: + post = {recipient: Account(balance=3 * value)} + + blockchain_test(pre=pre, blocks=blocks, post=post) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7708.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7708.py new file mode 100644 index 00000000000..1698e970731 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7708.py @@ -0,0 +1,188 @@ +""" +Cross-EIP tests for EIP-7928 block-level access lists and EIP-7708 +transfer logs. + +A single block pins both views of the same value flows: the receipts +carry the EIP-7708 Transfer logs while the block access list carries the +matching balance changes, and the priority-fee payment appears in the +access list only, with no Transfer log. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + BalAccountExpectation, + BalBalanceChange, + BalNonceChange, + Block, + BlockAccessListExpectation, + BlockchainTestFiller, + Environment, + Fork, + Header, + Op, + RecipientType, + Transaction, + TransactionReceipt, +) + +from ..eip7708_eth_transfer_logs.spec import transfer_log +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def test_transfer_logs_and_bal_balance_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, +) -> None: + """ + Ensure Transfer logs and BAL balance changes stay consistent within + one block. + + The first transaction is a plain value transfer paying a priority + fee: both parties get BAL balance changes, the receipt carries one + Transfer log, and the coinbase tip appears in the BAL only. The + second transaction sweeps a contract balance via SELFDESTRUCT with a + zero tip: the sweep shows up both as a Transfer log and as BAL + balance changes, while the coinbase entry stays fee-only from the + first transaction. + """ + coinbase = pre.fund_eoa(amount=0) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_gas_calculator( + calldata=b"", + contract_creation=False, + access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + expected_gas_used = intrinsic_gas + top_frame_state_gas + tx_gas_limit = expected_gas_used + 1000 # add a small buffer + gas_price = 0xA + tx_value = 100 + extra_balance = 1000 + + alice_initial_balance = ( + (tx_gas_limit * gas_price) + tx_value + extra_balance + ) + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) + + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + tip_to_coinbase = (gas_price - base_fee_per_gas) * expected_gas_used + alice_final_balance = ( + alice_initial_balance - tx_value - expected_gas_used * gas_price + ) + + tx_transfer = Transaction( + sender=alice, + to=bob, + value=tx_value, + gas_limit=tx_gas_limit, + gas_price=gas_price, + expected_receipt=TransactionReceipt( + logs=[transfer_log(alice, bob, tx_value)] + ), + ) + + # SELFDESTRUCT sweep with a zero tip, so the coinbase BAL entry + # stays fee-only from the first transaction. + sweep_value = 500 + carol = pre.fund_eoa() + dave = pre.fund_eoa(amount=0) + sweeper = pre.deploy_contract( + code=Op.SELFDESTRUCT(dave), balance=sweep_value + ) + + tx_sweep = Transaction( + sender=carol, + to=sweeper, + max_fee_per_gas=base_fee_per_gas, + max_priority_fee_per_gas=0, + expected_receipt=TransactionReceipt( + logs=[transfer_log(sweeper, dave, sweep_value)] + ), + ) + + block = Block( + txs=[tx_transfer, tx_sweep], + fee_recipient=coinbase, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1) + ], + balance_changes=[ + BalBalanceChange( + block_access_index=1, + post_balance=alice_final_balance, + ) + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=tx_value + ) + ], + ), + carol: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=2, post_nonce=1) + ], + ), + sweeper: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(block_access_index=2, post_balance=0) + ], + ), + dave: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=2, post_balance=sweep_value + ) + ], + ), + # The tip is a BAL-only flow: it must never produce a + # Transfer log, and the zero-tip second transaction must + # not add a second balance change. + coinbase: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, + post_balance=tip_to_coinbase, + ) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + bob: Account(balance=tx_value), + dave: Account(balance=sweep_value), + sweeper: Account(balance=0), + }, + genesis_environment=genesis_env, + ) From 6e02b3b1d5fb1eca0331dd74d46820b08499f87b Mon Sep 17 00:00:00 2001 From: spencer Date: Fri, 24 Jul 2026 12:19:48 +0200 Subject: [PATCH 05/55] chore(tests): improve EIP-7843 coverage, checklist, and ref-spec pin (#3221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 蔡佳誠 Louis Tsai <72684086+LouisTsai-Csie@users.noreply.github.com> --- .../execution_testing/fixtures/blockchain.py | 20 +- .../src/execution_testing/specs/blockchain.py | 11 + .../eip_checklist_external_coverage.txt | 3 + .../eip_checklist_not_applicable.txt | 27 ++ tests/amsterdam/eip7843_slotnum/spec.py | 6 +- .../eip7843_slotnum/test_eip_mainnet.py | 11 +- .../eip7843_slotnum/test_fork_transition.py | 103 ++++++- .../amsterdam/eip7843_slotnum/test_slotnum.py | 255 +++++++++++++++++- 8 files changed, 412 insertions(+), 24 deletions(-) create mode 100644 tests/amsterdam/eip7843_slotnum/eip_checklist_external_coverage.txt create mode 100644 tests/amsterdam/eip7843_slotnum/eip_checklist_not_applicable.txt diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index edd819ac30b..3a840769c06 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -370,19 +370,18 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: env.withdrawals ) environment_values["extra_data"] = env.extra_data - extras = { + extras: Dict[str, Any] = { "state_root": state_root, - "requests_hash": Requests() - if fork.header_requests_required() - else None, - "block_access_list_hash": ( - BlockAccessList().rlp_hash - if fork.header_bal_hash_required() - else None - ), - "slot_number": 0 if fork.header_slot_number_required() else None, "fork": fork, } + if fork.header_requests_required(): + extras["requests_hash"] = Requests() + if fork.header_bal_hash_required(): + extras["block_access_list_hash"] = BlockAccessList().rlp_hash + if fork.header_slot_number_required(): + extras["slot_number"] = ( + int(env.slot_number) if env.slot_number is not None else 0 + ) return cls(**environment_values, **extras) @@ -460,6 +459,7 @@ class FixtureExecutionPayloadModifier(CamelModel): ) block_access_list: Removable | Bytes | None = None + slot_number: Removable | HexNumber | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """Sentinel to specify that a payload field should be removed.""" diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 09913d04b9b..96436819460 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -348,6 +348,8 @@ class Block(Header): """EIP-7928: Block-level access lists (serialized).""" engine_new_payload_block_access_list: Bytes | None = None """EIP-7928: override only the engine newPayload blockAccessList field.""" + engine_new_payload_slot_number: HexNumber | None = None + """EIP-7843: override only the engine payload slotNumber field.""" expected_gas_used: int | None = None """Expected gas used for the block.""" @@ -463,6 +465,7 @@ class BuiltBlock(CamelModel): fork: Fork block_access_list: BlockAccessList | None engine_new_payload_block_access_list: Bytes | None = None + engine_new_payload_slot_number: HexNumber | None = None def cumulative_gas_used(self) -> int: """Return the last receipt's cumulative gas used.""" @@ -544,6 +547,10 @@ def engine_payload_modifier( the ``block_access_list`` body. So a header modifier that touches the BAL hash needs to drive a matching change on the payload body. """ + if self.engine_new_payload_slot_number is not None: + return FixtureExecutionPayloadModifier( + slot_number=self.engine_new_payload_slot_number, + ) if self.engine_new_payload_block_access_list is not None: return FixtureExecutionPayloadModifier( block_access_list=self.engine_new_payload_block_access_list, @@ -1041,6 +1048,9 @@ def generate_block_data( engine_new_payload_block_access_list=( block.engine_new_payload_block_access_list ), + engine_new_payload_slot_number=( + block.engine_new_payload_slot_number + ), ) built_block: BuiltBlock if transition_tool_output.engine_payload is not None: @@ -1061,6 +1071,7 @@ def generate_block_data( and block.requests is None and not block.skip_exception_verification and block.engine_new_payload_block_access_list is None + and block.engine_new_payload_slot_number is None and not ( block.expected_block_access_list is not None and block.expected_block_access_list._modifier is not None diff --git a/tests/amsterdam/eip7843_slotnum/eip_checklist_external_coverage.txt b/tests/amsterdam/eip7843_slotnum/eip_checklist_external_coverage.txt new file mode 100644 index 00000000000..c2ba0cb33d3 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/eip_checklist_external_coverage.txt @@ -0,0 +1,3 @@ +general/code_coverage/eels = EIP-7843 adds the slot_number instruction (vm/instructions/block.py), the OPCODE_SLOTNUM gas constant, the header field (blocks.py) and its BlockEnvironment plumbing (fork.py); every line is executed when filling this suite through the EELS t8n +general/code_coverage/test_coverage = suite logic is exercised end-to-end by filling tests/amsterdam/eip7843_slotnum with the EELS filler; every parametrized arm produces a fixture with a discriminating post-state +general/code_coverage/missed_lines = no missed lines; the EIP adds no branches beyond the single instruction body, which every test in this suite executes diff --git a/tests/amsterdam/eip7843_slotnum/eip_checklist_not_applicable.txt b/tests/amsterdam/eip7843_slotnum/eip_checklist_not_applicable.txt new file mode 100644 index 00000000000..62f77e4f6eb --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/eip_checklist_not_applicable.txt @@ -0,0 +1,27 @@ +precompile = EIP-7843 does not introduce a new precompile +removed_precompile = EIP-7843 does not remove a precompile +system_contract = EIP-7843 does not introduce a new system contract +transaction_type = EIP-7843 does not introduce a new transaction type +block_body_field = EIP-7843 does not add a new block body field +block_level_constraint = EIP-7843 does not introduce a new block-level constraint +gas_cost_changes = EIP-7843 does not modify existing gas costs; it only introduces a new opcode with a fixed cost +gas_refunds_changes = EIP-7843 does not change gas refunds +blob_count_changes = EIP-7843 does not change blob counts +execution_layer_request = EIP-7843 does not introduce an execution layer request +new_transaction_validity_constraint = EIP-7843 does not introduce a new transaction validity constraint +modified_transaction_validity_constraint = EIP-7843 does not modify transaction validity constraints +opcode/test/mem_exp = SLOTNUM does not read or write memory +opcode/test/stack_underflow = SLOTNUM pops nothing and has no minimum stack height +opcode/test/stack_complex_operations = SLOTNUM is a simple push with no data portion +opcode/test/data_portion = SLOTNUM has no data portion +opcode/test/contract_creation = SLOTNUM does not create contracts +opcode/test/terminating = SLOTNUM is not a terminating opcode +opcode/test/return_data = SLOTNUM does not write to the return data buffer +opcode/test/out_of_bounds = SLOTNUM takes no inputs +opcode/test/gas_usage/memory_expansion = SLOTNUM does not access memory +opcode/test/gas_usage/out_of_gas_memory = SLOTNUM does not access memory +opcode/test/gas_usage/order_of_operations = SLOTNUM charges a single fixed fee with no gas components to order +opcode/test/execution_context/tx_context = SLOTNUM does not depend on transaction properties +opcode/test/execution_context/initcode/reentry = SLOTNUM is not a stateful opcode +block_header_field/test/value_behavior/reject = the execution layer does not constrain the slot number value; the consensus layer is the source of truth and any u64 is valid +general/code_coverage/second_client = Optional diff --git a/tests/amsterdam/eip7843_slotnum/spec.py b/tests/amsterdam/eip7843_slotnum/spec.py index db53e40f62f..32627a094e2 100644 --- a/tests/amsterdam/eip7843_slotnum/spec.py +++ b/tests/amsterdam/eip7843_slotnum/spec.py @@ -13,9 +13,5 @@ class ReferenceSpec: ref_spec_7843 = ReferenceSpec( git_path="EIPS/eip-7843.md", - version="6bc5d6b7acbc016a79fa573f98975093b5c2ca52", + version="c3bfd4ba41cf0fcbfe8c404f33ba89f5174971e0", ) - - -class Spec: - """Constants and parameters from EIP-7843.""" diff --git a/tests/amsterdam/eip7843_slotnum/test_eip_mainnet.py b/tests/amsterdam/eip7843_slotnum/test_eip_mainnet.py index 0cca8f2cd21..613dde9b49f 100644 --- a/tests/amsterdam/eip7843_slotnum/test_eip_mainnet.py +++ b/tests/amsterdam/eip7843_slotnum/test_eip_mainnet.py @@ -26,12 +26,13 @@ def test_slotnum_mainnet( pre: Alloc, ) -> None: """ - Test that SLOTNUM is callable and returns a non-zero slot number. + Test that SLOTNUM executes and pushes one stack item. - Asserts on ``POP(SLOTNUM)`` rather than the slot value itself - so the test remains valid when ``execute``-ed against a live network, - where the slot number is whatever the consensus layer transmits and - cannot be controlled by the test. + Asserts on `POP(SLOTNUM)` followed by a storage write rather than + on the slot value itself, so the test remains valid when + `execute`-ed against a live network, where the slot number is + whatever the consensus layer transmits and cannot be controlled by + the test. """ contract = pre.deploy_contract( code=Op.POP(Op.SLOTNUM) + Op.SSTORE(0, 1), diff --git a/tests/amsterdam/eip7843_slotnum/test_fork_transition.py b/tests/amsterdam/eip7843_slotnum/test_fork_transition.py index 4c0bea784a0..0952f4c3078 100644 --- a/tests/amsterdam/eip7843_slotnum/test_fork_transition.py +++ b/tests/amsterdam/eip7843_slotnum/test_fork_transition.py @@ -1,11 +1,17 @@ """Tests for EIP-7843 fork transition behavior.""" +from typing import Any + import pytest from execution_testing import ( Account, Alloc, Block, BlockchainTestFiller, + BlockException, + EIPChecklist, + EngineAPIError, + Header, Op, Transaction, ) @@ -15,7 +21,12 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7843.git_path REFERENCE_SPEC_VERSION = ref_spec_7843.version +FORK_TIMESTAMP = 15_000 + +@EIPChecklist.Opcode.Test.ForkTransition.Invalid() +@EIPChecklist.Opcode.Test.ForkTransition.At() +@EIPChecklist.BlockHeaderField.Test.ForkTransition.Initial() @pytest.mark.valid_at_transition_to("EIP7843") def test_slotnum_at_fork_transition( blockchain_test: BlockchainTestFiller, @@ -51,9 +62,9 @@ def test_slotnum_at_fork_transition( txs=[Transaction(sender=sender, to=contract)], ) for ts, slot in [ - (14_999, None), - (15_000, at_fork_slot), - (15_001, post_fork_slot), + (FORK_TIMESTAMP - 1, None), + (FORK_TIMESTAMP, at_fork_slot), + (FORK_TIMESTAMP + 1, post_fork_slot), ] ] post = { @@ -67,3 +78,89 @@ def test_slotnum_at_fork_transition( } blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.BlockHeaderField.Test.ForkTransition.Before() +@pytest.mark.valid_at_transition_to("EIP7843") +@pytest.mark.exception_test +@pytest.mark.parametrize( + "block_kwargs", + [ + pytest.param( + {"rlp_modifier": Header(slot_number=0)}, + id="header_field", + ), + pytest.param( + {"engine_new_payload_slot_number": 0}, + id="engine_payload_field", + marks=pytest.mark.blockchain_test_engine_only, + ), + ], +) +def test_invalid_pre_fork_block_with_slot_number( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + block_kwargs: dict[str, Any], +) -> None: + """ + Reject a pre-fork block that carries the slot number field in its + header or its engine `newPayload`. + + The field must not be present before the fork activates: the extra + header field changes the header shape, while in the payload case + the block is otherwise valid, so clients that silently drop + unknown payload fields would answer VALID and must fail this test. + """ + sender = pre.fund_eoa() + receiver = pre.fund_eoa(amount=0) + + tx = Transaction(sender=sender, to=receiver, value=100) + + blockchain_test( + pre=pre, + post={}, + blocks=[ + Block( + timestamp=FORK_TIMESTAMP - 1, + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + engine_api_error_code=EngineAPIError.InvalidParams, + **block_kwargs, + ), + ], + ) + + +@EIPChecklist.BlockHeaderField.Test.ForkTransition.After() +@pytest.mark.valid_at_transition_to("EIP7843") +@pytest.mark.exception_test +def test_invalid_post_fork_block_without_slot_number( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Reject an activation block whose header lacks the `slot_number` + field. + + From the fork activation onward the field is mandatory: a header + without it is malformed and the engine payload is missing a + parameter required by its version. + """ + sender = pre.fund_eoa() + receiver = pre.fund_eoa(amount=0) + + tx = Transaction(sender=sender, to=receiver, value=100) + + blockchain_test( + pre=pre, + post={}, + blocks=[ + Block( + timestamp=FORK_TIMESTAMP, + txs=[tx], + rlp_modifier=Header(slot_number=Header.REMOVE_FIELD), + exception=BlockException.INCORRECT_BLOCK_FORMAT, + engine_api_error_code=EngineAPIError.InvalidParams, + ), + ], + ) diff --git a/tests/amsterdam/eip7843_slotnum/test_slotnum.py b/tests/amsterdam/eip7843_slotnum/test_slotnum.py index c567da8b441..32604c22352 100644 --- a/tests/amsterdam/eip7843_slotnum/test_slotnum.py +++ b/tests/amsterdam/eip7843_slotnum/test_slotnum.py @@ -4,15 +4,19 @@ from execution_testing import ( Account, Alloc, + AuthorizationTuple, Block, BlockchainTestFiller, + EIPChecklist, Environment, Fork, Op, StateTestFiller, Transaction, + compute_create_address, ) +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .spec import ref_spec_7843 REFERENCE_SPEC_GIT_PATH = ref_spec_7843.git_path @@ -21,6 +25,7 @@ pytestmark = pytest.mark.valid_from("EIP7843") +@EIPChecklist.Opcode.Test.GasUsage.ExtraGas() @pytest.mark.parametrize( "slot_number", [ @@ -41,10 +46,13 @@ def test_slotnum_value( The slot number is provided by the consensus layer and should be accessible via the SLOTNUM opcode (0x4B). + + Storage key 0 starts at a nonzero canary so the zero-slot case is + distinguishable from a transaction that failed before the SSTORE. """ # Store SLOTNUM result at storage key 0 code = Op.SSTORE(0, Op.SLOTNUM) - code_address = pre.deploy_contract(code) + code_address = pre.deploy_contract(code, storage={0: 0xBA5E}) tx = Transaction( sender=pre.fund_eoa(), @@ -65,6 +73,8 @@ def test_slotnum_value( ) +@EIPChecklist.Opcode.Test.GasUsage.Normal() +@EIPChecklist.Opcode.Test.GasUsage.OutOfGasExecution() @pytest.mark.parametrize( "gas_delta,call_succeeds", [ @@ -112,6 +122,8 @@ def test_slotnum_gas_cost( ) +@EIPChecklist.Opcode.Test.ExecutionContext.BlockContext() +@EIPChecklist.BlockHeaderField.Test.ValueBehavior.Accept() def test_slotnum_distinct_per_block( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -146,3 +158,244 @@ def test_slotnum_distinct_per_block( } blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.BlockHeaderField.Test.Genesis() +def test_slotnum_genesis( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Test that the slot number header field can be set at genesis. + + The genesis header of this fixture carries a nonzero `slot_number`, + so a client must decode the field to reproduce the genesis hash. + The following block then exposes its own slot number via SLOTNUM. + """ + genesis_slot = 999 + block_slot = 1000 + + contract = pre.deploy_contract( + Op.SSTORE(0, Op.SLOTNUM), storage={0: 0xBA5E} + ) + tx = Transaction(sender=pre.fund_eoa(), to=contract) + + blockchain_test( + genesis_environment=Environment(slot_number=genesis_slot), + pre=pre, + blocks=[Block(slot_number=block_slot, txs=[tx])], + post={contract: Account(storage={0: block_slot})}, + ) + + +@EIPChecklist.Opcode.Test.StackOverflow() +@EIPChecklist.Opcode.Test.ExceptionalAbort() +@pytest.mark.parametrize( + "push_count,call_succeeds", + [ + pytest.param(1024, True, id="stack_at_limit"), + pytest.param(1025, False, id="stack_overflow"), + ], +) +def test_slotnum_stack_overflow( + state_test: StateTestFiller, + pre: Alloc, + push_count: int, + call_succeeds: bool, +) -> None: + """ + Test that SLOTNUM aborts when pushing past the 1024-item stack limit. + + The callee executes `push_count` consecutive SLOTNUM opcodes: 1024 + pushes fill the stack exactly and succeed, while the 1025th push + aborts the frame exceptionally. The caller stores the call's success + flag over a nonzero canary. + """ + callee_code = Op.SLOTNUM * push_count + Op.STOP + callee_address = pre.deploy_contract(callee_code) + + caller_code = Op.SSTORE(0, Op.CALL(gas=Op.GAS, address=callee_address)) + caller_address = pre.deploy_contract(caller_code, storage={0: 0xBA5E}) + + tx = Transaction( + sender=pre.fund_eoa(), + to=caller_address, + ) + + post = { + caller_address: Account( + storage={0: 1 if call_succeeds else 0}, + ), + } + + state_test( + env=Environment(slot_number=12345), + pre=pre, + tx=tx, + post=post, + ) + + +@EIPChecklist.Opcode.Test.ExecutionContext.Call() +@EIPChecklist.Opcode.Test.ExecutionContext.Callcode() +@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall() +@EIPChecklist.Opcode.Test.ExecutionContext.Staticcall() +@pytest.mark.with_all_call_opcodes +def test_slotnum_call_contexts( + state_test: StateTestFiller, + pre: Alloc, + call_opcode: Op, +) -> None: + """ + Test that SLOTNUM returns the slot number in every call frame type. + + The callee writes SLOTNUM to memory and returns it, so the check + also holds inside STATICCALL frames where storage writes are banned. + The caller stores the call's success flag and the returned value. + """ + slot_number = 0xC0FFEE + + callee_code = Op.MSTORE(0, Op.SLOTNUM) + Op.RETURN(0, 32) + callee_address = pre.deploy_contract(callee_code) + + caller_code = Op.SSTORE( + 0, call_opcode(address=callee_address, ret_offset=0, ret_size=32) + ) + Op.SSTORE(1, Op.MLOAD(0)) + caller_address = pre.deploy_contract(caller_code) + + tx = Transaction( + sender=pre.fund_eoa(), + to=caller_address, + ) + + post = { + caller_address: Account( + storage={0: 1, 1: slot_number}, + ), + } + + state_test( + env=Environment(slot_number=slot_number), + pre=pre, + tx=tx, + post=post, + ) + + +@EIPChecklist.Opcode.Test.ExecutionContext.SetCode() +def test_slotnum_set_code( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Test SLOTNUM inside a set-code delegated account (EIP-7702). + """ + slot_number = 0xC0FFEE + + auth_signer = pre.fund_eoa(amount=0) + set_code = Op.SSTORE(0, Op.SLOTNUM) + Op.STOP + set_code_to_address = pre.deploy_contract(set_code) + + tx = Transaction( + to=auth_signer, + authorization_list=[ + AuthorizationTuple( + address=set_code_to_address, + nonce=0, + signer=auth_signer, + ), + ], + sender=pre.fund_eoa(), + ) + + post = { + set_code_to_address: Account(storage={}), + auth_signer: Account( + nonce=1, + code=Spec7702.delegation_designation(set_code_to_address), + storage={0: slot_number}, + ), + } + + state_test( + env=Environment(slot_number=slot_number), + pre=pre, + tx=tx, + post=post, + ) + + +@EIPChecklist.Opcode.Test.ExecutionContext.Initcode.Behavior() +@EIPChecklist.Opcode.Test.ExecutionContext.Initcode.Behavior.Tx() +def test_slotnum_initcode_tx( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Test SLOTNUM inside the initcode of a contract-creating transaction. + """ + slot_number = 0xC0FFEE + + init_code = Op.SSTORE(0, Op.SLOTNUM) + sender = pre.fund_eoa() + contract_address = compute_create_address(address=sender, nonce=0) + + tx = Transaction(to=None, data=init_code, sender=sender) + + post = { + contract_address: Account(storage={0: slot_number}), + } + + state_test( + env=Environment(slot_number=slot_number), + pre=pre, + tx=tx, + post=post, + ) + + +@EIPChecklist.Opcode.Test.ExecutionContext.Initcode.Behavior() +@EIPChecklist.Opcode.Test.ExecutionContext.Initcode.Behavior.Opcode() +@pytest.mark.parametrize("opcode", [Op.CREATE, Op.CREATE2]) +def test_slotnum_initcode_create( + state_test: StateTestFiller, + pre: Alloc, + opcode: Op, +) -> None: + """ + Test SLOTNUM inside initcode executed via CREATE and CREATE2. + """ + slot_number = 0xC0FFEE + + init_code = Op.SSTORE(0, Op.SLOTNUM) + + factory_code = ( + Op.CALLDATACOPY(offset=0, size=len(init_code)) + + opcode(offset=0, size=len(init_code)) + + Op.STOP + ) + factory_address = pre.deploy_contract(factory_code) + + created_contract_address = compute_create_address( + address=factory_address, + nonce=1, + initcode=init_code, + opcode=opcode, + ) + + tx = Transaction( + to=factory_address, + data=init_code, + sender=pre.fund_eoa(), + ) + + post = { + created_contract_address: Account(storage={0: slot_number}), + } + + state_test( + env=Environment(slot_number=slot_number), + pre=pre, + tx=tx, + post=post, + ) From 3c15c23e7de8f59ebb2a9d22ecc11f0c1f705a98 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 24 Jul 2026 11:29:29 +0100 Subject: [PATCH 06/55] Open fix(tests): search for parent fork that differs in gas costs from current fork (#3228) --- .../test_exact_balance_no_fallback.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py index 40376c8af55..0ef7c1f9459 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py @@ -18,12 +18,15 @@ the dimension genuinely got more expensive. """ +from typing import Callable, Tuple + import pytest from execution_testing import ( AccessList, Alloc, AuthorizationTuple, Fork, + GasCosts, StateTestFiller, Transaction, TransactionException, @@ -40,6 +43,24 @@ GAS_PRICE = 10 +def gas_costs_before_increase( + fork: Fork, costs: Callable[[GasCosts], Tuple[int, ...]] +) -> GasCosts: + """ + Return the gas cost schedule of the closest ancestor fork whose + constants selected by ``costs`` differ from ``fork``'s. + + Raises if no ancestor differs. When ``costs`` selects several + constants, the walk stops at the first fork where any of them + changed. + """ + current = costs(fork.gas_costs()) + ancestor = fork.parent_or_fail() + while costs(ancestor.gas_costs()) == current: + ancestor = ancestor.parent_or_fail() + return ancestor.gas_costs() + + @EIPChecklist.GasCostChanges.Test.OutOfGas() @pytest.mark.exception_test @pytest.mark.parametrize( @@ -68,7 +89,10 @@ def test_access_list_no_fallback( sender funded to the wei, that fallback must not slip through. """ new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() + old_costs = gas_costs_before_increase( + fork, + lambda c: (c.TX_ACCESS_LIST_ADDRESS, c.TX_ACCESS_LIST_STORAGE_KEY), + ) addr_delta = ( new_costs.TX_ACCESS_LIST_ADDRESS - old_costs.TX_ACCESS_LIST_ADDRESS ) @@ -138,7 +162,9 @@ def test_authorization_no_fallback( for that fallback. """ new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() + old_costs = gas_costs_before_increase( + fork, lambda c: (c.AUTH_PER_EMPTY_ACCOUNT,) + ) auth_delta = ( new_costs.AUTH_PER_EMPTY_ACCOUNT - old_costs.AUTH_PER_EMPTY_ACCOUNT ) @@ -198,7 +224,9 @@ def test_cold_account_access_no_fallback( must not execute. """ new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() + old_costs = gas_costs_before_increase( + fork, lambda c: (c.COLD_ACCOUNT_ACCESS,) + ) fallback_delta = ( new_costs.COLD_ACCOUNT_ACCESS - old_costs.COLD_ACCOUNT_ACCESS ) From 1ac58d7bce64c39731826d8ab0065187acbd4f94 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 24 Jul 2026 14:20:45 +0100 Subject: [PATCH 07/55] fix: skip ported static tests for amsterdam and later (#3231) --- tests/ported_static/conftest.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/ported_static/conftest.py b/tests/ported_static/conftest.py index b26f10ade4a..c5d9aff2a9d 100644 --- a/tests/ported_static/conftest.py +++ b/tests/ported_static/conftest.py @@ -1,9 +1,10 @@ """ Conftest for ported static tests. -Temporarily skip ported static tests that fail for Amsterdam due to EIP-8037's -two-dimensional gas model. The gas limits in these ported static test cases -have not yet been updated to account for state gas. +Temporarily skip ported static tests that fail on Amsterdam and its +descendant forks due to EIP-8037's two-dimensional gas model. The gas +limits in these ported static test cases have not yet been updated to +account for state gas. TODO: Update gas limits in the 3452 failing ported static test cases and remove this skip list. @@ -12,6 +13,7 @@ from pathlib import Path import pytest +from execution_testing.forks import Amsterdam _SKIP_LIST_PATH = Path(__file__).parent / "amsterdam_skip_list.txt" _AMSTERDAM_SKIP_CASES: frozenset[str] = frozenset( @@ -49,9 +51,17 @@ def pytest_collection_modifyitems( for item in items: if "ported_static" not in item.nodeid: continue - if "fork_Amsterdam" not in item.nodeid: + callspec = getattr(item, "callspec", None) + fork = callspec.params.get("fork") if callspec else None + if fork is None or not fork >= Amsterdam: continue - normalized = _normalize_nodeid(item.nodeid) + # The skip list is written against fork_Amsterdam, but the + # EIP-8037 breakage applies equally to its descendant forks. + # Rewriting the item's fork token to Amsterdam's lets one list + # cover them all. + normalized = _normalize_nodeid(item.nodeid).replace( + f"fork_{fork.name()}", "fork_Amsterdam" + ) for skip_case in _AMSTERDAM_SKIP_CASES: if skip_case in normalized: item.add_marker(skip_marker) From ca7cac5c41b82ec49cbbe0961ba7191411caa5d2 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Fri, 24 Jul 2026 15:36:29 +0200 Subject: [PATCH 08/55] fix(consume): make release resolution robust to GitHub API rate limits (#3182) Co-authored-by: spencer-tb --- .../plugins/consume/consume.py | 10 +- .../plugins/consume/releases.py | 146 ++++++--- .../tests/test_fixtures_source_input_types.py | 56 ++-- .../plugins/consume/tests/test_releases.py | 287 +++++++++++++++++- 4 files changed, 419 insertions(+), 80 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/consume.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/consume.py index 62ddb494040..339904a4d14 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/consume.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/consume.py @@ -43,10 +43,9 @@ from .releases import ( ReleaseTag, - get_release_page_url, - get_release_url, is_release_url, is_url, + resolve_release, ) CACHED_DOWNLOADS_DIRECTORY = ( @@ -264,8 +263,11 @@ def from_release_spec( """ if cache_folder is None: cache_folder = CACHED_DOWNLOADS_DIRECTORY - url = get_release_url(spec) - release_page = get_release_page_url(url) + # Resolve the spec once; the download URL and the release page + # both derive from the same release information. + release = resolve_release(spec) + url = release.get_asset(ReleaseTag.from_string(spec)).url + release_page = release.url destination_folder = extract_to or FixtureDownloader.get_cache_path( url, cache_folder diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py index f74f68b382e..7c0d503ae8e 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py @@ -1,8 +1,10 @@ """Procedures to consume fixtures from Github releases.""" import json +import logging import os import re +import tempfile from dataclasses import dataclass from datetime import datetime from pathlib import Path @@ -11,7 +13,9 @@ import platformdirs import requests -from pydantic import BaseModel, Field, RootModel +from pydantic import BaseModel, Field, RootModel, ValidationError + +logger = logging.getLogger(__name__) CACHED_RELEASE_INFORMATION_FILE = ( Path(platformdirs.user_cache_dir("ethereum-execution-spec-tests")) @@ -237,7 +241,15 @@ def download_release_information( pagination links up to `max_pages` pages, so resolution sees the 200 most recent releases per repo. Older releases fall outside this window and cannot be resolved. + + Authenticate with `GITHUB_TOKEN` (or `GH_TOKEN`) when set: + authenticated requests get 5000 requests/hour instead of the + unauthenticated 60/hour per IP address. """ + headers = {} + github_token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if github_token: + headers["Authorization"] = f"Bearer {github_token}" all_releases = [] for repo in SUPPORTED_REPOS: current_url: str | None = ( @@ -246,7 +258,7 @@ def download_release_information( max_pages = 2 while current_url and max_pages > 0: max_pages -= 1 - response = requests.get(current_url) + response = requests.get(current_url, headers=headers) response.raise_for_status() all_releases.extend(response.json()) current_url = None @@ -260,8 +272,17 @@ def download_release_information( if destination_file: destination_file.parent.mkdir(parents=True, exist_ok=True) - with open(destination_file, "w") as file: + # Write via a uniquely-named temporary file so a concurrent + # reader never sees a partially-written cache and concurrent + # writers never share a path. + with tempfile.NamedTemporaryFile( + "w", + dir=destination_file.parent, + suffix=".tmp", + delete=False, + ) as file: json.dump(all_releases, file) + Path(file.name).replace(destination_file) return parse_release_information(all_releases) @@ -310,6 +331,26 @@ def sort_key( return max(matches, key=sort_key) +def resolves_pinned_release( + release_string: str, + release_information: List[ReleaseInformation], +) -> bool: + """ + Check whether the release information resolves a pinned version. + + A release descriptor with an explicit version refers to an immutable + git tag: once it resolves, a refresh of the release information + cannot change the result. + """ + if ReleaseTag.from_string(release_string).version is None: + return False + try: + find_release(release_string, release_information) + except NoSuchReleaseError: + return False + return True + + def get_release_url_from_release_information( release_string: str, release_information: List[ReleaseInformation] ) -> str: @@ -318,63 +359,72 @@ def get_release_url_from_release_information( return release.get_asset(ReleaseTag.from_string(release_string)).url -def get_release_page_url(release_string: str) -> str: +def resolve_release(release_string: str) -> ReleaseInformation: """ - Return the GitHub Release page URL for a specific release descriptor. - - This function can handle: - - A release string (e.g., "tests@latest" or "bal-devnet@v7.0.0") from - any repo in `SUPPORTED_REPOS`. - - A direct asset download link (e.g., - "https://github.com/ethereum/execution-specs/releases/ - download/tests%40v20.0.0/fixtures.tar.gz"). + Resolve a release descriptor string to its release information. + + Refresh the cached release information beforehand as needed (see + `get_release_information`). """ - release_information = get_release_information() + return find_release( + release_string, get_release_information(release_string) + ) - # Case 1: If it's a direct GitHub Releases download link, find which - # release in `release_information` has an asset with this exact URL. - repo_pattern = "|".join(re.escape(repo) for repo in SUPPORTED_REPOS) - regex_pattern = rf"https://github\.com/({repo_pattern})/releases/download/" - if re.match(regex_pattern, release_string): - for release in release_information: - for asset in release.assets.root: - if asset.url == release_string: - return release.url # The HTML page for this release - raise NoSuchReleaseError( - f"No release found for asset URL: {release_string}" - ) - # Case 2: Otherwise, treat it as a release descriptor (e.g., - # "tests@latest") - return find_release(release_string, release_information).url +def get_release_page_url(release_string: str) -> str: + """Get the GitHub release page URL for a release descriptor.""" + return resolve_release(release_string).url -def get_release_information() -> List[ReleaseInformation]: +def get_release_information( + release_string: str | None = None, +) -> List[ReleaseInformation]: """ - Get the release information. - - First check if the cached release information file exists. If it does, but - it is older than 4 hours, delete the file, unless running inside a CI - environment or a Docker container. Then download the release information - from the Github API and save it to the cache file. + Get the release information, refreshing the cache file as needed. + + Return the cached release information if the cache file is fresh + (younger than 4 hours; any age when running inside a CI environment + or a Docker container). A stale cache is also used without + refreshing when `release_string` pins an exact version that the + cache already resolves: release tags are immutable, so the cached + entry cannot be outdated. Otherwise re-download the release + information, keeping the stale cache as a fallback in case the + GitHub API is unavailable (e.g. rate-limited). """ + cached_information: List[ReleaseInformation] | None = None if CACHED_RELEASE_INFORMATION_FILE.exists(): - last_modified = CACHED_RELEASE_INFORMATION_FILE.stat().st_mtime - if ( - datetime.now().timestamp() - last_modified - ) < 4 * 60 * 60 or is_docker_or_ci(): - return parse_release_information_from_file( + try: + cached_information = parse_release_information_from_file( CACHED_RELEASE_INFORMATION_FILE ) - CACHED_RELEASE_INFORMATION_FILE.unlink() - if not CACHED_RELEASE_INFORMATION_FILE.exists(): + except (json.JSONDecodeError, ValidationError): + logger.warning( + "Ignoring corrupt release information cache at " + f"{CACHED_RELEASE_INFORMATION_FILE}." + ) + else: + last_modified = CACHED_RELEASE_INFORMATION_FILE.stat().st_mtime + cache_age = datetime.now().timestamp() - last_modified + if cache_age < 4 * 60 * 60 or is_docker_or_ci(): + return cached_information + if release_string is not None and resolves_pinned_release( + release_string, cached_information + ): + return cached_information + try: return download_release_information(CACHED_RELEASE_INFORMATION_FILE) - return parse_release_information_from_file(CACHED_RELEASE_INFORMATION_FILE) + except requests.RequestException as error: + if cached_information is None: + raise + logger.warning( + f"Could not refresh release information from the GitHub API " + f"({error}); falling back to the stale cache at " + f"{CACHED_RELEASE_INFORMATION_FILE}." + ) + return cached_information def get_release_url(release_string: str) -> str: - """Get the URL for a specific release.""" - release_information = get_release_information() - return get_release_url_from_release_information( - release_string, release_information - ) + """Get the asset download URL for a release descriptor.""" + release = resolve_release(release_string) + return release.get_asset(ReleaseTag.from_string(release_string)).url diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_fixtures_source_input_types.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_fixtures_source_input_types.py index 5a5418006d3..97a229c49ef 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_fixtures_source_input_types.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_fixtures_source_input_types.py @@ -39,34 +39,36 @@ def test_fixtures_source_from_release_spec_makes_api_calls(self) -> None: test_spec = "tests@latest" with patch( - "execution_testing.cli.pytest_commands.plugins.consume.consume.get_release_url" - ) as mock_get_url: - mock_get_url.return_value = "https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0/fixtures.tar.gz" + "execution_testing.cli.pytest_commands.plugins.consume.consume.resolve_release" + ) as mock_resolve: + mock_release = MagicMock() + mock_release.url = "https://github.com/ethereum/execution-specs/releases/tag/tests%40v20.0.0" + mock_release.get_asset.return_value.url = "https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0/fixtures.tar.gz" + mock_resolve.return_value = mock_release with patch( - "execution_testing.cli.pytest_commands.plugins.consume.consume.get_release_page_url" - ) as mock_get_page: - mock_get_page.return_value = "https://github.com/ethereum/execution-specs/releases/tag/tests%40v20.0.0" - with patch( - "execution_testing.cli.pytest_commands.plugins.consume.consume.FixtureDownloader" - ) as mock_downloader: - mock_instance = MagicMock() - mock_instance.download_and_extract.return_value = ( - False, - Path("/tmp/test"), - ) - mock_downloader.return_value = mock_instance - - source = FixturesSource.from_release_spec(test_spec) - - # Verify API calls were made and release page is set - mock_get_url.assert_called_once_with(test_spec) - mock_get_page.assert_called_once_with( - "https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0/fixtures.tar.gz" - ) - assert ( - source.release_page - == "https://github.com/ethereum/execution-specs/releases/tag/tests%40v20.0.0" - ) + "execution_testing.cli.pytest_commands.plugins.consume.consume.FixtureDownloader" + ) as mock_downloader: + mock_instance = MagicMock() + mock_instance.download_and_extract.return_value = ( + False, + Path("/tmp/test"), + ) + mock_downloader.return_value = mock_instance + + source = FixturesSource.from_release_spec(test_spec) + + # The spec is resolved exactly once; the download URL and + # the release page both derive from the same release + # information. + mock_resolve.assert_called_once_with(test_spec) + assert ( + source.url + == "https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0/fixtures.tar.gz" + ) + assert ( + source.release_page + == "https://github.com/ethereum/execution-specs/releases/tag/tests%40v20.0.0" + ) def test_fixtures_source_from_regular_url_no_release_page(self) -> None: """Test that regular URLs (non-GitHub) don't have release page.""" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_releases.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_releases.py index cb347efc946..fbb0257b71f 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_releases.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_releases.py @@ -1,15 +1,23 @@ """Test release parsing given the github repository release JSON data.""" +import os +import shutil +import time from os.path import realpath from pathlib import Path -from typing import List +from typing import Any, Dict, List import pytest +import requests +from .. import releases from ..releases import ( SUPPORTED_REPOS, NoSuchReleaseError, ReleaseInformation, + download_release_information, + get_release_page_url, + get_release_url, get_release_url_from_release_information, is_release_url, parse_release_information_from_file, @@ -221,3 +229,280 @@ def test_supported_repos_contains_execution_specs() -> None: `tests-bal@v7.1.0` onward) and must be in `SUPPORTED_REPOS`. """ assert "ethereum/execution-specs" in SUPPORTED_REPOS + + +class FakeResponse: + """A minimal stand-in for `requests.Response`.""" + + def __init__( + self, payload: List[Dict], rate_limited: bool = False + ) -> None: + """Initialize with a JSON payload or a rate-limited failure.""" + self.payload = payload + self.rate_limited = rate_limited + self.headers: Dict[str, str] = {} + + def json(self) -> List[Dict]: + """Return the JSON payload.""" + return self.payload + + def raise_for_status(self) -> None: + """Raise an `HTTPError` if the response is rate-limited.""" + if self.rate_limited: + raise requests.exceptions.HTTPError( + "403 Client Error: rate limit exceeded" + ) + + +def fake_release(tag_name: str, asset_name: str) -> Dict: + """Build a minimal GitHub API release entry.""" + encoded_tag = tag_name.replace("@", "%40") + return { + "html_url": "https://github.com/ethereum/execution-specs/releases/" + f"tag/{encoded_tag}", + "id": 1, + "tag_name": tag_name, + "name": tag_name, + "created_at": "2026-07-15T00:00:00Z", + "published_at": "2026-07-15T00:00:00Z", + "assets": [ + { + "browser_download_url": "https://github.com/ethereum/" + f"execution-specs/releases/download/{encoded_tag}/" + f"{asset_name}", + "id": 1, + "name": asset_name, + "content_type": "application/gzip", + "size": 1, + } + ], + } + + +@pytest.fixture +def release_cache_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Path: + """ + Redirect the release-information cache to a temporary path. + + Also disable the CI/Docker detection so the freshness check applies + (in CI, the cache never expires). + """ + cache_file = tmp_path / "release_information.json" + monkeypatch.setattr( + releases, "CACHED_RELEASE_INFORMATION_FILE", cache_file + ) + monkeypatch.setattr(releases, "is_docker_or_ci", lambda: False) + return cache_file + + +@pytest.fixture +def release_information_cache(release_cache_path: Path) -> Path: + """Populate the redirected cache with a copy of the test manifest.""" + shutil.copyfile( + CURRENT_FOLDER / "release_information.json", release_cache_path + ) + return release_cache_path + + +def make_stale(cache_file: Path) -> None: + """Age the cache file's mtime beyond the 4-hour freshness window.""" + stale_time = time.time() - 5 * 60 * 60 + os.utime(cache_file, (stale_time, stale_time)) + + +def block_api(monkeypatch: pytest.MonkeyPatch) -> None: + """Make any GitHub API request fail the test.""" + + def no_api(*args: Any, **kwargs: Any) -> None: + del args, kwargs + pytest.fail("The GitHub API must not be hit") + + monkeypatch.setattr(releases.requests, "get", no_api) + + +def rate_limited_get(*args: Any, **kwargs: Any) -> FakeResponse: + """Return a rate-limited (403) GitHub API response.""" + del args, kwargs + return FakeResponse([], rate_limited=True) + + +def new_release_get(*args: Any, **kwargs: Any) -> FakeResponse: + """Return a single-page response with a new `tests@v21.0.0` release.""" + del args, kwargs + return FakeResponse([fake_release("tests@v21.0.0", "fixtures.tar.gz")]) + + +def test_pinned_release_resolves_from_stale_cache_without_api( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + A pinned version already resolvable from the cache must not refresh. + + Release tags are immutable, so a cached entry for an exact version + cannot be outdated, no matter how old the cache file is. Regression + test for `consume --input=tests@vX.Y.Z` raising INTERNALERROR when + the unauthenticated GitHub API rate limit is exhausted, even though + the (stale) cache resolved the release. + """ + make_stale(release_information_cache) + block_api(monkeypatch) + assert get_release_url("tests@v20.0.0") == ( + "https://github.com/ethereum/execution-specs/releases/download/" + "tests%40v20.0.0/fixtures.tar.gz" + ) + assert get_release_page_url("tests@v20.0.0") == ( + "https://github.com/ethereum/execution-specs/releases/tag/" + "tests%40v20.0.0" + ) + assert release_information_cache.exists() + + +def test_fresh_cache_resolves_latest_without_api( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A fresh cache resolves unpinned lookups without an API request.""" + del release_information_cache + block_api(monkeypatch) + assert get_release_url("tests@latest").endswith( + "tests%40v20.0.0/fixtures.tar.gz" + ) + + +def test_rate_limited_refresh_falls_back_to_stale_cache( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + A failed refresh must fall back to the stale cache, not delete it. + + Previously the stale cache file was deleted before the download was + attempted, so a rate-limited refresh crashed the run and left no + cache at all, forcing every subsequent run onto the API. + """ + make_stale(release_information_cache) + monkeypatch.setattr(releases.requests, "get", rate_limited_get) + assert get_release_url("tests@latest").endswith( + "tests%40v20.0.0/fixtures.tar.gz" + ) + assert release_information_cache.exists() + + +def test_rate_limited_refresh_without_cache_raises( + release_cache_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Without a cache file, a failed refresh is a hard error.""" + del release_cache_path + monkeypatch.setattr(releases.requests, "get", rate_limited_get) + with pytest.raises(requests.exceptions.HTTPError): + get_release_url("tests@latest") + + +def test_unpinned_release_refreshes_stale_cache( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + An unpinned lookup with a stale cache must refresh from the API. + + `latest` and bare feature names can resolve to a newer release at + any time, so the pinned-release fast path must not apply to them. + """ + make_stale(release_information_cache) + calls: List[str] = [] + + def fake_get(url: str, **kwargs: Any) -> FakeResponse: + calls.append(url) + return new_release_get(url, **kwargs) + + monkeypatch.setattr(releases.requests, "get", fake_get) + assert get_release_url("tests@latest").endswith( + "tests%40v21.0.0/fixtures.tar.gz" + ) + assert len(calls) == len(SUPPORTED_REPOS) + assert "tests@v21.0.0" in release_information_cache.read_text() + + +def test_pinned_release_not_in_stale_cache_refreshes( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + A pinned version missing from the stale cache must refresh. + + The pinned-release fast path only applies when the cache already + resolves the requested version. + """ + make_stale(release_information_cache) + monkeypatch.setattr(releases.requests, "get", new_release_get) + assert get_release_url("tests@v21.0.0").endswith( + "tests%40v21.0.0/fixtures.tar.gz" + ) + + +def test_corrupt_cache_file_is_refreshed( + release_information_cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + A corrupt cache file must be re-downloaded, not crash the run. + + A partially-written download (e.g. a killed process) must not wedge + every subsequent run until the file is manually deleted. + """ + release_information_cache.write_text("{ not json") + monkeypatch.setattr(releases.requests, "get", new_release_get) + assert get_release_url("tests@v21.0.0").endswith( + "tests%40v21.0.0/fixtures.tar.gz" + ) + + +@pytest.mark.parametrize( + "environment,expected_token", + [ + pytest.param({}, None, id="unauthenticated"), + pytest.param( + {"GITHUB_TOKEN": "ghp_test_token"}, + "ghp_test_token", + id="github_token", + ), + pytest.param( + {"GH_TOKEN": "gho_test_token"}, + "gho_test_token", + id="gh_token", + ), + pytest.param( + {"GITHUB_TOKEN": "ghp_test_token", "GH_TOKEN": "gho_other"}, + "ghp_test_token", + id="github_token_wins", + ), + ], +) +def test_download_release_information_github_token( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + environment: Dict[str, str], + expected_token: str | None, +) -> None: + """ + Authenticate GitHub API requests iff a GitHub token is set. + + `GITHUB_TOKEN` (preferred) or `GH_TOKEN` (the gh CLI's name) + authenticates the request: 5000 requests/hour instead of the + unauthenticated 60 requests/hour per IP. + """ + for variable in ("GITHUB_TOKEN", "GH_TOKEN"): + monkeypatch.delenv(variable, raising=False) + for variable, token in environment.items(): + monkeypatch.setenv(variable, token) + seen_headers: List[Dict[str, str]] = [] + + def fake_get(url: str, **kwargs: Any) -> FakeResponse: + seen_headers.append(kwargs.get("headers") or {}) + return new_release_get(url, **kwargs) + + monkeypatch.setattr(releases.requests, "get", fake_get) + download_release_information(tmp_path / "release_information.json") + expected_headers = ( + {} + if expected_token is None + else {"Authorization": f"Bearer {expected_token}"} + ) + assert seen_headers == [expected_headers] * len(SUPPORTED_REPOS) From 85aa48c742c38a2d5a876f84ebf8082a50273064 Mon Sep 17 00:00:00 2001 From: Stefan <22667037+qu0b@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:53:32 +0200 Subject: [PATCH 09/55] tests(amsterdam): EIP-2780 - pin receipt status of top-frame OOG tx in multi-tx blocks (#3232) A transaction that out-of-gases on an EIP-2780/EIP-8037 top-frame charge is included in the block but must produce a failed receipt. All existing top-frame OOG tests place the failing transaction alone in a block, so a client that derives the receipt status from stale shared per-block state still passes them: the stale value in a fresh block happens to be "failed". Add a blockchain test to test_top_frame_charges.py that sandwiches the top-frame failure between two successful transactions, making the status byte load-bearing in the header receiptsRoot. Parametrized over the three top-frame charge classes: contract-creation NEW_ACCOUNT state gas, value-to-empty NEW_ACCOUNT state gas, and delegated-recipient COLD_ACCOUNT_ACCESS regular gas. Every receipt is pinned explicitly via expected_receipt (status, cumulative gas, and gas_used on the failing tx). Catches the nimbus-eth1 1f8dd2122 regression that receipted top-frame failures with the previous transaction's status and rejected canonical finalized blocks on glamsterdam-devnet-7 (receiptRoot mismatch). Claude-Session: https://claude.ai/code/session_01GpkqKnXjpdXGJ4ChNGsxEY Co-authored-by: Guruprasad Kamath Co-authored-by: Claude Fable 5 --- .../test_top_frame_charges.py | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index a89a0fddbbe..9d59d9d7be8 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -20,6 +20,8 @@ created account's own destruction. """ +from enum import Enum, auto + import pytest from execution_testing import ( Account, @@ -812,3 +814,218 @@ def test_initcode_selfdestruct_state_gas_in_header( created: None, }, ) + + +class TopFrameFailureMode(Enum): + """The top-frame charge the failing transaction out-of-gases on.""" + + CREATE_STATE_OOG = auto() + NEW_ACCOUNT_STATE_OOG = auto() + DELEGATED_REGULAR_OOG = auto() + + +@pytest.mark.parametrize( + "failure_mode", + [ + pytest.param( + TopFrameFailureMode.CREATE_STATE_OOG, + id="create_state_oog", + ), + pytest.param( + TopFrameFailureMode.NEW_ACCOUNT_STATE_OOG, + id="new_account_state_oog", + ), + pytest.param( + TopFrameFailureMode.DELEGATED_REGULAR_OOG, + id="delegated_regular_oog", + ), + ], +) +def test_receipt_status_top_frame_oog_between_successful_txs( + fork: Fork, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + failure_mode: TopFrameFailureMode, +) -> None: + """ + Pin the failed receipt status of a top-frame OOG transaction that + sits between two successful transactions in one block. + + A transaction that out-of-gases on a top-frame charge never + dispatches into the EVM but is still included and must produce a + ``succeeded=False`` receipt, committed to the header + ``receiptsRoot``. The other top-frame OOG tests place the failing + transaction alone in its block, so an implementation that derives + the receipt status from stale shared per-block execution state + still passes them: the stale value in a fresh block happens to be + "failed". Sandwiching the failure between successful transactions + makes the status byte load-bearing. (Regression: nimbus-eth1 + ``1f8dd2122`` receipted top-frame failures with the previous + transaction's status and rejected finalized canonical blocks on + glamsterdam-devnet-7 with ``receiptRoot mismatch``.) + + The middle transaction passes the intrinsic check but out-of-gases + on a top-frame charge before any EVM bytecode runs: + + - ``create_state_oog``: contract creation; the created account's + ``NEW_ACCOUNT`` state charge fires at the top frame and the gas + limit is one short of covering it. + - ``new_account_state_oog``: value transfer to an empty recipient; + the ``NEW_ACCOUNT`` state charge fires and the gas limit is one + short. + - ``delegated_regular_oog``: recipient holds an EIP-7702 + delegation; the ``COLD_ACCOUNT_ACCESS`` regular charge fires and + the gas limit is one short. + + The failing transaction burns its full gas limit, bumps the sender + nonce, and must produce a ``succeeded=False`` receipt between two + ``succeeded=True`` receipts. + """ + gas_price = 1_000_000_000 + value = 1 + + sender_initial_balance = 10**18 + ok_sender_1 = pre.fund_eoa(sender_initial_balance) + ok_sender_2 = pre.fund_eoa(sender_initial_balance) + fail_sender = pre.fund_eoa(sender_initial_balance) + # Alive via balance, so the successful transfers to it incur no + # top-frame charge and consume exactly their intrinsic gas. + ok_recipient = pre.fund_eoa(amount=1) + + intrinsic_cost = fork.transaction_intrinsic_cost_calculator() + + fail_target: Address | None = None + fail_target_post: Account | None = None + if failure_mode is TopFrameFailureMode.CREATE_STATE_OOG: + intrinsic_gas = intrinsic_cost( + contract_creation=True, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + contract_creation=True, + ) + assert top_frame_state_gas > 0, ( + "contract creation must charge NEW_ACCOUNT at the top frame" + ) + fail_gas_limit = intrinsic_gas + top_frame_state_gas - 1 + fail_to: Address | None = None + elif failure_mode is TopFrameFailureMode.NEW_ACCOUNT_STATE_OOG: + intrinsic_gas = intrinsic_cost( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + assert top_frame_state_gas > 0, ( + "value transfer to an empty recipient must charge " + "NEW_ACCOUNT at the top frame" + ) + fail_gas_limit = intrinsic_gas + top_frame_state_gas - 1 + fail_to = pre.fund_eoa(amount=0) + fail_target = fail_to + # The rolled-back transfer must not bring the recipient into + # existence. + fail_target_post = None + elif failure_mode is TopFrameFailureMode.DELEGATED_REGULAR_OOG: + delegated_to = pre.deploy_contract(code=Op.STOP) + target_code = Spec7702.delegation_designation(delegated_to) + fail_to = pre.deploy_contract(code=target_code) + intrinsic_gas = intrinsic_cost( + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + recipient_type=RecipientType.DELEGATION_7702, + ) + assert top_frame_gas > 0, ( + "a delegated recipient must charge COLD_ACCOUNT_ACCESS " + "at the top frame" + ) + fail_gas_limit = intrinsic_gas + top_frame_gas - 1 + fail_target = fail_to + fail_target_post = Account(balance=0, code=target_code) + else: + raise ValueError(f"unhandled failure mode: {failure_mode}") + + # The successful transfers go to an alive EOA: no top-frame charge, + # no EVM execution, so each consumes exactly its intrinsic gas. + ok_intrinsic_gas = intrinsic_cost( + sends_value=True, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + assert ( + fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EOA, + ) + == 0 + ), "an alive recipient must not incur a top-frame state charge" + + ok_tx_1 = Transaction( + sender=ok_sender_1, + to=ok_recipient, + value=value, + gas_limit=ok_intrinsic_gas, + gas_price=gas_price, + expected_receipt=TransactionReceipt( + status=1, + cumulative_gas_used=ok_intrinsic_gas, + ), + ) + fail_tx = Transaction( + sender=fail_sender, + to=fail_to, + value=( + value + if failure_mode is TopFrameFailureMode.NEW_ACCOUNT_STATE_OOG + else 0 + ), + gas_limit=fail_gas_limit, + gas_price=gas_price, + expected_receipt=TransactionReceipt( + status=0, + gas_used=fail_gas_limit, + cumulative_gas_used=ok_intrinsic_gas + fail_gas_limit, + ), + ) + ok_tx_2 = Transaction( + sender=ok_sender_2, + to=ok_recipient, + value=value, + gas_limit=ok_intrinsic_gas, + gas_price=gas_price, + expected_receipt=TransactionReceipt( + status=1, + cumulative_gas_used=2 * ok_intrinsic_gas + fail_gas_limit, + ), + ) + + ok_sender_final_balance = ( + sender_initial_balance - value - ok_intrinsic_gas * gas_price + ) + post: dict[Address, Account | None] = { + ok_sender_1: Account(nonce=1, balance=ok_sender_final_balance), + ok_sender_2: Account(nonce=1, balance=ok_sender_final_balance), + ok_recipient: Account(balance=1 + 2 * value), + # The failing transaction is included: the nonce bumps and the + # full gas limit is paid, but nothing else happens. + fail_sender: Account( + nonce=1, + balance=sender_initial_balance - fail_gas_limit * gas_price, + ), + } + if failure_mode is TopFrameFailureMode.CREATE_STATE_OOG: + post[fail_tx.created_contract] = None + else: + assert fail_target is not None + post[fail_target] = fail_target_post + + blockchain_test( + pre=pre, + blocks=[Block(txs=[ok_tx_1, fail_tx, ok_tx_2])], + post=post, + ) From 00bd585693f092e2cf7cc82032ec421abefc30d3 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Sun, 26 Jul 2026 07:49:34 -0300 Subject: [PATCH 10/55] fix(tests): update withdrawal and consolidation tests to use Header for requests verification (#3235) --- .../test_modified_withdrawal_contract.py | 21 ++++++++--------- .../test_modified_consolidation_contract.py | 23 ++++++++----------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py index f218318274f..f83b13d5082 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py @@ -12,6 +12,7 @@ Block, BlockchainTestFiller, Bytecode, + Header, Op, Requests, SystemContractInteractionTransaction, @@ -94,21 +95,19 @@ def test_extra_withdrawals( """ modified_code: Bytecode = Bytecode() memory_offset: int = 0 - amount_of_requests: int = 0 for withdrawal_request in requests_list: - # update memory_offset with the correct value - withdrawal_request_bytes_amount: int = len(bytes(withdrawal_request)) - assert withdrawal_request_bytes_amount == 76, ( + record = bytes(withdrawal_request) + assert len(record) == 76, ( "Expected withdrawal request to be of size 76 but got size " - f"{withdrawal_request_bytes_amount}" + f"{len(record)}" ) - memory_offset += withdrawal_request_bytes_amount + # Store records contiguously from offset 0 so the returned data is + # exactly the concatenated records (no gap, no trailing padding). + modified_code += Om.MSTORE(record, memory_offset) + memory_offset += len(record) - modified_code += Om.MSTORE(bytes(withdrawal_request), memory_offset) - amount_of_requests += 1 - - modified_code += Op.RETURN(0, Op.MSIZE()) + modified_code += Op.RETURN(0, memory_offset) pre[Spec_EIP7002.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS] = Account( code=modified_code, @@ -131,7 +130,7 @@ def test_extra_withdrawals( blocks=[ Block( txs=txs, - requests_hash=Requests(*requests_list), + header_verify=Header(requests_hash=Requests(*requests_list)), ), ], post={}, diff --git a/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py b/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py index 81def366386..ef63436c21c 100644 --- a/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py +++ b/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py @@ -12,6 +12,7 @@ Block, BlockchainTestFiller, Bytecode, + Header, Op, Requests, SystemContractInteractionTransaction, @@ -93,23 +94,19 @@ def test_extra_consolidations( """ modified_code: Bytecode = Bytecode() memory_offset: int = 0 - amount_of_requests: int = 0 for consolidation_request in requests_list: - # update memory_offset with the correct value - consolidation_request_bytes_amount: int = len( - bytes(consolidation_request) - ) - assert consolidation_request_bytes_amount == 116, ( + record = bytes(consolidation_request) + assert len(record) == 116, ( "Expected consolidation request to be of size 116 but got size " - f"{consolidation_request_bytes_amount}" + f"{len(record)}" ) - memory_offset += consolidation_request_bytes_amount - - modified_code += Om.MSTORE(bytes(consolidation_request), memory_offset) - amount_of_requests += 1 + # Store records contiguously from offset 0 so the returned data is + # exactly the concatenated records (no gap, no trailing padding). + modified_code += Om.MSTORE(record, memory_offset) + memory_offset += len(record) - modified_code += Op.RETURN(0, Op.MSIZE()) + modified_code += Op.RETURN(0, memory_offset) pre[Spec_EIP7251.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS] = Account( code=modified_code, @@ -132,7 +129,7 @@ def test_extra_consolidations( blocks=[ Block( txs=txs, - requests_hash=Requests(*requests_list), + header_verify=Header(requests_hash=Requests(*requests_list)), ), ], post={}, From 853585f326bca3db05af886695db4d48bcb63bc5 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Mon, 27 Jul 2026 10:38:21 +0100 Subject: [PATCH 11/55] fix: max balance < 2^128 in ported static tests (#3230) --- tests/ported_static/stCreate2/test_create2_bounds.py | 4 +--- tests/ported_static/stCreate2/test_create2_bounds2.py | 4 +--- tests/ported_static/stCreate2/test_create2_bounds3.py | 4 +--- tests/ported_static/stMemoryStressTest/test_call_bounds.py | 4 +--- tests/ported_static/stMemoryStressTest/test_call_bounds2.py | 4 +--- tests/ported_static/stMemoryStressTest/test_call_bounds2a.py | 4 +--- tests/ported_static/stMemoryStressTest/test_call_bounds3.py | 4 +--- .../ported_static/stMemoryStressTest/test_callcode_bounds.py | 4 +--- .../ported_static/stMemoryStressTest/test_callcode_bounds2.py | 4 +--- .../ported_static/stMemoryStressTest/test_callcode_bounds3.py | 4 +--- .../ported_static/stMemoryStressTest/test_callcode_bounds4.py | 4 +--- tests/ported_static/stMemoryStressTest/test_create_bounds.py | 4 +--- tests/ported_static/stMemoryStressTest/test_create_bounds2.py | 4 +--- tests/ported_static/stMemoryStressTest/test_create_bounds3.py | 4 +--- .../stMemoryStressTest/test_delegatecall_bounds.py | 4 +--- .../stMemoryStressTest/test_delegatecall_bounds2.py | 4 +--- .../stMemoryStressTest/test_delegatecall_bounds3.py | 4 +--- tests/ported_static/stMemoryStressTest/test_mstore_bounds.py | 4 +--- tests/ported_static/stMemoryStressTest/test_mstore_bounds2.py | 4 +--- tests/ported_static/stMemoryStressTest/test_return_bounds.py | 4 +--- .../stMemoryStressTest/test_static_call_bounds.py | 4 +--- .../stMemoryStressTest/test_static_call_bounds2.py | 4 +--- .../stMemoryStressTest/test_static_call_bounds2a.py | 4 +--- .../stMemoryStressTest/test_static_call_bounds3.py | 4 +--- tests/ported_static/stTransactionTest/test_high_gas_limit.py | 4 +--- 25 files changed, 25 insertions(+), 75 deletions(-) diff --git a/tests/ported_static/stCreate2/test_create2_bounds.py b/tests/ported_static/stCreate2/test_create2_bounds.py index 42c5cb34ec6..ae0bebb10a2 100644 --- a/tests/ported_static/stCreate2/test_create2_bounds.py +++ b/tests/ported_static/stCreate2/test_create2_bounds.py @@ -56,9 +56,7 @@ def test_create2_bounds( """Test_create2_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stCreate2/test_create2_bounds2.py b/tests/ported_static/stCreate2/test_create2_bounds2.py index 175cddce83a..edc9271e1cf 100644 --- a/tests/ported_static/stCreate2/test_create2_bounds2.py +++ b/tests/ported_static/stCreate2/test_create2_bounds2.py @@ -56,9 +56,7 @@ def test_create2_bounds2( """Test_create2_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stCreate2/test_create2_bounds3.py b/tests/ported_static/stCreate2/test_create2_bounds3.py index d7c3c6c325f..0bf880de2b3 100644 --- a/tests/ported_static/stCreate2/test_create2_bounds3.py +++ b/tests/ported_static/stCreate2/test_create2_bounds3.py @@ -62,9 +62,7 @@ def test_create2_bounds3( """Test_create2_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_call_bounds.py b/tests/ported_static/stMemoryStressTest/test_call_bounds.py index 49a2518ece9..de23205f95c 100644 --- a/tests/ported_static/stMemoryStressTest/test_call_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_call_bounds.py @@ -54,9 +54,7 @@ def test_call_bounds( ) -> None: """Test_call_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_call_bounds2.py b/tests/ported_static/stMemoryStressTest/test_call_bounds2.py index 3638beda9b6..3f17cdaf2a9 100644 --- a/tests/ported_static/stMemoryStressTest/test_call_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_call_bounds2.py @@ -54,9 +54,7 @@ def test_call_bounds2( ) -> None: """Test_call_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_call_bounds2a.py b/tests/ported_static/stMemoryStressTest/test_call_bounds2a.py index 1065f6cec49..d6f4738f0c4 100644 --- a/tests/ported_static/stMemoryStressTest/test_call_bounds2a.py +++ b/tests/ported_static/stMemoryStressTest/test_call_bounds2a.py @@ -54,9 +54,7 @@ def test_call_bounds2a( ) -> None: """Test_call_bounds2a.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_call_bounds3.py b/tests/ported_static/stMemoryStressTest/test_call_bounds3.py index 81ccd53caf4..e255a89a186 100644 --- a/tests/ported_static/stMemoryStressTest/test_call_bounds3.py +++ b/tests/ported_static/stMemoryStressTest/test_call_bounds3.py @@ -60,9 +60,7 @@ def test_call_bounds3( ) -> None: """Test_call_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_callcode_bounds.py b/tests/ported_static/stMemoryStressTest/test_callcode_bounds.py index 3a7a1cd03ea..2b32019efda 100644 --- a/tests/ported_static/stMemoryStressTest/test_callcode_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_callcode_bounds.py @@ -54,9 +54,7 @@ def test_callcode_bounds( ) -> None: """Test_callcode_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_callcode_bounds2.py b/tests/ported_static/stMemoryStressTest/test_callcode_bounds2.py index 5496732ba6d..6899bd3a562 100644 --- a/tests/ported_static/stMemoryStressTest/test_callcode_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_callcode_bounds2.py @@ -54,9 +54,7 @@ def test_callcode_bounds2( ) -> None: """Test_callcode_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_callcode_bounds3.py b/tests/ported_static/stMemoryStressTest/test_callcode_bounds3.py index 9bddda9e3be..376b9832100 100644 --- a/tests/ported_static/stMemoryStressTest/test_callcode_bounds3.py +++ b/tests/ported_static/stMemoryStressTest/test_callcode_bounds3.py @@ -54,9 +54,7 @@ def test_callcode_bounds3( ) -> None: """Test_callcode_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_callcode_bounds4.py b/tests/ported_static/stMemoryStressTest/test_callcode_bounds4.py index fc8373060be..d0d8eeb698a 100644 --- a/tests/ported_static/stMemoryStressTest/test_callcode_bounds4.py +++ b/tests/ported_static/stMemoryStressTest/test_callcode_bounds4.py @@ -60,9 +60,7 @@ def test_callcode_bounds4( ) -> None: """Test_callcode_bounds4.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_create_bounds.py b/tests/ported_static/stMemoryStressTest/test_create_bounds.py index 992a097d01c..a03f36c4026 100644 --- a/tests/ported_static/stMemoryStressTest/test_create_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_create_bounds.py @@ -56,9 +56,7 @@ def test_create_bounds( """Test_create_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_create_bounds2.py b/tests/ported_static/stMemoryStressTest/test_create_bounds2.py index d136a0b2ed3..24898d687b2 100644 --- a/tests/ported_static/stMemoryStressTest/test_create_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_create_bounds2.py @@ -56,9 +56,7 @@ def test_create_bounds2( """Test_create_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_create_bounds3.py b/tests/ported_static/stMemoryStressTest/test_create_bounds3.py index d78ccc430d7..54b0f78cc8f 100644 --- a/tests/ported_static/stMemoryStressTest/test_create_bounds3.py +++ b/tests/ported_static/stMemoryStressTest/test_create_bounds3.py @@ -62,9 +62,7 @@ def test_create_bounds3( """Test_create_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x1000000000000000000000000000000000000000) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds.py b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds.py index bdc3e3a6b0d..32b18469cf5 100644 --- a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds.py @@ -54,9 +54,7 @@ def test_delegatecall_bounds( ) -> None: """Test_delegatecall_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds2.py b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds2.py index 4dde13d2569..7a93cbfab0b 100644 --- a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds2.py @@ -54,9 +54,7 @@ def test_delegatecall_bounds2( ) -> None: """Test_delegatecall_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds3.py b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds3.py index 804fc02744b..963fd456f91 100644 --- a/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds3.py +++ b/tests/ported_static/stMemoryStressTest/test_delegatecall_bounds3.py @@ -60,9 +60,7 @@ def test_delegatecall_bounds3( ) -> None: """Test_delegatecall_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_mstore_bounds.py b/tests/ported_static/stMemoryStressTest/test_mstore_bounds.py index 49713f67bc8..0d101040d1a 100644 --- a/tests/ported_static/stMemoryStressTest/test_mstore_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_mstore_bounds.py @@ -54,9 +54,7 @@ def test_mstore_bounds( ) -> None: """Test_mstore_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_mstore_bounds2.py b/tests/ported_static/stMemoryStressTest/test_mstore_bounds2.py index 93358c7451f..ab33179dd2a 100644 --- a/tests/ported_static/stMemoryStressTest/test_mstore_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_mstore_bounds2.py @@ -54,9 +54,7 @@ def test_mstore_bounds2( ) -> None: """Test_mstore_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_return_bounds.py b/tests/ported_static/stMemoryStressTest/test_return_bounds.py index 8918cd9d9d0..cd09e0777dc 100644 --- a/tests/ported_static/stMemoryStressTest/test_return_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_return_bounds.py @@ -63,9 +63,7 @@ def test_return_bounds( ) -> None: """Test_return_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_static_call_bounds.py b/tests/ported_static/stMemoryStressTest/test_static_call_bounds.py index c6a2e86cb91..375e9fe7edc 100644 --- a/tests/ported_static/stMemoryStressTest/test_static_call_bounds.py +++ b/tests/ported_static/stMemoryStressTest/test_static_call_bounds.py @@ -54,9 +54,7 @@ def test_static_call_bounds( ) -> None: """Test_static_call_bounds.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_static_call_bounds2.py b/tests/ported_static/stMemoryStressTest/test_static_call_bounds2.py index b4d70c3de24..f9e56c4eaa8 100644 --- a/tests/ported_static/stMemoryStressTest/test_static_call_bounds2.py +++ b/tests/ported_static/stMemoryStressTest/test_static_call_bounds2.py @@ -54,9 +54,7 @@ def test_static_call_bounds2( ) -> None: """Test_static_call_bounds2.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_static_call_bounds2a.py b/tests/ported_static/stMemoryStressTest/test_static_call_bounds2a.py index c27269691ae..026e3c43c98 100644 --- a/tests/ported_static/stMemoryStressTest/test_static_call_bounds2a.py +++ b/tests/ported_static/stMemoryStressTest/test_static_call_bounds2a.py @@ -54,9 +54,7 @@ def test_static_call_bounds2a( ) -> None: """Test_static_call_bounds2a.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stMemoryStressTest/test_static_call_bounds3.py b/tests/ported_static/stMemoryStressTest/test_static_call_bounds3.py index fe5980fd434..912976897c3 100644 --- a/tests/ported_static/stMemoryStressTest/test_static_call_bounds3.py +++ b/tests/ported_static/stMemoryStressTest/test_static_call_bounds3.py @@ -54,9 +54,7 @@ def test_static_call_bounds3( ) -> None: """Test_static_call_bounds3.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) + sender = pre.fund_eoa(amount=2**128 - 1) env = Environment( fee_recipient=coinbase, diff --git a/tests/ported_static/stTransactionTest/test_high_gas_limit.py b/tests/ported_static/stTransactionTest/test_high_gas_limit.py index 60069018353..7ef8e7f9202 100644 --- a/tests/ported_static/stTransactionTest/test_high_gas_limit.py +++ b/tests/ported_static/stTransactionTest/test_high_gas_limit.py @@ -55,9 +55,7 @@ def test_high_gas_limit( gas_limit=9223372036854775807, ) - pre[sender] = Account( - balance=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + pre[sender] = Account(balance=2**128 - 1) # EIP-2780 charges ``NEW_ACCOUNT`` state gas at the top frame when # value is sent to an empty recipient; with the default zero From 7c4177ace2fcabd6aaa86be043cbc813065c4bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 27 Jul 2026 11:49:45 +0200 Subject: [PATCH 12/55] feat(tests): add ef_prefix deposit-halt mode to EIP-8037 state-gas test (#3233) Co-authored-by: spencer-tb --- .../test_state_gas_create.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index 882872cff2d..b2274b5e5f8 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -1243,6 +1243,7 @@ def test_create_no_double_charge_new_account( [ pytest.param("oversized_code", id="oversized_code"), pytest.param("oog_deposit", id="oog_deposit"), + pytest.param("ef_prefix", id="ef_prefix"), ], ) @pytest.mark.valid_from("EIP8037") @@ -1258,11 +1259,12 @@ def test_code_deposit_halt_discards_initcode_state_gas( A CREATE tx runs initcode that first performs a state-creating operation (charging GAS_NEW_ACCOUNT state gas), then returns - code that triggers a deposit failure (oversized or OOG). The - exceptional halt reverts all initcode state changes including - the new account. The reverted GAS_NEW_ACCOUNT must NOT count - in block_state_gas_used, which determines the block header - gas_used via max(block_regular_gas, block_state_gas). + code that triggers a deposit failure (oversized, OOG, or an + EIP-3541 0xEF prefix). The exceptional halt reverts all initcode + state changes including the new account. The reverted + GAS_NEW_ACCOUNT must NOT count in block_state_gas_used, which + determines the block header gas_used via + max(block_regular_gas, block_state_gas). """ subcall_forwarded_value = 1 entry_account_value = 1 @@ -1278,11 +1280,15 @@ def test_code_deposit_halt_discards_initcode_state_gas( if deposit_fail_mode == "oversized_code": deposit_fail = Op.RETURN(0, fork.max_code_size() + 1) - else: - # Return code at max size — passes the size check but code + elif deposit_fail_mode == "oog_deposit": + # Return code at max size: passes the size check but code # deposit state gas (max_code_size * cost_per_state_byte) # exceeds available state gas in the child frame, causing OOG. deposit_fail = Op.RETURN(0, fork.max_code_size()) + else: + # Return single 0xEF byte: EIP-3541 rejects the code before + # the size check or any deposit charging, halting the deposit. + deposit_fail = Op.MSTORE8(0, 0xEF) + Op.RETURN(0, 1) initcode = state_op + deposit_fail From 2cc42a8757a4a63d58411aad39e2c09240e8687f Mon Sep 17 00:00:00 2001 From: kevaundray Date: Mon, 27 Jul 2026 18:47:22 +0100 Subject: [PATCH 13/55] refactor(spec): make state interface fully implementation agnostic (#3218) * initial commit * fix(tests): point module-level ethereum.state imports at state_mpt The State class and its helpers moved to ethereum.state_mpt, but two test files import the module itself rather than names from it, which the import rewrite missed: test_optimized_state.py aliases it for state_root calls (caught by mypy in CI) and load_vm_tests.py returns it as the fallback fork state module (hidden behind an Any return). * refactor(t8n): resolve each fork's state provider through the fork The evm tools hardcoded the MPT-backed provider: alloc loading built ethereum.state_mpt.State directly, and t8n applied diffs, serialized allocs, and backed up state by reaching into MPT trie internals. Resolve the provider through the fork instead: every fork's fork module imports its State class, so ForkLoad.state_provider derives the provider module from it. Alloc loading, diff application, serialization, and backup/restore all go through that module. The provider gains the uniform helpers this needs: copy_state, restore_state, all_accounts, and account_storage. With this, a fork whose commitment is not the Merkle Patricia Trie works with the tooling by supplying a provider with the same module surface as ethereum.state_mpt. * guru's comments * remove global _EMPTY_DIFF constant and just use `default` * style: ruff format test_alloc_prestate * minor fixes --------- Co-authored-by: Guruprasad Kamath --- .../test_types/account_types.py | 43 ++- .../test_types/tests/test_alloc_prestate.py | 41 +-- src/ethereum/forks/amsterdam/blocks.py | 4 +- src/ethereum/forks/amsterdam/fork.py | 13 +- src/ethereum/forks/amsterdam/state_tracker.py | 2 +- src/ethereum/forks/arrow_glacier/blocks.py | 4 +- src/ethereum/forks/arrow_glacier/fork.py | 14 +- .../forks/arrow_glacier/state_tracker.py | 2 +- src/ethereum/forks/berlin/blocks.py | 4 +- src/ethereum/forks/berlin/fork.py | 14 +- src/ethereum/forks/berlin/state_tracker.py | 2 +- src/ethereum/forks/bpo1/blocks.py | 4 +- src/ethereum/forks/bpo1/fork.py | 12 +- src/ethereum/forks/bpo1/state_tracker.py | 2 +- src/ethereum/forks/bpo2/blocks.py | 4 +- src/ethereum/forks/bpo2/fork.py | 12 +- src/ethereum/forks/bpo2/state_tracker.py | 2 +- src/ethereum/forks/bpo3/blocks.py | 4 +- src/ethereum/forks/bpo3/fork.py | 12 +- src/ethereum/forks/bpo3/state_tracker.py | 2 +- src/ethereum/forks/bpo4/blocks.py | 4 +- src/ethereum/forks/bpo4/fork.py | 12 +- src/ethereum/forks/bpo4/state_tracker.py | 2 +- src/ethereum/forks/bpo5/blocks.py | 4 +- src/ethereum/forks/bpo5/fork.py | 12 +- src/ethereum/forks/bpo5/state_tracker.py | 2 +- src/ethereum/forks/byzantium/blocks.py | 4 +- src/ethereum/forks/byzantium/fork.py | 14 +- src/ethereum/forks/byzantium/state_tracker.py | 2 +- src/ethereum/forks/cancun/blocks.py | 4 +- src/ethereum/forks/cancun/fork.py | 12 +- src/ethereum/forks/cancun/state_tracker.py | 2 +- src/ethereum/forks/constantinople/blocks.py | 4 +- src/ethereum/forks/constantinople/fork.py | 14 +- .../forks/constantinople/state_tracker.py | 2 +- src/ethereum/forks/dao_fork/blocks.py | 4 +- src/ethereum/forks/dao_fork/dao.py | 4 +- src/ethereum/forks/dao_fork/fork.py | 22 +- src/ethereum/forks/dao_fork/state_tracker.py | 2 +- src/ethereum/forks/frontier/blocks.py | 4 +- src/ethereum/forks/frontier/fork.py | 22 +- src/ethereum/forks/frontier/state_tracker.py | 2 +- src/ethereum/forks/gray_glacier/blocks.py | 4 +- src/ethereum/forks/gray_glacier/fork.py | 14 +- .../forks/gray_glacier/state_tracker.py | 2 +- src/ethereum/forks/homestead/blocks.py | 4 +- src/ethereum/forks/homestead/fork.py | 22 +- src/ethereum/forks/homestead/state_tracker.py | 2 +- src/ethereum/forks/istanbul/blocks.py | 4 +- src/ethereum/forks/istanbul/fork.py | 14 +- src/ethereum/forks/istanbul/state_tracker.py | 2 +- src/ethereum/forks/london/blocks.py | 4 +- src/ethereum/forks/london/fork.py | 14 +- src/ethereum/forks/london/state_tracker.py | 2 +- src/ethereum/forks/muir_glacier/blocks.py | 4 +- src/ethereum/forks/muir_glacier/fork.py | 14 +- .../forks/muir_glacier/state_tracker.py | 2 +- src/ethereum/forks/osaka/blocks.py | 4 +- src/ethereum/forks/osaka/fork.py | 12 +- src/ethereum/forks/osaka/state_tracker.py | 2 +- src/ethereum/forks/paris/blocks.py | 4 +- src/ethereum/forks/paris/fork.py | 14 +- src/ethereum/forks/paris/state_tracker.py | 2 +- src/ethereum/forks/prague/blocks.py | 4 +- src/ethereum/forks/prague/fork.py | 12 +- src/ethereum/forks/prague/state_tracker.py | 2 +- src/ethereum/forks/shanghai/blocks.py | 4 +- src/ethereum/forks/shanghai/fork.py | 14 +- src/ethereum/forks/shanghai/state_tracker.py | 2 +- src/ethereum/forks/spurious_dragon/blocks.py | 4 +- src/ethereum/forks/spurious_dragon/fork.py | 22 +- .../forks/spurious_dragon/state_tracker.py | 2 +- .../forks/tangerine_whistle/blocks.py | 4 +- src/ethereum/forks/tangerine_whistle/fork.py | 22 +- .../forks/tangerine_whistle/state_tracker.py | 2 +- src/ethereum/merkle_patricia_trie.py | 18 +- src/ethereum/state.py | 261 ++---------------- src/ethereum/state_mpt.py | 221 +++++++++++++++ .../evm_tools/loaders/fixture_loader.py | 10 +- .../evm_tools/loaders/fork_loader.py | 12 + .../evm_tools/t8n/result.py | 4 +- .../helpers/load_blockchain_tests.py | 2 +- tests/json_loader/helpers/load_vm_tests.py | 4 +- tests/json_loader/test_genesis.py | 11 +- tests/json_loader/test_optimized_state.py | 2 +- 85 files changed, 478 insertions(+), 658 deletions(-) create mode 100644 src/ethereum/state_mpt.py diff --git a/packages/testing/src/execution_testing/test_types/account_types.py b/packages/testing/src/execution_testing/test_types/account_types.py index f0c51791e51..e5e03717f28 100644 --- a/packages/testing/src/execution_testing/test_types/account_types.py +++ b/packages/testing/src/execution_testing/test_types/account_types.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from enum import Enum, auto from typing import ( - AbstractSet, Any, Dict, ItemsView, @@ -13,13 +12,12 @@ Literal, Optional, Self, - Tuple, ) import ethereum.state as spec_state +import ethereum.state_mpt as spec_state_mpt from ethereum.crypto.hash import Hash32 from ethereum.crypto.hash import keccak256 as spec_keccak256 -from ethereum.merkle_patricia_trie import InternalNode from ethereum_types.bytes import Bytes, Bytes20 from ethereum_types.numeric import U256, Bytes32, Uint from pydantic import PrivateAttr @@ -301,7 +299,7 @@ def empty_accounts(self) -> List[Address]: def state_root(self) -> Hash: """Return state root of the allocation.""" - return Hash(spec_state.state_root(self._materialize_state())) + return Hash(spec_state_mpt.state_root(self._materialize_state())) def verify_post_alloc(self, got_alloc: "Alloc") -> None: """ @@ -358,15 +356,16 @@ def _ensure_live(self) -> None: self._build_cache() self._phase = _Phase.LIVE - def _materialize_state(self) -> spec_state.State: + def _materialize_state(self) -> spec_state_mpt.State: """ - Build an in-memory `ethereum.state.State` mirror of `self.root`. + Build an in-memory `ethereum.state_mpt.State` mirror of + `self.root`. - Used as the trie-backed delegate for - `compute_state_root_and_trie_changes` (a cold, once-per-block call). - The materialized state is not retained. + Used as the trie-backed delegate for `compute_state_root` (a + cold, once-per-block call). The materialized state is not + retained. """ - state = spec_state.State() + state = spec_state_mpt.State() for address, account in self.root.items(): if account is None: continue @@ -375,7 +374,7 @@ def _materialize_state(self) -> spec_state.State: code_hash = ( spec_keccak256(code) if code else spec_state.EMPTY_CODE_HASH ) - spec_state.set_account( + spec_state_mpt.set_account( state, addr, spec_state.Account( @@ -388,7 +387,7 @@ def _materialize_state(self) -> spec_state.State: value_int = int(value_hi) if value_int == 0: continue - spec_state.set_storage( + spec_state_mpt.set_storage( state, addr, Bytes32(int(key_hi).to_bytes(32, "big")), @@ -456,24 +455,18 @@ def account_has_storage(self, address: Bytes20) -> bool: account = self.root.get(Address(address)) return account is not None and bool(account.storage.root) - def compute_state_root_and_trie_changes( - self, - account_changes: Dict[Bytes20, Optional[spec_state.Account]], - storage_changes: Dict[Bytes20, Dict[Bytes32, U256]], - storage_clears: AbstractSet[Bytes20] = frozenset(), - ) -> Tuple[Hash32, List["InternalNode"]]: + def compute_state_root(self, block_diff: spec_state.BlockDiff) -> Hash32: """ - Compute the state root after applying `*_changes` to the pre-state. + Compute the state root after applying `block_diff` to the + pre-state. - Conforms to - `ethereum.state.PreState.compute_state_root_and_trie_changes`. - Builds the trie inline; `Alloc` does not cache `Trie` instances. + Conforms to `ethereum.state.PreState.compute_state_root`. + Builds the trie inline; `Alloc` does not cache `Trie` + instances. """ self._ensure_live() state = self._materialize_state() - return state.compute_state_root_and_trie_changes( - account_changes, storage_changes, storage_clears - ) + return state.compute_state_root(block_diff) # ------------------------------------------------------------------ # Lifecycle: apply_diff and freeze diff --git a/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py b/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py index 30cb39af3d6..cc7798af169 100644 --- a/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py +++ b/packages/testing/src/execution_testing/test_types/tests/test_alloc_prestate.py @@ -4,8 +4,8 @@ Covers four invariants of the lifecycle phase machinery: 1. The code-hash → bytes cache is built correctly when the alloc goes LIVE, and the PreState read methods agree with the source dict. - 2. `compute_state_root_and_trie_changes` on `Alloc` matches the same - call on a freshly built `ethereum.state.State` over the same data. + 2. `compute_state_root` on `Alloc` matches the same call on a + freshly built `ethereum.state_mpt.State` over the same data. 3. Mutating an `Alloc` via `__setitem__`/`__delitem__` is rejected after it has been used as a PreState. 4. Building alloc B by `apply_diff`ing a diff onto alloc A produces a @@ -16,6 +16,7 @@ from typing import Dict, Optional import ethereum.state as spec_state +import ethereum.state_mpt as spec_state_mpt import pytest from ethereum.crypto.hash import keccak256 from ethereum_types.bytes import Bytes20, Bytes32 @@ -58,16 +59,16 @@ def _fixture_alloc() -> Alloc: ) -def _state_from_alloc(alloc: Alloc) -> spec_state.State: +def _state_from_alloc(alloc: Alloc) -> spec_state_mpt.State: """Build a spec `State` mirroring `alloc` for parity comparisons.""" - state = spec_state.State() + state = spec_state_mpt.State() for address, account in alloc.root.items(): if account is None: continue addr = Bytes20(address) code = bytes(account.code) if account.code else b"" code_hash = keccak256(code) if code else spec_state.EMPTY_CODE_HASH - spec_state.set_account( + spec_state_mpt.set_account( state, addr, spec_state.Account( @@ -81,7 +82,7 @@ def _state_from_alloc(alloc: Alloc) -> spec_state.State: for key_hi, value_hi in account.storage.root.items(): if int(value_hi) == 0: continue - spec_state.set_storage( + spec_state_mpt.set_storage( state, addr, Bytes32(int(key_hi).to_bytes(32, "big")), @@ -138,12 +139,12 @@ def test_cache_build_and_read_methods_agree_with_source() -> None: def test_state_root_parity_against_spec_state() -> None: - """`Alloc.compute_state_root_and_trie_changes` matches spec `State`.""" + """`Alloc.compute_state_root` matches spec `State`.""" alloc = _fixture_alloc() state = _state_from_alloc(alloc) - alloc_root, _ = alloc.compute_state_root_and_trie_changes({}, {}) - spec_root, _ = state.compute_state_root_and_trie_changes({}, {}) + alloc_root = alloc.compute_state_root(spec_state.BlockDiff()) + spec_root = state.compute_state_root(spec_state.BlockDiff()) assert alloc_root == spec_root # Same parity under non-trivial change sets. @@ -155,11 +156,19 @@ def test_state_root_parity_against_spec_state() -> None: storage_changes: Dict[Bytes20, Dict[Bytes32, U256]] = { ADDR_B: {Bytes32(b"\x00" * 31 + b"\x01"): U256(0x99)}, } - alloc_root_changed, _ = alloc.compute_state_root_and_trie_changes( - account_changes, storage_changes + alloc_root_changed = alloc.compute_state_root( + spec_state.BlockDiff( + account_changes=account_changes, + storage_changes=storage_changes, + code_changes={}, + ) ) - spec_root_changed, _ = state.compute_state_root_and_trie_changes( - account_changes, storage_changes + spec_root_changed = state.compute_state_root( + spec_state.BlockDiff( + account_changes=account_changes, + storage_changes=storage_changes, + code_changes={}, + ) ) assert alloc_root_changed == spec_root_changed assert alloc_root_changed != alloc_root @@ -258,9 +267,9 @@ def test_apply_diff_round_trip_matches_independent_post_state() -> None: alloc_pre.apply_diff(diff) # State roots should match. - pre_root, _ = alloc_pre.compute_state_root_and_trie_changes({}, {}) - expected_root, _ = alloc_post_expected.compute_state_root_and_trie_changes( - {}, {} + pre_root = alloc_pre.compute_state_root(spec_state.BlockDiff()) + expected_root = alloc_post_expected.compute_state_root( + spec_state.BlockDiff() ) assert pre_root == expected_root diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index 57b0e2c2874..68732a167d4 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 302e0887ed2..d76b92a24cc 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -30,13 +30,8 @@ ) from ethereum.forks.bpo5.blocks import Header as PreviousHeader from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - BlockDiff, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address, BlockDiff +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .block_access_lists import ( @@ -343,9 +338,7 @@ def execute_block( withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = pre_state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index 5f7d0eaf33c..9e0e3b24b67 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/arrow_glacier/blocks.py b/src/ethereum/forks/arrow_glacier/blocks.py index 6d9c41f774d..c15f5bd8334 100644 --- a/src/ethereum/forks/arrow_glacier/blocks.py +++ b/src/ethereum/forks/arrow_glacier/blocks.py @@ -71,12 +71,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/arrow_glacier/fork.py b/src/ethereum/forks/arrow_glacier/fork.py index 484d474c983..5a37ddabcec 100644 --- a/src/ethereum/forks/arrow_glacier/fork.py +++ b/src/ethereum/forks/arrow_glacier/fork.py @@ -29,12 +29,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt @@ -204,11 +200,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/arrow_glacier/state_tracker.py b/src/ethereum/forks/arrow_glacier/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/arrow_glacier/state_tracker.py +++ b/src/ethereum/forks/arrow_glacier/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/berlin/blocks.py b/src/ethereum/forks/berlin/blocks.py index 0bb4d2103ea..a52bce35813 100644 --- a/src/ethereum/forks/berlin/blocks.py +++ b/src/ethereum/forks/berlin/blocks.py @@ -63,12 +63,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/berlin/fork.py b/src/ethereum/forks/berlin/fork.py index 14f72b34c54..e2ee2e0d3cb 100644 --- a/src/ethereum/forks/berlin/fork.py +++ b/src/ethereum/forks/berlin/fork.py @@ -29,12 +29,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt @@ -196,11 +192,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/berlin/state_tracker.py b/src/ethereum/forks/berlin/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/berlin/state_tracker.py +++ b/src/ethereum/forks/berlin/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/bpo1/blocks.py b/src/ethereum/forks/bpo1/blocks.py index cd4b2daca34..f3eb9b40bf4 100644 --- a/src/ethereum/forks/bpo1/blocks.py +++ b/src/ethereum/forks/bpo1/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/bpo1/fork.py b/src/ethereum/forks/bpo1/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/bpo1/fork.py +++ b/src/ethereum/forks/bpo1/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/bpo1/state_tracker.py b/src/ethereum/forks/bpo1/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/bpo1/state_tracker.py +++ b/src/ethereum/forks/bpo1/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/bpo2/blocks.py b/src/ethereum/forks/bpo2/blocks.py index 209862680f1..2fb877682e1 100644 --- a/src/ethereum/forks/bpo2/blocks.py +++ b/src/ethereum/forks/bpo2/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/bpo2/fork.py b/src/ethereum/forks/bpo2/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/bpo2/fork.py +++ b/src/ethereum/forks/bpo2/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/bpo2/state_tracker.py b/src/ethereum/forks/bpo2/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/bpo2/state_tracker.py +++ b/src/ethereum/forks/bpo2/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/bpo3/blocks.py b/src/ethereum/forks/bpo3/blocks.py index df26affccfb..e5931b35c35 100644 --- a/src/ethereum/forks/bpo3/blocks.py +++ b/src/ethereum/forks/bpo3/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/bpo3/fork.py b/src/ethereum/forks/bpo3/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/bpo3/fork.py +++ b/src/ethereum/forks/bpo3/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/bpo3/state_tracker.py b/src/ethereum/forks/bpo3/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/bpo3/state_tracker.py +++ b/src/ethereum/forks/bpo3/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/bpo4/blocks.py b/src/ethereum/forks/bpo4/blocks.py index 5fcadec15d9..c09fd2907e1 100644 --- a/src/ethereum/forks/bpo4/blocks.py +++ b/src/ethereum/forks/bpo4/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/bpo4/fork.py b/src/ethereum/forks/bpo4/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/bpo4/fork.py +++ b/src/ethereum/forks/bpo4/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/bpo4/state_tracker.py b/src/ethereum/forks/bpo4/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/bpo4/state_tracker.py +++ b/src/ethereum/forks/bpo4/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/bpo5/blocks.py b/src/ethereum/forks/bpo5/blocks.py index eed86b7e175..83e98d6345f 100644 --- a/src/ethereum/forks/bpo5/blocks.py +++ b/src/ethereum/forks/bpo5/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/bpo5/fork.py b/src/ethereum/forks/bpo5/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/bpo5/fork.py +++ b/src/ethereum/forks/bpo5/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/bpo5/state_tracker.py b/src/ethereum/forks/bpo5/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/bpo5/state_tracker.py +++ b/src/ethereum/forks/bpo5/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/byzantium/blocks.py b/src/ethereum/forks/byzantium/blocks.py index 26091316a9c..39d50db5a77 100644 --- a/src/ethereum/forks/byzantium/blocks.py +++ b/src/ethereum/forks/byzantium/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/byzantium/fork.py b/src/ethereum/forks/byzantium/fork.py index a83087f4f0a..6d0d1b461b2 100644 --- a/src/ethereum/forks/byzantium/fork.py +++ b/src/ethereum/forks/byzantium/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -191,11 +187,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/byzantium/state_tracker.py b/src/ethereum/forks/byzantium/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/byzantium/state_tracker.py +++ b/src/ethereum/forks/byzantium/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/cancun/blocks.py b/src/ethereum/forks/cancun/blocks.py index 3507f8a3284..d1697870b1e 100644 --- a/src/ethereum/forks/cancun/blocks.py +++ b/src/ethereum/forks/cancun/blocks.py @@ -108,12 +108,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/cancun/fork.py b/src/ethereum/forks/cancun/fork.py index 67e3b0197d0..5d3e2c56040 100644 --- a/src/ethereum/forks/cancun/fork.py +++ b/src/ethereum/forks/cancun/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -224,9 +220,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/cancun/state_tracker.py b/src/ethereum/forks/cancun/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/cancun/state_tracker.py +++ b/src/ethereum/forks/cancun/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/constantinople/blocks.py b/src/ethereum/forks/constantinople/blocks.py index 94dd37899b8..48582187726 100644 --- a/src/ethereum/forks/constantinople/blocks.py +++ b/src/ethereum/forks/constantinople/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/constantinople/fork.py b/src/ethereum/forks/constantinople/fork.py index 62654a56386..46a71bb0e36 100644 --- a/src/ethereum/forks/constantinople/fork.py +++ b/src/ethereum/forks/constantinople/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -191,11 +187,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/constantinople/state_tracker.py b/src/ethereum/forks/constantinople/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/constantinople/state_tracker.py +++ b/src/ethereum/forks/constantinople/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/dao_fork/blocks.py b/src/ethereum/forks/dao_fork/blocks.py index 7e6320bb827..138ac721c88 100644 --- a/src/ethereum/forks/dao_fork/blocks.py +++ b/src/ethereum/forks/dao_fork/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/dao_fork/dao.py b/src/ethereum/forks/dao_fork/dao.py index 93bed096cdb..714841d10bd 100644 --- a/src/ethereum/forks/dao_fork/dao.py +++ b/src/ethereum/forks/dao_fork/dao.py @@ -5,7 +5,7 @@ The recovery contract was previously created using normal contract deployment. """ -from ethereum.state import State +from ethereum.state_mpt import State from .state_tracker import TransactionState, get_account, move_ether from .utils.hexadecimal import hex_to_address @@ -354,7 +354,7 @@ def apply_dao(state: State) -> None: [`DAO_ACCOUNTS`]: ref:ethereum.forks.dao_fork.dao.DAO_ACCOUNTS [`DAO_RECOVERY`]: ref:ethereum.forks.dao_fork.dao.DAO_RECOVERY """ - from ethereum.state import apply_changes_to_state + from ethereum.state_mpt import apply_changes_to_state from .state_tracker import ( BlockState, diff --git a/src/ethereum/forks/dao_fork/fork.py b/src/ethereum/forks/dao_fork/fork.py index f12f29e3923..c3a1c327e59 100644 --- a/src/ethereum/forks/dao_fork/fork.py +++ b/src/ethereum/forks/dao_fork/fork.py @@ -31,12 +31,8 @@ ) from ethereum.fork_criteria import ByBlockNumber from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import FORK_CRITERIA, vm from .blocks import Block, Header, Log, Receipt @@ -197,11 +193,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -710,12 +702,8 @@ def process_transaction( block_state = block_env.state block_diff = extract_block_diff(block_state) - intermediate_state_root, _ = ( - block_state.pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + intermediate_state_root = block_state.pre_state.compute_state_root( + block_diff ) receipt = make_receipt( diff --git a/src/ethereum/forks/dao_fork/state_tracker.py b/src/ethereum/forks/dao_fork/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/dao_fork/state_tracker.py +++ b/src/ethereum/forks/dao_fork/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/frontier/blocks.py b/src/ethereum/forks/frontier/blocks.py index b6a4db96624..090f85eaf7c 100644 --- a/src/ethereum/forks/frontier/blocks.py +++ b/src/ethereum/forks/frontier/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/frontier/fork.py b/src/ethereum/forks/frontier/fork.py index 820baf10184..f2cd3ca61b7 100644 --- a/src/ethereum/forks/frontier/fork.py +++ b/src/ethereum/forks/frontier/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -185,11 +181,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -689,12 +681,8 @@ def process_transaction( block_state = block_env.state block_diff = extract_block_diff(block_state) - intermediate_state_root, _ = ( - block_state.pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + intermediate_state_root = block_state.pre_state.compute_state_root( + block_diff ) receipt = make_receipt( diff --git a/src/ethereum/forks/frontier/state_tracker.py b/src/ethereum/forks/frontier/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/frontier/state_tracker.py +++ b/src/ethereum/forks/frontier/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/gray_glacier/blocks.py b/src/ethereum/forks/gray_glacier/blocks.py index e36a9a38aba..17fb9c1afc2 100644 --- a/src/ethereum/forks/gray_glacier/blocks.py +++ b/src/ethereum/forks/gray_glacier/blocks.py @@ -71,12 +71,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/gray_glacier/fork.py b/src/ethereum/forks/gray_glacier/fork.py index fca169f4123..921551c7b05 100644 --- a/src/ethereum/forks/gray_glacier/fork.py +++ b/src/ethereum/forks/gray_glacier/fork.py @@ -29,12 +29,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt @@ -204,11 +200,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/gray_glacier/state_tracker.py b/src/ethereum/forks/gray_glacier/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/gray_glacier/state_tracker.py +++ b/src/ethereum/forks/gray_glacier/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/homestead/blocks.py b/src/ethereum/forks/homestead/blocks.py index bc21e326b4e..19936d6b4b8 100644 --- a/src/ethereum/forks/homestead/blocks.py +++ b/src/ethereum/forks/homestead/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/homestead/fork.py b/src/ethereum/forks/homestead/fork.py index cf0a29d826f..64bb280734a 100644 --- a/src/ethereum/forks/homestead/fork.py +++ b/src/ethereum/forks/homestead/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -185,11 +181,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -689,12 +681,8 @@ def process_transaction( block_state = block_env.state block_diff = extract_block_diff(block_state) - intermediate_state_root, _ = ( - block_state.pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + intermediate_state_root = block_state.pre_state.compute_state_root( + block_diff ) receipt = make_receipt( diff --git a/src/ethereum/forks/homestead/state_tracker.py b/src/ethereum/forks/homestead/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/homestead/state_tracker.py +++ b/src/ethereum/forks/homestead/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/istanbul/blocks.py b/src/ethereum/forks/istanbul/blocks.py index 3cdd475a33a..4a03bd2223d 100644 --- a/src/ethereum/forks/istanbul/blocks.py +++ b/src/ethereum/forks/istanbul/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/istanbul/fork.py b/src/ethereum/forks/istanbul/fork.py index da84266e3f9..a1bfce15ab8 100644 --- a/src/ethereum/forks/istanbul/fork.py +++ b/src/ethereum/forks/istanbul/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -191,11 +187,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/istanbul/state_tracker.py b/src/ethereum/forks/istanbul/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/istanbul/state_tracker.py +++ b/src/ethereum/forks/istanbul/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/london/blocks.py b/src/ethereum/forks/london/blocks.py index d6708cc2f68..44bba156b59 100644 --- a/src/ethereum/forks/london/blocks.py +++ b/src/ethereum/forks/london/blocks.py @@ -71,12 +71,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/london/fork.py b/src/ethereum/forks/london/fork.py index c457b885c6d..fd5155d1c82 100644 --- a/src/ethereum/forks/london/fork.py +++ b/src/ethereum/forks/london/fork.py @@ -30,12 +30,8 @@ ) from ethereum.fork_criteria import ByBlockNumber from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import FORK_CRITERIA, vm from .blocks import Block, Header, Log, Receipt, encode_receipt @@ -206,11 +202,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/london/state_tracker.py b/src/ethereum/forks/london/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/london/state_tracker.py +++ b/src/ethereum/forks/london/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/muir_glacier/blocks.py b/src/ethereum/forks/muir_glacier/blocks.py index 426cfdb5dde..0fd12b2efc0 100644 --- a/src/ethereum/forks/muir_glacier/blocks.py +++ b/src/ethereum/forks/muir_glacier/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/muir_glacier/fork.py b/src/ethereum/forks/muir_glacier/fork.py index e64fcf08654..580281b7905 100644 --- a/src/ethereum/forks/muir_glacier/fork.py +++ b/src/ethereum/forks/muir_glacier/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -191,11 +187,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/muir_glacier/state_tracker.py b/src/ethereum/forks/muir_glacier/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/muir_glacier/state_tracker.py +++ b/src/ethereum/forks/muir_glacier/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/osaka/blocks.py b/src/ethereum/forks/osaka/blocks.py index f1f174f892a..1055ba3407c 100644 --- a/src/ethereum/forks/osaka/blocks.py +++ b/src/ethereum/forks/osaka/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/osaka/fork.py b/src/ethereum/forks/osaka/fork.py index c2c43c631dc..71cca148fac 100644 --- a/src/ethereum/forks/osaka/fork.py +++ b/src/ethereum/forks/osaka/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -253,9 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/osaka/state_tracker.py b/src/ethereum/forks/osaka/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/osaka/state_tracker.py +++ b/src/ethereum/forks/osaka/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/paris/blocks.py b/src/ethereum/forks/paris/blocks.py index 15852ce82a1..84b89949ee0 100644 --- a/src/ethereum/forks/paris/blocks.py +++ b/src/ethereum/forks/paris/blocks.py @@ -73,12 +73,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/paris/fork.py b/src/ethereum/forks/paris/fork.py index 318cbdf2163..cfe47564565 100644 --- a/src/ethereum/forks/paris/fork.py +++ b/src/ethereum/forks/paris/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt @@ -197,11 +193,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: transactions=block.transactions, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/paris/state_tracker.py b/src/ethereum/forks/paris/state_tracker.py index 964acb0682a..1de225db4bc 100644 --- a/src/ethereum/forks/paris/state_tracker.py +++ b/src/ethereum/forks/paris/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/prague/blocks.py b/src/ethereum/forks/prague/blocks.py index e34b1dc4e6f..2b2c50cbba9 100644 --- a/src/ethereum/forks/prague/blocks.py +++ b/src/ethereum/forks/prague/blocks.py @@ -109,12 +109,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/prague/fork.py b/src/ethereum/forks/prague/fork.py index f5c4f81a6f8..9a322c36626 100644 --- a/src/ethereum/forks/prague/fork.py +++ b/src/ethereum/forks/prague/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -243,9 +239,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, block_diff.storage_changes - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/prague/state_tracker.py b/src/ethereum/forks/prague/state_tracker.py index 8ce889a833b..52d28c95b7c 100644 --- a/src/ethereum/forks/prague/state_tracker.py +++ b/src/ethereum/forks/prague/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/shanghai/blocks.py b/src/ethereum/forks/shanghai/blocks.py index 04990ab04ff..ad2b2b01293 100644 --- a/src/ethereum/forks/shanghai/blocks.py +++ b/src/ethereum/forks/shanghai/blocks.py @@ -107,12 +107,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/shanghai/fork.py b/src/ethereum/forks/shanghai/fork.py index c038d947014..16c4207aca4 100644 --- a/src/ethereum/forks/shanghai/fork.py +++ b/src/ethereum/forks/shanghai/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -198,11 +194,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals=block.withdrawals, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) diff --git a/src/ethereum/forks/shanghai/state_tracker.py b/src/ethereum/forks/shanghai/state_tracker.py index 964acb0682a..1de225db4bc 100644 --- a/src/ethereum/forks/shanghai/state_tracker.py +++ b/src/ethereum/forks/shanghai/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/spurious_dragon/blocks.py b/src/ethereum/forks/spurious_dragon/blocks.py index f1f063a18b3..cdb35bed696 100644 --- a/src/ethereum/forks/spurious_dragon/blocks.py +++ b/src/ethereum/forks/spurious_dragon/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/spurious_dragon/fork.py b/src/ethereum/forks/spurious_dragon/fork.py index 01c8b269d16..f04455028e9 100644 --- a/src/ethereum/forks/spurious_dragon/fork.py +++ b/src/ethereum/forks/spurious_dragon/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -189,11 +185,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -712,12 +704,8 @@ def process_transaction( block_state = block_env.state block_diff = extract_block_diff(block_state) - intermediate_state_root, _ = ( - block_state.pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + intermediate_state_root = block_state.pre_state.compute_state_root( + block_diff ) receipt = make_receipt( diff --git a/src/ethereum/forks/spurious_dragon/state_tracker.py b/src/ethereum/forks/spurious_dragon/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/spurious_dragon/state_tracker.py +++ b/src/ethereum/forks/spurious_dragon/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/forks/tangerine_whistle/blocks.py b/src/ethereum/forks/tangerine_whistle/blocks.py index ddc52041b03..e97790708a8 100644 --- a/src/ethereum/forks/tangerine_whistle/blocks.py +++ b/src/ethereum/forks/tangerine_whistle/blocks.py @@ -62,12 +62,12 @@ class Header: Root hash ([`keccak256`]) of the state trie after executing all transactions in this block. It represents the state of the Ethereum Virtual Machine (EVM) after all transactions in this block have been processed. It - is computed using [`compute_state_root_and_trie_changes()`][changes], + is computed using [`compute_state_root()`][changes], which computes the root of the Merkle-Patricia [Trie] representing the Ethereum world state after applying the block's state changes. [`keccak256`]: ref:ethereum.crypto.hash.keccak256 - [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [changes]: ref:ethereum.state_mpt.State.compute_state_root [Trie]: ref:ethereum.merkle_patricia_trie.Trie """ # noqa: E501 diff --git a/src/ethereum/forks/tangerine_whistle/fork.py b/src/ethereum/forks/tangerine_whistle/fork.py index cf0a29d826f..64bb280734a 100644 --- a/src/ethereum/forks/tangerine_whistle/fork.py +++ b/src/ethereum/forks/tangerine_whistle/fork.py @@ -28,12 +28,8 @@ NonceMismatchError, ) from ethereum.merkle_patricia_trie import root, trie_set -from ethereum.state import ( - EMPTY_CODE_HASH, - Address, - State, - apply_changes_to_state, -) +from ethereum.state import EMPTY_CODE_HASH, Address +from ethereum.state_mpt import State, apply_changes_to_state from . import vm from .blocks import Block, Header, Log, Receipt @@ -185,11 +181,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ommers=block.ommers, ) block_diff = extract_block_diff(block_state) - block_state_root, _ = chain.state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + block_state_root = chain.state.compute_state_root(block_diff) transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -689,12 +681,8 @@ def process_transaction( block_state = block_env.state block_diff = extract_block_diff(block_state) - intermediate_state_root, _ = ( - block_state.pre_state.compute_state_root_and_trie_changes( - block_diff.account_changes, - block_diff.storage_changes, - block_diff.storage_clears, - ) + intermediate_state_root = block_state.pre_state.compute_state_root( + block_diff ) receipt = make_receipt( diff --git a/src/ethereum/forks/tangerine_whistle/state_tracker.py b/src/ethereum/forks/tangerine_whistle/state_tracker.py index dd7a3c3bb8c..d7a607e6436 100644 --- a/src/ethereum/forks/tangerine_whistle/state_tracker.py +++ b/src/ethereum/forks/tangerine_whistle/state_tracker.py @@ -3,7 +3,7 @@ Track state changes on top of a read-only ``PreState``. At block end, accumulated diffs feed into -``PreState.compute_state_root_and_trie_changes()``. +``PreState.compute_state_root()``. .. contents:: Table of Contents :backlinks: none diff --git a/src/ethereum/merkle_patricia_trie.py b/src/ethereum/merkle_patricia_trie.py index cb088793454..76b26896fc5 100644 --- a/src/ethereum/merkle_patricia_trie.py +++ b/src/ethereum/merkle_patricia_trie.py @@ -44,7 +44,6 @@ import copy from dataclasses import dataclass, field from typing import ( - TYPE_CHECKING, Callable, Dict, Generic, @@ -65,16 +64,11 @@ from ethereum_types.numeric import Uint from typing_extensions import assert_type -from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.crypto.hash import keccak256 +from ethereum.state import Account, Address, Root from ethereum.utils.hexadecimal import hex_to_bytes -if TYPE_CHECKING: - from ethereum.state import Account, Address, Root - -# Note: `Hash32` is used here rather than `Root` because `Root` is defined in -# `ethereum.state`, which imports from this module — referring to it at module -# scope would create a circular import. -EMPTY_TRIE_ROOT = Hash32( +EMPTY_TRIE_ROOT = Root( hex_to_bytes( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ) @@ -266,8 +260,6 @@ def encode_node(node: Extended, storage_root: Bytes | None = None) -> Bytes: [`Account`]: ref:ethereum.state.Account [`encode_account`]: ref:ethereum.merkle_patricia_trie.encode_account """ - from ethereum.state import Account - if isinstance(node, Account): assert storage_root is not None return encode_account(node, storage_root) @@ -432,8 +424,6 @@ def _prepare_trie( [bnl]: ref:ethereum.merkle_patricia_trie.bytes_to_nibble_list [`keccak256`]: ref:ethereum.crypto.hash.keccak256 """ - from ethereum.state import Account, Address - mapped: MutableMapping[Bytes, Bytes] = {} for preimage, value in trie._data.items(): @@ -477,8 +467,6 @@ def root( [`Hash32`]: ref:ethereum.crypto.hash.Hash32 [`Account`]: ref:ethereum.state.Account """ - from ethereum.state import Root - obj = _prepare_trie(trie, get_storage_root) root_node = encode_internal_node(patricialize(obj, Uint(0))) diff --git a/src/ethereum/state.py b/src/ethereum/state.py index 5a34472390e..e7903318c83 100644 --- a/src/ethereum/state.py +++ b/src/ethereum/state.py @@ -1,27 +1,26 @@ """ -Shared state types and the `PreState` protocol used by the state transition -function. +Shared state model and the `PreState` protocol used by the state +transition function. -The `PreState` protocol specifies the operations that any pre-execution state -provider must support, allowing multiple backing implementations (in-memory -`dict`, on-disk database, witness, etc.). - -The `State` class is the in-memory implementation of `PreState`. It consists -of a main account trie and storage tries for each contract. +The `PreState` protocol specifies the operations that any +pre-execution state provider must support, allowing multiple backing +implementations (in-memory `dict`, on-disk database, witness, etc.). +This module is commitment-agnostic: it defines what state *is*, not +how it is committed to. The Merkle-Patricia-Trie-backed in-memory +implementation lives in [`ethereum.state_mpt`]. There is a distinction between an account that does not exist and `EMPTY_ACCOUNT`. + +[`ethereum.state_mpt`]: ref:ethereum.state_mpt """ from dataclasses import dataclass, field from typing import ( - AbstractSet, Dict, - List, Optional, Protocol, Set, - Tuple, final, ) @@ -30,15 +29,6 @@ from ethereum_types.numeric import U256, Uint from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.merkle_patricia_trie import ( - EMPTY_TRIE_ROOT, - InternalNode, - Trie, - copy_trie, - root, - trie_get, - trie_set, -) Address = Bytes20 Root = Hash32 @@ -73,13 +63,17 @@ class BlockDiff: State changes produced by executing a block. """ - account_changes: Dict[Address, Optional[Account]] + account_changes: Dict[Address, Optional[Account]] = field( + default_factory=dict + ) """Per-address account diffs produced by execution.""" - storage_changes: Dict[Address, Dict[Bytes32, U256]] + storage_changes: Dict[Address, Dict[Bytes32, U256]] = field( + default_factory=dict + ) """Per-address storage diffs produced by execution.""" - code_changes: Dict[Hash32, Bytes] + code_changes: Dict[Hash32, Bytes] = field(default_factory=dict) """New bytecodes (keyed by code hash) introduced by execution.""" storage_clears: Set[Address] = field(default_factory=set) @@ -133,218 +127,19 @@ def account_has_storage(self, address: Address) -> bool: """ ... - def compute_state_root_and_trie_changes( - self, - account_changes: Dict[Address, Optional[Account]], - storage_changes: Dict[Address, Dict[Bytes32, U256]], - storage_clears: AbstractSet[Address] = frozenset(), - ) -> Tuple[Root, List["InternalNode"]]: + def compute_state_root(self, block_diff: BlockDiff) -> Root: """ - Compute the state root after applying changes to the pre-state. + Compute the state root after applying `block_diff` to the + pre-state. The pre-state itself is not modified. - ``storage_clears`` lists addresses whose pre-existing storage - tries must be dropped before ``storage_changes`` is applied, so - any post-wipe writes begin from empty storage. + The diff carries bytecode deployed during the block in + ``code_changes``, keyed by code hash. Commitments over code + hashes alone can ignore it; a commitment over code contents + resolves each account's bytecode through its ``code_hash``, + joining ``account_changes`` to ``code_changes``, because the + new bytecode is not yet in the provider's code store when the + root is computed. - Return the new state root together with the internal trie nodes - that were created or modified. + Return the new state root. """ ... - - -@final -@dataclass -class State: - """ - Contains all information that is preserved between transactions. - """ - - _main_trie: Trie[Address, Optional[Account]] = field( - default_factory=lambda: Trie(secured=True, default=None) - ) - _storage_tries: Dict[Address, Trie[Bytes32, U256]] = field( - default_factory=dict - ) - _code_store: Dict[Hash32, Bytes] = field( - default_factory=dict, compare=False - ) - - def get_code(self, code_hash: Hash32) -> Bytes: - """ - Get the bytecode for a given code hash. - - Return ``b""`` for ``EMPTY_CODE_HASH``. - """ - if code_hash == EMPTY_CODE_HASH: - return b"" - return self._code_store[code_hash] - - def get_account_optional(self, address: Address) -> Optional[Account]: - """ - Get the account at an address. - - Return ``None`` if there is no account at the address. - """ - return trie_get(self._main_trie, address) - - def get_storage(self, address: Address, key: Bytes32) -> U256: - """ - Get a storage value. - - Return ``U256(0)`` if the key has not been set. - """ - trie = self._storage_tries.get(address) - if trie is None: - return U256(0) - - value = trie_get(trie, key) - - assert isinstance(value, U256) - return value - - def account_has_storage(self, address: Address) -> bool: - """ - Check whether an account has any storage. - - Only needed for EIP-7610. - """ - return address in self._storage_tries - - def compute_state_root_and_trie_changes( - self, - account_changes: Dict[Address, Optional[Account]], - storage_changes: Dict[Address, Dict[Bytes32, U256]], - storage_clears: AbstractSet[Address] = frozenset(), - ) -> Tuple[Root, List["InternalNode"]]: - """ - Compute the state root after applying changes to the pre-state. - - ``storage_clears`` lists addresses whose pre-existing storage - tries are dropped before ``storage_changes`` is applied, so any - post-wipe writes begin from empty storage. - - Return the new state root together with the internal trie nodes - that were created or modified. - """ - main_trie = copy_trie(self._main_trie) - storage_tries = { - k: copy_trie(v) - for k, v in self._storage_tries.items() - if k not in storage_clears - } - - for address, account in account_changes.items(): - trie_set(main_trie, address, account) - - for address, slots in storage_changes.items(): - trie = storage_tries.get(address) - if trie is None: - trie = Trie(secured=True, default=U256(0)) - storage_tries[address] = trie - for key, value in slots.items(): - trie_set(trie, key, value) - if trie._data == {}: - del storage_tries[address] - - def get_storage_root(addr: Address) -> Root: - if addr in storage_tries: - return root(storage_tries[addr]) - return EMPTY_TRIE_ROOT - - state_root_value = root(main_trie, get_storage_root=get_storage_root) - - return state_root_value, [] - - -def close_state(state: State) -> None: - """ - Free resources held by the state. Used by optimized implementations to - release file descriptors. - """ - del state._main_trie - del state._storage_tries - del state._code_store - - -def apply_changes_to_state(state: State, diff: BlockDiff) -> None: - """ - Apply block-level diff to the ``State`` for the next block. - - Parameters - ---------- - state : - The state to update. - diff : - Account, storage, and code changes to apply. - - """ - for address in diff.storage_clears: - state._storage_tries.pop(address, None) - - for address, account in diff.account_changes.items(): - trie_set(state._main_trie, address, account) - - for address, slots in diff.storage_changes.items(): - trie = state._storage_tries.get(address) - if trie is None: - trie = Trie(secured=True, default=U256(0)) - state._storage_tries[address] = trie - for key, value in slots.items(): - trie_set(trie, key, value) - if trie._data == {}: - del state._storage_tries[address] - - state._code_store.update(diff.code_changes) - - -def store_code(state: State, code: Bytes) -> Hash32: - """ - Store bytecode in ``State``. - """ - code_hash = keccak256(code) - if code_hash != EMPTY_CODE_HASH: - state._code_store[code_hash] = code - return code_hash - - -def set_account( - state: State, - address: Address, - account: Optional[Account], -) -> None: - """ - Set an account in a ``State``. - - Setting to ``None`` deletes the account. - """ - trie_set(state._main_trie, address, account) - - -def set_storage( - state: State, - address: Address, - key: Bytes32, - value: U256, -) -> None: - """ - Set a storage value in a ``State``. - - Setting to ``U256(0)`` deletes the key. - """ - assert trie_get(state._main_trie, address) is not None - - trie = state._storage_tries.get(address) - if trie is None: - trie = Trie(secured=True, default=U256(0)) - state._storage_tries[address] = trie - trie_set(trie, key, value) - if trie._data == {}: - del state._storage_tries[address] - - -def state_root(state: State) -> Root: - """ - Compute the state root of the current state. - """ - root_value, _ = state.compute_state_root_and_trie_changes({}, {}) - return root_value diff --git a/src/ethereum/state_mpt.py b/src/ethereum/state_mpt.py new file mode 100644 index 00000000000..dbdd7a718e0 --- /dev/null +++ b/src/ethereum/state_mpt.py @@ -0,0 +1,221 @@ +""" +Merkle-Patricia-Trie-backed implementation of the shared state model. + +The [`State`] class here is the in-memory implementation of the +[`PreState`] protocol used on Ethereum mainnet: accounts and storage +live in Merkle Patricia Tries and the state root is the MPT +commitment. Other providers, such as databases, witnesses, or other +commitment schemes, are separate implementations of [`PreState`]. + +[`State`]: ref:ethereum.state_mpt.State +[`PreState`]: ref:ethereum.state.PreState +""" + +from dataclasses import dataclass, field +from typing import Dict, Optional, final + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U256 + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.merkle_patricia_trie import ( + EMPTY_TRIE_ROOT, + Trie, + copy_trie, + root, + trie_get, + trie_set, +) +from ethereum.state import EMPTY_CODE_HASH, Account, Address, BlockDiff, Root + + +@final +@dataclass +class State: + """ + Contains all information that is preserved between transactions. + """ + + _main_trie: Trie[Address, Optional[Account]] = field( + default_factory=lambda: Trie(secured=True, default=None) + ) + _storage_tries: Dict[Address, Trie[Bytes32, U256]] = field( + default_factory=dict + ) + _code_store: Dict[Hash32, Bytes] = field( + default_factory=dict, compare=False + ) + + def get_code(self, code_hash: Hash32) -> Bytes: + """ + Get the bytecode for a given code hash. + + Return ``b""`` for ``EMPTY_CODE_HASH``. + """ + if code_hash == EMPTY_CODE_HASH: + return b"" + return self._code_store[code_hash] + + def get_account_optional(self, address: Address) -> Optional[Account]: + """ + Get the account at an address. + + Return ``None`` if there is no account at the address. + """ + return trie_get(self._main_trie, address) + + def get_storage(self, address: Address, key: Bytes32) -> U256: + """ + Get a storage value. + + Return ``U256(0)`` if the key has not been set. + """ + trie = self._storage_tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + def account_has_storage(self, address: Address) -> bool: + """ + Check whether an account has any storage. + + Only needed for EIP-7610. + """ + return address in self._storage_tries + + def compute_state_root(self, block_diff: BlockDiff) -> Root: + """ + Compute the state root after applying `block_diff` to the + pre-state. The pre-state itself is not modified. + + The diff's ``code_changes`` play no part: the Merkle Patricia + Trie commits to accounts' code hashes, never to code + contents, so account diffs alone determine the root. + + Return the new state root. + """ + main_trie = copy_trie(self._main_trie) + storage_tries = { + k: copy_trie(v) + for k, v in self._storage_tries.items() + if k not in block_diff.storage_clears + } + + for address, account in block_diff.account_changes.items(): + trie_set(main_trie, address, account) + + for address, slots in block_diff.storage_changes.items(): + trie = storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + storage_tries[address] = trie + for key, value in slots.items(): + trie_set(trie, key, value) + if trie._data == {}: + del storage_tries[address] + + def get_storage_root(addr: Address) -> Root: + if addr in storage_tries: + return root(storage_tries[addr]) + return EMPTY_TRIE_ROOT + + state_root_value = root(main_trie, get_storage_root=get_storage_root) + + return state_root_value + + +def close_state(state: State) -> None: + """ + Free resources held by the state. Used by optimized implementations to + release file descriptors. + """ + del state._main_trie + del state._storage_tries + del state._code_store + + +def apply_changes_to_state(state: State, diff: BlockDiff) -> None: + """ + Apply block-level diff to the ``State`` for the next block. + + Parameters + ---------- + state : + The state to update. + diff : + Account, storage, and code changes to apply. + + """ + for address in diff.storage_clears: + state._storage_tries.pop(address, None) + + for address, account in diff.account_changes.items(): + trie_set(state._main_trie, address, account) + + for address, slots in diff.storage_changes.items(): + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + for key, value in slots.items(): + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + state._code_store.update(diff.code_changes) + + +def store_code(state: State, code: Bytes) -> Hash32: + """ + Store bytecode in ``State``. + """ + code_hash = keccak256(code) + if code_hash != EMPTY_CODE_HASH: + state._code_store[code_hash] = code + return code_hash + + +def set_account( + state: State, + address: Address, + account: Optional[Account], +) -> None: + """ + Set an account in a ``State``. + + Setting to ``None`` deletes the account. + """ + trie_set(state._main_trie, address, account) + + +def set_storage( + state: State, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a storage value in a ``State``. + + Setting to ``U256(0)`` deletes the key. + """ + assert trie_get(state._main_trie, address) is not None + + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + +def state_root(state: State) -> Root: + """ + Compute the state root of the current state. + """ + return state.compute_state_root(BlockDiff()) diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py index 3a93f850fbd..f4bf3aa4e5b 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -12,7 +12,6 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.exceptions import StateWithEmptyAccount -from ethereum.state import State, set_account, set_storage, store_code from ethereum.utils.hexadecimal import ( hex_to_bytes, hex_to_bytes8, @@ -60,7 +59,8 @@ def __init__(self, fork_module: str | Hardfork): def json_to_state(self, raw: Any) -> Any: """Converts json state data to a state object.""" - state = State() + provider = self.fork.state_provider + state = provider.State() EMPTY_ACCOUNT = self.fork.EMPTY_ACCOUNT # noqa N806 for address_hex, account_state in raw.items(): @@ -69,7 +69,7 @@ def json_to_state(self, raw: Any) -> Any: balance = U256(hex_to_uint(account_state.get("balance", "0x0"))) code = hex_to_bytes(account_state.get("code", "")) - code_hash = store_code(state, code) + code_hash = provider.store_code(state, code) account = self.fork.Account( nonce=nonce, balance=balance, @@ -79,10 +79,10 @@ def json_to_state(self, raw: Any) -> Any: if self.fork.proof_of_stake and account == EMPTY_ACCOUNT: raise StateWithEmptyAccount(f"Empty account at {address_hex}.") - set_account(state, address, account) + provider.set_account(state, address, account) for k, v in account_state.get("storage", {}).items(): - set_storage( + provider.set_storage( state, address, hex_to_bytes32(k), diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index fda1bac008b..eebd406f7c7 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -2,6 +2,7 @@ Loader for code from the relevant fork. """ +from importlib import import_module from inspect import signature from typing import Any, Final @@ -303,6 +304,17 @@ def decode_transaction(self) -> Any: """decode_transaction function of the fork.""" return self._module("transactions").decode_transaction + @property + def state_provider(self) -> Any: + """ + Module implementing the fork's state provider. + + Resolved through the ``State`` class the fork's ``fork`` + module imports, so each fork selects its own commitment + scheme (``ethereum.state_mpt``, ``ethereum.state_pbt``, ...). + """ + return import_module(self._module("fork").State.__module__) + @property def BlockState(self) -> Any: """BlockState class of the fork.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/result.py b/src/ethereum_spec_tools/evm_tools/t8n/result.py index 8e434fdd412..5bab2af3b75 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/result.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/result.py @@ -79,9 +79,7 @@ def build_result( from execution_testing.client_clis.cli_types import Result as TestingResult diff = t8n.fork.extract_block_diff(t8n._block_state) - state_root, _ = t8n.alloc.compute_state_root_and_trie_changes( - diff.account_changes, diff.storage_changes, diff.storage_clears - ) + state_root = t8n.alloc.compute_state_root(diff) arguments: Dict[str, Any] = { "state_root": state_root, diff --git a/tests/json_loader/helpers/load_blockchain_tests.py b/tests/json_loader/helpers/load_blockchain_tests.py index d4d21a43012..feec8e44b0f 100644 --- a/tests/json_loader/helpers/load_blockchain_tests.py +++ b/tests/json_loader/helpers/load_blockchain_tests.py @@ -12,7 +12,7 @@ from ethereum.crypto.hash import keccak256 from ethereum.exceptions import EthereumException, StateWithEmptyAccount -from ethereum.state import close_state +from ethereum.state_mpt import close_state from ethereum.utils.hexadecimal import hex_to_bytes from ethereum_spec_tools.evm_tools.loaders.fixture_loader import Load diff --git a/tests/json_loader/helpers/load_vm_tests.py b/tests/json_loader/helpers/load_vm_tests.py index 73660b42a05..d0acecbab25 100644 --- a/tests/json_loader/helpers/load_vm_tests.py +++ b/tests/json_loader/helpers/load_vm_tests.py @@ -92,9 +92,9 @@ def _state_module(self) -> Any: try: return self._module("state") except ModuleNotFoundError: - import ethereum.state + import ethereum.state_mpt - return ethereum.state + return ethereum.state_mpt def run_test_from_dict(self, json_data: Dict[str, Any]) -> None: """ diff --git a/tests/json_loader/test_genesis.py b/tests/json_loader/test_genesis.py index 5fb45fcd887..a90210fd1ff 100644 --- a/tests/json_loader/test_genesis.py +++ b/tests/json_loader/test_genesis.py @@ -15,8 +15,8 @@ get_genesis_configuration, ) from ethereum.merkle_patricia_trie import Trie, root -from ethereum.state import ( - Address, +from ethereum.state import Address +from ethereum.state_mpt import ( State, set_account, set_storage, @@ -68,11 +68,10 @@ def fork_name(fork: Hardfork) -> str: def test_genesis(fork: Hardfork) -> None: """Tests genesis block creation for all hardforks.""" # TODO: remove once the changes have been back-ported - from ethereum.merkle_patricia_trie import Trie - from ethereum.state import ( - Address, + from ethereum.merkle_patricia_trie import Trie, root + from ethereum.state import Address + from ethereum.state_mpt import ( State, - root, set_account, set_storage, state_root, diff --git a/tests/json_loader/test_optimized_state.py b/tests/json_loader/test_optimized_state.py index 28763cee324..c910a99835b 100644 --- a/tests/json_loader/test_optimized_state.py +++ b/tests/json_loader/test_optimized_state.py @@ -5,7 +5,7 @@ import pytest from ethereum_types.numeric import U256 -import ethereum.state as state +import ethereum.state_mpt as state from ethereum.forks.tangerine_whistle.utils.hexadecimal import hex_to_address from ethereum.state import EMPTY_ACCOUNT from ethereum_spec_tools.forks import Hardfork From d5d230ca57ead2a208e0f9241b7a97793fffb72f Mon Sep 17 00:00:00 2001 From: Kumarutkarsh9470 Date: Mon, 27 Jul 2026 23:17:37 +0530 Subject: [PATCH 14/55] bug(spec-tools): make t8n daemon module importable on Windows (#3212) `daemon.py` defined `_UnixSocketHttpServer` by subclassing `socketserver.UnixStreamServer`, which does not exist on Windows. Since `ethereum_spec_tools.evm_tools` imports this module at load time, importing the tooling (and therefore collecting the test suite) crashed on Windows with `AttributeError`. Select the base class per platform so the module stays importable everywhere, and reject running the daemon on Windows with a clear error, as it inherently relies on Unix domain sockets. Add a regression test for the platform guard. Co-authored-by: Claude Fable 5 --- src/ethereum_spec_tools/evm_tools/daemon.py | 18 +++++++++++++++++- tests/evm_tools/test_daemon.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/evm_tools/test_daemon.py diff --git a/src/ethereum_spec_tools/evm_tools/daemon.py b/src/ethereum_spec_tools/evm_tools/daemon.py index 39268f04df2..4ee58fc6872 100644 --- a/src/ethereum_spec_tools/evm_tools/daemon.py +++ b/src/ethereum_spec_tools/evm_tools/daemon.py @@ -6,6 +6,7 @@ import json import os.path import socketserver +import sys import time from http.server import BaseHTTPRequestHandler from io import StringIO, TextIOWrapper @@ -116,7 +117,17 @@ def do_POST(self) -> None: # noqa N802 main(args=args, out_file=out_wrapper, in_file=input) -class _UnixSocketHttpServer(socketserver.UnixStreamServer): +if sys.platform == "win32": + # Windows has no Unix domain sockets, so ``socketserver.UnixStreamServer`` + # is undefined there. The daemon cannot run on Windows, but this module + # must stay importable (``Daemon.run`` rejects the platform explicitly), + # so fall back to a base class that exists everywhere. + _UnixStreamServerBase = socketserver.TCPServer +else: + _UnixStreamServerBase = socketserver.UnixStreamServer + + +class _UnixSocketHttpServer(_UnixStreamServerBase): last_response: float shutdown_timeout: int @@ -179,6 +190,11 @@ def __init__(self, options: argparse.Namespace) -> None: self.timeout = options.timeout def _run(self) -> int: + if sys.platform == "win32": + raise RuntimeError( + "The t8n daemon relies on Unix domain sockets, which are " + "not available on Windows." + ) try: os.remove(self.uds) except IOError: diff --git a/tests/evm_tools/test_daemon.py b/tests/evm_tools/test_daemon.py new file mode 100644 index 00000000000..6d4d08b74c3 --- /dev/null +++ b/tests/evm_tools/test_daemon.py @@ -0,0 +1,18 @@ +"""Test platform handling in the t8n daemon.""" + +import argparse + +import pytest + +from ethereum_spec_tools.evm_tools import daemon +from ethereum_spec_tools.evm_tools.daemon import Daemon + + +def test_daemon_run_rejects_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """`Daemon.run` fails clearly on Windows, which has no Unix sockets.""" + monkeypatch.setattr(daemon.sys, "platform", "win32") + instance = Daemon(argparse.Namespace(uds="daemon.sock", timeout=0)) + with pytest.raises(RuntimeError, match="Unix domain sockets"): + instance.run() From 3d3afa6cce22956c806f5d0178d8dd564ffe21b3 Mon Sep 17 00:00:00 2001 From: Kumarutkarsh9470 Date: Mon, 27 Jul 2026 23:32:07 +0530 Subject: [PATCH 15/55] bug(test-eest): reconfigure output streams to UTF-8 on Windows consoles (#3209) The `eest` commands print Unicode characters (box drawing in `info`, emoji in `clean` and `make`) via `click.echo`. On a Windows console using a legacy code page such as `cp1252`, these characters cannot be encoded and the command aborts with `UnicodeEncodeError`. Reconfigure `sys.stdout`/`sys.stderr` to UTF-8 in the `eest` group callback, guarded so streams that do not support reconfiguration (for example captured output under tests) are left untouched. Add regression tests covering a legacy-encoded stdout. Co-authored-by: Claude Fable 5 --- .../src/execution_testing/cli/eest/cli.py | 24 +++++++++- .../cli/eest/tests/__init__.py | 1 + .../cli/eest/tests/test_cli.py | 47 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/testing/src/execution_testing/cli/eest/tests/__init__.py create mode 100644 packages/testing/src/execution_testing/cli/eest/tests/test_cli.py diff --git a/packages/testing/src/execution_testing/cli/eest/cli.py b/packages/testing/src/execution_testing/cli/eest/cli.py index b93469a5cb1..70b5cdc2889 100644 --- a/packages/testing/src/execution_testing/cli/eest/cli.py +++ b/packages/testing/src/execution_testing/cli/eest/cli.py @@ -3,12 +3,34 @@ Invoke using `uv run eest`. """ +import sys + import click from .commands import clean, info from .make.cli import make +def ensure_utf8_output() -> None: + """ + Reconfigure the standard streams to UTF-8 so output cannot crash. + + The `eest` commands print Unicode characters (box drawing, emoji) + that a legacy console code page such as Windows `cp1252` cannot + encode, otherwise raising `UnicodeEncodeError` mid-command. Streams + that do not support reconfiguration (for example when output is + captured in tests) are left untouched. + """ + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is None: + continue + try: + reconfigure(encoding="utf-8") + except (OSError, ValueError): + pass + + @click.group( context_settings={ "help_option_names": ["-h", "--help"], @@ -17,7 +39,7 @@ ) def eest() -> None: """`eest` is a CLI tool that helps with routine tasks.""" - pass + ensure_utf8_output() """ diff --git a/packages/testing/src/execution_testing/cli/eest/tests/__init__.py b/packages/testing/src/execution_testing/cli/eest/tests/__init__.py new file mode 100644 index 00000000000..a3645c910f2 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/eest/tests/__init__.py @@ -0,0 +1 @@ +"""Test cases for the `eest` CLI group.""" diff --git a/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py b/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py new file mode 100644 index 00000000000..90d3ec80412 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py @@ -0,0 +1,47 @@ +"""Tests for the `eest` CLI group.""" + +import io +import sys + +import pytest +from click.testing import CliRunner + +from ..cli import eest, ensure_utf8_output + + +def test_info_runs_successfully() -> None: + """`eest info` exits cleanly and reports the EEST banner.""" + result = CliRunner().invoke(eest, ["info"]) + assert result.exit_code == 0 + assert "EEST" in result.output + + +def test_info_survives_legacy_console_encoding( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """ + `eest info` must not crash on a non-UTF-8 console code page. + + Regression test for the Windows `cp1252` console, whose codec + cannot encode the box-drawing characters printed by the command. + """ + stream = io.TextIOWrapper(io.BytesIO(), encoding="cp1252") + monkeypatch.setattr(sys, "stdout", stream) + + # Without the UTF-8 reconfiguration this raises UnicodeEncodeError. + eest.main(["info"], standalone_mode=False) + + stream.flush() + assert "EEST" in stream.buffer.getvalue().decode("utf-8") + + +def test_ensure_utf8_output_reconfigures_stream( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """`ensure_utf8_output` switches a legacy stream to UTF-8.""" + stream = io.TextIOWrapper(io.BytesIO(), encoding="cp1252") + monkeypatch.setattr(sys, "stdout", stream) + + ensure_utf8_output() + + assert stream.encoding.lower() == "utf-8" From 36fbbabeef91eccc47459e954f12c62f77e44c16 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 28 Jul 2026 06:28:14 +0800 Subject: [PATCH 16/55] fix(test-specs): parenthesize walrus when counting failing txs (#3190) Without parentheses, `:=` binds after `>`, so failing_tx_count became a bool and the multi-failure check never fired. --- packages/testing/src/execution_testing/specs/blockchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 96436819460..72145a35e4f 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -875,7 +875,7 @@ def generate_block_data( ] txs = [tx.with_signature_and_sender() for tx in txs] - if failing_tx_count := len([tx for tx in txs if tx.error]) > 0: + if (failing_tx_count := len([tx for tx in txs if tx.error])) > 0: if failing_tx_count > 1: raise Exception( "test correctness: only one transaction can produce " From 85a36ccae03b0958d9bfb0a6e6d9e08f0e5c79db Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 28 Jul 2026 06:41:34 +0800 Subject: [PATCH 17/55] fix(test-specs): use integer division for genesis base fee (#3189) Avoid float64 (53-bit mantissa) precision loss when scaling base_fee_per_gas by 8/7 for values beyond 2^53. --- packages/testing/src/execution_testing/specs/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/specs/state.py b/packages/testing/src/execution_testing/specs/state.py index ac050f0c9ba..f5c0cea7a19 100644 --- a/packages/testing/src/execution_testing/specs/state.py +++ b/packages/testing/src/execution_testing/specs/state.py @@ -280,7 +280,7 @@ def _generate_blockchain_genesis_environment(self) -> Environment: if self.env.base_fee_per_gas: # Calculate genesis base fee per gas from state test's block#1 env kwargs["base_fee_per_gas"] = HexNumber( - int(int(str(self.env.base_fee_per_gas), 0) * 8 / 7) + int(str(self.env.base_fee_per_gas), 0) * 8 // 7 ) if self.env.excess_blob_gas: From c69d54ba274a222412a0ee897cccf1af6a4e8671 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Tue, 28 Jul 2026 10:59:44 +0200 Subject: [PATCH 18/55] fix(test-ci): Skip `test_cli.py` until #3241 is resolved (#3242) --- .../testing/src/execution_testing/cli/eest/tests/test_cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py b/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py index 90d3ec80412..22dee2deb9a 100644 --- a/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py +++ b/packages/testing/src/execution_testing/cli/eest/tests/test_cli.py @@ -8,6 +8,10 @@ from ..cli import eest, ensure_utf8_output +pytestmark = pytest.mark.skip( + "Issue #3241: eest info queries github.com to get release information" +) + def test_info_runs_successfully() -> None: """`eest info` exits cleanly and reports the EEST banner.""" From 608f8783af569bd2833e90c42eb617439045412d Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 28 Jul 2026 16:32:50 +0200 Subject: [PATCH 19/55] feat(tests): add EIP-7997 case where factory is not present at fork block (#3243) --- .../test_fork_transition.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py index b124a0c5884..8f3d20b983b 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py @@ -104,3 +104,71 @@ def test_factory_deploys_across_transition( ), }, ) + + +@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.pre_alloc_mutable +def test_factory_absent_across_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + A chain that never deployed the factory transitions to Amsterdam + through valid blocks, and the factory address stays nonexistent. + + The client MUST NOT check for the existence of the contract at + the fork boundary. Therefore, we verify that the BAL does + not contain the factory account read. + The block itself is valid. It is the responsibility of the + chain activating EIP-7997 to ensure the factory is valid + at the start of the fork block. + """ + factory = Address(Spec.FACTORY_ADDRESS) + # Merging an all-zero account into the fork's pre-allocation removes + # the factory predeploy from the genesis allocation entirely. + pre[factory] = Account(nonce=0, balance=0, code=b"") + + sender = pre.fund_eoa() + receiver = pre.fund_eoa(amount=0) + transfer_value = 1 + + timestamps = [FORK_TIMESTAMP - 1, FORK_TIMESTAMP, FORK_TIMESTAMP + 1] + + blocks = [] + for i, timestamp in enumerate(timestamps): + blocks.append( + Block( + timestamp=timestamp, + txs=[ + Transaction( + sender=sender, + to=receiver, + value=transfer_value, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: None, + sender: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=1, + post_nonce=i + 1, + ) + ], + ), + } + ) + if timestamp >= FORK_TIMESTAMP + else None, + ) + ) + + blockchain_test( + pre=pre, + blocks=blocks, + post={ + factory: Account.NONEXISTENT, + receiver: Account(balance=len(timestamps) * transfer_value), + }, + ) From 3850ce51d490d7e4a37a517b16a8e16f40ee0bf4 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Tue, 28 Jul 2026 09:22:35 -0600 Subject: [PATCH 20/55] refactor(tests): Remove `fork.gas_costs()` reconstruction from Amsterdam EIP-8037/8038 tests (#3169) Co-authored-by: spencer-tb --- .claude/commands/write-test.md | 16 +- docs/writing_tests/fork_methods.md | 9 +- docs/writing_tests/opcode_metadata.md | 15 + .../src/execution_testing/forks/base_fork.py | 14 + .../test_block_2d_gas_accounting.py | 15 +- .../test_state_gas_call.py | 329 ++++++++------- .../test_state_gas_calldata_floor.py | 42 +- .../test_state_gas_create.py | 376 +++++++----------- .../test_state_gas_delegation_pointer.py | 18 +- .../test_state_gas_multi_block.py | 30 +- .../test_state_gas_ordering.py | 100 ++--- .../test_state_gas_pricing.py | 22 +- .../test_state_gas_reservoir.py | 15 +- .../test_state_gas_selfdestruct.py | 68 ++-- .../test_state_gas_set_code.py | 1 - .../test_state_gas_sstore.py | 47 +-- .../test_access_list_gas.py | 56 +-- .../test_call_gas.py | 156 +++----- .../test_create_gas.py | 67 +--- .../test_eip_mainnet.py | 2 +- .../test_ext_code_opcodes_gas.py | 40 +- .../test_fork_transition.py | 66 +-- .../test_selfdestruct_gas.py | 69 +--- .../test_set_code_auth_gas.py | 62 ++- .../test_sstore_gas.py | 4 +- .../test_sstore_refunds.py | 25 +- .../test_transient_storage_regression.py | 25 +- 27 files changed, 686 insertions(+), 1003 deletions(-) diff --git a/.claude/commands/write-test.md b/.claude/commands/write-test.md index 3895d81e0ef..bf6e8eee42a 100644 --- a/.claude/commands/write-test.md +++ b/.claude/commands/write-test.md @@ -43,8 +43,20 @@ Conventions and patterns for writing consensus tests. Run this skill before writ ## Fork-Aware Logic - `fork >= Cancun` for conditional behavior based on fork -- `fork.gas_costs()` returns `GasCosts` dataclass with constants like `G_WARM_SLOAD`, `G_COLD_ACCOUNT_ACCESS`, `G_BASE`, etc. -- `fork.transaction_intrinsic_cost_calculator()` for computing tx intrinsic gas +- `fork.fork_at(timestamp=...)` gives the fork active before/after a transition boundary +- For gas amounts, see **Gas Cost Expectations** below — prefer framework cost constructs over reading `fork.gas_costs()` constants directly + +## Gas Cost Expectations + +Never hand-reconstruct a gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule duplicates the framework's own calculation and silently breaks when a future fork reprices. Instead: + +- **Read the cost off the bytecode under test.** Set the relevant opcode metadata (`account_new`, `value_transfer`, `address_warm`, `key_warm`/`original_value`/`current_value`/`new_value`, `init_code_size`, `code_deposit_size`, `new_memory_size`, ...) and use `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior — e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`; `fork.transaction_top_frame_state_gas(contract_creation=True)` for the created account's `NEW_ACCOUNT` (under EIP-2780 it is NOT part of the intrinsic — never subtract it from the intrinsic); `fork.transaction_data_floor_cost_calculator()`; `fork.call_value_stipend()`. +- **A single bare opcode/schedule cost** (e.g. an account-access constant) comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)`. +- **Fork-transition / cross-fork comparisons:** evaluate the same bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare `before` vs `after` costs — do not compare raw schedule constants. +- **Do not add "self-check" asserts** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork; they add no coverage over the runtime behavior the test already exercises and only break on repricing. +- **If the framework cannot express a cost, fix the framework** (wire the opcode into its gas/state map, add an accessor) rather than reconstructing it in the test. If the use case does not support the framework, the framework needs an update. +- **Exception:** a test whose *subject* is a specific schedule value (e.g. a regression that an opcode's cost is unchanged) may compare a runtime measurement (`CodeGasMeasure`) against `fork.gas_costs().OPCODE_*`. Even then, never hardcode the literal value. ## Transactions diff --git a/docs/writing_tests/fork_methods.md b/docs/writing_tests/fork_methods.md index efb4c90d4e3..6f29d6f3a0c 100644 --- a/docs/writing_tests/fork_methods.md +++ b/docs/writing_tests/fork_methods.md @@ -38,11 +38,13 @@ def test_some_feature(fork): ```python def test_transaction_gas(fork, state_test): - gas_cost = fork.gas_costs().GAS_TX_BASE + # Derive the fork's intrinsic gas from the calculator rather than + # summing raw `gas_costs()` constants (see the Gas Parameters warning). + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() # Create a transaction with the correct gas parameters for this fork tx = Transaction( - gas_limit=gas_cost + 10000, + gas_limit=intrinsic_gas + 10000, # ... ) @@ -114,6 +116,9 @@ fork.memory_expansion_gas_calculator() # Returns a callable fork.transaction_intrinsic_cost_calculator() # Returns a callable ``` +!!! warning "Do not reconstruct expected gas from `gas_costs()` constants" + `fork.gas_costs()` exposes the raw schedule for framework internals. When a test needs an *expected* gas amount, derive it from a cost construct that tracks the live schedule (`bytecode.gas_cost(fork)` / `.regular_cost(fork)` / `.state_cost(fork)` / `.refund(fork)`, opcode metadata, the intrinsic/top-frame/data-floor calculators, `fork.call_value_stipend()`) rather than hand-summing constants — hand-built expectations silently break when a fork reprices. See [Opcode Metadata and Gas Calculations](opcode_metadata.md#do-not-hand-reconstruct-gas-from-constants). + ### Transaction Types Methods for determining valid transaction types: diff --git a/docs/writing_tests/opcode_metadata.md b/docs/writing_tests/opcode_metadata.md index e6530e42674..5149fb9ea0b 100644 --- a/docs/writing_tests/opcode_metadata.md +++ b/docs/writing_tests/opcode_metadata.md @@ -9,6 +9,21 @@ The execution testing package provides capabilities to calculate gas costs and r - Validating gas cost calculations for specific opcode scenarios - Future-proofing tests against breaking in upcoming forks that change gas rules +## Do Not Hand-Reconstruct Gas From Constants + +Never build an expected gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule by hand duplicates the framework's own calculation and silently breaks when a future fork reprices or restructures a cost. Always derive the expectation from a framework construct that tracks the live schedule: + +- **The bytecode/opcode under test:** set the relevant metadata (see below) and read `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior, e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **A single bare opcode/schedule cost** comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)` yields the cold account-access cost with no operand pushes. +- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`, `fork.transaction_top_frame_state_gas(contract_creation=True)` (the created account's new-account state gas — on recent forks it is charged at the top frame, *not* in the intrinsic, so never subtract it from the intrinsic), `fork.transaction_data_floor_cost_calculator()`, and `fork.call_value_stipend()`. +- **Cross-fork / fork-transition comparisons:** evaluate the *same* bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare the resulting costs — do not compare raw schedule constants. + +Additional rules: + +- **Do not add "self-check" assertions** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork. They add no coverage over the runtime behavior the test already exercises and only break on repricing. +- **If the framework cannot express a cost, extend the framework** (wire the opcode into its gas/state map, add an accessor) rather than working around it in the test. +- **Exception:** a test whose *subject* is a specific schedule value — for example a regression asserting that an opcode's cost is unchanged across a fork — may compare a runtime measurement (`CodeGasMeasure`) against the fork's declared `fork.gas_costs().OPCODE_*` value. Even then, never hardcode the literal number. + ## Opcode Metadata Many opcodes accept metadata parameters that affect their gas cost calculations. Metadata represents runtime state information that influences gas consumption. diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 911867deec8..f24bc228d77 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -826,6 +826,20 @@ def transaction_top_frame_state_gas( del contract_creation, sends_value, recipient_type, authorizations return 0 + @classmethod + def call_value_stipend(cls) -> int: + """ + Return the gas stipend forwarded to the callee of a value-bearing + CALL/CALLCODE. + + The stipend is added to the child frame's gas and returned to the + caller when the callee does not consume it, so tests that pin + value-call gas at an exact boundary subtract it from the charged + total. Exposed as a named accessor so tests need not read + ``gas_costs().CALL_STIPEND`` directly. + """ + return cls.gas_costs().CALL_STIPEND + @classmethod def system_call_gas_limit(cls) -> int: """ diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py index 42775006b8c..662af4980f2 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py @@ -368,24 +368,27 @@ def test_block_gas_used_call_new_account( GAS_NEW_ACCOUNT state gas) then SSTORE. Combined with a STOP tx, the 2D max must reflect state gas from account creation. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) target = pre.fund_eoa(amount=0) + call = Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) parent_storage = Storage() parent = pre.deploy_contract( - code=( - Op.CALL(gas=100_000, address=target, value=1) - + Op.SSTORE(parent_storage.store_next(1), 1) - ), + code=(call + Op.SSTORE(parent_storage.store_next(1), 1)), balance=10**18, ) txs = [ Transaction( to=parent, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=call.state_cost(fork) + sstore_state_gas, sender=pre.fund_eoa(), ), ] + stop_txs(pre, fork, 1) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py index 62f3a91eeca..170217fa7cf 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py @@ -433,26 +433,36 @@ def test_call_value_transfer_new_account( A CALL that transfers value to a non-existent account creates a new account, charging new-account state gas of state gas. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - # Target address that doesn't exist in pre-state - target = 0xDEAD + target = pre.nonexistent_account() parent_storage = Storage() - parent = pre.deploy_contract( - code=( - Op.SSTORE( - parent_storage.store_next(1), - Op.CALL(gas=100_000, address=target, value=1), - ) + # Capture the CALL result in a pre-existing slot (2 -> 1) so the + # instrumentation SSTORE modifies rather than creates a key and + # adds no state gas; the reservoir then covers exactly the CALL's + # new-account charge. + slot = parent_storage.store_next(1) + parent_code = Op.SSTORE( + slot, + Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, ), - balance=1, + original_value=2, + current_value=2, + new_value=1, + key_warm=False, + ) + parent = pre.deploy_contract( + code=parent_code, balance=1, storage={slot: 2} ) tx = Transaction( to=parent, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=parent_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -803,7 +813,6 @@ def test_call_pre_charged_costs_excluded_from_forwarding( pre-charged costs (access gas, memory expansion, or both) causes the child to OOG and the SSTORE to revert. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: SSTORE(0, 1) as proof of execution @@ -817,9 +826,9 @@ def test_call_pre_charged_costs_excluded_from_forwarding( ret_size = 512 * 32 # 512 words memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - extra_gas = gas_costs.COLD_ACCOUNT_ACCESS # cold call, value=0 - - # Wrapper: CALL child requesting max gas with memory expansion + # Wrapper: CALL child requesting max gas with memory expansion. The + # memory metadata makes `wrapper_code.regular_cost(fork)` fold the + # cold access, the 7 argument pushes and the memory expansion. wrapper_code = Op.CALL( gas=0xFFFFFFFF, address=child, @@ -828,17 +837,16 @@ def test_call_pre_charged_costs_excluded_from_forwarding( args_size=0, ret_offset=0, ret_size=ret_size, + new_memory_size=ret_size, ) wrapper = pre.deploy_contract(wrapper_code) - wrapper_pushes = 7 * gas_costs.VERY_LOW # 7 CALL args - - # After the pre-charge of extra_gas + memory_cost, the wrapper has - # gas_remaining left. The 63/64 rule should forward - # gas_remaining * 63/64 to the child — just enough for its SSTORE. + # After the up-front pre-charge, the wrapper has gas_remaining left. + # The 63/64 rule should forward gas_remaining * 63/64 to the child — + # just enough for its SSTORE. gas_remaining = child_regular_gas * 64 // 63 + memory_cost // 2 - wrapper_gas = wrapper_pushes + extra_gas + memory_cost + gas_remaining + wrapper_gas = wrapper_code.regular_cost(fork) + gas_remaining caller = pre.deploy_contract( Op.POP(Op.CALL(gas=wrapper_gas, address=wrapper)) @@ -871,25 +879,35 @@ def test_call_new_account_header_gas_used( GAS_NEW_ACCOUNT state gas. The block must be accepted with correct 2D max(regular, state) accounting in the header. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - target = pre.fund_eoa(amount=0) storage = Storage() - contract = pre.deploy_contract( - code=( - Op.SSTORE( - storage.store_next(1, "call_succeeds"), - Op.CALL(gas=100_000, address=target, value=1), - ) + # Capture the CALL result in a pre-existing slot (2 -> 1) so the + # instrumentation SSTORE modifies rather than creates a key and + # adds no state gas; the reservoir then covers exactly the CALL's + # new-account charge. + slot = storage.store_next(1, "call_succeeds") + contract_code = Op.SSTORE( + slot, + Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, ), - balance=1, + original_value=2, + current_value=2, + new_value=1, + key_warm=False, + ) + contract = pre.deploy_contract( + code=contract_code, balance=1, storage={slot: 2} ) tx = Transaction( to=contract, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=contract_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -929,34 +947,29 @@ def test_call_value_to_self_destructed_same_tx_account( the no charge behavior lives in `test_call_value_to_self_destructed_header_gas_used`. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) storage = Storage() - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.SSTORE( - storage.store_next(1, "call_succeeds"), - Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1), - ) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.SSTORE( + storage.store_next(1, "call_succeeds"), + Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1), + ) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -998,8 +1011,6 @@ def test_call_value_to_self_destructed_header_gas_used( targeted itself or an external beneficiary, so the no charge behavior holds across both cases. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - if selfdestruct_beneficiary == "self": inner_code = Op.SELFDESTRUCT(Op.ADDRESS) else: @@ -1009,24 +1020,22 @@ def test_call_value_to_self_destructed_header_gas_used( inner_code = Op.SELFDESTRUCT(alive_beneficiary) mstore_value, size = init_code_at_high_bytes(inner_code) - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1)) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1)) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1069,31 +1078,29 @@ def test_call_value_to_self_destructed_burns_value( address. At the end of the transaction the account is removed and the accumulated balance is lost. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) initial_balance = 2 * call_value - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(call_value, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(call_value, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP( - Op.CALL( - gas=Op.GAS, - address=Op.MLOAD(0x20), - value=call_value, - ) + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(call_value, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(call_value, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP( + Op.CALL( + gas=Op.GAS, + address=Op.MLOAD(0x20), + value=call_value, ) - ), - balance=initial_balance, + ) + ) + orchestrator = pre.deploy_contract( + code=orchestrator_code, balance=initial_balance ) created_address = compute_create_address( address=orchestrator, @@ -1105,7 +1112,7 @@ def test_call_value_to_self_destructed_burns_value( tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1147,29 +1154,25 @@ def test_call_zero_value_to_self_destructed_same_tx_account( value CALL (value gate broken) would double the state gas component. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=0)) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=0)) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1455,12 +1458,20 @@ def test_call_new_account_no_regular_account_creation_cost( Verify CALL with value to a non-existent account does not charge a regular account-creation cost on top of state gas. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - target = pre.fund_eoa(amount=0) - caller_code = Op.POP(Op.CALL(gas=0, address=target, value=1)) + Op.STOP + caller_code = ( + Op.POP( + Op.CALL( + gas=0, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) + ) + + Op.STOP + ) caller = pre.deploy_contract(code=caller_code, balance=1) # Tight budget: slack is less than the old pre-Amsterdam regular @@ -1468,13 +1479,7 @@ def test_call_new_account_no_regular_account_creation_cost( intrinsic = fork.transaction_intrinsic_cost_calculator()() tx = Transaction( to=caller, - gas_limit=( - intrinsic - + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - + new_account_state_gas - + 20_000 - ), + gas_limit=(intrinsic + caller_code.gas_cost(fork) + 20_000), sender=pre.fund_eoa(), ) @@ -1498,20 +1503,26 @@ def test_call_new_account_state_gas_boundary( materialized; one gas short the caller frame goes out of gas, so nothing is created and the value transfer is rolled back. """ - gas_costs = fork.gas_costs() - target = 0xDEAD - caller_code = Op.CALL(gas=0, address=target, value=1) + Op.STOP + target = pre.nonexistent_account() + caller_code = ( + Op.CALL( + gas=0, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) + + Op.STOP + ) caller = pre.deploy_contract(code=caller_code, balance=1) exact_fit = ( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - + gas_costs.NEW_ACCOUNT ) post: dict if gas_delta == 0: - gas_used = exact_fit - gas_costs.CALL_STIPEND + gas_used = exact_fit - fork.call_value_stipend() post = {target: Account(balance=1), caller: Account(balance=0)} else: gas_used = exact_fit + gas_delta @@ -1555,7 +1566,6 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( tight regular stipend. Covers SSTORE and CALL-value (new account) state-gas charge paths. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) probe_storage = Storage() @@ -1564,14 +1574,19 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( if charge_via == "sstore": child_code: Bytecode = Op.SSTORE(0, 1) + Op.REVERT(0, 0) child_balance = 0 - child_state_charge = sstore_state_gas else: fresh_target = pre.fund_eoa(amount=0) child_code = Op.POP( - Op.CALL(gas=Op.GAS, address=fresh_target, value=1) + Op.CALL( + gas=Op.GAS, + address=fresh_target, + value=1, + value_transfer=True, + account_new=True, + ) ) + Op.REVERT(0, 0) child_balance = 1 - child_state_charge = gas_costs.NEW_ACCOUNT + child_state_charge = child_code.state_cost(fork) child = pre.deploy_contract(code=child_code, balance=child_balance) probe = pre.deploy_contract(probe_code) @@ -1620,9 +1635,7 @@ def test_call_insufficient_balance_refunds_new_account_state_gas( Refill NEW_ACCOUNT state gas on a value CALL that fails the balance check before the child frame. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT probe_storage = Storage() probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) @@ -1632,14 +1645,22 @@ def test_call_insufficient_balance_refunds_new_account_state_gas( non_existent_account = pre.nonexistent_account() + value_call = Op.CALL( + gas=Op.GAS, + address=non_existent_account, + value=1, + value_transfer=True, + account_new=True, + ) parent = pre.deploy_contract( code=( - Op.POP(Op.CALL(gas=Op.GAS, address=non_existent_account, value=1)) + Op.POP(value_call) + Op.POP(Op.CALL(gas=probe_stipend, address=probe)) ), balance=0, ) + new_account_state_gas = value_call.state_cost(fork) assert new_account_state_gas >= sstore_state_gas reservoir = new_account_state_gas @@ -1663,9 +1684,7 @@ def test_call_value_precompile_halt_refunds_new_account_state_gas( Refill NEW_ACCOUNT state gas on a value CALL to an unfunded precompile that halts in the child frame. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT probe_storage = Storage() probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) @@ -1675,14 +1694,18 @@ def test_call_value_precompile_halt_refunds_new_account_state_gas( ecpairing = 0x08 + value_call = Op.CALL( + 1, ecpairing, 1, 0, 0, 0, 0, value_transfer=True, account_new=True + ) parent = pre.deploy_contract( code=( - Op.POP(Op.CALL(1, ecpairing, 1, 0, 0, 0, 0)) + Op.POP(value_call) + Op.POP(Op.CALL(gas=probe_stipend, address=probe)) ), balance=1, ) + new_account_state_gas = value_call.state_cost(fork) assert new_account_state_gas >= sstore_state_gas reservoir = new_account_state_gas @@ -1734,10 +1757,17 @@ def test_call_value_new_account_state_gas_consumed_on_caller_halt( if target_kind == "precompile" else pre.nonexistent_account() ) - caller = pre.deploy_contract( - code=Op.CALL(gas=0, address=target, value=value) + Op.INVALID, - balance=value, + caller_code = ( + Op.CALL( + gas=0, + address=target, + value=value, + value_transfer=True, + account_new=True, + ) + + Op.INVALID ) + caller = pre.deploy_contract(code=caller_code, balance=value) sender = pre.fund_eoa() gas_limit_cap = fork.transaction_gas_limit_cap() @@ -1745,7 +1775,7 @@ def test_call_value_new_account_state_gas_consumed_on_caller_halt( if reservoir == "over_cap": # The excess over the EIP-7825 cap becomes the reservoir. - gas_limit = gas_limit_cap + fork.gas_costs().NEW_ACCOUNT // 2 + gas_limit = gas_limit_cap + caller_code.state_cost(fork) // 2 expected_gas_used = gas_limit_cap else: gas_limit = 1_000_000 @@ -1787,27 +1817,32 @@ def test_call_value_new_account_state_gas_returned_on_caller_revert( """ value = 1 target = pre.nonexistent_account() - caller_code = Op.CALL(gas=0, address=target, value=value) + Op.REVERT(0, 0) + caller_code = Op.CALL( + gas=0, + address=target, + value=value, + value_transfer=True, + account_new=True, + ) + Op.REVERT(0, 0) caller = pre.deploy_contract(code=caller_code, balance=value) sender = pre.fund_eoa() - gas_costs = fork.gas_costs() - # Only regular execution is billed: the spilled and reservoir-funded parts - # of the NEW_ACCOUNT charge are both refunded, so the cost matches in-cap - # and over-cap. `gas_cost` covers the pushes and cold access; the value - # transfer is added on top and the empty child returns its stipend unused. + # Only regular execution is billed: the spilled and reservoir-funded + # parts of the NEW_ACCOUNT charge are both refunded, so the cost + # matches in-cap and over-cap. `regular_cost` covers the pushes, cold + # access and the value transfer (NEW_ACCOUNT lands in the state + # dimension); the empty child returns its stipend unused. expected_gas_used = ( fork.transaction_intrinsic_cost_calculator()() - + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - - gas_costs.CALL_STIPEND + + caller_code.regular_cost(fork) + - fork.call_value_stipend() ) receipt = TransactionReceipt(cumulative_gas_used=expected_gas_used) gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None gas_limit = ( - gas_limit_cap + gas_costs.NEW_ACCOUNT // 2 + gas_limit_cap + caller_code.state_cost(fork) // 2 if reservoir == "over_cap" else 1_000_000 ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py index d0c8f7e4ed9..2a58f8e6d55 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py @@ -157,33 +157,29 @@ def test_calldata_floor_exceeding_tx_gas_limit_cap( exceeds_cap: one byte more tips the floor over the cap — transaction rejected. """ - gas_costs = fork.gas_costs() cap = fork.transaction_gas_limit_cap() assert cap is not None floor_cost = fork.transaction_data_floor_cost_calculator() - floor_token = gas_costs.TX_DATA_TOKEN_FLOOR - # EIP-2780 anchors the floor on the decomposed intrinsic base; the tx - # targets a contract, so the base includes the recipient-access charge. - floor_base = gas_costs.TX_BASE + gas_costs.COLD_ACCOUNT_ACCESS - max_tokens = (cap - floor_base) // floor_token - - if fork.is_eip_enabled(7976): - # EIP-7976: all bytes contribute 4 floor tokens regardless of - # value, so the token count is len(data) * 4. - tokens_per_byte = 4 - max_bytes = max_tokens // tokens_per_byte - if exceeds_cap: - max_bytes += 1 - calldata = b"\x01" * max_bytes - else: - # EIP-7623: non-zero bytes contribute 4 tokens, zero bytes 1. - tokens_per_nonzero = 4 - nonzero_bytes = max_tokens // tokens_per_nonzero - zero_bytes = max_tokens - nonzero_bytes * tokens_per_nonzero - if exceeds_cap: - zero_bytes += 1 - calldata = b"\x01" * nonzero_bytes + b"\x00" * zero_bytes + # Binary-search the largest all-nonzero calldata whose floor cost fits + # within the gas cap; `exceeds_cap` adds one more byte to tip the floor + # over. Driven by the floor calculator directly so it tracks the + # per-byte token pricing across forks. + def floor_fits(num_bytes: int) -> bool: + return floor_cost(data=b"\x01" * num_bytes) <= cap + + high = 1 + while floor_fits(high): + high *= 2 + low = high // 2 + while low < high: + mid = (low + high + 1) // 2 + if floor_fits(mid): + low = mid + else: + high = mid - 1 + max_bytes = low + 1 if exceeds_cap else low + calldata = b"\x01" * max_bytes contract = pre.deploy_contract(Op.STOP) floor = floor_cost(data=calldata) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index b2274b5e5f8..fe8a624fc16 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -17,7 +17,6 @@ Block, BlockchainTestFiller, Bytecode, - CodeGasMeasure, Fork, Header, Initcode, @@ -97,9 +96,6 @@ def test_create_with_reservoir( Provide gas above TX_MAX_GAS_LIMIT so the new account state gas is drawn from the reservoir rather than gas_left. """ - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - storage = Storage() init_code = Op.STOP @@ -124,7 +120,7 @@ def test_create_with_reservoir( tx = Transaction( to=contract, - state_gas_reservoir=create_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -266,18 +262,14 @@ def test_code_deposit_state_gas_exact_fit_boundary( ``gas_left`` and burns it all, billing the full ``gas_limit``. The scaling tests assert success only. """ - gas_costs = fork.gas_costs() cap = fork.transaction_gas_limit_cap() assert cap is not None code_size = fork.max_code_size() if funding == "reservoir" else 1000 - words = (code_size + 31) // 32 - memory_gas = gas_costs.MEMORY_PER_WORD * words + words * words // 512 - init_code = Op.RETURN(0, code_size) - init_exec_regular = init_code.regular_cost(fork) + memory_gas - keccak_gas = gas_costs.OPCODE_KECCAK256_PER_WORD * words - deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) + init_code = Op.RETURN( + 0, code_size, code_deposit_size=code_size, new_memory_size=code_size + ) intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( calldata=bytes(init_code), @@ -285,13 +277,13 @@ def test_code_deposit_state_gas_exact_fit_boundary( return_cost_deducted_prior_execution=True, ) # The fresh target's NEW_ACCOUNT is a top-frame state charge under - # EIP-2780, no longer folded into the intrinsic. + # EIP-2780, no longer folded into the intrinsic. The RETURN metadata + # folds the memory expansion, code-hash keccak and code-deposit state + # gas into `init_code`'s own cost. exact_fit_gas = ( intrinsic_regular - + gas_costs.NEW_ACCOUNT - + init_exec_regular - + keccak_gas - + deposit_state_gas + + fork.transaction_top_frame_state_gas(contract_creation=True) + + init_code.gas_cost(fork) ) if funding == "reservoir": assert exact_fit_gas > cap @@ -469,6 +461,7 @@ def test_create_insufficient_state_gas( returning 0. """ init_code = Op.STOP + create_call = Op.CREATE(0, 0, len(init_code)) storage = Storage() contract = pre.deploy_contract( @@ -480,17 +473,15 @@ def test_create_insufficient_state_gas( ) + Op.SSTORE( storage.store_next(0), # CREATE returns 0 on OOG - Op.CREATE(0, 0, len(init_code)), + create_call, ) ), ) # Tight gas — enough for intrinsic + CREATE regular gas but not # enough for the new account state gas - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - regular_create_gas = gas_costs.OPCODE_CREATE_BASE - gas_limit = intrinsic_cost() + regular_create_gas + 10_000 + gas_limit = intrinsic_cost() + create_call.regular_cost(fork) + 10_000 tx = Transaction( to=contract, @@ -658,14 +649,17 @@ def test_code_deposit_oog_preserves_parent_reservoir( CREATE proves the reservoir was not inflated by a spill-then-halt refund. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Small deploy size; code deposit state gas will exceed the # limited gas available in the CREATE child frame. deploy_size = 4096 init_code = Op.RETURN(0, deploy_size) + create_call = Op.CREATE( + value=0, + offset=32 - len(init_code), + size=len(init_code), + ) # Limited regular gas forwarded to the factory. After CREATE # takes 63/64, the factory retains ~23 K for its SSTOREs. @@ -677,11 +671,7 @@ def test_code_deposit_oog_preserves_parent_reservoir( Op.MSTORE(0, Op.PUSH32(bytes(init_code))) + Op.SSTORE( factory_storage.store_next(0, "create_fails"), - Op.CREATE( - value=0, - offset=32 - len(init_code), - size=len(init_code), - ), + create_call, ) # Reservoir must be fully preserved after failed CREATE; # parent can still perform its own SSTORE. @@ -702,7 +692,7 @@ def test_code_deposit_oog_preserves_parent_reservoir( # gas_left, which the limited CALL gas cannot cover. tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=create_call.state_cost(fork) + sstore_state_gas, sender=pre.fund_eoa(), ) @@ -754,26 +744,34 @@ def test_parent_state_gas_after_child_failure( """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT initcode = Op.SSTORE(0, 1, original_value=0, new_value=1) + failure_op + create_call = Op.CREATE( + value=0, + offset=32 - len(initcode), + size=len(initcode), + init_code_size=len(initcode), + ) + factory_storage = Storage() - factory_code = ( - Op.MSTORE(0, Op.PUSH32(bytes(initcode))) - + Op.SSTORE( - factory_storage.store_next(0, "create_fails"), - Op.CREATE( - value=0, - offset=32 - len(initcode), - size=len(initcode), - ), - original_value=0, - new_value=0, + # Split the factory into the CREATE run (memory setup + CREATE, whose + # result is left on the stack) and the post-CREATE stores, so each + # step's regular gas is read off `.regular_cost(fork)` rather than + # rebuilt from constants. + factory_create_code = ( + Op.MSTORE(0, Op.PUSH32(bytes(initcode)), new_memory_size=32) + + create_call + ) + factory_post_create_code = ( + # Store the CREATE result (0 on failure): a cold 0 -> 0 no-op. + Op.PUSH1(factory_storage.store_next(0, "create_fails")) + + Op.SSTORE.with_metadata(original_value=0, new_value=0)( + unchecked=True ) + # Factory's own cold 0 -> 1 SSTORE. + Op.SSTORE( factory_storage.store_next(1, "post_create"), 1, @@ -781,50 +779,16 @@ def test_parent_state_gas_after_child_failure( new_value=1, ) ) + factory_code = factory_create_code + factory_post_create_code factory = pre.deploy_contract(code=factory_code) + new_account_state_gas = create_call.state_cost(fork) gas_limit = ( gas_limit_cap + new_account_state_gas + sstore_state_gas * 2 if with_reservoir else 5_000_000 ) - # `bytecode.gas_cost(fork)` accounts for opcode base costs and - # state-gas charges, but does NOT track memory-expansion or CREATE - # init-code word costs. Add those back to recover runtime regular - # gas consumption. - init_code_word_count = (len(initcode) + 31) // 32 - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD * init_code_word_count - mstore_memory_expansion = gas_costs.MEMORY_PER_WORD # 1 word - gas_cost_helper_extras = init_code_word_cost + mstore_memory_expansion - - # Factory bytecode shape costs, derived from fork.gas_costs(): - # pre-CREATE: PUSH32 + PUSH1 + MSTORE (with 1-word expansion) - # + 3 PUSHes for CREATE inputs - # post-CREATE: PUSH key + SSTORE (cold no-op: access cost only) - # + 2 PUSHes + SSTORE (cold zero-to-nonzero: - # access + write, the compound COLD_STORAGE_WRITE) - factory_pre_create_regular = ( - gas_costs.VERY_LOW * 2 - + gas_costs.OPCODE_MSTORE_BASE - + mstore_memory_expansion - + gas_costs.VERY_LOW * 3 - ) - factory_post_create_regular = ( - gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_ACCESS - + gas_costs.VERY_LOW * 2 - + gas_costs.COLD_STORAGE_WRITE - ) - - factory_regular = ( - factory_code.gas_cost(fork) - - new_account_state_gas - - sstore_state_gas - + gas_cost_helper_extras - ) - initcode_regular_revert = initcode.gas_cost(fork) - sstore_state_gas - if failure_op == Op.INVALID: # Simulate runtime gas for HALT under EIP-8037 LIFO refills: # 1. Regular pool capped by transaction_gas_limit_cap. The @@ -846,8 +810,9 @@ def test_parent_state_gas_after_child_failure( sim_gas_left = min(regular_budget, execution_gas) sim_state_gas_left = execution_gas - sim_gas_left - sim_gas_left -= factory_pre_create_regular - sim_gas_left -= gas_costs.OPCODE_CREATE_BASE + init_code_word_cost + # Memory setup, the CREATE arg pushes and the CREATE regular + # cost are all consumed before the 63/64 split. + sim_gas_left -= factory_create_code.regular_cost(fork) # CREATE new_account state gas: reservoir first, spill tracked. new_account_from_reservoir = min( @@ -871,7 +836,7 @@ def test_parent_state_gas_after_child_failure( sim_gas_left += new_account_spill sim_state_gas_left += new_account_from_reservoir - sim_gas_left -= factory_post_create_regular + sim_gas_left -= factory_post_create_code.regular_cost(fork) # Factory post-CREATE SSTORE: reservoir first, spill otherwise. if sim_state_gas_left >= sstore_state_gas: @@ -887,8 +852,9 @@ def test_parent_state_gas_after_child_failure( # factory's own post-CREATE SSTORE consumes net state gas. expected_cumulative = ( intrinsic_cost - + factory_regular - + initcode_regular_revert + + factory_create_code.regular_cost(fork) + + factory_post_create_code.regular_cost(fork) + + initcode.regular_cost(fork) + sstore_state_gas ) @@ -922,9 +888,8 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( code deposit after init code runs. The CREATE increments the factory nonce but code deposit fails, so no contract is deployed. """ - init_code = Op.RETURN(0, 1) - gas_costs = fork.gas_costs() - code_deposit_state = fork.code_deposit_state_gas(code_size=1) + init_code = Op.RETURN(0, 1, new_memory_size=32) + code_deposit_state = Op.RETURN(0, 1, code_deposit_size=1).state_cost(fork) factory_mstore = Op.MSTORE( 0, Op.PUSH32(bytes(init_code)), new_memory_size=32 @@ -942,7 +907,7 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( # Init code child execution: PUSH1 + PUSH1 + RETURN's mem_exp. # Code deposit (keccak + state) is charged AFTER the child returns. - init_cost = 2 * gas_costs.VERY_LOW + gas_costs.MEMORY_PER_WORD + init_cost = init_code.regular_cost(fork) # Target child: enough for init, not enough for code deposit state. target_child = (init_cost + code_deposit_state) // 2 # Invert EIP-150 63/64ths rule: ceil(target_child * 64 / 63). @@ -955,7 +920,7 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( intrinsic_cost + factory_mstore.regular_cost(fork) + factory_create.regular_cost(fork) - + gas_costs.NEW_ACCOUNT + + factory_create.state_cost(fork) + factory_remaining ) @@ -1343,7 +1308,6 @@ def test_create_tx_header_gas_used( regular intrinsic and the floor, and fails if a stray NEW_ACCOUNT is charged. """ - gas_costs = fork.gas_costs() initcode = Op.STOP create_state_gas = fork.create_state_gas(code_size=1) @@ -1387,7 +1351,9 @@ def test_create_tx_header_gas_used( else: # For a minimal CREATE tx deploying Op.STOP (1 byte), # state gas (new account) dominates regular gas. - expected_gas_used = gas_costs.NEW_ACCOUNT + expected_gas_used = fork.transaction_top_frame_state_gas( + contract_creation=True + ) blockchain_test( pre=pre, @@ -1587,7 +1553,6 @@ def test_create_silent_failure_refunds_state_gas( balance) refund `GAS_NEW_ACCOUNT` to the reservoir. Block state gas reflects only the probe SSTORE, not the refunded CREATE. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() @@ -1613,13 +1578,8 @@ def test_create_silent_failure_refunds_state_gas( # CREATE's GAS_NEW_ACCOUNT is refunded (silent failure, no child # spawned). SSTORE's state portion is tracked separately in - # tx_state. - tx_regular = ( - intrinsic_cost - + factory_code.gas_cost(fork) - - gas_costs.NEW_ACCOUNT - - sstore_state_gas - ) + # tx_state, so only the regular dimension remains here. + tx_regular = intrinsic_cost + factory_code.regular_cost(fork) tx_state = sstore_state_gas expected = max(tx_regular, tx_state) blockchain_test( @@ -1657,7 +1617,6 @@ def test_create_child_revert_refunds_state_gas( """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() @@ -1695,9 +1654,7 @@ def test_create_child_revert_refunds_state_gas( # incorporate_child_on_error. tx_regular = ( intrinsic_cost - + factory_code.gas_cost(fork) - - gas_costs.NEW_ACCOUNT - - sstore_state_gas + + factory_code.regular_cost(fork) + init_code.gas_cost(fork) ) tx_state = sstore_state_gas @@ -1736,9 +1693,7 @@ def test_create_child_halt_refunds_state_gas( but not enough to spill the state portion, so the probe SSTORE can only succeed via the refunded reservoir. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT init_code: Op | Bytecode if failure_mode == "initcode_halt": @@ -1772,9 +1727,9 @@ def test_create_child_halt_refunds_state_gas( # regular fits but state gas spillover from `gas_left` under # the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular # Reservoir sized for CREATE charge only — SSTORE must pull @@ -1784,7 +1739,7 @@ def test_create_child_halt_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1866,9 +1821,7 @@ def test_create_collision_refunds_state_gas( probe SSTORE can only succeed via the refunded reservoir, not by spilling state gas from `gas_left`. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT init_code = Op.STOP mstore_value, size = init_code_at_high_bytes(init_code) @@ -1903,9 +1856,9 @@ def test_create_collision_refunds_state_gas( # the probe SSTORE regular fits but state gas spillover from # `gas_left` under the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular # Reservoir sized for CREATE charge only — SSTORE must pull from @@ -1915,7 +1868,7 @@ def test_create_collision_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1939,9 +1892,7 @@ def test_create_code_deposit_oog_refunds_state_gas( `gas_left` so the probe SSTORE can only succeed via the refunded reservoir, not by spilling state gas from `gas_left`. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT max_code_size = fork.max_code_size() # Init code returns (max_code_size + 1) bytes, triggering the @@ -1969,9 +1920,9 @@ def test_create_code_deposit_oog_refunds_state_gas( # discrimination window so SSTORE regular fits but state gas # spillover fails. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular caller = pre.deploy_contract( @@ -1979,7 +1930,7 @@ def test_create_code_deposit_oog_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -2071,7 +2022,7 @@ def test_create_account_charge_reduces_child_gas( The target is a pre-existing balance-only leaf, the EIP-8037 success-refund path that the old conditional charge skipped. """ - new_account = fork.gas_costs().NEW_ACCOUNT + new_account = create_opcode(account_new=True).state_cost(fork) memory_gas = fork.memory_expansion_gas_calculator() # Factory `gas_left` at the NEW_ACCOUNT charge. Three times @@ -2134,12 +2085,9 @@ def test_create_account_charge_reduces_child_gas( pre.fund_address(create_address, amount=1) # Regular gas the factory spends before the NEW_ACCOUNT charge: the - # initcode setup MSTORE plus the create opcode regular portion - # (`gas_cost` folds NEW_ACCOUNT into the create op, so strip it). + # initcode setup MSTORE plus the create opcode's regular portion. setup = Op.MSTORE(0, mstore_value) - pre_charge_regular = ( - setup.gas_cost(fork) + create_call.gas_cost(fork) - new_account - ) + pre_charge_regular = setup.gas_cost(fork) + create_call.regular_cost(fork) forwarded_gas = gas_at_charge + pre_charge_regular caller = pre.deploy_contract( code=Op.CALL(gas=forwarded_gas, address=factory) @@ -2193,7 +2141,6 @@ def test_failed_create_tx_refills_top_frame_new_account( * HALT (INVALID) refills the spilled ``NEW_ACCOUNT`` to ``gas_left`` and then burns all of it, so the sender pays the full ``gas_limit``. """ - gas_costs = fork.gas_costs() intrinsic_calc = fork.transaction_intrinsic_cost_calculator() intrinsic_regular = intrinsic_calc( @@ -2205,7 +2152,7 @@ def test_failed_create_tx_refills_top_frame_new_account( # regular execution so the initcode runs to completion. gas_limit = ( intrinsic_regular - + gas_costs.NEW_ACCOUNT + + fork.transaction_top_frame_state_gas(contract_creation=True) + init_code.regular_cost(fork) + 1000 ) @@ -2387,9 +2334,7 @@ def test_create_onto_alive_refunds_to_gas_left( pre.fund_address(target, amount=1) gas_limit = ( - fork.transaction_intrinsic_cost_calculator()() - + create.regular_cost(fork) - + fork.gas_costs().NEW_ACCOUNT + fork.transaction_intrinsic_cost_calculator()() + create.gas_cost(fork) ) tx = Transaction(to=contract, gas_limit=gas_limit, sender=pre.fund_eoa()) @@ -2481,9 +2426,6 @@ def test_oversized_initcode_opcode_no_state_gas( initcode = Initcode(deploy_code=Op.STOP, initcode_length=size) initcode_bytes = bytes(initcode) - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - create_call = ( create_opcode( value=0, @@ -2515,7 +2457,7 @@ def test_oversized_initcode_opcode_no_state_gas( sender=pre.fund_eoa(), to=factory, data=initcode_bytes, - state_gas_reservoir=create_state_gas, + state_gas_reservoir=create_call.state_cost(fork), ) post: dict = {factory: Account(storage=storage)} @@ -2547,7 +2489,6 @@ def test_selfdestruct_in_create_tx_initcode( created contract's ``NEW_ACCOUNT`` plus the fresh beneficiary's ``NEW_ACCOUNT`` charged by the SELFDESTRUCT. """ - gas_costs = fork.gas_costs() create_state_gas = fork.create_state_gas(code_size=0) beneficiary = 0xDEAD @@ -2563,10 +2504,10 @@ def test_selfdestruct_in_create_tx_initcode( # State: the created contract's top-frame NEW_ACCOUNT plus the fresh # beneficiary's NEW_ACCOUNT from the SELFDESTRUCT. - expected_state = create_state_gas + gas_costs.NEW_ACCOUNT + expected_state = create_state_gas + initcode.state_cost(fork) initcode_gas = initcode.gas_cost(fork) - gas_limit = intrinsic_regular + gas_costs.NEW_ACCOUNT + initcode_gas + 1000 + gas_limit = intrinsic_regular + create_state_gas + initcode_gas + 1000 tx = Transaction( sender=sender, @@ -2615,17 +2556,15 @@ def test_inner_create_succeeds_code_deposit_state_gas( gas. On success the block state gas is the outer ``NEW_ACCOUNT`` plus the inner account creation and code deposit. """ - gas_costs = fork.gas_costs() outer_state_gas = fork.create_state_gas(code_size=0) - inner_code_deposit = fork.code_deposit_state_gas(code_size=1) - inner_state_gas = gas_costs.NEW_ACCOUNT + inner_code_deposit deploy_code = Op.STOP inner_initcode = Op.MSTORE( 0, int.from_bytes(bytes(deploy_code), "big") << 248, - ) + Op.RETURN(31, 1) + ) + Op.RETURN(31, 1, code_deposit_size=len(deploy_code)) inner_bytes = bytes(inner_initcode) + inner_code_deposit = inner_initcode.state_cost(fork) setup = Op.MSTORE( 0, @@ -2635,6 +2574,8 @@ def test_inner_create_succeeds_code_deposit_state_gas( inner_create = Op.POP(Op.CREATE2(0, 0, len(inner_bytes), 0)) else: inner_create = Op.POP(Op.CREATE(0, 0, len(inner_bytes))) + # Inner account creation plus the inner contract's code deposit. + inner_state_gas = inner_create.state_cost(fork) + inner_code_deposit if outer_outcome == "succeeds": termination = Op.RETURN(0, 0) @@ -2660,7 +2601,7 @@ def test_inner_create_succeeds_code_deposit_state_gas( # the inner code deposit. gas_limit = ( intrinsic_total - + gas_costs.NEW_ACCOUNT + + outer_state_gas + initcode_gas + inner_code_deposit + 1000 @@ -2716,9 +2657,6 @@ def test_nested_create_fail_parent_revert_state_gas( Verify factory nonce is rolled back when the factory reverts after a failed inner CREATE, and preserved when the factory returns. """ - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - if child_failure == "revert": init_code = Op.REVERT(0, 0) else: @@ -2729,6 +2667,7 @@ def test_nested_create_fail_parent_revert_state_gas( if create_opcode == Op.CREATE2 else create_opcode(value=0, offset=0, size=len(init_code)) ) + create_state_gas = create_call.state_cost(fork) factory = pre.deploy_contract( code=( @@ -2838,7 +2777,6 @@ def test_inner_create_fail_refunds_in_creation_tx( Verify failed inner CREATEs inside a creation tx refund state gas so only the outer intrinsic state gas remains. """ - gas_costs = fork.gas_costs() outer_state_gas = fork.create_state_gas(code_size=0) inner_initcode = bytes(Op.REVERT(0, 0)) @@ -2871,10 +2809,11 @@ def test_inner_create_fail_refunds_in_creation_tx( initcode_gas = initcode.gas_cost(fork) per_inner_slack = 2_000 + new_account = create_opcode(account_new=True).state_cost(fork) gas_limit = ( intrinsic_total + initcode_gas - + num_inner_ops * (gas_costs.NEW_ACCOUNT + per_inner_slack) + + num_inner_ops * (new_account + per_inner_slack) ) create_address = compute_create_address(address=sender, nonce=0) @@ -2972,10 +2911,10 @@ def test_create_collision_burned_gas_counted_in_block_regular( @pytest.mark.parametrize( - "target", + "account_new", [ - pytest.param("new", id="new_account"), - pytest.param("existing", id="existing_account"), + pytest.param(True, id="new_account"), + pytest.param(False, id="existing_account"), ], ) @pytest.mark.with_all_create_opcodes() @@ -2985,133 +2924,122 @@ def test_create_account_creation_charge( pre: Alloc, fork: Fork, create_opcode: Op, - target: str, + account_new: bool, ) -> None: """ - Verify NEW_ACCOUNT is charged for a new account and refunded for a - pre-existing balance-only leaf. + Verify NEW_ACCOUNT is charged only when the created account does not + already exist in the trie. Empty init code means zero code deposit, so NEW_ACCOUNT is the only create state cost. A fresh target is charged it; a pre-existing - balance-only target (balance, no code, zero nonce) refunds it on - success. The probe SSTORE both confirms the create succeeded and - makes state gas dominate, so gas_used drops by exactly NEW_ACCOUNT - when refunded. + balance-only target (balance, no code, zero nonce) is not. The probe + SSTORE both confirms the create succeeded and makes state gas dominate + the header, so gas_used differs by exactly NEW_ACCOUNT between the two + cases. """ - new_account = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) mstore_value, size = init_code_at_high_bytes(Op.STOP) - create_call = ( - create_opcode(value=0, offset=0, size=size, salt=0) - if create_opcode == Op.CREATE2 - else create_opcode(value=0, offset=0, size=size) + create_call = create_opcode( + value=0, offset=0, size=size, account_new=account_new ) - storage = Storage() - factory = pre.deploy_contract( - code=Op.MSTORE(0, mstore_value) - + Op.SSTORE( - storage.store_next(1, "create_succeeds"), Op.GT(create_call, 0) - ) + factory_code = Op.MSTORE(0, mstore_value) + Op.SSTORE( + storage.store_next(1, "create_succeeds"), Op.GT(create_call, 0) ) + factory = pre.deploy_contract(code=factory_code) # Factory deployed via deploy_contract starts at nonce 1. - if create_opcode == Op.CREATE2: - create_address = compute_create2_address( - address=factory, salt=0, initcode=bytes(Op.STOP) - ) - else: - create_address = compute_create_address(address=factory, nonce=1) - if target == "existing": + create_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=bytes(Op.STOP), + opcode=create_opcode, + ) + if not account_new: pre.fund_address(create_address, amount=1) + # State gas dominates the header, so gas_used equals the factory's + # state cost: NEW_ACCOUNT plus the probe SSTORE for a fresh target, + # just the SSTORE for a pre-existing one. + state_cost = factory_code.state_cost(fork) tx = Transaction( to=factory, - state_gas_reservoir=new_account + sstore_state_gas, + state_gas_reservoir=state_cost, sender=pre.fund_eoa(), ) - # State gas dominates regular: a new account adds NEW_ACCOUNT on top - # of the probe SSTORE, a pre-existing target refunds it. - expected = sstore_state_gas + (new_account if target == "new" else 0) state_test( pre=pre, tx=tx, post={factory: Account(storage=storage)}, - blockchain_test_header_verify=Header(gas_used=expected), + blockchain_test_header_verify=Header(gas_used=state_cost), ) @pytest.mark.with_all_create_opcodes() +@pytest.mark.parametrize( + "sufficient_gas", + [ + pytest.param(True, id="sufficient_gas"), + pytest.param(False, id="insufficient_gas"), + ], +) @pytest.mark.valid_from("EIP8037") -def test_create_refund_credited_against_child_spill( +def test_no_account_charge_on_existing_account( state_test: StateTestFiller, pre: Alloc, fork: Fork, create_opcode: Op, + sufficient_gas: bool, ) -> None: """ - Verify the NEW_ACCOUNT refund routing is visible through GAS. + Verify the create opcode is not charged NEW_ACCOUNT when the target + account already exists in the trie. - The reservoir covers exactly the CREATE NEW_ACCOUNT charge, leaving - none for the child frame, whose initcode SSTOREs then spill more - than NEW_ACCOUNT of state gas from gas_left. The target is alive - (pre-funded), so NEW_ACCOUNT is refunded and credited LIFO against - the incorporated child spill, landing in the parent's gas_left - where GAS (which excludes the reservoir) observes it. + The factory is forwarded exactly the create's regular gas, with no + NEW_ACCOUNT included. Because the target is pre-funded (alive), that + budget is sufficient and the create succeeds, deploying empty code + (created nonce 1). With one gas less it runs out of gas at the + create's upfront charge, before the nonce bump, leaving the target + untouched (nonce 0). The empty reservoir keeps the state-gas + dimension from masking the boundary. """ - gas_costs = fork.gas_costs() - - initcode = Op.SSTORE(0, 1) + Op.SSTORE(1, 1) + Op.STOP - child_spill = initcode.state_cost(fork) - assert child_spill >= gas_costs.NEW_ACCOUNT - - mstore_value, initcode_size = init_code_at_high_bytes(initcode) - create_call = ( - create_opcode( - value=0, - offset=0, - size=initcode_size, - salt=0, - init_code_size=initcode_size, - ) - if create_opcode == Op.CREATE2 - else create_opcode( - value=0, - offset=0, - size=initcode_size, - init_code_size=initcode_size, - ) + factory_code = create_opcode( + value=0, + offset=0, + size=1, # Nothing in memory, equivalent to Op.STOP + # Gas accounting + init_code_size=1, + new_memory_size=1, + account_new=False, ) - factory = pre.deploy_contract( - code=Op.MSTORE(0, mstore_value) - + CodeGasMeasure(code=create_call, extra_stack_items=1), - ) + factory = pre.deploy_contract(code=factory_code) + created = compute_create_address( address=factory, nonce=1, salt=0, - initcode=initcode, + initcode=Op.STOP, opcode=create_opcode, ) pre.fund_address(created, amount=1) - expected_gas = ( - create_call.regular_cost(fork) - + initcode.regular_cost(fork) - + child_spill - - gas_costs.NEW_ACCOUNT # refund credited to gas_left - ) + call_gas = factory_code.gas_cost(fork) + if not sufficient_gas: + call_gas -= 1 + entry_code = Op.CALL(gas=call_gas, address=factory) + entry = pre.deploy_contract(code=entry_code) tx = Transaction( - to=factory, - state_gas_reservoir=gas_costs.NEW_ACCOUNT, + to=entry, + state_gas_reservoir=0, # To allow subcall to run OOG sender=pre.fund_eoa(), ) post = { - factory: Account(storage={0: expected_gas}), - created: Account(nonce=1, balance=1, storage={0: 1, 1: 1}), + created: Account( + nonce=1 if sufficient_gas else 0, balance=1, code=b"" + ), } state_test(pre=pre, post=post, tx=tx) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py index 4f31f16b85d..406550445af 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py @@ -121,21 +121,21 @@ def test_delegation_pointer_new_account_state_gas( via a delegation pointer, the new-account state gas is charged identically to a direct call. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - target = pre.nonexistent_account() parent_storage = Storage() + call = Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) contract = pre.deploy_contract( - code=( - Op.SSTORE( - parent_storage.store_next(1), - Op.CALL(gas=100_000, address=target, value=1), - ) - ), + code=Op.SSTORE(parent_storage.store_next(1), call), balance=1, ) + new_account_state_gas = call.state_cost(fork) # EOA delegates to the contract delegator = pre.fund_eoa(delegation=contract, amount=1) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py index 04a109b5cd8..fecc4871775 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py @@ -52,28 +52,20 @@ def test_exact_coinbase_fee_simple_sstore( Motivated by BAL devnet-3 ethrex/besu coinbase balance mismatch where clients diverged on cumulative `receipt_gas_used`. """ - gas_costs = fork.gas_costs() - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - - # Gas breakdown for tx 1 (SSTORE zero-to-nonzero, no calldata): - # PUSH1(1) + PUSH1(0) + SSTORE(cold, zero-to-nonzero) + STOP - intrinsic_regular = gas_costs.TX_BASE - if fork.is_eip_enabled(2780): - # EIP-2780 surfaces an explicit recipient-access charge for - # non-self, non-create transactions on top of ``TX_BASE``. - intrinsic_regular += gas_costs.COLD_ACCOUNT_ACCESS - evm_regular = ( - 2 * gas_costs.VERY_LOW # PUSH1 + PUSH1 - + gas_costs.COLD_STORAGE_WRITE # SSTORE cold zero-to-nonzero - ) - tx1_gas_used = intrinsic_regular + evm_regular + sstore_state_gas - expected_coinbase = tx1_gas_used - # Tx 1: single SSTORE zero-to-nonzero sstore_storage = Storage() - sstore_contract = pre.deploy_contract( - code=(Op.SSTORE(sstore_storage.store_next(1), 1)), + sstore_code = Op.SSTORE(sstore_storage.store_next(1), 1, new_value=1) + sstore_state_gas = sstore_code.state_cost(fork) + sstore_contract = pre.deploy_contract(code=sstore_code) + + # tx 1 gas used: the intrinsic (TX_BASE plus the EIP-2780 + # recipient-access charge) plus the SSTORE code's own regular and + # state cost. + tx1_gas_used = ( + fork.transaction_intrinsic_cost_calculator()() + + sstore_code.gas_cost(fork) ) + expected_coinbase = tx1_gas_used # Tx 2: reporter reads BALANCE(COINBASE) into slot 0 reporter = pre.deploy_contract( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py index d834129c84a..b9b1f8c5074 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py @@ -43,11 +43,7 @@ def _single_sstore_probe_gas(fork: Fork) -> int: The probe bytecode is Op.SSTORE(0, 1): two pushes + SSTORE. """ - gas_costs = fork.gas_costs() - sstore_regular = gas_costs.COLD_STORAGE_WRITE - sstore_state = Op.SSTORE(new_value=1).state_cost(fork) - push_gas = 2 * gas_costs.VERY_LOW - return push_gas + sstore_regular + sstore_state - 1 + return Op.SSTORE(0, 1).gas_cost(fork) - 1 @pytest.mark.valid_from("EIP8037") @@ -69,7 +65,6 @@ def test_sstore_oog_reservoir_inflation_detection( With wrong ordering (state gas first): reservoir is inflated, probe succeeds. """ - gas_costs = fork.gas_costs() initcode = Initcode(deploy_code=Op.STOP) initcode_len = len(initcode) @@ -105,14 +100,13 @@ def test_sstore_oog_reservoir_inflation_detection( # Compute probe gas: enough for 4 SSTOREs' regular gas + pushes, # but after 4th regular charge, gas_left < the state gas spill. - sstore_regular = gas_costs.COLD_STORAGE_WRITE sstore_state = Op.SSTORE(new_value=1).state_cost(fork) - push_per_sstore = 2 * gas_costs.VERY_LOW + sstore_regular = Op.SSTORE(0, 1).regular_cost(fork) create_state_gas = fork.create_state_gas( code_size=len(initcode.deploy_code) ) spill = 4 * sstore_state - create_state_gas - probe_gas = 4 * (push_per_sstore + sstore_regular) + spill // 2 + probe_gas = 4 * sstore_regular + spill // 2 caller_storage = Storage() caller = pre.deploy_contract( @@ -165,9 +159,6 @@ def test_call_oog_reservoir_inflation_detection( A single-SSTORE probe detects the inflation: with correct reservoir (0) it OOGs; with inflated reservoir it succeeds. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - dead_address = 0xDEAD child_code = Op.CALL( gas=0, @@ -177,10 +168,12 @@ def test_call_oog_reservoir_inflation_detection( args_size=0, ret_offset=0, ret_size=0, + value_transfer=True, + account_new=True, ) - pushes_gas = 7 * gas_costs.VERY_LOW - call_regular_gas = gas_costs.COLD_ACCOUNT_ACCESS + gas_costs.CALL_VALUE - child_gas = pushes_gas + call_regular_gas + new_account_state_gas - 1 + # One gas short of the CALL's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -221,18 +214,11 @@ def test_selfdestruct_oog_reservoir_inflation_detection( Single-SSTORE probe detects the inflation. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - dead_beneficiary = 0xBEEF - child_code = Op.SELFDESTRUCT(dead_beneficiary) - pushes_gas = gas_costs.VERY_LOW - selfdestruct_regular_gas = ( - gas_costs.OPCODE_SELFDESTRUCT_BASE + gas_costs.COLD_ACCOUNT_ACCESS - ) - child_gas = ( - pushes_gas + selfdestruct_regular_gas + new_account_state_gas - 1 - ) + child_code = Op.SELFDESTRUCT(dead_beneficiary, account_new=True) + # One gas short of the SELFDESTRUCT's full cost (regular plus the + # NEW_ACCOUNT state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code, balance=1) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -280,39 +266,32 @@ def test_create_oog_reservoir_inflation_detection( (empty initcode) and `oog_on_init_code_word_cost` (32-byte initcode). """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - if oog_step == "create_base": initcode_size = 0 - setup_gas = 0 - init_code_word_cost = 0 else: initcode_size = WORD_SIZE - setup_gas = ( - Op.MSTORE.popped_stack_items * gas_costs.VERY_LOW - + gas_costs.OPCODE_MSTORE_BASE - + gas_costs.MEMORY_PER_WORD - ) - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD if create_opcode == Op.CREATE: - create_op = create_opcode(value=0, offset=0, size=initcode_size) + create_op = create_opcode( + value=0, offset=0, size=initcode_size, init_code_size=initcode_size + ) else: create_op = create_opcode( - value=0, offset=0, size=initcode_size, salt=0 + value=0, + offset=0, + size=initcode_size, + salt=0, + init_code_size=initcode_size, ) - pushes_gas = create_opcode.popped_stack_items * gas_costs.VERY_LOW if oog_step == "create_base": child_code = create_op else: - child_code = Op.MSTORE(0, 0) + create_op + child_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op - create_regular_gas = gas_costs.OPCODE_CREATE_BASE + init_code_word_cost - child_gas = ( - setup_gas + pushes_gas + create_regular_gas + new_account_state_gas - 1 - ) + # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -358,40 +337,33 @@ def test_create_oog_full_burn_no_state_credit( Verify a CREATE OOG inside a non-creation tx burns the whole tx gas_limit — no state-gas leftover is credited at tx-end. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - if oog_step == "create_base": initcode_size = 0 - setup_gas = 0 - init_code_word_cost = 0 else: initcode_size = WORD_SIZE - setup_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.OPCODE_MSTORE_BASE - + gas_costs.MEMORY_PER_WORD - ) - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD if create_opcode == Op.CREATE: - create_op = create_opcode(value=0, offset=0, size=initcode_size) + create_op = create_opcode( + value=0, offset=0, size=initcode_size, init_code_size=initcode_size + ) else: create_op = create_opcode( - value=0, offset=0, size=initcode_size, salt=0 + value=0, + offset=0, + size=initcode_size, + salt=0, + init_code_size=initcode_size, ) - pushes_gas = create_opcode.popped_stack_items * gas_costs.VERY_LOW if oog_step == "create_base": factory_code = create_op else: - factory_code = Op.MSTORE(0, 0) + create_op + factory_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op factory = pre.deploy_contract(factory_code) - create_regular_gas = gas_costs.OPCODE_CREATE_BASE + init_code_word_cost - body_gas = ( - setup_gas + pushes_gas + create_regular_gas + new_account_state_gas - 1 - ) + # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + body_gas = factory_code.gas_cost(fork) - 1 intrinsic_calc = fork.transaction_intrinsic_cost_calculator() tx_gas_limit = intrinsic_calc() + body_gas diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py index d52ac0d60b1..99af67b5a2c 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py @@ -556,22 +556,21 @@ def test_call_new_account_state_gas_scales_with_cpsb( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None env = Environment(gas_limit=block_gas_limit) - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - empty = pre.fund_eoa(0) + call = Op.CALL( + gas=100_000, + address=empty, + value=1, + value_transfer=True, + account_new=True, + ) storage = Storage() contract = pre.deploy_contract( - code=( - Op.SSTORE( - storage.store_next(1, "call_success"), - Op.CALL(gas=100_000, address=empty, value=1), - ) - ), + code=Op.SSTORE(storage.store_next(1, "call_success"), call), balance=1, ) - tx_gas = min(gas_limit_cap + new_account_state_gas, block_gas_limit) + tx_gas = min(gas_limit_cap + call.state_cost(fork), block_gas_limit) tx = Transaction( to=contract, gas_limit=tx_gas, @@ -599,8 +598,7 @@ def test_selfdestruct_new_beneficiary_scales_with_cpsb( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None env = Environment(gas_limit=block_gas_limit) - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = pre.fund_eoa(0) storage = Storage() diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py index 445cb1d885c..5ac3ba9226c 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py @@ -174,15 +174,13 @@ def test_insufficient_gas_for_sstore_state_cost( gas, but not enough to also cover the SSTORE state gas. The SSTORE should OOG, leaving storage slot 0 unchanged at zero. """ - gas_costs = fork.gas_costs() - contract = pre.deploy_contract( - code=Op.SSTORE(0, 1), - ) + contract_code = Op.SSTORE(0, 1) + contract = pre.deploy_contract(code=contract_code) # Enough for intrinsic + warm SSTORE regular gas, but not the # state gas cost for zero-to-nonzero transition intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - gas_limit = intrinsic_cost() + gas_costs.COLD_STORAGE_WRITE + gas_limit = intrinsic_cost() + contract_code.regular_cost(fork) tx = Transaction( to=contract, @@ -690,12 +688,13 @@ def test_create_tx_reservoir( beyond TX_MAX_GAS_LIMIT feeds the reservoir. When False, all state gas comes from gas_left (reservoir is zero). """ - gas_costs = fork.gas_costs() gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None init_code = Op.STOP - create_state_gas = gas_costs.NEW_ACCOUNT + create_state_gas = fork.transaction_top_frame_state_gas( + contract_creation=True + ) if gas_above_cap: gas_limit = gas_limit_cap + create_state_gas @@ -1247,7 +1246,7 @@ def test_nested_failure_resets_to_tx_reservoir( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT + new_account_state_gas = Op.CREATE(account_new=True).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() body_state_total = sum(b.state_cost(fork) for b in frame_bodies) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py index ff0fec86530..f02eaca1336 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py @@ -50,7 +50,7 @@ def test_selfdestruct_new_beneficiary_state_gas( spilled into `gas_left` (in-cap tx): the block bills NEW_ACCOUNT in the state dimension and the beneficiary is created. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = 0xDEAD contract = pre.deploy_contract( @@ -184,8 +184,7 @@ def test_selfdestruct_new_beneficiary_header_gas_used( beneficiary, charging GAS_NEW_ACCOUNT state gas. The block must be accepted with correct 2D gas accounting in the header. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = pre.fund_eoa(amount=0) @@ -232,7 +231,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( transfer remains billed. """ beneficiary = 0xDEAD - inner_code = Op.SELFDESTRUCT(beneficiary) + inner_code = Op.SELFDESTRUCT(beneficiary, account_new=True) inner = pre.deploy_contract(code=inner_code, balance=1) caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.REVERT(0, 0) caller = pre.deploy_contract(code=caller_code) @@ -240,8 +239,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( expected_regular = ( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) - + inner_code.gas_cost(fork) - + fork.gas_costs().ACCOUNT_WRITE + + inner_code.regular_cost(fork) ) tx = Transaction(to=caller, sender=pre.fund_eoa()) @@ -271,8 +269,6 @@ def test_create_selfdestruct_no_refund_account_and_storage( num_slots: int, ) -> None: """Verify same tx CREATE+SELFDESTRUCT does not refund state gas.""" - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() init_code = Bytecode() @@ -299,7 +295,9 @@ def test_create_selfdestruct_no_refund_account_and_storage( factory_code = mstore + Op.POP(create_call) factory = pre.deploy_contract(code=factory_code) - total_state_gas = new_account_state_gas + num_slots * sstore_state_gas + total_state_gas = factory_code.state_cost(fork) + init_code.state_cost( + fork + ) regular_used = ( intrinsic_gas + factory_code.gas_cost(fork) @@ -344,8 +342,6 @@ def test_create_selfdestruct_no_refund_code_deposit_state_gas( state gas. """ assert code_size >= 2 - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - code_deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) if beneficiary_type == "self": selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) @@ -382,7 +378,7 @@ def test_create_selfdestruct_no_refund_code_deposit_state_gas( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = new_account_state_gas + code_deposit_state_gas + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) tx = Transaction( to=factory, data=bytes(initcode), @@ -407,9 +403,6 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( Verify block header gas reflects the full account plus code-deposit state-gas charge on a same-tx CREATE+SELFDESTRUCT. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) sd_len = len(bytes(selfdestruct)) code_size = 256 @@ -417,7 +410,6 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( deployed = bytes(selfdestruct) + b"\x00" * (code_size - sd_len) initcode = Initcode(deploy_code=deployed) initcode_len = len(initcode) - code_deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) factory_code = Op.CALLDATACOPY( 0, @@ -439,7 +431,7 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = new_account_state_gas + code_deposit_state_gas + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) tx = Transaction( to=factory, data=bytes(initcode), @@ -472,7 +464,6 @@ def test_create_selfdestruct_sstore_restoration_refund( Verify SSTORE restoration still refunds its slot state gas when the surrounding contract SELFDESTRUCTs. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() @@ -500,6 +491,7 @@ def test_create_selfdestruct_sstore_restoration_refund( factory_code = mstore + Op.POP(create_call) factory = pre.deploy_contract(code=factory_code) + new_account_state_gas = factory_code.state_cost(fork) state_used = new_account_state_gas regular_used = ( intrinsic_gas @@ -595,8 +587,6 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( Verify SELFDESTRUCT in a nested DELEGATECALL/CALLCODE frame below a same-tx-created contract does not refund state gas. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() # Bottom of the chain does the SELFDESTRUCT; intermediate helpers @@ -627,9 +617,6 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( + Op.STOP ) deployed = bytes(deployed_code) - code_deposit_state_gas = fork.code_deposit_state_gas( - code_size=len(deployed) - ) initcode = Initcode(deploy_code=deployed) initcode_len = len(initcode) @@ -678,18 +665,14 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = ( - new_account_state_gas + code_deposit_state_gas + 2 * sstore_state_gas - ) + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) regular_used = ( intrinsic_gas + factory_code.gas_cost(fork) + initcode.gas_cost(fork) + deployed_code.gas_cost(fork) + chain_regular_gas - - new_account_state_gas - - code_deposit_state_gas - - 2 * sstore_state_gas + - total_state_gas ) expected_gas_used = max(regular_used, total_state_gas) @@ -768,9 +751,8 @@ def test_create_tx_selfdestruct_initcode_state_gas( ) -> None: """ Verify a creation tx whose initcode SELFDESTRUCTs the new contract - still pays the intrinsic NEW_ACCOUNT state gas. + still pays the top-frame NEW_ACCOUNT state gas. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT intrinsic_calc = fork.transaction_intrinsic_cost_calculator() sender = pre.fund_eoa(amount=10**18) @@ -783,30 +765,32 @@ def test_create_tx_selfdestruct_initcode_state_gas( else: beneficiary = pre.fund_eoa(amount=0) + creates_new_beneficiary = beneficiary_kind == "empty" and tx_value > 0 + # `current_target` is added to `accessed_addresses` at message # entry, so SELFDESTRUCT to self skips the cold-access surcharge. if beneficiary_kind == "self": - init_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)( - beneficiary - ) + init_code = Op.SELFDESTRUCT.with_metadata( + address_warm=True, account_new=creates_new_beneficiary + )(beneficiary) else: - init_code = Op.SELFDESTRUCT(beneficiary) - intrinsic_total = intrinsic_calc( + init_code = Op.SELFDESTRUCT.with_metadata( + account_new=creates_new_beneficiary + )(beneficiary) + intrinsic_regular = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) - intrinsic_regular = intrinsic_total - new_account_state_gas - creates_new_beneficiary = beneficiary_kind == "empty" and tx_value > 0 - expected_state = new_account_state_gas + ( - new_account_state_gas if creates_new_beneficiary else 0 - ) + expected_state = fork.transaction_top_frame_state_gas( + contract_creation=True + ) + init_code.state_cost(fork) expected_regular = intrinsic_regular + init_code.regular_cost(fork) expected_gas_used = max(expected_regular, expected_state) tx = Transaction( to=None, data=init_code, - gas_limit=intrinsic_total + 100_000 + expected_state, + gas_limit=intrinsic_regular + 100_000 + expected_state, sender=sender, value=tx_value, ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py index f0df65f9569..c537b42088b 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py @@ -2092,7 +2092,6 @@ def test_same_tx_clear_then_reset_pre_delegated( intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( fork, authorization_list ) - assert top_frame_regular == fork.gas_costs().ACCOUNT_WRITE assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( intrinsic_regular, top_frame_regular, top_frame_state diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py index 653a986f8cc..71dcef99b11 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py @@ -436,8 +436,7 @@ def test_sstore_stipend_check_excludes_reservoir( With below_stipend: SSTORE fails (gas_left too low, reservoir ignored). With at_stipend: SSTORE has full regular gas and proceeds. """ - gas_costs = fork.gas_costs() - stipend = gas_costs.CALL_STIPEND + 1 + stipend = fork.call_value_stipend() + 1 sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: Op.SSTORE(0, 1) = 2 pushes + SSTORE opcode. @@ -446,12 +445,12 @@ def test_sstore_stipend_check_excludes_reservoir( # Full regular gas for the child (pushes + SSTORE regular cost). # State gas comes from the reservoir so it doesn't affect gas_left. - child_full_regular = child_code.gas_cost(fork) - sstore_state_gas + child_full_regular = child_code.regular_cost(fork) # below_stipend: give 1 less than stipend after pushes, fails check. # at_stipend: give full regular gas, passes check and completes. if gas_above_stipend < 0: - push_gas = 2 * gas_costs.VERY_LOW + push_gas = 2 * Op.PUSH1(0).regular_cost(fork) child_gas = push_gas + stipend - 1 else: child_gas = child_full_regular @@ -812,14 +811,8 @@ def test_sstore_restoration_charge_in_ancestor( refund must propagate up the chain to the ancestor that charged the 0 to x. A probe SSTORE sized to OOG by 1 detects any loss. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + sstore_state_gas - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 # Innermost frame does x to 0; each hop above delegates down. delegate_target = pre.deploy_contract( @@ -885,15 +878,9 @@ def test_sstore_restoration_sub_frame_revert( to OOG by 1 then fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 child_code = Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.REVERT(0, 0) child = pre.deploy_contract(code=child_code) @@ -940,16 +927,10 @@ def test_sstore_restoration_ancestor_revert( sized to OOG by 1 fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 set_op = Op.SSTORE.with_metadata( key_warm=False, @@ -1039,17 +1020,11 @@ def test_sstore_restoration_charge_in_ancestor_intermediate_revert( amount must reach the caller via `incorporate_child_on_error`. A probe SSTORE sized to OOG by 1 detects loss. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() # Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1, # so it OOGs by 1 when the reservoir is 0 and succeeds otherwise. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + sstore_state_gas - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 inner_code = ( Op.SSTORE.with_metadata( @@ -1137,15 +1112,9 @@ def test_sstore_restoration_create_init_revert( fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 init_code = Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.REVERT(0, 0) probe = pre.deploy_contract(code=Op.SSTORE(0, 1)) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py index 1337c77b230..d214e83bc21 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py @@ -41,25 +41,6 @@ pytestmark = pytest.mark.valid_from("Amsterdam") -def _access_list_floor_token_gas( - access_list: List[AccessList], fork: Fork -) -> int: - """ - Return the EIP-7981 calldata-floor-token gas the Amsterdam intrinsic - calculator charges for an access list. - - Every byte of each address (20) and storage key (32) is four floor - tokens, each priced at ``TX_DATA_TOKEN_FLOOR``. Subtracting this from - the measured intrinsic delta isolates the pure EIP-8038 per-entry - surcharge. - """ - total_bytes = 0 - for access in access_list: - total_bytes += len(access.address) - total_bytes += 32 * len(access.storage_keys) - return total_bytes * 4 * fork.gas_costs().TX_DATA_TOKEN_FLOOR - - def _make_access_list( n_addr: int, n_keys_each: int, *, duplicate: bool = False ) -> List[AccessList]: @@ -101,25 +82,7 @@ def test_access_list_intrinsic_surcharge( A simple value-less transaction then exercises the access list end to end. """ - gas_costs = fork.gas_costs() - intrinsic = fork.transaction_intrinsic_cost_calculator() - access_list = _make_access_list(n_addr, n_keys_each, duplicate=duplicate) - n_keys = n_addr * n_keys_each - - base = intrinsic(return_cost_deducted_prior_execution=True) - with_al = intrinsic( - access_list=access_list, - return_cost_deducted_prior_execution=True, - ) - surcharge = ( - with_al - base - _access_list_floor_token_gas(access_list, fork) - ) - expected = ( - n_addr * gas_costs.TX_ACCESS_LIST_ADDRESS - + n_keys * gas_costs.TX_ACCESS_LIST_STORAGE_KEY - ) - assert surcharge == expected contract = pre.deploy_contract(code=Op.STOP) tx = Transaction( @@ -149,8 +112,6 @@ def test_access_list_duplicate_address_key_intrinsic_and_warmth( runtime the slot is nonetheless warm on its first ``SLOAD`` (``WARM_SLOAD``), since warmth is set-membership, not a counter. """ - gas_costs = fork.gas_costs() - intrinsic = fork.transaction_intrinsic_cost_calculator() slot = 0x42 # First runtime SLOAD of the listed slot stores the warm access cost. @@ -175,20 +136,6 @@ def test_access_list_duplicate_address_key_intrinsic_and_warmth( AccessList(address=contract, storage_keys=[slot]), ] - base = intrinsic(return_cost_deducted_prior_execution=True) - with_al = intrinsic( - access_list=access_list, - return_cost_deducted_prior_execution=True, - ) - surcharge = ( - with_al - base - _access_list_floor_token_gas(access_list, fork) - ) - expected_surcharge = ( - 2 * gas_costs.TX_ACCESS_LIST_ADDRESS - + 2 * gas_costs.TX_ACCESS_LIST_STORAGE_KEY - ) - assert surcharge == expected_surcharge - expected_gas = Op.SLOAD(key_warm=True).gas_cost(fork) tx = Transaction( to=contract, @@ -218,8 +165,7 @@ def test_access_list_warms_storage_slot( overwrite of a non-zero original to a new non-zero value pays ``WARM_SLOAD + STORAGE_WRITE``. """ - gas_costs = fork.gas_costs() - very_low = gas_costs.VERY_LOW + very_low = Op.PUSH1(0).regular_cost(fork) slot = 0x42 if op == "SLOAD": diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py index 37e543f4376..c58fc42934d 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py @@ -89,8 +89,6 @@ def test_call_access_gas( EIP-8038 charges ``COLD_ACCOUNT_ACCESS`` (3,000) cold and ``WARM_ACCESS`` (100) warm for all four call opcodes. """ - gas_costs = fork.gas_costs() - target = pre.deploy_contract(Op.STOP) measured_code = call_opcode(gas=0, address=target) @@ -99,11 +97,8 @@ def test_call_access_gas( pre, fork, measured_code, call_opcode(address_warm=False) ) - expected_gas = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # Cross-check the framework opcode model agrees with the formula. - assert expected_gas == cost_metadata.gas_cost(fork) + # The opcode's own cost is the expected access gas. + expected_gas = cost_metadata.gas_cost(fork) access_list = ( [AccessList(address=target, storage_keys=[])] if warm else None @@ -143,24 +138,14 @@ def test_call_value_alive_target_gas( the caller is ``access + ACCOUNT_WRITE`` while the *charged* schedule is ``access + CALL_VALUE``. Both are asserted. """ - gas_costs = fork.gas_costs() transfers_value = call_opcode in (Op.CALL, Op.CALLCODE) - # Verify the EIP-8038 decomposition of the value-transfer charge. - assert gas_costs.CALL_VALUE == gas_costs.ACCOUNT_WRITE + ( - gas_costs.CALL_STIPEND - ) # The measured-vs-charged duality below hinges on the callee being a - # pure `STOP`: it executes no opcodes, so the forwarded `CALL_STIPEND` - # is wholly unused and returned. Pin that the callee is exactly the - # single zero byte with no gas cost, and that the returned stipend is - # precisely `CALL_VALUE - ACCOUNT_WRITE`. + # pure `STOP`: it executes no opcodes, so the forwarded value-call + # stipend is wholly unused and returned. callee = Op.STOP assert bytes(callee) == b"\x00" assert callee.gas_cost(fork) == 0 - assert gas_costs.CALL_VALUE - gas_costs.ACCOUNT_WRITE == ( - gas_costs.CALL_STIPEND - ) # Alive target with balance so no account creation occurs. target = pre.deploy_contract(callee, balance=1) @@ -191,22 +176,14 @@ def test_call_value_alive_target_gas( pre, fork, measured_code, own_cold, balance=1 ) - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # Charged schedule: access + CALL_VALUE (verified via the opcode - # model). CALL gas is wholly regular under EIP-8038 (no state map). - charged_gas = access_cost + ( - gas_costs.CALL_VALUE if transfers_value else 0 - ) - assert charged_gas == cost_metadata.gas_cost(fork) + # CALL gas is wholly regular under EIP-8038 (no state map). assert cost_metadata.state_cost(fork) == 0 - # Consumed gas: the STOP callee returns the forwarded CALL_STIPEND, - # so the caller's measured consumption is access + ACCOUNT_WRITE for - # value transfers, and just access otherwise. - measured_gas = access_cost + ( - gas_costs.ACCOUNT_WRITE if transfers_value else 0 + # Consumed gas: the STOP callee returns the forwarded stipend, so the + # caller's measured consumption is the opcode's charged cost minus the + # stipend for value transfers, and just the access cost otherwise. + measured_gas = cost_metadata.gas_cost(fork) - ( + fork.call_value_stipend() if transfers_value else 0 ) access_list = ( @@ -237,7 +214,6 @@ def test_callcode_value_to_nonexistent_no_new_account( created. The block ``gas_used`` therefore equals the regular tx cost with ``CALL_VALUE`` but with no 183,600 state-gas component. """ - gas_costs = fork.gas_costs() intrinsic = fork.transaction_intrinsic_cost_calculator()() target = 0xDEAD # non-existent @@ -260,24 +236,18 @@ def test_callcode_value_to_nonexistent_no_new_account( caller_code = Op.POP(callcode) + Op.STOP caller = pre.deploy_contract(code=caller_code, balance=1) - # CALLCODE-to-nonexistent regular charge: access + CALL_VALUE, no - # NEW_ACCOUNT (asserted via the metadata-only opcode model). + # CALLCODE carries no state-gas (NEW_ACCOUNT) component: the value + # stays in the caller's own context, so no beneficiary is created. callcode_meta = Op.CALLCODE(address_warm=False, value_transfer=True) - assert callcode_meta.gas_cost(fork) == gas_costs.COLD_ACCOUNT_ACCESS + ( - gas_costs.CALL_VALUE - ) - # CALLCODE carries no state-gas (NEW_ACCOUNT) component. assert callcode_meta.state_cost(fork) == 0 # Whole tx is regular gas; no NEW_ACCOUNT state component appears. - # The CALLCODE forwards CALL_STIPEND to the callee, which (running in - # the caller's own context with empty code) leaves it unused and - # returns it, so consumed gas is the charge minus the stipend. + # The CALLCODE forwards the value-call stipend to the callee, which + # (running in the caller's own context with empty code) leaves it + # unused and returns it, so consumed gas is the charge minus stipend. expected_gas_used = ( - intrinsic + caller_code.gas_cost(fork) - gas_costs.CALL_STIPEND + intrinsic + caller_code.gas_cost(fork) - fork.call_value_stipend() ) - # Guard the no-state assertion: NEW_ACCOUNT would dominate if charged. - assert expected_gas_used < gas_costs.NEW_ACCOUNT tx = Transaction( to=caller, @@ -305,16 +275,13 @@ def test_call_value_to_new_account_seam( dimension. The block header reflects ``max(regular, state)``, which is dominated by the state charge. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT intrinsic = fork.transaction_intrinsic_cost_calculator()() # Fresh, value-receiving target (state-empty, will be created). target = pre.fund_eoa(amount=0) - # Metadata-bearing CALL so `caller_code.gas_cost(fork)` folds the - # value transfer and account-creation charges; we then split off the - # NEW_ACCOUNT state component for the 2D header accounting. + # Metadata-bearing CALL so its cost splits into the regular + # (access + value transfer) and state (NEW_ACCOUNT) dimensions. call = Op.CALL.with_metadata( address_warm=False, value_transfer=True, account_new=True )( @@ -329,23 +296,15 @@ def test_call_value_to_new_account_seam( caller_code = Op.POP(call) + Op.STOP caller = pre.deploy_contract(code=caller_code, balance=1) - # Regular dimension: access + value (NOT new account, which is the - # state dimension). Asserted via the metadata-only opcode model. - call_meta = Op.CALL( - address_warm=False, value_transfer=True, account_new=True - ) - call_regular = call_meta.gas_cost(fork) - new_account_state_gas - assert call_regular == gas_costs.COLD_ACCOUNT_ACCESS + gas_costs.CALL_VALUE - assert call_regular == 13_300 - - # block_gas_used = max(block_regular, block_state). The CALL opcode - # has no state-gas map, so its NEW_ACCOUNT charge spills as regular - # gas in the bytecode total; strip it back out to isolate the - # regular axis and re-add NEW_ACCOUNT explicitly on the state axis. - tx_regular = intrinsic + caller_code.gas_cost(fork) - new_account_state_gas - tx_state = new_account_state_gas + new_account_state_gas = call.state_cost(fork) + + # block_gas_used = max(block_regular, block_state). The CALL's + # NEW_ACCOUNT lands on the state axis; the regular axis is the + # access plus value-transfer cost. + tx_regular = intrinsic + caller_code.regular_cost(fork) + tx_state = caller_code.state_cost(fork) expected_gas_used = max(tx_regular, tx_state) - # State must dominate here, proving the 183,600 hit the state axis. + # State must dominate here, proving NEW_ACCOUNT hit the state axis. assert expected_gas_used == new_account_state_gas tx = Transaction( @@ -390,8 +349,6 @@ def test_call_to_delegated_target_double_access( ``STATICCALL`` carry no value but still pay the delegation surcharge. """ - gas_costs = fork.gas_costs() - # Final code-bearing account that the delegation points at. delegate = pre.deploy_contract(Op.STOP) # EOA delegated (EIP-7702) to `delegate`. @@ -407,16 +364,8 @@ def test_call_to_delegated_target_double_access( pre, fork, measured_code, call_opcode(address_warm=False) ) - target_cost = ( - gas_costs.WARM_ACCESS if target_warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - delegate_cost = ( - gas_costs.WARM_ACCESS - if delegate_warm - else gas_costs.COLD_ACCOUNT_ACCESS - ) - expected_gas = target_cost + delegate_cost - assert expected_gas == cost_metadata.gas_cost(fork) + # The opcode's own cost folds the target and delegate accesses. + expected_gas = cost_metadata.gas_cost(fork) # Warm the target and/or the delegate leaf via the access list. access_entries = [] @@ -493,8 +442,6 @@ def test_call_self_is_warm( The current target is in the accessed-addresses set on message entry, so a call to ``ADDRESS`` pays only ``WARM_ACCESS`` (100). """ - gas_costs = fork.gas_costs() - # `Op.ADDRESS` is the call's address argument, embedded inside the # runnable call; the self address is in the accessed set on entry, so # the call is warm. The overhead subtracts the call's own cold cost, @@ -505,7 +452,6 @@ def test_call_self_is_warm( ) expected_gas = call_opcode(address_warm=True).gas_cost(fork) - assert expected_gas == gas_costs.WARM_ACCESS tx = Transaction(to=measure_address, sender=pre.fund_eoa()) @@ -539,15 +485,15 @@ def test_call_forwarded_gas_63_64( ``gas_left`` already net of the post-8038 cold access cost (not before it, and not double-charging it). """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: a single cold zero-to-nonzero SSTORE as proof of execution. # Its regular need is the two operand pushes plus the cold storage # write (the state portion is funded separately via the reservoir, # which is passed to the child in full with no 63/64 rule). - child = pre.deploy_contract(Op.SSTORE(0, 1)) - child_regular = 2 * gas_costs.VERY_LOW + gas_costs.COLD_STORAGE_WRITE + child_code = Op.SSTORE(0, 1) + child = pre.deploy_contract(child_code) + child_regular = child_code.regular_cost(fork) # Smallest budget whose 63/64 floor still reaches `child_regular`. forward_budget = child_regular * 64 // 63 @@ -557,24 +503,21 @@ def test_call_forwarded_gas_63_64( # Wrapper: cold zero-value CALL requesting max gas (so the forwarded # amount is bound by `gas_left`, not by the request). ret_size=0 # avoids any memory-expansion term. - wrapper = pre.deploy_contract( - Op.CALL( - gas=0xFFFFFFFF, - address=child, - value=0, - args_offset=0, - args_size=0, - ret_offset=0, - ret_size=0, - ) + wrapper_call = Op.CALL( + gas=0xFFFFFFFF, + address=child, + value=0, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, ) + wrapper = pre.deploy_contract(wrapper_call) - # At the wrapper's CALL the access charge (`extra_gas`) is deducted - # first, leaving exactly `forward_budget` as `gas_left` for the 63/64 - # floor. The seven CALL operand pushes precede it. - wrapper_pushes = 7 * gas_costs.VERY_LOW - extra_gas = gas_costs.COLD_ACCOUNT_ACCESS # cold call, value 0 - wrapper_gas = wrapper_pushes + extra_gas + forward_budget + # At the wrapper's CALL the cold access charge is deducted first + # (folded with the operand pushes into its regular cost), leaving + # exactly `forward_budget` as `gas_left` for the 63/64 floor. + wrapper_gas = wrapper_call.regular_cost(fork) + forward_budget # Outer caller hands the wrapper exactly `wrapper_gas`. caller = pre.deploy_contract( @@ -610,9 +553,7 @@ def test_account_warmth_reverts_on_subcall_revert( rolled back on revert (mirrors the ``SLOAD`` warmth-revert case for the account dimension). """ - gas_costs = fork.gas_costs() cold_gas = Op.BALANCE(address_warm=False).gas_cost(fork) - assert cold_gas == gas_costs.COLD_ACCOUNT_ACCESS # Address whose warmth we probe; left out of the access list so its # first runtime touch is cold. @@ -664,8 +605,6 @@ def test_call_to_double_delegated_target_single_hop( second hop, so ``final``'s leaf is not charged. Both the framework opcode model and a runtime ``CodeGasMeasure`` confirm the value. """ - gas_costs = fork.gas_costs() - # A -> B -> C delegation chain. `mid` is an EOA whose code is the # 7702 delegation designator pointing at `final`; `target` delegates # to `mid` in turn. @@ -680,8 +619,8 @@ def test_call_to_double_delegated_target_single_hop( delegated_address=True, delegated_address_warm=False, ) - expected_gas = 2 * gas_costs.COLD_ACCOUNT_ACCESS - assert expected_gas == cost_metadata.gas_cost(fork) + # Cold target leaf plus cold delegation leaf; no state gas. + expected_gas = cost_metadata.gas_cost(fork) assert cost_metadata.state_cost(fork) == 0 measured_code = Op.CALL(gas=0, address=target) @@ -711,8 +650,6 @@ def test_call_precompile_is_warm( every transaction, so a call to one pays only ``WARM_ACCESS`` (100). The identity precompile (address 4) is used as the target. """ - gas_costs = fork.gas_costs() - identity_precompile = Address(4) measured_code = call_opcode(gas=0, address=identity_precompile) @@ -721,7 +658,6 @@ def test_call_precompile_is_warm( ) expected_gas = call_opcode(address_warm=True).gas_cost(fork) - assert expected_gas == gas_costs.WARM_ACCESS tx = Transaction(to=measure_address, sender=pre.fund_eoa()) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py index d76057ecf1e..f91e6091f87 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py @@ -74,15 +74,6 @@ def test_create_regular_gas( account-creation state gas is excluded by subtracting ``create_state_gas(0)``. """ - gas_costs = fork.gas_costs() - # The EIP-8038 CREATE regular base equals ACCOUNT_WRITE + - # COLD_STORAGE_ACCESS = 11,000. - assert gas_costs.OPCODE_CREATE_BASE == 11_000 - assert ( - gas_costs.OPCODE_CREATE_BASE - == gas_costs.ACCOUNT_WRITE + gas_costs.COLD_STORAGE_ACCESS - ) - # Isolate the regular dimension: opcode total minus its account # creation state gas (the only state component carried by the CREATE # opcode itself; code deposit is charged on RETURN inside initcode). @@ -93,17 +84,6 @@ def test_create_regular_gas( # Equivalent isolation via the regular_cost helper. assert regular_gas == create_meta.regular_cost(fork) - init_code_words = (init_code_size + 31) // 32 - expected_regular = ( - gas_costs.OPCODE_CREATE_BASE - + gas_costs.CODE_INIT_PER_WORD * init_code_words - ) - if create_opcode == Op.CREATE2: - expected_regular += ( - gas_costs.OPCODE_KECCAK256_PER_WORD * init_code_words - ) - assert regular_gas == expected_regular - # Runtime confirmation via CodeGasMeasure: a factory whose CREATE # deploys empty code, so no code-deposit state gas is charged and the # only state component is the account-creation gas funded from the @@ -125,7 +105,8 @@ def test_create_regular_gas( if create_opcode == Op.CREATE2 else Op.CREATE(value=0, offset=0, size=init_code_size) ) - arg_pushes = (4 if create_opcode == Op.CREATE2 else 3) * gas_costs.VERY_LOW + push_cost = Op.PUSH1(0).regular_cost(fork) + arg_pushes = (4 if create_opcode == Op.CREATE2 else 3) * push_cost memory_setup = ( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE, new_memory_size=init_code_size) @@ -177,36 +158,24 @@ def test_create2_keccak_word_delta( regular cost shared with ``CREATE``. Both opcodes carry the identical EIP-8038 ``CREATE_ACCESS`` base and EIP-3860 word cost. - The regular-gas delta is asserted via the opcode model - (``create2_regular - create_regular`` equals the keccak word - surcharge). At runtime a factory then measures a single ``CREATE2`` - with ``CodeGasMeasure`` and stores its absolute regular cost: the - surcharge is established by the model assertion, and the runtime leg - confirms the absolute ``CREATE2`` regular cost. + A factory measures a single ``CREATE2`` with ``CodeGasMeasure`` and + stores its absolute regular cost, confirming the opcode's own + ``regular_cost`` (which folds the keccak word surcharge) against the + runtime charge. """ - gas_costs = fork.gas_costs() - init_code_words = (init_code_size + 31) // 32 - keccak_surcharge = gas_costs.OPCODE_KECCAK256_PER_WORD * init_code_words - - create_regular = Op.CREATE(init_code_size=init_code_size).regular_cost( - fork - ) create2_regular = Op.CREATE2(init_code_size=init_code_size).regular_cost( fork ) - assert create2_regular - create_regular == keccak_surcharge - - # Runtime confirmation. Init code is all-zero bytes (`STOP`), so the - # child frame halts immediately (zero gas) depositing empty code; the - # CREATE2 charges no code-deposit state gas and no child execution gas - # is folded into the measurement. The single CREATE2 regular cost is - # measured via CodeGasMeasure with a reservoir sized for its account - # creation state gas, keeping the GAS-measured `gas_left` free of - # state-gas spill. The opcode-model assertion above is the - # load-bearing keccak-delta check; this confirms the absolute value. + + # Init code is all-zero bytes (`STOP`), so the child frame halts + # immediately (zero gas) depositing empty code; the CREATE2 charges no + # code-deposit state gas and no child execution gas is folded into the + # measurement. The single CREATE2 regular cost is measured via + # CodeGasMeasure with a reservoir sized for its account creation state + # gas, keeping the GAS-measured `gas_left` free of state-gas spill. padded = b"\x00" * init_code_size - push4 = 4 * gas_costs.VERY_LOW + push4 = 4 * Op.PUSH1(0).regular_cost(fork) storage = Storage() measure_create2 = CodeGasMeasure( code=Op.CREATE2(value=0, offset=0, size=init_code_size, salt=0), @@ -303,7 +272,9 @@ def exact_execution_gas( flat regular per-byte deposit cost. The single call is therefore correct in either regime. """ - execution = exact_intrinsic_gas + fork.gas_costs().NEW_ACCOUNT + execution = exact_intrinsic_gas + fork.transaction_top_frame_state_gas( + contract_creation=True + ) execution += initcode.execution_gas(fork) execution += initcode.deployment_gas(fork) return execution @@ -377,7 +348,9 @@ def test_create_tx_gas_boundary( elif succeeds: # Fresh target: top-frame NEW_ACCOUNT plus the per-byte code # deposit are the state-gas axis; the rest is regular. - state_used = fork.gas_costs().NEW_ACCOUNT + state_used = fork.transaction_top_frame_state_gas( + contract_creation=True + ) state_used += fork.code_deposit_state_gas( code_size=len(initcode.deploy_code) ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py index 210879bf6f3..667cedbb638 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py @@ -180,7 +180,7 @@ def test_selfdestruct_funds_new_account( tx = Transaction( to=suicidal, gas_limit=1_000_000, - state_gas_reservoir=fork.gas_costs().NEW_ACCOUNT, + state_gas_reservoir=Op.SELFDESTRUCT(account_new=True).state_cost(fork), sender=pre.fund_eoa(), ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py index 90f446398ce..5470eeb0436 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py @@ -97,7 +97,7 @@ def test_ext_code_opcode_gas( more than ``BALANCE``/``EXTCODEHASH`` at equal warmth (the second, code-reading database access). """ - gas_costs = fork.gas_costs() + del code_read_surcharge # encoded in `cost_metadata` target = pre.deploy_contract(Op.STOP) @@ -117,13 +117,10 @@ def test_ext_code_opcode_gas( ) measure_address = pre.deploy_contract(code=code_gas_measure) - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - surcharge = gas_costs.WARM_ACCESS if code_read_surcharge else 0 - expected_gas = access_cost + surcharge - # Cross-check the framework opcode model agrees with the formula. - assert expected_gas == cost_metadata(warm).gas_cost(fork) + # The opcode's own cost is the expected measured gas: it folds the + # access cost and, for EXTCODESIZE/EXTCODECOPY, the code-read + # surcharge. + expected_gas = cost_metadata(warm).gas_cost(fork) # Warm the target via the access list when required; the cold case # leaves it absent so its first runtime access is cold. @@ -163,8 +160,6 @@ def test_extcodecopy_nonzero_composes_additively( flat add-on that does not interact with the copy or memory terms, so the measured gas must equal the sum of all four components. """ - gas_costs = fork.gas_costs() - # Target carries enough code to satisfy the copy; STOP padding keeps # it a deployable contract with a non-empty code hash. target = pre.deploy_contract(Op.STOP * copy_size) @@ -192,21 +187,6 @@ def test_extcodecopy_nonzero_composes_additively( ) expected_gas = oracle.gas_cost(fork) - # Additive decomposition the surcharge must satisfy. - words = (copy_size + 31) // 32 - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - memory_expansion = fork.memory_expansion_gas_calculator()( - new_bytes=copy_size, previous_bytes=0 - ) - assert expected_gas == ( - access_cost - + gas_costs.WARM_ACCESS # EIP-8038 code-read surcharge - + gas_costs.OPCODE_COPY_PER_WORD * words - + memory_expansion - ) - code_gas_measure = CodeGasMeasure( code=measured_code, overhead_cost=measured_code.gas_cost(fork) - oracle.gas_cost(fork), @@ -243,16 +223,12 @@ def test_extcodehash_empty_account( or ``WARM_ACCESS`` (warm) regardless of the target being empty. The returned hash of an empty/non-existent account is ``0``. """ - gas_costs = fork.gas_costs() - # A non-existent (empty) target: never deployed, no balance, no code. empty_addr = Address(0xDEAD) - expected_gas = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # No code-read surcharge for EXTCODEHASH; the opcode model must agree. - assert expected_gas == Op.EXTCODEHASH(address_warm=warm).gas_cost(fork) + # EXTCODEHASH reads only the account leaf (no code-read surcharge), so + # its bare cost is the plain account access. + expected_gas = Op.EXTCODEHASH(address_warm=warm).gas_cost(fork) # Measure the access cost, then store the returned hash so the # empty-account 0 result is asserted alongside the pricing. The diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py index a1de15f4108..cc7f3d21802 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py @@ -5,8 +5,9 @@ "Same operation, different gas" across the Amsterdam boundary. A block at ``timestamp=14_999`` runs under the pre-fork (parent) schedule; a block at ``timestamp=15_000`` runs under the EIP-8038 schedule. Every -before/after magnitude is derived from -``fork.fork_at(timestamp=...).gas_costs()`` — nothing is hardcoded. +before/after magnitude is derived from the opcode's own cost at each +fork (``bytecode.gas_cost`` / ``regular_cost`` / ``refund``) — nothing +is hardcoded. Two proof styles are used: @@ -14,11 +15,12 @@ access and the ``EXT*`` code-read surcharge) are measured exactly with ``CodeGasMeasure`` in each regime and asserted against the derived cost. -* Constant repricings that the runtime opcode model cannot isolate - without state-gas confounders (``CALL_VALUE``, ``CREATE`` base, - ``SELFDESTRUCT`` account-write) are asserted at the constant level - from the derived schedules while the operation is still exercised in - both blocks to prove it runs in each regime. +* Repricings that the runtime opcode model cannot isolate without + state-gas confounders (``CALL`` with value, ``CREATE``, + ``SELFDESTRUCT`` to a fresh beneficiary, ``SSTORE`` first change) are + exercised in both blocks to prove the operation still runs in each + regime, with the ``SSTORE`` regular/state split and clear refund + compared across forks via the bytecode's own cost methods. * The authorization intrinsic rise is proven behaviourally: a tx whose ``gas_limit`` equals the old auth intrinsic is valid before the fork and rejected with ``INTRINSIC_GAS_TOO_LOW`` after. @@ -126,8 +128,10 @@ def test_cold_account_access_at_transition( before = fork.fork_at(timestamp=BEFORE_TS) after = fork.fork_at(timestamp=AFTER_TS) - cost_before = before.gas_costs().COLD_ACCOUNT_ACCESS - cost_after = after.gas_costs().COLD_ACCOUNT_ACCESS + # BALANCE's bare cost equals COLD_ACCOUNT_ACCESS in each regime. + cold_balance = Op.BALANCE.with_metadata(address_warm=False) + cost_before = cold_balance.gas_cost(before) + cost_after = cold_balance.gas_cost(after) assert cost_after > cost_before target = pre.deploy_contract(code=Op.STOP) @@ -179,7 +183,6 @@ def test_ext_code_surcharge_at_transition( after ) - Op.BALANCE(address_warm=True).gas_cost(after) assert surcharge_before == 0 - assert surcharge_after == after.gas_costs().WARM_ACCESS assert surcharge_after > surcharge_before extcodesize_cost_before = Op.EXTCODESIZE(address_warm=False).gas_cost( @@ -221,13 +224,6 @@ def test_call_value_cost_at_transition( is exercised in both blocks to prove it still succeeds in each regime. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - call_value_before = before.gas_costs().CALL_VALUE - call_value_after = after.gas_costs().CALL_VALUE - assert call_value_after > call_value_before - callee_before = pre.deploy_contract(code=Op.STOP, balance=0) callee_after = pre.deploy_contract(code=Op.STOP, balance=0) @@ -271,17 +267,6 @@ def test_create_base_cost_at_transition( asserted from the derived schedules and a ``CREATE`` is exercised in both blocks to prove it still deploys. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - create_base_before = before.gas_costs().OPCODE_CREATE_BASE - create_base_after = after.gas_costs().OPCODE_CREATE_BASE - assert create_base_after != create_base_before - # Post-fork base is the harmonized ACCOUNT_WRITE + COLD_STORAGE_ACCESS. - assert create_base_after == ( - after.gas_costs().ACCOUNT_WRITE + after.gas_costs().COLD_STORAGE_ACCESS - ) - init_code = Op.STOP init_word = int.from_bytes(bytes(init_code), "big") << ( 256 - 8 * len(init_code) @@ -332,13 +317,6 @@ def test_selfdestruct_account_write_at_transition( ``SELFDESTRUCT`` to a fresh beneficiary is exercised in both blocks to prove it still runs. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - account_write_before = before.gas_costs().ACCOUNT_WRITE - account_write_after = after.gas_costs().ACCOUNT_WRITE - assert account_write_after > account_write_before - # Fresh empty beneficiaries so the positive-balance-to-empty branch # that adds ACCOUNT_WRITE is taken in each regime. beneficiary_before = pre.fund_eoa(amount=0) @@ -406,20 +384,12 @@ def test_sstore_write_cost_at_transition( assert state_after > 0 assert total_after != total_before - # After the fork the regular portion is the EIP-8038 split: - # COLD_STORAGE_ACCESS plus the standalone STORAGE_WRITE (modeled as - # COLD_STORAGE_WRITE minus COLD_STORAGE_ACCESS). - after_costs = after.gas_costs() - storage_write_after = ( - after_costs.COLD_STORAGE_WRITE - after_costs.COLD_STORAGE_ACCESS - ) - assert regular_after == ( - after_costs.COLD_STORAGE_ACCESS + storage_write_after - ) - # The storage-clear refund also rises across the boundary. - refund_before = before.gas_costs().REFUND_STORAGE_CLEAR - refund_after = after_costs.REFUND_STORAGE_CLEAR + clear_sstore = Op.SSTORE.with_metadata( + original_value=1, current_value=1, new_value=0 + ) + refund_before = clear_sstore.refund(before) + refund_after = clear_sstore.refund(after) assert refund_after > refund_before # Exercise the zero-to-nonzero SSTORE in both regimes; the slot ends diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py index 2fb565c7a36..99d0797731c 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py @@ -66,30 +66,6 @@ pytestmark = pytest.mark.valid_from("Amsterdam") -def _selfdestruct_regular(fork: Fork, *, warm: bool, account_new: bool) -> int: - """ - Return the EIP-8038 *regular* gas charged by SELFDESTRUCT. - - ``OPCODE_SELFDESTRUCT_BASE + access + (ACCOUNT_WRITE if account_new)``; - the ``GAS_NEW_ACCOUNT`` account-creation cost is the EIP-8037 state - dimension and is excluded from ``regular_cost``. - """ - gas_costs = fork.gas_costs() - regular = Op.SELFDESTRUCT( - address_warm=warm, account_new=account_new - ).regular_cost(fork) - # SELFDESTRUCT charges a cold-access surcharge only; a warm - # beneficiary adds nothing beyond the base (no WARM_ACCESS). - access = 0 if warm else gas_costs.COLD_ACCOUNT_ACCESS - expected = ( - gas_costs.OPCODE_SELFDESTRUCT_BASE - + access - + (gas_costs.ACCOUNT_WRITE if account_new else 0) - ) - assert regular == expected - return regular - - def _destructor_code( beneficiary: Address | Bytecode, *, warm: bool, account_new: bool ) -> Bytecode: @@ -123,11 +99,7 @@ def test_selfdestruct_new_beneficiary_regular_gas( EIP-8037 suite asserts it); here it is funded from the reservoir and the value transfer to the new beneficiary confirms the path. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - - regular = _selfdestruct_regular(fork, warm=warm, account_new=True) - assert regular == (13_000 if warm else 16_000) + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = Address(0xDEAD) # empty, non-existent @@ -176,9 +148,6 @@ def test_selfdestruct_alive_beneficiary_no_account_write( ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 cold) and no state gas is charged. The block header reflects the pure regular consumption. """ - regular = _selfdestruct_regular(fork, warm=warm, account_new=False) - assert regular == (5_000 if warm else 8_000) - beneficiary = pre.fund_eoa(amount=1) # alive destructor_code = _destructor_code( @@ -248,9 +217,6 @@ def test_selfdestruct_codebearing_zero_balance_beneficiary_no_account_write( alive-via-balance case, which exercises the same path through a different liveness source. """ - regular = _selfdestruct_regular(fork, warm=warm, account_new=False) - assert regular == (5_000 if warm else 8_000) - # Alive via code (non-empty code), with zero balance. beneficiary = pre.deploy_contract(code=Op.STOP, balance=0) @@ -316,9 +282,6 @@ def test_selfdestruct_zero_balance_no_account_write( No value is transferred, so even a non-existent beneficiary is not created: regular = ``5,000 + access`` and no state gas is charged. """ - regular = _selfdestruct_regular(fork, warm=warm, account_new=False) - assert regular == (5_000 if warm else 8_000) - beneficiary = Address(0xDEAD) # non-existent, but no value sent destructor_code = _destructor_code( @@ -391,12 +354,6 @@ def test_selfdestruct_self_or_precompile_beneficiary( transfer would otherwise create one and charge ``GAS_NEW_ACCOUNT`` on the state axis). """ - gas_costs = fork.gas_costs() - - regular = _selfdestruct_regular(fork, warm=True, account_new=False) - # SELFDESTRUCT has no warm-access surcharge: warm == base only. - assert regular == gas_costs.OPCODE_SELFDESTRUCT_BASE - if beneficiary_kind == "self": # Self is warm on entry; the PUSH is `ADDRESS` (BASE=2). A # non-zero balance is transferred to self (no creation). @@ -469,15 +426,7 @@ def test_selfdestruct_oog_boundary( gas short OOGs (CALL returns 0) before the value transfer, so the beneficiary is never created. """ - gas_costs = fork.gas_costs() - beneficiary = Address(0xDEAD) - regular = _selfdestruct_regular(fork, warm=False, account_new=True) - assert regular == ( - gas_costs.OPCODE_SELFDESTRUCT_BASE - + gas_costs.COLD_ACCOUNT_ACCESS - + gas_costs.ACCOUNT_WRITE - ) destructor_code = _destructor_code( beneficiary, warm=False, account_new=True @@ -564,9 +513,6 @@ def test_same_tx_created_selfdestruct_self_burn( # Self-beneficiary on a balance-bearing same-tx-created contract is # alive: account_new is false, so only the warm base is charged. - regular = _selfdestruct_regular(fork, warm=True, account_new=False) - assert regular == fork.gas_costs().OPCODE_SELFDESTRUCT_BASE - # Creation intrinsic is regular-only under EIP-2780; the pre-existing # target adds no top-frame NEW_ACCOUNT and the self-burn adds no state # gas, so net state gas is zero. The regular consumption exceeds the @@ -627,7 +573,6 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( created target is alive at message entry (EIP-8037), while the fresh beneficiary's ``NEW_ACCOUNT`` persists. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT intrinsic_calc = fork.transaction_intrinsic_cost_calculator() amount = 1 @@ -644,16 +589,14 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( init_code = Op.SELFDESTRUCT.with_metadata( address_warm=False, account_new=True )(beneficiary) + # The creation NEW_ACCOUNT is refunded (target alive at entry) and is + # not part of the intrinsic under EIP-2780; only the fresh + # beneficiary's NEW_ACCOUNT (the SELFDESTRUCT state cost) persists. + new_account_state_gas = init_code.state_cost(fork) - regular = _selfdestruct_regular(fork, warm=False, account_new=True) - assert regular == 16_000 - - intrinsic_total = intrinsic_calc( + intrinsic_regular = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) - # The creation NEW_ACCOUNT is refunded (target alive at entry); only - # the fresh beneficiary's NEW_ACCOUNT remains as net state gas. - intrinsic_regular = intrinsic_total - new_account_state_gas expected_state = new_account_state_gas expected_regular = intrinsic_regular + init_code.regular_cost(fork) expected_gas_used = max(expected_regular, expected_state) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py index a3a50b782a5..1d4a9b0624c 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py @@ -11,10 +11,11 @@ cold/warm account-access costs that an authorized delegation incurs when later accessed by a ``CALL``. -The regular per-authorization intrinsic magnitude is -``fork.gas_costs().REGULAR_PER_AUTH_BASE_COST`` (``7816`` on Amsterdam: +The regular per-authorization intrinsic magnitude is the fixed +per-authorization base cost charged by the intrinsic (on Amsterdam, ``101 * 16`` calldata tokens plus the ``3000`` ecrecover, ``3000`` cold -and ``2 * 100`` warm accesses of the EIP-7702 base). The top-frame +and ``2 * 100`` warm accesses of the EIP-7702 base), isolated here as +the intrinsic delta of adding one authorization. The top-frame state charges are asserted by the sibling ``eip8037_state_creation_gas_cost_increase`` and ``eip2780_reduce_intrinsic_tx_gas`` suites; this suite does not @@ -34,6 +35,7 @@ CodeGasMeasure, Environment, Fork, + Hash, Op, StateTestFiller, Storage, @@ -58,11 +60,19 @@ def _regular_per_auth(fork: Fork) -> int: authorization. Under EIP-2780 the intrinsic charges only the state-independent - ``REGULAR_PER_AUTH_BASE_COST`` per authorization; the account-write - (``ACCOUNT_WRITE``) and delegation-write (``AUTH_BASE``) costs are - charged lazily at the top frame, not in the intrinsic. + per-authorization base cost; the account-write (``ACCOUNT_WRITE``) + and delegation-write (``AUTH_BASE``) costs are charged lazily at the + top frame, not in the intrinsic. Isolated as the intrinsic delta of + adding one authorization. """ - return fork.gas_costs().REGULAR_PER_AUTH_BASE_COST + calc = fork.transaction_intrinsic_cost_calculator() + return calc( + authorization_list_or_count=1, + return_cost_deducted_prior_execution=True, + ) - calc( + authorization_list_or_count=0, + return_cost_deducted_prior_execution=True, + ) def _regular_intrinsic( @@ -490,12 +500,13 @@ def test_auth_account_warming( ``COLD_ACCOUNT_ACCESS``. When the sponsor is the authority, the authority is already warm for the same reason. - All costs are taken from ``fork.gas_costs()`` so the repricing is - asserted against the live schedule rather than hardcoded constants. + All costs are derived from the fork's opcode schedule (not + hardcoded) so the repricing is asserted against the live values. """ - gas_costs = fork.gas_costs() - cold = gas_costs.COLD_ACCOUNT_ACCESS - warm = gas_costs.WARM_ACCESS + # Bare account-access costs, isolated via BALANCE (no code-read + # surcharge and no operand push). + cold = Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork) + warm = Op.BALANCE.with_metadata(address_warm=True).gas_cost(fork) delegation_target = pre.deploy_contract(code=Op.STOP) @@ -529,7 +540,7 @@ def test_auth_account_warming( # Measure the cost of a single CALL to the authority. The CALL # opcode leaves one stack item (success); the overhead is the PUSHes # for its arguments. - overhead_cost = gas_costs.VERY_LOW * len(Op.CALL.kwargs) + overhead_cost = Op.PUSH1(0).regular_cost(fork) * len(Op.CALL.kwargs) storage = Storage() callee_code = CodeGasMeasure( code=Op.CALL(gas=0, address=authority), @@ -573,7 +584,29 @@ def test_many_auths_block_limit( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - per_auth_total = fork.gas_costs().AUTH_PER_EMPTY_ACCOUNT + contract = pre.deploy_contract(code=Op.STOP) + + # Per-authorization total for a fresh (empty) authority: the regular + # intrinsic base plus the top-frame account-write, account-creation + # and delegation-write charges, derived from the fork's calculators + # so it tracks the repricing. The probe only feeds the gas + # calculators, so it is signed with a fixed dummy key rather than a + # throwaway pre-state signer. + probe_auth = AuthorizationTuple( + address=contract, + nonce=0, + secret_key=Hash(1), + creates_account=True, + writes_delegation=True, + first_write=True, + ) + per_auth_total = ( + _regular_per_auth(fork) + + fork.transaction_top_frame_gas_calculator()( + authorizations=[probe_auth] + ) + + fork.transaction_top_frame_state_gas(authorizations=[probe_auth]) + ) base = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=0, ) @@ -581,7 +614,6 @@ def test_many_auths_block_limit( num_auths = (gas_limit_cap - base) // per_auth_total assert num_auths >= 2 - contract = pre.deploy_contract(code=Op.STOP) signers = [pre.fund_eoa() for _ in range(num_auths)] authorization_list = [ AuthorizationTuple(address=contract, nonce=0, signer=signer) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py index e6ba6c912fe..f3cdf3e1669 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py @@ -183,8 +183,8 @@ def test_sstore_cold_then_warm_same_slot( ) second = second_bare(data_slot, 3) - expected_first = first.regular_cost(fork) - 2 * fork.gas_costs().VERY_LOW - expected_second = second.regular_cost(fork) - 2 * fork.gas_costs().VERY_LOW + expected_first = first_bare.regular_cost(fork) + expected_second = second_bare.regular_cost(fork) # Each measured write stores its own runtime cost; the overhead # subtraction strips the two operand PUSHes so the stored value is the diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py index 7becbe2bb22..4fbe570673d 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py @@ -84,9 +84,6 @@ def test_sstore_clear_grants_refund( observed in ``cumulative_gas_used``. The non-zero original means no EIP-8037 state refund participates. """ - gas_costs = fork.gas_costs() - refund_clear = gas_costs.REFUND_STORAGE_CLEAR - clear = Op.SSTORE.with_metadata( key_warm=False, original_value=1, @@ -100,8 +97,8 @@ def test_sstore_clear_grants_refund( contract = pre.deploy_contract(code=code, storage={0: 1}) - # Sanity: the slot's refund counter accrues exactly one clear grant. - assert code.refund(fork) == refund_clear + # The slot's clear grants exactly one REFUND_STORAGE_CLEAR. + refund_clear = code.refund(fork) expected_cumulative = _cumulative_gas_used(code, fork) # The cap must not bind here, so the full grant is visible. intrinsic = fork.transaction_intrinsic_cost_calculator()( @@ -181,11 +178,6 @@ def test_sstore_restore_nonzero_refunds_write( burned so the quotient cap does not bind and the full refund is observable. """ - gas_costs = fork.gas_costs() - storage_write = ( - gas_costs.COLD_STORAGE_WRITE - gas_costs.COLD_STORAGE_ACCESS - ) - code = Op.SSTORE.with_metadata( key_warm=False, original_value=1, @@ -202,7 +194,8 @@ def test_sstore_restore_nonzero_refunds_write( contract = pre.deploy_contract(code=code, storage={0: 1}) - assert code.refund(fork) == storage_write + # Restoring the non-zero original refunds STORAGE_WRITE. + storage_write = code.refund(fork) expected_cumulative = _cumulative_gas_used(code, fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True @@ -241,9 +234,6 @@ def test_sstore_refund_quotient_cap( always below the accrued refund, so the applied refund is the cap and ``cumulative_gas_used`` reflects ``min(gas_used // 5, accrued)``. """ - gas_costs = fork.gas_costs() - accrued = num_clears * gas_costs.REFUND_STORAGE_CLEAR - code = Bytecode() for slot in range(num_clears): code += Op.SSTORE.with_metadata( @@ -258,7 +248,8 @@ def test_sstore_refund_quotient_cap( storage=dict.fromkeys(range(num_clears), 1), ) - assert code.refund(fork) == accrued + # num_clears distinct clears accrue num_clears * REFUND_STORAGE_CLEAR. + accrued = code.refund(fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) @@ -299,9 +290,7 @@ def test_sstore_refund_cap_exact_equality( *exactly*, the boundary between the cap binding and not binding. The full refund applies and ``cumulative_gas_used`` is ``gross - accrued``. """ - gas_costs = fork.gas_costs() quotient = fork.max_refund_quotient() - accrued = gas_costs.REFUND_STORAGE_CLEAR clear = Op.SSTORE.with_metadata( key_warm=False, @@ -309,6 +298,7 @@ def test_sstore_refund_cap_exact_equality( current_value=1, new_value=0, )(0, 0) + accrued = clear.refund(fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True @@ -330,7 +320,6 @@ def test_sstore_refund_cap_exact_equality( code = clear + Op.JUMPDEST * num_jumpdest contract = pre.deploy_contract(code=code, storage={0: 1}) - assert code.refund(fork) == accrued gross = intrinsic + code.regular_cost(fork) + code.state_cost(fork) # Exact equality: the cap is neither under nor over the accrued refund. assert gross == target_gross diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py index fc79ee51299..5716c9dc277 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py @@ -46,28 +46,23 @@ def test_transient_storage_gas_unchanged( write repricing did not bleed into transient storage. """ gas_costs = fork.gas_costs() - very_low = gas_costs.VERY_LOW - - # Bare opcode costs: subtract the PUSH wrapper from each. - tload_bare = Op.TLOAD(0).gas_cost(fork) - 1 * very_low - tstore_bare = Op.TSTORE(0, 1).gas_cost(fork) - 2 * very_low - - assert tload_bare == gas_costs.OPCODE_TLOAD == 100 - assert tstore_bare == gas_costs.OPCODE_TSTORE == 100 - # Guard against over-eager repricing: transient write must not have - # been folded into the (repriced) persistent cold write cost. + # Guard against over-eager repricing: the transient write must not + # have been folded into the (repriced) persistent cold write cost. assert gas_costs.OPCODE_TSTORE != gas_costs.COLD_STORAGE_WRITE - # Measure TSTORE then TLOAD of the same transient slot in one frame. + # Measure TSTORE then TLOAD of the same transient slot in one frame, + # subtracting the PUSH wrapper so the stored value is the bare opcode + # cost. + push_cost = Op.PUSH1(0).regular_cost(fork) tstore_code = CodeGasMeasure( code=Op.TSTORE(0, 1), - overhead_cost=2 * very_low, + overhead_cost=2 * push_cost, extra_stack_items=0, sstore_key=0, ) tload_code = CodeGasMeasure( code=Op.TLOAD(0), - overhead_cost=1 * very_low, + overhead_cost=1 * push_cost, extra_stack_items=1, sstore_key=1, ) @@ -75,7 +70,9 @@ def test_transient_storage_gas_unchanged( tx = Transaction(to=contract, sender=pre.fund_eoa()) - # Slot 0: measured TSTORE cost. Slot 1: measured TLOAD cost. + # Slot 0: measured TSTORE cost. Slot 1: measured TLOAD cost. Both must + # equal the fork's declared transient-storage opcode costs, which + # EIP-8038 leaves unchanged. post = { contract: Account( storage={ From e0ce65c82db289e07f01378491c1150fffb1edb4 Mon Sep 17 00:00:00 2001 From: spencer Date: Tue, 28 Jul 2026 17:39:01 +0200 Subject: [PATCH 21/55] chore(tests): improve EIP-7981 coverage, checklist, and ref-spec pin (#3223) --- .../eip_checklist_external_coverage.txt | 3 + .../eip_checklist_not_applicable.txt | 13 + .../eip7981_increase_access_list_cost/spec.py | 13 +- .../test_access_list_cost.py | 99 ++++- .../test_eip_mainnet.py | 3 + .../test_floor_boundary_exact_balance.py | 2 + .../test_fork_transition.py | 377 ++++++++++++++++++ .../test_transaction_validity.py | 83 ++++ 8 files changed, 580 insertions(+), 13 deletions(-) create mode 100644 tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_external_coverage.txt create mode 100644 tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_not_applicable.txt create mode 100644 tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_external_coverage.txt b/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_external_coverage.txt new file mode 100644 index 00000000000..33168127d48 --- /dev/null +++ b/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_external_coverage.txt @@ -0,0 +1,3 @@ +general/code_coverage/eels = Covered in EELS +general/code_coverage/test_coverage = Run locally +general/code_coverage/missed_lines = No missed lines; the EIP adds only the access list token accounting in calculate_intrinsic_cost, fully exercised by this suite diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_not_applicable.txt b/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_not_applicable.txt new file mode 100644 index 00000000000..3d9be76c246 --- /dev/null +++ b/tests/amsterdam/eip7981_increase_access_list_cost/eip_checklist_not_applicable.txt @@ -0,0 +1,13 @@ +general/code_coverage/second_client = Optional +opcode = EIP does not introduce or modify an opcode +precompile = EIP does not introduce a precompile +removed_precompile = EIP does not remove a precompile +system_contract = EIP does not introduce a system contract +transaction_type = EIP does not introduce a new transaction type +block_header_field = EIP does not add any new block header fields +block_body_field = EIP does not add any new block body fields +gas_refunds_changes = EIP does not introduce any gas refund changes +blob_count_changes = EIP does not introduce any blob count changes +execution_layer_request = EIP does not introduce an execution layer request +new_transaction_validity_constraint = EIP modifies the existing intrinsic and floor gas validity constraints rather than introducing a new one +block_level_constraint = EIP does not introduce a block-level validation constraint diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/spec.py b/tests/amsterdam/eip7981_increase_access_list_cost/spec.py index 92f945e1bc1..2bc5a72f1f4 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/spec.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/spec.py @@ -12,16 +12,5 @@ class ReferenceSpec: ref_spec_7981 = ReferenceSpec( - "EIPS/eip-7981.md", "954963fb6315dffadd9c40d48e4dae313e20cff5" + "EIPS/eip-7981.md", "747b78c0edfdf04e9e2933ad1bec592d3318e1d9" ) - - -# Constants -class Spec: - """ - Parameters from the EIP-7981 specifications as defined at - https://eips.ethereum.org/EIPS/eip-7981. - """ - - ACCESS_LIST_ADDRESS_COST = 2400 - ACCESS_LIST_STORAGE_KEY_COST = 1900 diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py index 411b96322ad..2d093e6d0bb 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py @@ -8,8 +8,10 @@ Address, Alloc, Bytes, + EIPChecklist, Fork, Hash, + Op, StateTestFiller, Transaction, TransactionReceipt, @@ -24,6 +26,7 @@ pytestmark = pytest.mark.valid_at("EIP7981") +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list,expected_floor_tokens", @@ -137,6 +140,7 @@ def test_access_list_token_calculation( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list,tx_data", @@ -190,6 +194,7 @@ def test_access_list_floor_cost_with_calldata( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list", @@ -220,7 +225,8 @@ def test_large_access_list_cost( Test gas costs for large access lists. With EIP-7981, large access lists should incur: - 1. Storage access costs (2400 per address + 1900 per key) + 1. Storage access costs (per-address and per-key charges, priced + at the fork's cold access costs since EIP-8038) 2. Data footprint costs (16 per floor token) """ state_test( @@ -230,6 +236,7 @@ def test_large_access_list_cost( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list", @@ -264,3 +271,93 @@ def test_duplicate_access_list_entries( post={}, tx=tx, ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type in (1, 2)) +@pytest.mark.parametrize( + "access_list", + [ + pytest.param( + [ + AccessList( + address=Address(1), + storage_keys=[Hash(0), Hash(1)], + ) + ], + id="single_address_two_keys", + ), + ], +) +def test_access_list_data_cost_with_execution( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + tx_type: int, + access_list: list, +) -> None: + """ + Test that the access list data cost is charged when execution gas + dominates. + + EIP-7981 charges the access list data cost as a flat surcharge on + both sides of the gas-used max, so it is paid in full even when the + intrinsic-plus-execution side exceeds the floor. An implementation + that only counts access list bytes toward the floor undercharges + exactly the surcharge here, failing the receipt pin. + """ + gas_costs = fork.gas_costs() + surcharge = ( + calculate_access_list_floor_tokens(access_list) + * gas_costs.TX_DATA_TOKEN_FLOOR + ) + # One gas per JUMPDEST, sized so the execution gas strictly exceeds + # the surcharge under test. + code = Op.JUMPDEST * (surcharge + 1) + Op.STOP + contract = pre.deploy_contract(code) + execution_gas = code.gas_cost(fork) + assert execution_gas > surcharge + + intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_cost_calculator( + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + # The surcharge must be an explicit term of the intrinsic cost, on + # top of the per-entry access charges of the same transaction + # without an access list. + entry_charges = ( + gas_costs.TX_ACCESS_LIST_ADDRESS + + 2 * gas_costs.TX_ACCESS_LIST_STORAGE_KEY + ) + intrinsic_gas_no_access_list = intrinsic_cost_calculator( + return_cost_deducted_prior_execution=True, + ) + assert ( + intrinsic_gas + == intrinsic_gas_no_access_list + entry_charges + surcharge + ) + + # The execution side must win the max against the floor. + expected_gas_used = intrinsic_gas + execution_gas + floor_gas = fork.transaction_data_floor_cost_calculator()( + data=b"", access_list=access_list + ) + assert expected_gas_used > floor_gas + + tx = Transaction( + ty=tx_type, + sender=pre.fund_eoa(), + to=contract, + access_list=access_list, + gas_limit=expected_gas_used, + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used + ), + ) + + state_test( + pre=pre, + post={}, + tx=tx, + ) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_eip_mainnet.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_eip_mainnet.py index 8a20c3c6bed..3090870f27f 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_eip_mainnet.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_eip_mainnet.py @@ -7,6 +7,7 @@ AccessList, Address, Alloc, + EIPChecklist, Hash, StateTestFiller, Transaction, @@ -20,6 +21,7 @@ pytestmark = [pytest.mark.valid_at("EIP7981"), pytest.mark.mainnet] +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list", @@ -92,6 +94,7 @@ def test_access_list_gas_cost( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list", diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py index c961dfe18ab..a6ddbcc7747 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py @@ -9,6 +9,7 @@ Address, Alloc, Bytes, + EIPChecklist, Fork, Hash, StateTestFiller, @@ -24,6 +25,7 @@ pytestmark = pytest.mark.valid_at("EIP7981") +@EIPChecklist.GasCostChanges.Test.OutOfGas() @pytest.mark.exception_test @pytest.mark.parametrize( "tx_type", diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py new file mode 100644 index 00000000000..62a899b346a --- /dev/null +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py @@ -0,0 +1,377 @@ +""" +Fork-transition tests for [EIP-7981: Increase Access List Cost](https://eips.ethereum.org/EIPS/eip-7981). + +EIP-7981 adds a data-footprint surcharge for access list bytes at the +Amsterdam fork boundary. These tests send identical access-list +transactions in a pre-fork block and a post-fork block (straddling the +transition timestamp) and pin the per-transaction gas paid on each side, +plus the validity flip for gas limits inside the uplift gap. + +The post-fork intrinsic composes three repricings; the hand-derived +expectations below keep each term explicit so the EIP-7981 surcharge is +individually visible: + +- EIP-2780 decomposes the flat pre-fork `TX_BASE` into the lowered base + plus the `COLD_ACCOUNT_ACCESS` recipient charge. +- EIP-8038 reprices the per-address and per-storage-key access list + charges to the fork's cold access costs. +- EIP-7981 adds four floor tokens per access list byte, charged at + `TX_DATA_TOKEN_FLOOR` in the intrinsic and counted in the floor. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + EIPChecklist, + Hash, + Transaction, + TransactionException, + TransactionReceipt, + TransitionFork, +) + +from .helpers import calculate_access_list_floor_tokens +from .spec import ref_spec_7981 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path +REFERENCE_SPEC_VERSION = ref_spec_7981.version + +pytestmark = pytest.mark.valid_at_transition_to("EIP7981") + +# Transition forks switch at timestamp 15_000. +PRE_FORK_TIMESTAMP = 14_999 +POST_FORK_TIMESTAMP = 15_000 + + +def access_list_shape(addresses: int, keys_per_address: int) -> list: + """Build an access list with the given shape.""" + return [ + AccessList( + address=Address(i + 1), + storage_keys=[Hash(k) for k in range(keys_per_address)], + ) + for i in range(addresses) + ] + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +@pytest.mark.parametrize( + "addresses,keys_per_address", + [ + pytest.param(1, 0, id="single_address_no_keys"), + pytest.param(1, 2, id="single_address_two_keys"), + pytest.param(2, 3, id="two_addresses_three_keys_each"), + ], +) +def test_access_list_intrinsic_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + addresses: int, + keys_per_address: int, +) -> None: + """ + Pin the access list intrinsic change across the Amsterdam boundary. + + The same access-list transaction shape is sent in a pre-fork block + (flat base plus the EIP-2930 per-entry charges, no data cost) and a + post-fork block (decomposed base, repriced entries, plus the + EIP-7981 byte surcharge). Each block uses a distinct sender so its + post-tx balance pins the fork-appropriate intrinsic; the recipient + is an existing EOA, so no EVM bytecode runs and `gas_used` equals + the intrinsic exactly. + + The per-fork intrinsic returned by the calculator is also checked + against a hand-derived per-EIP decomposition, so a calculator + regression fails here with a clear message rather than only as a + downstream balance mismatch. + """ + gas_price = 1_000_000_000 + access_list = access_list_shape(addresses, keys_per_address) + total_keys = addresses * keys_per_address + + pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP) + post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP) + pre_costs = pre_fork.gas_costs() + post_costs = post_fork.gas_costs() + + # Pre-fork: flat base plus the EIP-2930 per-entry charges; access + # list bytes carry no data cost. + expected_pre = ( + pre_costs.TX_BASE + + addresses * pre_costs.TX_ACCESS_LIST_ADDRESS + + total_keys * pre_costs.TX_ACCESS_LIST_STORAGE_KEY + ) + # Post-fork: EIP-2780 decomposed base and recipient charge, EIP-8038 + # repriced entry charges, and the EIP-7981 byte surcharge. + surcharge = ( + calculate_access_list_floor_tokens(access_list) + * post_costs.TX_DATA_TOKEN_FLOOR + ) + expected_post = ( + post_costs.TX_BASE + + post_costs.COLD_ACCOUNT_ACCESS + + addresses * post_costs.TX_ACCESS_LIST_ADDRESS + + total_keys * post_costs.TX_ACCESS_LIST_STORAGE_KEY + + surcharge + ) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_intrinsics = [expected_pre, expected_post] + blocks = [] + post: dict[Address, Account] = {} + + for timestamp, expected_intrinsic in zip( + timestamps, expected_intrinsics, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + assert intrinsic_gas == expected_intrinsic, ( + f"intrinsic at timestamp {timestamp} ({sub_fork}) is " + f"{intrinsic_gas}, expected {expected_intrinsic}" + ) + # The intrinsic side must bind so gas_used equals the intrinsic. + floor_gas = sub_fork.transaction_data_floor_cost_calculator()( + data=b"", access_list=access_list + ) + assert floor_gas <= intrinsic_gas + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=target, + gas_limit=intrinsic_gas, + gas_price=gas_price, + access_list=access_list, + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + post[sender] = Account( + nonce=1, + balance=sender_initial_balance - intrinsic_gas * gas_price, + ) + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedBeforeFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedBeforeFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedAfterFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedAfterFork() +@pytest.mark.exception_test +def test_access_list_validity_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """ + Pin the intrinsic-validity flip across the Amsterdam boundary. + + For an access list with one address and two storage keys the + EIP-7981 byte surcharge (plus the EIP-8038 entry repricing) outgrows + the EIP-2780 base reduction, so the post-fork intrinsic is strictly + higher than the pre-fork one. Off-by-one gas limits around each + fork's requirement then pin all four boundary behaviors: + + 1. Pre-fork block with `gas_limit` one below the pre-fork intrinsic + is rejected. + 2. Pre-fork block accepts both the exact pre-fork intrinsic and a + gas limit one below the post-fork intrinsic (the new constraint + is not met, the old one is). + 3. Post-fork block with that same one-below gas limit is rejected. + 4. Post-fork block with the exact post-fork intrinsic is accepted. + """ + gas_price = 1_000_000_000 + access_list = access_list_shape(addresses=1, keys_per_address=2) + + pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP) + post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP) + + intrinsic_pre = pre_fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + intrinsic_post = post_fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + # The gas limit straddling the boundary must be valid pre-fork and + # invalid post-fork. + straddle_gas_limit = intrinsic_post - 1 + assert intrinsic_pre <= straddle_gas_limit, ( + f"access list shape does not discriminate: pre-fork intrinsic " + f"{intrinsic_pre} exceeds post-fork intrinsic - 1 " + f"({straddle_gas_limit})" + ) + # The intrinsic side must bind over the floor on both forks. + for sub_fork, intrinsic in [ + (pre_fork, intrinsic_pre), + (post_fork, intrinsic_post), + ]: + floor_gas = sub_fork.transaction_data_floor_cost_calculator()( + data=b"", access_list=access_list + ) + assert floor_gas <= intrinsic + + def make_tx( + gas_limit: int, error: TransactionException | None = None + ) -> Transaction: + return Transaction( + sender=pre.fund_eoa(), + to=pre.fund_eoa(amount=0), + gas_limit=gas_limit, + gas_price=gas_price, + access_list=access_list, + error=error, + ) + + blocks = [ + # 1. Rejected before the fork: below the pre-fork intrinsic. + Block( + timestamp=PRE_FORK_TIMESTAMP, + txs=[ + make_tx( + intrinsic_pre - 1, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + ], + exception=TransactionException.INTRINSIC_GAS_TOO_LOW, + ), + # 2. Accepted before the fork: the exact pre-fork intrinsic and + # the straddling gas limit that the post-fork rules will reject. + Block( + timestamp=PRE_FORK_TIMESTAMP, + txs=[make_tx(intrinsic_pre), make_tx(straddle_gas_limit)], + ), + # 3. Rejected after the fork: the same straddling gas limit. + Block( + timestamp=POST_FORK_TIMESTAMP, + txs=[ + make_tx( + straddle_gas_limit, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + ], + exception=TransactionException.INTRINSIC_GAS_TOO_LOW, + ), + # 4. Accepted after the fork: the exact post-fork intrinsic. + Block( + timestamp=POST_FORK_TIMESTAMP, + txs=[make_tx(intrinsic_post)], + ), + ] + + blockchain_test(pre=pre, blocks=blocks, post={}) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_access_list_floor_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """ + Pin access list bytes entering the calldata floor at the boundary. + + A calldata-heavy access-list transaction binds the floor on both + sides of the transition: pre-fork the floor counts calldata bytes + only (access list bytes contribute nothing), post-fork the EIP-7981 + tokens raise it. Each block's gas limit is pinned to its fork's + floor, so the billed gas equals the floor exactly and an + implementation that mistimes the floor change fails the receipt and + balance pins. + """ + gas_price = 1_000_000_000 + # Sized so the floor dominates the intrinsic on both sides + # (asserted below): each non-zero byte adds 40 - 16 = 24 gas of + # floor headroom pre-fork and 64 - 16 = 48 post-fork, outgrowing + # the per-entry access charges that only the intrinsic carries. + data = b"\x01" * 400 + access_list = access_list_shape(addresses=1, keys_per_address=2) + + pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP) + post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP) + pre_costs = pre_fork.gas_costs() + post_costs = post_fork.gas_costs() + + # Pre-fork (EIP-7623): content-weighted calldata tokens only; the + # access list bytes contribute nothing to the floor. + pre_tokens = len(data) * 4 + expected_pre = int( + pre_costs.TX_BASE + pre_tokens * pre_costs.TX_DATA_TOKEN_FLOOR + ) + assert pre_fork.transaction_data_floor_cost_calculator()( + data=data, access_list=access_list + ) == pre_fork.transaction_data_floor_cost_calculator()(data=data) + # Post-fork: uniform calldata tokens plus the EIP-7981 access list + # tokens, anchored on the EIP-2780 decomposed base. + post_tokens = len(data) * int( + post_costs.TX_DATA_TOKEN_STANDARD + ) + calculate_access_list_floor_tokens(access_list) + expected_post = int( + post_costs.TX_BASE + + post_costs.COLD_ACCOUNT_ACCESS + + post_tokens * post_costs.TX_DATA_TOKEN_FLOOR + ) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_floors = [expected_pre, expected_post] + blocks = [] + post: dict[Address, Account] = {} + + for timestamp, expected_floor in zip( + timestamps, expected_floors, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + floor_gas = sub_fork.transaction_data_floor_cost_calculator()( + data=data, access_list=access_list + ) + assert floor_gas == expected_floor, ( + f"floor at timestamp {timestamp} ({sub_fork}) is {floor_gas}, " + f"expected {expected_floor}" + ) + # The floor must dominate the intrinsic so the transaction is + # billed exactly the floor. + intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()( + calldata=data, + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + assert floor_gas > intrinsic_gas, ( + f"floor {floor_gas} does not dominate intrinsic " + f"{intrinsic_gas} at timestamp {timestamp} ({sub_fork})" + ) + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + tx = Transaction( + sender=sender, + to=pre.fund_eoa(amount=0), + data=data, + gas_limit=floor_gas, + gas_price=gas_price, + access_list=access_list, + expected_receipt=TransactionReceipt(cumulative_gas_used=floor_gas), + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + post[sender] = Account( + nonce=1, + balance=sender_initial_balance - floor_gas * gas_price, + ) + + blockchain_test(pre=pre, blocks=blocks, post=post) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py index 97ddba5bc45..dbeed9ded11 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py @@ -5,12 +5,17 @@ import pytest from execution_testing import ( AccessList, + Account, Address, Alloc, Bytes, + EIPChecklist, + Fork, Hash, StateTestFiller, Transaction, + TransactionException, + compute_create_address, ) from .spec import ref_spec_7981 @@ -21,6 +26,7 @@ pytestmark = pytest.mark.valid_at("EIP7981") +@EIPChecklist.GasCostChanges.Test.OutOfGas() @pytest.mark.exception_test @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( @@ -76,6 +82,7 @@ def test_insufficient_gas_for_access_list( ) +@EIPChecklist.GasCostChanges.Test.OutOfGas() @pytest.mark.exception_test @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( @@ -122,6 +129,7 @@ def test_floor_cost_validation_with_access_list( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list,tx_gas_delta", @@ -168,6 +176,7 @@ def test_valid_gas_limits_with_access_list( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type >= 1) @pytest.mark.parametrize( "access_list,tx_data", @@ -223,6 +232,7 @@ def test_mixed_zero_nonzero_bytes_floor_cost( ) +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.parametrize( "tx_type,access_list", [ @@ -277,3 +287,76 @@ def test_transactions_without_access_list( post={}, tx=tx, ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type in (1, 2)) +@pytest.mark.parametrize( + "valid", + [ + pytest.param(True, id="exact_gas"), + pytest.param( + False, + id="insufficient_gas_by_one", + marks=pytest.mark.exception_test, + ), + ], +) +def test_contract_creation_with_access_list( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + tx_type: int, + valid: bool, +) -> None: + """ + Test the intrinsic boundary of a contract-creating transaction with + an access list. + + The EIP-7981 access list data cost stacks on top of the creation + intrinsic (creation access and init code charges). The created + account's state charge is applied at the top frame, after intrinsic + validation, so the exact-gas arm funds it separately while the + off-by-one arm pins the intrinsic requirement alone. + """ + access_list = [ + AccessList(address=Address(1), storage_keys=[Hash(0), Hash(1)]) + ] + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + contract_creation=True, + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + floor_gas = fork.transaction_data_floor_cost_calculator()( + data=b"", access_list=access_list, contract_creation=True + ) + assert floor_gas <= intrinsic_gas + + sender = pre.fund_eoa() + post: dict = {} + if valid: + gas_limit = intrinsic_gas + fork.transaction_top_frame_state_gas( + contract_creation=True + ) + error = None + created = compute_create_address(address=sender, nonce=sender.nonce) + post[created] = Account(nonce=1, code=b"") + else: + gas_limit = intrinsic_gas - 1 + error = TransactionException.INTRINSIC_GAS_TOO_LOW + + tx = Transaction( + ty=tx_type, + sender=sender, + to=None, + access_list=access_list, + gas_limit=gas_limit, + error=error, + ) + + state_test( + pre=pre, + post=post, + tx=tx, + ) From 44d2b9cbd028b48f13e6ebf2635f977141cc397b Mon Sep 17 00:00:00 2001 From: Tamaghna Choudhuri Date: Wed, 29 Jul 2026 03:43:29 +0530 Subject: [PATCH 22/55] feat(test-types): Introduce SSZ model into base types (#3196) * init ssz-aware testing:wq * add progressive support * add scaffolding * refactor * fix pypy problem * nits * add ssz none functionality Claude-Session: https://claude.ai/code/session_01LxVkSo6sGs8bsNz4yD8KiJ * resolve reviews --- packages/testing/pyproject.toml | 1 + .../src/execution_testing/base_types/ssz.py | 955 +++++++++++++++++ .../base_types/tests/test_ssz.py | 960 ++++++++++++++++++ .../execution_testing/tools/ssz_vectors.py | 476 +++++++++ .../tools/tests/test_ssz_vectors.py | 296 ++++++ pyproject.toml | 5 + uv.lock | 11 + 7 files changed, 2704 insertions(+) create mode 100644 packages/testing/src/execution_testing/base_types/ssz.py create mode 100644 packages/testing/src/execution_testing/base_types/tests/test_ssz.py create mode 100644 packages/testing/src/execution_testing/tools/ssz_vectors.py create mode 100644 packages/testing/src/execution_testing/tools/tests/test_ssz_vectors.py diff --git a/packages/testing/pyproject.toml b/packages/testing/pyproject.toml index 11577956ef7..bca724ebe52 100644 --- a/packages/testing/pyproject.toml +++ b/packages/testing/pyproject.toml @@ -54,6 +54,7 @@ dependencies = [ "tenacity>=9.0.0,<10", "Jinja2>=3,<4", "ijson>=3.3,<4", + "eth-remerkleable==0.1.31", ] [project.urls] diff --git a/packages/testing/src/execution_testing/base_types/ssz.py b/packages/testing/src/execution_testing/base_types/ssz.py new file mode 100644 index 00000000000..4605aaf3147 --- /dev/null +++ b/packages/testing/src/execution_testing/base_types/ssz.py @@ -0,0 +1,955 @@ +""" +Native SSZ serialization for base_types models. + +Declare a container once as a pydantic SszModel, in the ordinary base types, +and get SSZ encoding, hash_tree_root, and defaults for them. + +Each field's SSZ type is derived from its Python type, so the model stays the +single source of truth: + +* fixed byte types self-describe by byte_length + (Hash -> ByteVector[32], Address -> ByteVector[20]); +* the width ints defined here carry it (Uint64 -> uint64); +* bool -> boolean; a nested SszModel -> Container; +* the only facts a Python type cannot express -- list / vector / bytelist / bit + caps -- ride as Annotated markers (ssz_list(N), ssz_vector(N), byte_list(N), + bitvector(N), bitlist(N)). Element types are derived from the annotation, so + a marker carries only the cap/length, never a duplicated element spec. + +Each field's SSZ type is described by an SszType value (SszUint, SszByteList, +SszList, SszContainer, ...). The engine turns that into a remerkleable type +on demand (build_ssz_type) and delegates the actual encoding, merkleization, +and default (zero) values to it. + +Fork-scoped models: one model can serve every fork. Future-fork fields are +declared T | None (None == absent in older forks, omitted from JSON), and a +__ssz_schema__ = SszForkSchema(...) table beside the fields says which fork +introduces what, in canonical SSZ order (the class body's order stays free +for JSON). Such models require fork= on encode / hash_tree_root / decode / +ssz_default / describe_schema / build_ssz_type +""" + +from dataclasses import dataclass +from functools import lru_cache +from types import UnionType +from typing import ( + Any, + ClassVar, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + get_args, + get_origin, +) + +from remerkleable.basic import ( + boolean, + uint8, + uint16, + uint32, + uint64, + uint128, + uint256, +) +from remerkleable.bitfields import Bitlist as RmkBitlist +from remerkleable.bitfields import Bitvector as RmkBitvector +from remerkleable.byte_arrays import ByteList, ByteVector +from remerkleable.complex import Container +from remerkleable.complex import List as RmkList +from remerkleable.complex import Vector as RmkVector +from remerkleable.core import View +from remerkleable.progressive import ( + ProgressiveBitlist as RmkProgressiveBitlist, +) +from remerkleable.progressive import ProgressiveContainer +from remerkleable.progressive import ProgressiveList as RmkProgressiveList + +from .base_types import Bytes, FixedSizeBytes, HexNumber +from .pydantic import CamelModel + +_UINTS = { + 8: uint8, + 16: uint16, + 32: uint32, + 64: uint64, + 128: uint128, + 256: uint256, +} + + +class SszType: + """A description of a field's SSZ type.""" + + +@dataclass(frozen=True) +class SszUint(SszType): + """An unsigned integer of bits width (8/16/32/64/128/256).""" + + bits: int + + +@dataclass(frozen=True) +class SszByteVector(SszType): + """A fixed-length byte vector of length bytes.""" + + length: int + + +@dataclass(frozen=True) +class SszByteList(SszType): + """A variable byte list capped at limit bytes.""" + + limit: int + + +@dataclass(frozen=True) +class SszList(SszType): + """A list of element capped at limit items.""" + + element: SszType + limit: int + + +@dataclass(frozen=True) +class SszVector(SszType): + """A fixed-length vector of exactly length element items.""" + + element: SszType + length: int + + +@dataclass(frozen=True) +class SszBitvector(SszType): + """A fixed-length bit vector of length bits.""" + + length: int + + +@dataclass(frozen=True) +class SszBitlist(SszType): + """A variable bit list capped at limit bits.""" + + limit: int + + +@dataclass(frozen=True) +class SszBool(SszType): + """The SSZ boolean type.""" + + +@dataclass(frozen=True) +class SszContainer(SszType): + """A nested container backed by pydantic model.""" + + model: Type["SszModel"] + + +@dataclass(frozen=True) +class SszProgressiveList(SszType): + """An uncapped progressive list of element (EIP-7916).""" + + element: SszType + + +@dataclass(frozen=True) +class SszProgressiveBitlist(SszType): + """An uncapped progressive bit list.""" + + +@dataclass(frozen=True) +class SszProgressiveContainer(SszType): + """A forward-compatible progressive container backed by model.""" + + model: Type["SszModel"] + + +_M = TypeVar("_M", bound="SszModel") + + +@dataclass(frozen=True, eq=False) +class SszForkSchema: + """ + Fork-scoped field sets for a fork-evolving container. + + One model declares every fork's fields; this table says which fields + exist at which fork and in which SSZ order. base holds the fields of + base_fork; appended maps each later fork (in order) to the fields it + adds, which must be declared Optional (T | None) on the model. + + Fork keys are opaque strings: base_types knows nothing about forks; + """ + + base_fork: str + base: Tuple[str, ...] + appended: Mapping[str, Tuple[str, ...]] + + def forks(self) -> Tuple[str, ...]: + """Every known fork key, oldest first.""" + return (self.base_fork, *self.appended) + + def fields_at(self, fork: str) -> Tuple[str, ...]: + """The SSZ field names of fork, in canonical order.""" + if fork == self.base_fork: + return self.base + if fork not in self.appended: + raise TypeError( + f"unknown fork {fork!r}; known forks: {self.forks()}" + ) + names = list(self.base) + for key, fields in self.appended.items(): + names.extend(fields) + if key == fork: + break + return tuple(names) + + def all_fields(self) -> Tuple[str, ...]: + """Every field of the newest fork, in canonical order.""" + keys = self.forks() + return self.fields_at(keys[-1]) + + +def _unwrap_optional(annotation: Any) -> Tuple[Any, bool]: + """Strip a T | None union; return.""" + if get_origin(annotation) in (Union, UnionType): + args = [a for a in get_args(annotation) if a is not type(None)] + if len(args) != 1: + raise TypeError( + f"only T | None unions are supported: {annotation!r}" + ) + return args[0], True + return annotation, False + + +def _is_fork_optional(model_cls: Type["SszModel"], name: str) -> bool: + ann = model_cls.model_fields[name].annotation + return _unwrap_optional(ann)[1] + + +def _is_ssz_excluded(model_cls: Type["SszModel"], name: str) -> bool: + """Whether name carries the ssz_exclude() marker (JSON-only).""" + metadata = model_cls.model_fields[name].metadata + return any(isinstance(m, _SszExclude) for m in metadata) + + +def _included_fields(model_cls: Type["SszModel"]) -> Tuple[str, ...]: + """Every SSZ-participating field, in declaration order.""" + return tuple( + name + for name in model_cls.model_fields + if not _is_ssz_excluded(model_cls, name) + ) + + +def _check_fork_schema(model_cls: Type["SszModel"]) -> None: + """ + Validate a model's __ssz_schema__ against its fields, at class + definition. + + Optional (T | None) fields require a schema naming their fork; the + schema must cover exactly the model's SSZ fields; base fields must + be required and appended fields Optional with a None default -- so + a mis-declared container fails at import. + """ + schema = model_cls.__ssz_schema__ + included = _included_fields(model_cls) + optional = { + name for name in included if _is_fork_optional(model_cls, name) + } + progressive = globals().get("ProgressiveModel") + if progressive is not None and issubclass(model_cls, progressive): + if schema is not None: + raise TypeError( + f"{model_cls.__name__}: __ssz_schema__ is not supported " + f"on ProgressiveModel (progressive containers evolve via " + f"__active_fields__)" + ) + if optional: + raise TypeError( + f"{model_cls.__name__}: T | None fields are not supported " + f"on ProgressiveModel; reserve future slots with 0s in " + f"__active_fields__ instead" + ) + return + if schema is None: + if optional: + raise TypeError( + f"{model_cls.__name__} has fork-optional fields " + f"{sorted(optional)} but no __ssz_schema__ declaring " + f"which fork introduces them" + ) + return + all_names = schema.all_fields() + dupes = sorted({n for n in all_names if all_names.count(n) > 1}) + if dupes: + raise TypeError( + f"{model_cls.__name__}.__ssz_schema__ names fields more than " + f"once: {dupes}" + ) + declared = set(all_names) + fields = set(included) + if declared != fields: + raise TypeError( + f"{model_cls.__name__}.__ssz_schema__ does not match the " + f"model: schema-only={sorted(declared - fields)} " + f"model-only={sorted(fields - declared)}" + ) + appended = fields - set(schema.base) + if optional != appended: + raise TypeError( + f"{model_cls.__name__}: appended fields must be T | None and " + f"base fields required; non-optional appended=" + f"{sorted(appended - optional)} optional base=" + f"{sorted(optional - appended)}" + ) + no_default = sorted( + name for name in appended if model_cls.model_fields[name].is_required() + ) + if no_default: + raise TypeError( + f"{model_cls.__name__}: appended fields must default to None " + f"(decode of older forks constructs without them): " + f"{no_default}" + ) + + +class _Marker: + """Base for cap-only Annotated markers resolved by spec_of.""" + + +@dataclass(frozen=True) +class _ListCap(_Marker): + limit: int + + +@dataclass(frozen=True) +class _VectorLen(_Marker): + length: int + + +@dataclass(frozen=True) +class _ProgressiveListMark(_Marker): + pass + + +@dataclass(frozen=True) +class _SszExclude(_Marker): + pass + + +def byte_list(limit: int) -> SszByteList: + """Annotate a Bytes field as a capped SSZ byte list.""" + return SszByteList(limit) + + +def ssz_list(limit: int) -> _ListCap: + """Annotate a list[...] field as a capped SSZ list.""" + return _ListCap(limit) + + +def ssz_vector(length: int) -> _VectorLen: + """Annotate a list[...] field as a fixed SSZ vector.""" + return _VectorLen(length) + + +def bitvector(length: int) -> SszBitvector: + """Annotate a list[bool] field as a fixed SSZ bit vector.""" + return SszBitvector(length) + + +def bitlist(limit: int) -> SszBitlist: + """Annotate a list[bool] field as a capped SSZ bit list.""" + return SszBitlist(limit) + + +def progressive_list() -> _ProgressiveListMark: + """Annotate a list[...] field as an uncapped progressive list.""" + return _ProgressiveListMark() + + +def progressive_bitlist() -> SszProgressiveBitlist: + """Annotate a list[bool] field as an uncapped progressive bit list.""" + return SszProgressiveBitlist() + + +def ssz_exclude() -> _SszExclude: + """ + Annotate a field as JSON-only: SSZ ignores it entirely. + + Such a field must carry a default: decode never sees it on the wire + and so cannot reconstruct it. + """ + return _SszExclude() + + +class SszModel(CamelModel): + """ + A pydantic model whose fields carry SSZ types. + + Every field must resolve to an SszType, or be excluded from SSZ with + an ssz_exclude() marker (JSON-only fields); each Annotated marker + must be consistent with the field's Python type. + """ + + __ssz_schema__: ClassVar[Optional[SszForkSchema]] = None + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: + """Validate every field resolves to a consistent SSZ type.""" + super().__pydantic_init_subclass__(**kwargs) + for name in cls.model_fields: + if _is_ssz_excluded(cls, name): + if cls.model_fields[name].is_required(): + raise TypeError( + f"{cls.__name__}.{name} is SSZ-excluded but has " + f"no default; decode cannot reconstruct it" + ) + continue + spec_of(cls, name) # raises TypeError on unmapped/inconsistent + _check_fork_schema(cls) + + +class ProgressiveModel(SszModel): + """ + A forward-compatible progressive container. + + __active_fields__ is the active-field bitvector; it defaults to all SSZ + fields active. A 0 marks a reserved gap with no declared field, so new + fields can be slotted in later without shifting existing roots -- the + SSZ fields fill the 1 positions in order. + """ + + __active_fields__: ClassVar[Sequence[int]] = () + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: + """Check the active-field bitvector agrees with the field count.""" + super().__pydantic_init_subclass__(**kwargs) + active = cls.__active_fields__ + included = len(_included_fields(cls)) + if active and sum(active) != included: + raise TypeError( + f"{cls.__name__}.__active_fields__ has {sum(active)} active " + f"entries but the container declares " + f"{included} SSZ fields" + ) + + +def _marker_in(metadata: Any) -> Any: + """The first SSZ marker in metadata.""" + return next( + (m for m in metadata if isinstance(m, (SszType, _Marker))), None + ) + + +def _spec_for_type_bare(annotation: Any) -> SszType: + """Derive the SSZ type of a plain Python type.""" + ssz = getattr(annotation, "__ssz__", None) + if isinstance(ssz, SszType): + return ssz + if isinstance(annotation, type): + if issubclass(annotation, FixedSizeBytes): + return SszByteVector(annotation.byte_length) + if issubclass(annotation, ProgressiveModel): + return SszProgressiveContainer(annotation) + if issubclass(annotation, SszModel): + return SszContainer(annotation) + if annotation is bool: + return SszBool() + raise TypeError(f"no SSZ type for {annotation!r}") + + +def _spec_for_type(annotation: Any) -> SszType: + """Resolve an SSZ type, honoring an inner Annotated marker if present.""" + meta = getattr(annotation, "__metadata__", None) + if meta is not None: + return _resolve(_marker_in(meta), annotation.__origin__) + return _spec_for_type_bare(annotation) + + +def _element_of(annotation: Any, ctx: str) -> SszType: + """Resolve the element SSZ type of a list[...] annotation.""" + if get_origin(annotation) not in (list, List): + raise TypeError(f"{ctx} requires a list[...] field: {annotation!r}") + args = get_args(annotation) + if len(args) != 1: + raise TypeError(f"{ctx} needs a single list element type") + return _spec_for_type(args[0]) + + +def _resolve(marker: Any, annotation: Any) -> SszType: + """ + Resolve a field/element into an SSZ type. + + Cap-only markers (ssz_list/ssz_vector/progressive_list) derive their + element from the annotation; complete markers (byte_list/bitvector/...) + are checked for consistency with it. Byte-list elements are expressed as + Annotated[Bytes, byte_list(N)] so the inner cap lives on the element. + """ + if marker is None: + return _spec_for_type(annotation) + if isinstance(marker, _ListCap): + return SszList(_element_of(annotation, "ssz_list"), marker.limit) + if isinstance(marker, _VectorLen): + return SszVector(_element_of(annotation, "ssz_vector"), marker.length) + if isinstance(marker, _ProgressiveListMark): + return SszProgressiveList(_element_of(annotation, "progressive_list")) + if isinstance(marker, SszByteList): + is_bytes = isinstance(annotation, type) and issubclass( + annotation, Bytes + ) + if not is_bytes: + raise TypeError( + f"byte_list requires a Bytes field/element: {annotation!r}" + ) + return marker + if isinstance(marker, (SszBitvector, SszBitlist, SszProgressiveBitlist)): + if not isinstance(_element_of(annotation, "bit markers"), SszBool): + raise TypeError( + f"bit markers require a list[bool] field: {annotation!r}" + ) + return marker + if isinstance(marker, _SszExclude): + raise TypeError( + f"field is ssz_exclude()d; it has no SSZ type: {annotation!r}" + ) + # Raw SszType instances (SszUint, SszContainer, ...) as markers would + # bypass the consistency checks above; only the marker helpers are + # supported. + raise TypeError( + f"unsupported Annotated SSZ marker {marker!r}; use the marker " + f"helpers (ssz_list, ssz_vector, byte_list, bitvector, ...)" + ) + + +@lru_cache(maxsize=None) +def spec_of(model_cls: Type["SszModel"], name: str) -> SszType: + """ + The resolved SSZ type of a field. + + An Annotated marker takes precedence over the bare type; cap-only markers + derive their element from the annotation, and every marker is checked for + consistency with it. A T | None union resolves to T's SSZ type -- the + None arm means "absent in older forks" (see SszForkSchema), which is a + schema fact, not an SSZ type. Cached per (model_cls, name). + """ + field = model_cls.model_fields[name] + annotation, _ = _unwrap_optional(field.annotation) + if _is_ssz_excluded(model_cls, name): + raise TypeError( + f"{model_cls.__name__}.{name} is SSZ-excluded; it has no " + f"SSZ type: {annotation!r}" + ) + return _resolve(_marker_in(field.metadata), annotation) + + +def _rmk_type(spec: SszType, fork: Optional[str] = None) -> Type[View]: + if isinstance(spec, SszUint): + return _UINTS[spec.bits] + if isinstance(spec, SszByteVector): + return ByteVector[spec.length] + if isinstance(spec, SszByteList): + return ByteList[spec.limit] + if isinstance(spec, SszList): + return RmkList[_rmk_type(spec.element, fork), spec.limit] + if isinstance(spec, SszVector): + return RmkVector[_rmk_type(spec.element, fork), spec.length] + if isinstance(spec, SszBitvector): + return RmkBitvector[spec.length] + if isinstance(spec, SszBitlist): + return RmkBitlist[spec.limit] + if isinstance(spec, SszProgressiveList): + return RmkProgressiveList[_rmk_type(spec.element, fork)] + if isinstance(spec, SszProgressiveBitlist): + return RmkProgressiveBitlist + if isinstance(spec, (SszContainer, SszProgressiveContainer)): + return build_ssz_type(spec.model, _nested_fork(spec.model, fork)) + if isinstance(spec, SszBool): + return boolean + raise TypeError(f"unhandled SSZ type {spec!r}") + + +def _active_fields(model_cls: Type["SszModel"]) -> Sequence[int]: + """The active-field bitvector, defaulting to every SSZ field active.""" + declared = getattr(model_cls, "__active_fields__", ()) + return declared if declared else [1] * len(_included_fields(model_cls)) + + +def _nested_fork( + model_cls: Type["SszModel"], fork: Optional[str] +) -> Optional[str]: + """ + The fork a nested container is projected at. + + One fork propagates down the whole value tree: everything inside one + message is at the same chain fork, so a fork-scoped nested model + inherits the outer fork, while a + complete nested model takes no fork at all. + """ + return fork if model_cls.__ssz_schema__ is not None else None + + +def _schema_fields( + model_cls: Type["SszModel"], fork: Optional[str] +) -> Tuple[str, ...]: + """ + The SSZ field names of model_cls, in canonical order. + + A fork-scoped model requires fork and gets + that fork's fields in the schema's order; a complete model forbids + fork and gets every non-excluded field in declaration order. + """ + schema = model_cls.__ssz_schema__ + if schema is None: + if fork is not None: + raise TypeError( + f"{model_cls.__name__} is not fork-scoped; do not pass fork" + ) + return _included_fields(model_cls) + if fork is None: + raise TypeError( + f"{model_cls.__name__} is fork-scoped; pass fork= " + f"(one of {schema.forks()})" + ) + return schema.fields_at(fork) + + +def ssz_fields( + model_cls: Type["SszModel"], fork: Optional[str] = None +) -> Tuple[str, ...]: + """ + The SSZ field names of model_cls, in canonical (wire) order. + + The public twin of the engine's internal field selection: callers + (vector generators, fixtures tooling) can enumerate exactly the + fields a model encodes -- per fork for fork-scoped models. + """ + return _schema_fields(model_cls, fork) + + +def _check_populated( + model: "SszModel", names: Tuple[str, ...], fork: str +) -> None: + """Raise unless the populated fields exactly match the fork schema.""" + missing = [n for n in names if getattr(model, n) is None] + unexpected = sorted( + n + for n in _included_fields(type(model)) + if n not in names and getattr(model, n) is not None + ) + if missing or unexpected: + raise TypeError( + f"{type(model).__name__} does not fit the {fork!r} SSZ schema: " + f"missing={missing} unexpected={unexpected}; " + f"refusing to drop data" + ) + + +def build_ssz_type( + model_cls: Type["SszModel"], fork: Optional[str] = None +) -> Type[Container]: + """ + Build the remerkleable container type mirroring model_cls. + + Cached per (class object, fork) -- distinct same-named models get + distinct types, and each fork of a fork-scoped model gets its own + genuinely distinct container (different offsets and merkle shape). + """ + return _build_ssz_type(model_cls, fork) + + +@lru_cache(maxsize=None) +def _build_ssz_type( + model_cls: Type["SszModel"], fork: Optional[str] +) -> Type[Container]: + names = _schema_fields(model_cls, fork) + anns = {name: _rmk_type(spec_of(model_cls, name), fork) for name in names} + if issubclass(model_cls, ProgressiveModel): + base: Any = ProgressiveContainer( + active_fields=list(_active_fields(model_cls)) + ) + else: + base = Container + cls_name = model_cls.__name__ + (fork if fork else "") + return type(cls_name, (base,), {"__annotations__": anns}) + + +def _to_rmk(spec: SszType, value: Any, fork: Optional[str] = None) -> Any: + if isinstance(spec, (SszContainer, SszProgressiveContainer)): + return _rmk_instance(value, _nested_fork(spec.model, fork)) + if isinstance(spec, (SszList, SszVector, SszProgressiveList)): + return [_to_rmk(spec.element, v, fork) for v in value] + if isinstance(spec, (SszBitvector, SszBitlist, SszProgressiveBitlist)): + return list(value) + return value # scalar / byte-vector / byte-list: remerkleable coerces + + +def _rmk_instance(model: "SszModel", fork: Optional[str] = None) -> Container: + model_cls: Type[SszModel] = type(model) + names = _schema_fields(model_cls, fork) + if fork is not None: + _check_populated(model, names, fork) + container = build_ssz_type(model_cls, fork) + values = { + name: _to_rmk(spec_of(model_cls, name), getattr(model, name), fork) + for name in names + } + return container(**values) + + +def _to_py(spec: SszType, value: Any, fork: Optional[str] = None) -> Any: + if isinstance(spec, (SszContainer, SszProgressiveContainer)): + nested = _nested_fork(spec.model, fork) + return _view_to_model( + spec.model, value, _schema_fields(spec.model, nested), nested + ) + if isinstance(spec, (SszList, SszVector, SszProgressiveList)): + return [_to_py(spec.element, v, fork) for v in value] + if isinstance(spec, (SszBitvector, SszBitlist, SszProgressiveBitlist)): + return [bool(b) for b in value] + if isinstance(spec, (SszByteVector, SszByteList)): + return bytes(value) + if isinstance(spec, SszUint): + return int(value) + if isinstance(spec, SszBool): + return bool(value) + raise TypeError(f"unhandled SSZ type {spec!r}") + + +def _view_to_model( + model_cls: Type[_M], + view: Container, + names: Optional[Tuple[str, ...]] = None, + fork: Optional[str] = None, +) -> _M: + if names is None: + names = _included_fields(model_cls) + # Fields beyond `names` (older-fork decodes) keep their None default. + return model_cls( + **{ + name: _to_py(spec_of(model_cls, name), getattr(view, name), fork) + for name in names + } + ) + + +def default_value(spec: SszType, fork: Optional[str] = None) -> Any: + """Return the SSZ default (zero) value for spec as a pydantic value.""" + if isinstance(spec, SszUint): + return 0 + if isinstance(spec, SszByteVector): + return b"\x00" * spec.length + if isinstance( + spec, + (SszByteList, SszList, SszBitlist, SszProgressiveList), + ): + return [] + if isinstance(spec, SszProgressiveBitlist): + return [] + if isinstance(spec, SszVector): + # A fresh value per slot: container defaults are mutable, so a shared + # [x] * n would alias one instance across every position. + return [default_value(spec.element, fork) for _ in range(spec.length)] + if isinstance(spec, SszBitvector): + return [False] * spec.length + if isinstance(spec, (SszContainer, SszProgressiveContainer)): + return ssz_default(spec.model, _nested_fork(spec.model, fork)) + if isinstance(spec, SszBool): + return False + raise TypeError(f"no default for SSZ type {spec!r}") + + +def ssz_default(model_cls: Type[_M], fork: Optional[str] = None) -> _M: + """ + Build the SSZ default (all-zero) instance of model_cls. + + Fork-scoped models require fork; fields beyond it stay None. + """ + return model_cls( + **{ + name: default_value(spec_of(model_cls, name), fork) + for name in _schema_fields(model_cls, fork) + } + ) + + +def describe_type(spec: SszType) -> str: + """Render an SSZ type as text (uint64, List[T, N], ...).""" + if isinstance(spec, SszUint): + return f"uint{spec.bits}" + if isinstance(spec, SszByteVector): + return f"ByteVector[{spec.length}]" + if isinstance(spec, SszByteList): + return f"ByteList[{spec.limit}]" + if isinstance(spec, SszList): + return f"List[{describe_type(spec.element)}, {spec.limit}]" + if isinstance(spec, SszVector): + return f"Vector[{describe_type(spec.element)}, {spec.length}]" + if isinstance(spec, SszBitvector): + return f"Bitvector[{spec.length}]" + if isinstance(spec, SszBitlist): + return f"Bitlist[{spec.limit}]" + if isinstance(spec, SszProgressiveList): + return f"ProgressiveList[{describe_type(spec.element)}]" + if isinstance(spec, SszProgressiveBitlist): + return "ProgressiveBitlist" + if isinstance(spec, SszContainer): + return spec.model.__name__ + if isinstance(spec, SszProgressiveContainer): + return f"Progressive[{spec.model.__name__}]" + if isinstance(spec, SszBool): + return "boolean" + raise TypeError(f"unhandled SSZ type {spec!r}") + + +def describe_schema( + model_cls: Type["SszModel"], fork: Optional[str] = None +) -> str: + """ + Render the resolved SSZ layout, one 'field: type' line per field. + + Fork-scoped models require fork and render that fork's projection. + """ + title = model_cls.__name__ + (f" @ {fork}" if fork else "") + lines = [f"{title}:"] + for name in _schema_fields(model_cls, fork): + lines.append(f" {name}: {describe_type(spec_of(model_cls, name))}") + return "\n".join(lines) + + +def encode(model: "SszModel", fork: Optional[str] = None) -> bytes: + """ + Return the SSZ wire bytes of model. + + A fork-scoped model requires fork and is + checked against that fork's schema before encoding. + """ + return _rmk_instance(model, fork).encode_bytes() + + +def hash_tree_root(model: "SszModel", fork: Optional[str] = None) -> bytes: + """ + Return the 32-byte SSZ hash_tree_root of model. + + Fork-scoped models require fork, exactly as encode does. + """ + return bytes(_rmk_instance(model, fork).hash_tree_root()) + + +def decode(model_cls: Type[_M], data: bytes, fork: Optional[str] = None) -> _M: + """ + Decode SSZ data into an instance of model_cls. + + For a fork-scoped model, data is decoded as fork's container and + fields beyond that fork come back as None. + """ + view = build_ssz_type(model_cls, fork).decode_bytes(data) + return _view_to_model( + model_cls, view, _schema_fields(model_cls, fork), fork + ) + + +# width-carrying integer types (base_types.HexNumber underneath) +class _SizedUint(HexNumber): + """ + A width-checked unsigned integer. + """ + + __bits__: ClassVar[int] = 0 + + def __new__(cls, input_number: Any) -> "_SizedUint": + """Create the integer, enforcing 0 <= value < 2**bits.""" + value = super().__new__(cls, input_number) + if not 0 <= int(value) < (1 << cls.__bits__): + raise ValueError(f"{cls.__name__} out of range: {int(value)}") + return value + + +class Uint8(_SizedUint): + """An 8-bit unsigned integer.""" + + __bits__: ClassVar[int] = 8 + __ssz__: ClassVar[SszType] = SszUint(8) + + +class Uint16(_SizedUint): + """A 16-bit unsigned integer.""" + + __bits__: ClassVar[int] = 16 + __ssz__: ClassVar[SszType] = SszUint(16) + + +class Uint32(_SizedUint): + """A 32-bit unsigned integer.""" + + __bits__: ClassVar[int] = 32 + __ssz__: ClassVar[SszType] = SszUint(32) + + +class Uint64(_SizedUint): + """A 64-bit unsigned integer.""" + + __bits__: ClassVar[int] = 64 + __ssz__: ClassVar[SszType] = SszUint(64) + + +class Uint128(_SizedUint): + """A 128-bit unsigned integer.""" + + __bits__: ClassVar[int] = 128 + __ssz__: ClassVar[SszType] = SszUint(128) + + +class Uint256(_SizedUint): + """A 256-bit unsigned integer.""" + + __bits__: ClassVar[int] = 256 + __ssz__: ClassVar[SszType] = SszUint(256) + + +__all__ = [ + "ProgressiveModel", + "SszBitlist", + "SszBitvector", + "SszBool", + "SszByteList", + "SszByteVector", + "SszContainer", + "SszForkSchema", + "SszList", + "SszModel", + "SszProgressiveBitlist", + "SszProgressiveContainer", + "SszProgressiveList", + "SszType", + "SszUint", + "SszVector", + "Uint128", + "Uint16", + "Uint256", + "Uint32", + "Uint64", + "Uint8", + "bitlist", + "bitvector", + "byte_list", + "build_ssz_type", + "decode", + "default_value", + "describe_schema", + "describe_type", + "encode", + "hash_tree_root", + "progressive_bitlist", + "progressive_list", + "spec_of", + "ssz_default", + "ssz_exclude", + "ssz_fields", + "ssz_list", + "ssz_vector", +] diff --git a/packages/testing/src/execution_testing/base_types/tests/test_ssz.py b/packages/testing/src/execution_testing/base_types/tests/test_ssz.py new file mode 100644 index 00000000000..7b1b8ee649f --- /dev/null +++ b/packages/testing/src/execution_testing/base_types/tests/test_ssz.py @@ -0,0 +1,960 @@ +""" +Tests for SSZ support in base_types. +""" + +from typing import Annotated, Callable, List, Optional, Tuple + +import pytest +from pydantic import ValidationError +from remerkleable.basic import boolean, uint8, uint64, uint256 +from remerkleable.bitfields import Bitlist as RmkBitlist +from remerkleable.bitfields import Bitvector as RmkBitvector +from remerkleable.byte_arrays import ByteList, ByteVector +from remerkleable.complex import Container +from remerkleable.complex import List as RmkList +from remerkleable.complex import Vector as RmkVector +from remerkleable.progressive import ( + ProgressiveBitlist as RmkProgressiveBitlist, +) +from remerkleable.progressive import ProgressiveContainer +from remerkleable.progressive import ProgressiveList as RmkProgressiveList + +from execution_testing.base_types import Address, Bloom, Bytes, Hash +from execution_testing.base_types.ssz import ( + ProgressiveModel, + SszForkSchema, + SszModel, + SszUint, + Uint8, + Uint16, + Uint32, + Uint64, + Uint128, + Uint256, + bitlist, + bitvector, + build_ssz_type, + byte_list, + decode, + describe_schema, + encode, + hash_tree_root, + progressive_bitlist, + progressive_list, + spec_of, + ssz_default, + ssz_exclude, + ssz_fields, + ssz_list, + ssz_vector, +) + +MAX_EXTRA = 32 +MAX_BYTES_PER_TX = 2**30 +MAX_TXS = 2**20 +MAX_WITHDRAWALS = 16 +CELLS = 128 +BITS = [i % 3 == 0 for i in range(CELLS)] + + +class Withdrawal(SszModel): + """A pydantic model declared to check the SSZ machinery.""" + + index: Uint64 + validator_index: Uint64 + address: Address + amount: Uint64 + + +class ExecutionPayload(SszModel): + """An Amsterdam-shaped payload exercising every field kind.""" + + parent_hash: Hash + fee_recipient: Address + state_root: Hash + logs_bloom: Bloom + block_number: Uint64 + base_fee_per_gas: Uint256 + extra_data: Annotated[Bytes, byte_list(MAX_EXTRA)] + transactions: Annotated[ + List[Annotated[Bytes, byte_list(MAX_BYTES_PER_TX)]], + ssz_list(MAX_TXS), + ] + withdrawals: Annotated[List[Withdrawal], ssz_list(MAX_WITHDRAWALS)] + + +class Status(SszModel): + """A boolean and a fixed bit vector.""" + + ok: bool + columns: Annotated[List[bool], bitvector(CELLS)] + + +class Committee(SszModel): + """A fixed Vector[uint64, N] and a variable Bitlist[N].""" + + seats: Annotated[List[Uint64], ssz_vector(3)] + flags: Annotated[List[bool], bitlist(8)] + + +class Ballot(SszModel): + """An uncapped progressive bit list.""" + + votes: Annotated[List[bool], progressive_bitlist()] + + +class Prog(ProgressiveModel): + """EIP-7916 progressive container with a progressive list.""" + + a: Uint64 + b: Uint8 + items: Annotated[List[Uint64], progressive_list()] + + +class GapProg(ProgressiveModel): + """Two fields around a reserved (0) middle slot.""" + + __active_fields__ = [1, 0, 1] + + a: Uint64 + c: Uint64 + + +class MixedProg(ProgressiveModel): + """A progressive container carrying a JSON-only (excluded) field.""" + + a: Uint64 + note: Annotated[str, ssz_exclude()] = "json-only" + c: Uint64 + + +class ForkedPayload(SszModel): + """One model for every fork.""" + + parent_hash: Hash + blob_gas_used: Uint64 | None = None + block_number: Uint64 + transactions: Annotated[ # JSON order differs from SSZ. + List[Annotated[Bytes, byte_list(MAX_BYTES_PER_TX)]], + ssz_list(MAX_TXS), + ] + withdrawals: ( + Annotated[List[Withdrawal], ssz_list(MAX_WITHDRAWALS)] | None + ) = None + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("parent_hash", "block_number", "transactions"), + appended={ + "Shanghai": ("withdrawals",), + "Cancun": ("blob_gas_used",), + }, + ) + + +class Mixed(SszModel): + """An SSZ container carrying a JSON-only (excluded) field.""" + + a: Uint64 + note: Annotated[str, ssz_exclude()] = "json-only" + + +class RefWithdrawal(Container): + """Hand-written twin of Withdrawal.""" + + index: uint64 + validator_index: uint64 + address: ByteVector[20] + amount: uint64 + + +class RefPayload(Container): + """Hand-written twin of ExecutionPayload.""" + + parent_hash: ByteVector[32] + fee_recipient: ByteVector[20] + state_root: ByteVector[32] + logs_bloom: ByteVector[256] + block_number: uint64 + base_fee_per_gas: uint256 + extra_data: ByteList[MAX_EXTRA] + transactions: RmkList[ByteList[MAX_BYTES_PER_TX], MAX_TXS] + withdrawals: RmkList[RefWithdrawal, MAX_WITHDRAWALS] + + +class RefStatus(Container): + """Hand-written twin of Status.""" + + ok: boolean + columns: RmkBitvector[CELLS] + + +class RefCommittee(Container): + """Hand-written twin of Committee.""" + + seats: RmkVector[uint64, 3] + flags: RmkBitlist[8] + + +class RefBallot(Container): + """Hand-written twin of Ballot.""" + + votes: RmkProgressiveBitlist + + +class RefProg(ProgressiveContainer(active_fields=[1, 1, 1])): # type: ignore[misc] + """Hand-written twin of Prog.""" + + a: uint64 + b: uint8 + items: RmkProgressiveList[uint64] + + +class RefGapProg(ProgressiveContainer(active_fields=[1, 0, 1])): # type: ignore[misc] + """Hand-written twin of GapProg.""" + + a: uint64 + c: uint64 + + +class RefMixedProg(ProgressiveContainer(active_fields=[1, 1])): # type: ignore[misc] + """Hand-written twin of MixedProg: the excluded field takes no slot.""" + + a: uint64 + c: uint64 + + +class RefForkedParis(Container): + """Hand-written twin of ForkedPayload at Paris.""" + + parent_hash: ByteVector[32] + block_number: uint64 + transactions: RmkList[ByteList[MAX_BYTES_PER_TX], MAX_TXS] + + +class RefForkedShanghai(Container): + """Hand-written twin of ForkedPayload at Shanghai.""" + + parent_hash: ByteVector[32] + block_number: uint64 + transactions: RmkList[ByteList[MAX_BYTES_PER_TX], MAX_TXS] + withdrawals: RmkList[RefWithdrawal, MAX_WITHDRAWALS] + + +class RefMixed(Container): # the excluded field simply does not exist + """Hand-written twin of Mixed (no excluded field).""" + + a: uint64 + + +def _withdrawal() -> Withdrawal: + return Withdrawal( + index=7, + validator_index=42, + address=Address(b"\x11" * 20), + amount=32_000_000_000, + ) + + +def _ref_withdrawal() -> Container: + return RefWithdrawal( + index=7, + validator_index=42, + address=b"\x11" * 20, + amount=32_000_000_000, + ) + + +def _payload() -> ExecutionPayload: + return ExecutionPayload( + parent_hash=Hash(b"\xaa" * 32), + fee_recipient=Address(b"\xbb" * 20), + state_root=Hash(b"\xcc" * 32), + logs_bloom=Bloom(b"\x00" * 256), + block_number=21_000_000, + base_fee_per_gas=10**18, + extra_data=Bytes(b"\xde\xad"), + transactions=[Bytes(b"\x02\xf8"), Bytes(b"\x03" * 5)], + withdrawals=[_withdrawal()], + ) + + +def _ref_payload() -> Container: + return RefPayload( + parent_hash=b"\xaa" * 32, + fee_recipient=b"\xbb" * 20, + state_root=b"\xcc" * 32, + logs_bloom=b"\x00" * 256, + block_number=21_000_000, + base_fee_per_gas=10**18, + extra_data=b"\xde\xad", + transactions=[b"\x02\xf8", b"\x03" * 5], + withdrawals=[_ref_withdrawal()], + ) + + +def _paris_payload() -> ForkedPayload: + return ForkedPayload( + parent_hash=Hash(b"\xaa" * 32), + block_number=100, + transactions=[Bytes(b"\x02\xf8")], + ) + + +def _shanghai_payload() -> ForkedPayload: + return ForkedPayload( + parent_hash=Hash(b"\xaa" * 32), + block_number=100, + transactions=[Bytes(b"\x02\xf8")], + withdrawals=[_withdrawal()], + ) + + +def assert_matches_reference( + model: SszModel, ref: Container, fork: Optional[str] = None +) -> None: + """ + Compare the engine against a hand-written remerkleable twin. + + The twin is the ground truth: everything observable must + """ + model_cls = type(model) + ref_cls = type(ref) + raw = encode(model, fork) + # populated instance: wire bytes + merkle root + assert raw == ref.encode_bytes() + assert hash_tree_root(model, fork) == bytes(ref.hash_tree_root()) + # decode round-trips losslessly, back to an equal pydantic model + restored = decode(model_cls, raw, fork) + assert encode(restored, fork) == raw + assert restored == model + # both sides agree on the zero value + zero = ssz_default(model_cls, fork) + assert encode(zero, fork) == ref_cls().encode_bytes() + assert hash_tree_root(zero, fork) == bytes(ref_cls().hash_tree_root()) + + +TWIN_CASES: List[ + Tuple[ + str, + Callable[[], SszModel], + Callable[[], Container], + Optional[str], + ] +] = [ + ("withdrawal", _withdrawal, _ref_withdrawal, None), + ("payload", _payload, _ref_payload, None), + ( + "bool-bitvector", + lambda: Status(ok=True, columns=BITS), + lambda: RefStatus(ok=True, columns=BITS), + None, + ), + ( + "vector-bitlist", + lambda: Committee(seats=[1, 2, 3], flags=[True, False, True]), + lambda: RefCommittee(seats=[1, 2, 3], flags=[True, False, True]), + None, + ), + ( + "progressive-bitlist", + lambda: Ballot(votes=[True, False, True, True]), + lambda: RefBallot(votes=[True, False, True, True]), + None, + ), + ( + "progressive", + lambda: Prog(a=5, b=9, items=[10, 20, 30]), + lambda: RefProg(a=5, b=9, items=[10, 20, 30]), + None, + ), + ( + "progressive-gap", + lambda: GapProg(a=1, c=3), + lambda: RefGapProg(a=1, c=3), + None, + ), + ( + "progressive-excluded", + lambda: MixedProg(a=1, c=3), + lambda: RefMixedProg(a=1, c=3), + None, + ), + ( + # default-valued excluded field: decode restores the default, so + # the harness's restored == model leg holds; the non-default case + # is covered by test_excluded_field_is_json_only. + "excluded-field", + lambda: Mixed(a=7), + lambda: RefMixed(a=7), + None, + ), + ( + "forked-paris", + _paris_payload, + lambda: RefForkedParis( + parent_hash=b"\xaa" * 32, + block_number=100, + transactions=[b"\x02\xf8"], + ), + "Paris", + ), + ( + "forked-shanghai", + _shanghai_payload, + lambda: RefForkedShanghai( + parent_hash=b"\xaa" * 32, + block_number=100, + transactions=[b"\x02\xf8"], + withdrawals=[_ref_withdrawal()], + ), + "Shanghai", + ), +] + + +@pytest.mark.parametrize( + "make_model,make_ref,fork", + [pytest.param(m, r, f, id=name) for name, m, r, f in TWIN_CASES], +) +def test_matches_remerkleable_reference( + make_model: Callable[[], SszModel], + make_ref: Callable[[], Container], + fork: Optional[str], +) -> None: + """Every model kind is byte-identical to its hand-written twin.""" + assert_matches_reference(make_model(), make_ref(), fork) + + +def test_full_payload_round_trips() -> None: + """A container with every field kind round-trips pydantic<->SSZ.""" + payload = _payload() + restored = decode(ExecutionPayload, encode(payload)) + assert restored.parent_hash == payload.parent_hash + assert int(restored.base_fee_per_gas) == 10**18 + assert [bytes(t) for t in restored.transactions] == [ + b"\x02\xf8", + b"\x03" * 5, + ] + assert int(restored.withdrawals[0].amount) == 32_000_000_000 + assert len(hash_tree_root(payload)) == 32 + + +def test_ssz_default_matches_remerkleable_zero() -> None: + """ssz_default builds the SSZ zero value, like remerkleable's default.""" + zero = ssz_default(ExecutionPayload) + assert int(zero.block_number) == 0 + assert zero.transactions == [] + assert zero.withdrawals == [] + assert bytes(zero.parent_hash) == b"\x00" * 32 + # zero encodes identically to a freshly-defaulted remerkleable container + assert encode(zero) == build_ssz_type(ExecutionPayload)().encode_bytes() + + +def test_describe_schema_renders_every_field_kind() -> None: + """describe_schema renders the resolved SSZ type of each field.""" + schema = describe_schema(ExecutionPayload) + assert "block_number: uint64" in schema + assert "base_fee_per_gas: uint256" in schema + assert "parent_hash: ByteVector[32]" in schema + assert f"extra_data: ByteList[{MAX_EXTRA}]" in schema + assert ( + f"transactions: List[ByteList[{MAX_BYTES_PER_TX}], {MAX_TXS}]" + in schema + ) + assert f"withdrawals: List[Withdrawal, {MAX_WITHDRAWALS}]" in schema + # progressive kinds render their consensus-style names + assert "items: ProgressiveList[uint64]" in describe_schema(Prog) + assert "votes: ProgressiveBitlist" in describe_schema(Ballot) + + +def test_default_vector_of_container_has_independent_slots() -> None: + """A defaulted Vector-of-container has independent (non-aliased) slots.""" + + class Inner(SszModel): + x: Uint64 + + class Outer(SszModel): + items: Annotated[List[Inner], ssz_vector(3)] + + zero = ssz_default(Outer) + assert len(zero.items) == 3 + zero.items[0].x = Uint64(99) + # Mutating one slot must not bleed into its siblings. + assert int(zero.items[1].x) == 0 + assert int(zero.items[2].x) == 0 + + +def test_forked_model_json_omission_unchanged() -> None: + """The JSON leg keeps today's exclude_none single-model behavior.""" + dumped = _shanghai_payload().model_dump( + mode="json", by_alias=True, exclude_none=True + ) + assert "withdrawals" in dumped + assert "blobGasUsed" not in dumped # pre-Cancun: key simply absent + + +def test_forked_model_decode_fills_none() -> None: + """Decoding an older fork's bytes restores the one model with None.""" + shanghai = _shanghai_payload() + raw = encode(shanghai, fork="Shanghai") + restored = decode(ForkedPayload, raw, fork="Shanghai") + assert restored == shanghai + assert restored.blob_gas_used is None # beyond-fork field stays None + assert restored.withdrawals is not None + + +def test_fork_scoped_nested_in_complete_model_raises() -> None: + """ + A COMPLETE model cannot carry a fork-scoped one. + + Without a fork at the outer encode there is nothing to propagate to + the nested container, so the encode must refuse rather than pick a + schema silently. (Fork-scoped outer models propagate their fork; see + test_fork_propagates_to_nested_containers.) + """ + + class Wrapper(SszModel): + payload: ForkedPayload + + wrapper = Wrapper(payload=_shanghai_payload()) + with pytest.raises(TypeError, match="fork-scoped"): + encode(wrapper) + + +def test_fork_propagates_to_nested_containers() -> None: + """ + One fork projects the whole value tree (envelope contains payload). + + This is the general #793 shape: fork-evolving containers nest other + fork-evolving containers, and everything inside one message is at + the same chain fork. The outer fork= selects every nested projection. + """ + + class Envelope(SszModel): + payload: ForkedPayload + blob_count: Uint64 | None = None # Shanghai-era envelope field + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("payload",), + appended={"Shanghai": ("blob_count",)}, + ) + + class RefEnvelopeShanghai(Container): + payload: RefForkedShanghai + blob_count: uint64 + + envelope = Envelope(payload=_shanghai_payload(), blob_count=3) + ref = RefEnvelopeShanghai( + payload=RefForkedShanghai( + parent_hash=b"\xaa" * 32, + block_number=100, + transactions=[b"\x02\xf8"], + withdrawals=[_ref_withdrawal()], + ), + blob_count=3, + ) + assert_matches_reference(envelope, ref, fork="Shanghai") + # decode restores both levels, beyond-fork fields None at each level + restored = decode(Envelope, encode(envelope, "Shanghai"), fork="Shanghai") + assert restored.payload.blob_gas_used is None + # a nested payload that does not fit the propagated fork still raises + paris_inside = Envelope(payload=_paris_payload(), blob_count=1) + with pytest.raises(TypeError, match="missing=\\['withdrawals'\\]"): + encode(paris_inside, fork="Shanghai") + + +def test_forked_model_describe_schema_per_fork() -> None: + """describe_schema renders each fork's projection in SSZ order.""" + paris = describe_schema(ForkedPayload, fork="Paris") + cancun = describe_schema(ForkedPayload, fork="Cancun") + assert "blob_gas_used" not in paris + assert cancun.splitlines()[-1].strip() == "blob_gas_used: uint64" + # SSZ order comes from the schema tuples, not the class body: the + # model declares blob_gas_used second, but it encodes LAST. + assert cancun.splitlines()[1].strip() == "parent_hash: ByteVector[32]" + + +def _bad_vector_marker_on_scalar() -> None: + class Bad(SszModel): + seats: Annotated[Uint64, ssz_vector(3)] # not a list + + +def _bad_byte_list_on_int() -> None: + class Bad(SszModel): + data: Annotated[Uint64, byte_list(8)] # not Bytes + + +def _bad_bit_marker_on_ints() -> None: + class Bad(SszModel): + flags: Annotated[List[Uint64], bitlist(8)] # not list[bool] + + +def _bad_raw_ssz_type_marker() -> None: + class Bad(SszModel): + x: Annotated[Uint64, SszUint(32)] # raw SszType, not a helper + + +def _bad_unmapped_str() -> None: + class Bad(SszModel): + s: str # no SSZ mapping and not excluded + + +def _bad_bare_bytes() -> None: + class Bad(SszModel): + data: Bytes # variable bytes need a byte_list cap + + +def _bad_bare_list() -> None: + class Bad(SszModel): + items: List[Uint64] # lists need a cap/length marker + + +def _bad_multi_arm_union() -> None: + class Bad(SszModel): + x: Uint64 | Uint8 | None = None # only T | None supported + + +def _bad_optional_without_schema() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 | None = None # optional but no schema + + +def _bad_schema_field_typo() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 | None = None + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("a",), + appended={"Shanghai": ("typo",)}, + ) + + +def _bad_required_appended() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 # appended but not optional + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("a",), + appended={"Shanghai": ("b",)}, + ) + + +def _bad_optional_base() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 | None = None # optional but declared in base + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("a", "b"), + appended={}, + ) + + +def _bad_appended_no_default() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 | None # optional type but NO None default + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("a",), + appended={"Shanghai": ("b",)}, + ) + + +def _bad_duplicate_schema_names() -> None: + class Bad(SszModel): + a: Uint64 + b: Uint64 | None = None + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("a", "a"), + appended={"Shanghai": ("b",)}, + ) + + +def _bad_required_excluded() -> None: + class Bad(SszModel): + a: Uint64 + note: Annotated[str, ssz_exclude()] # excluded but required + + +def _bad_progressive_with_schema() -> None: + class Bad(ProgressiveModel): + a: Uint64 + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", base=("a",), appended={} + ) + + +def _bad_progressive_with_optional() -> None: + class Bad(ProgressiveModel): + a: Uint64 + b: Uint64 | None = None + + +def _bad_progressive_active_count() -> None: + class Bad(ProgressiveModel): + __active_fields__ = [1, 1] # two active, three declared fields + + a: Uint64 + b: Uint64 + c: Uint64 + + +def _bad_progressive_active_counts_excluded() -> None: + # An excluded field takes no slot, so the third 1 has no field to + # fill it: caught here rather than inside remerkleable at first + # build_ssz_type. + class Bad(ProgressiveModel): + __active_fields__ = [1, 1, 1] # three active, two SSZ fields + + a: Uint64 + note: Annotated[str, ssz_exclude()] = "json-only" + c: Uint64 + + +BAD_DECLARATIONS: List[Tuple[str, Callable[[], None], str]] = [ + ("vector-on-scalar", _bad_vector_marker_on_scalar, "requires a list"), + ("byte-list-on-int", _bad_byte_list_on_int, "byte_list requires"), + ("bits-on-ints", _bad_bit_marker_on_ints, "list\\[bool\\]"), + ("raw-marker", _bad_raw_ssz_type_marker, "unsupported Annotated"), + ("unmapped-str", _bad_unmapped_str, "no SSZ type"), + ("bare-bytes", _bad_bare_bytes, "no SSZ type"), + ("bare-list", _bad_bare_list, "no SSZ type"), + ("multi-arm-union", _bad_multi_arm_union, "only T \\| None"), + ("optional-no-schema", _bad_optional_without_schema, "no __ssz_schema__"), + ("schema-typo", _bad_schema_field_typo, "does not match the model"), + ("required-appended", _bad_required_appended, "must be T \\| None"), + ("optional-base", _bad_optional_base, "optional base"), + ("appended-no-default", _bad_appended_no_default, "default to None"), + ("dup-schema-names", _bad_duplicate_schema_names, "more than once"), + ("required-excluded", _bad_required_excluded, "no default"), + ("progressive-schema", _bad_progressive_with_schema, "not supported"), + ("progressive-optional", _bad_progressive_with_optional, "not supported"), + ("progressive-count", _bad_progressive_active_count, "active"), + ( + "progressive-count-excluded", + _bad_progressive_active_counts_excluded, + "3 active entries but the container declares 2 SSZ fields", + ), +] + + +@pytest.mark.parametrize( + "define,match", + [pytest.param(fn, match, id=name) for name, fn, match in BAD_DECLARATIONS], +) +def test_bad_declaration_fails_at_import( + define: Callable[[], None], match: str +) -> None: + """Every mis-declared container fails at class definition, named.""" + with pytest.raises(TypeError, match=match): + define() + + +STRICTNESS: List[Tuple[str, Optional[str], str]] = [ + ("bare-encode", None, "fork-scoped"), + ("older-fork", "Paris", "unexpected=\\['withdrawals'\\]"), + ("newer-fork", "Cancun", "missing=\\['blob_gas_used'\\]"), + ("unknown-fork", "Osaka", "unknown fork"), +] + + +@pytest.mark.parametrize( + "fork,match", + [pytest.param(f, m, id=name) for name, f, m in STRICTNESS], +) +def test_forked_model_strictness(fork: Optional[str], match: str) -> None: + """A Shanghai payload only encodes under the Shanghai schema.""" + with pytest.raises(TypeError, match=match): + encode(_shanghai_payload(), fork=fork) + + +NOT_FORK_SCOPED: List[Tuple[str, Callable[[], object]]] = [ + ("encode", lambda: encode(_withdrawal(), fork="Paris")), + ("decode", lambda: decode(Withdrawal, b"", fork="Paris")), + ("describe", lambda: describe_schema(Withdrawal, fork="Paris")), + ("default", lambda: ssz_default(Withdrawal, "Paris")), + ("fields", lambda: ssz_fields(Withdrawal, "Paris")), +] + + +@pytest.mark.parametrize( + "call", + [pytest.param(c, id=name) for name, c in NOT_FORK_SCOPED], +) +def test_fork_on_complete_model_raises(call: Callable[[], object]) -> None: + """Passing fork= to a non-fork-scoped model raises on every path.""" + with pytest.raises(TypeError, match="is not fork-scoped"): + call() + + +def test_ssz_default_per_fork() -> None: + """ssz_default(fork) zeroes that fork's fields, leaves the rest None.""" + zero = ssz_default(ForkedPayload, "Shanghai") + assert zero.withdrawals == [] + assert zero.blob_gas_used is None # beyond Shanghai: absent, not zero + assert encode(zero, "Shanghai") == RefForkedShanghai().encode_bytes() + with pytest.raises(TypeError, match="fork-scoped"): + ssz_default(ForkedPayload) # bare default: must name the fork + + +@pytest.mark.parametrize("mutation", ["truncate", "extend"]) +def test_decode_of_malformed_bytes_raises(mutation: str) -> None: + """Truncated or oversized SSZ data raises, never mis-decodes.""" + raw = encode(_withdrawal()) + data = raw[:-1] if mutation == "truncate" else raw + b"\x00" + with pytest.raises(ValueError): + decode(Withdrawal, data) + + +def test_decode_under_wrong_fork_does_not_silently_succeed() -> None: + """Shanghai bytes decoded as Cancun raise (schema sizes differ).""" + raw = encode(_shanghai_payload(), fork="Shanghai") + with pytest.raises(Exception): # noqa: B017 - remerkleable's error + decode(ForkedPayload, raw, fork="Cancun") + + +def test_build_ssz_type_cache_identity() -> None: + """One cache entry per (class, fork); distinct classes never share.""" + assert build_ssz_type(Withdrawal) is build_ssz_type(Withdrawal, None) + assert build_ssz_type(ForkedPayload, "Paris") is build_ssz_type( + ForkedPayload, "Paris" + ) + assert build_ssz_type(ForkedPayload, "Paris") is not build_ssz_type( + ForkedPayload, "Shanghai" + ) + + def make_dup() -> type: + class Dup(SszModel): + a: Uint64 + + return Dup + + first, second = make_dup(), make_dup() + assert first is not second + assert build_ssz_type(first) is not build_ssz_type(second) + + +UINT_WIDTHS = [ + (Uint8, 8), + (Uint16, 16), + (Uint32, 32), + (Uint64, 64), + (Uint128, 128), + (Uint256, 256), +] + + +@pytest.mark.parametrize( + "uint_cls,bits", + [pytest.param(c, b, id=c.__name__) for c, b in UINT_WIDTHS], +) +def test_uint_width_checked_at_construction(uint_cls: type, bits: int) -> None: + """A wrong-width value fails when built, not at first encode.""" + assert int(uint_cls((1 << bits) - 1)) == (1 << bits) - 1 + with pytest.raises(ValueError, match="out of range"): + uint_cls(1 << bits) + with pytest.raises(ValueError, match="out of range"): + uint_cls(-1) + + +def test_uint_width_checked_at_model_parse() -> None: + """Pydantic parsing of an overflowing value fails loudly.""" + with pytest.raises(ValidationError): + Withdrawal( + index=1, + validator_index=2, + address=Address(b"\x00" * 20), + amount=1 << 64, # one past uint64 + ) + + +def test_excluded_field_is_json_only() -> None: + """ssz_exclude()d fields exist in JSON but are invisible to SSZ.""" + value = Mixed(a=7, note="kept in JSON") + assert "note" in value.model_dump(mode="json") + assert ssz_fields(Mixed) == ("a",) + # decode cannot see the field; it comes back as the default + restored = decode(Mixed, encode(value)) + assert int(restored.a) == 7 + assert restored.note == "json-only" + + +def test_excluded_field_on_fork_scoped_model() -> None: + """Exclusion composes with __ssz_schema__ (schema skips the field).""" + + class ForkedMixed(SszModel): + a: Uint64 + b: Uint64 | None = None + note: Annotated[str, ssz_exclude()] = "aux" + + __ssz_schema__ = SszForkSchema( + base_fork="One", + base=("a",), + appended={"Two": ("b",)}, + ) + + value = ForkedMixed(a=1, note="ride-along") + assert ssz_fields(ForkedMixed, "One") == ("a",) + restored = decode(ForkedMixed, encode(value, "One"), "One") + assert restored.b is None + assert restored.note == "aux" + + +def test_excluded_field_takes_no_active_slot() -> None: + """An excluded field is absent from the active-field bitvector.""" + assert ssz_fields(MixedProg) == ("a", "c") + + class GapMixedProg(ProgressiveModel): + __active_fields__ = [1, 0, 1] # two active, two SSZ fields + + a: Uint64 + note: Annotated[str, ssz_exclude()] = "json-only" + c: Uint64 + + assert ssz_fields(GapMixedProg) == ("a", "c") + assert hash_tree_root(GapMixedProg(a=1, c=3)) == hash_tree_root( + GapProg(a=1, c=3) + ) + + +def test_exclusion_is_inherited() -> None: + """A subclass keeps the base's excluded fields excluded.""" + + class MixedChild(Mixed): + b: Uint64 + + assert ssz_fields(MixedChild) == ("a", "b") + + +def test_spec_of_rejects_excluded_field() -> None: + """spec_of refuses excluded fields instead of resolving the type.""" + with pytest.raises(TypeError, match="SSZ-excluded"): + spec_of(Mixed, "note") + + +def test_single_fork_schema_works_end_to_end() -> None: + """A schema with no appended forks is valid and encodable.""" + + class OnlyFork(SszModel): + a: Uint64 + + __ssz_schema__ = SszForkSchema( + base_fork="Only", base=("a",), appended={} + ) + + value = OnlyFork(a=5) + assert ssz_fields(OnlyFork, "Only") == ("a",) + assert decode(OnlyFork, encode(value, "Only"), "Only") == value diff --git a/packages/testing/src/execution_testing/tools/ssz_vectors.py b/packages/testing/src/execution_testing/tools/ssz_vectors.py new file mode 100644 index 00000000000..2bc26b7a9dc --- /dev/null +++ b/packages/testing/src/execution_testing/tools/ssz_vectors.py @@ -0,0 +1,476 @@ +""" +SSZ static-vector generation on top of the base_types SSZ engine. + +Suites mirror consensus-specs exactly, one per RandomizationMode plus a chaos +suite (ssz_random, ssz_zero, ssz_max, ssz_nil, ssz_one, ssz_lengthy, +ssz_random_chaos). Mode semantics are: +zero/max pin scalar CONTENT (0 / all-ones) but keep collections short +(1-byte byte-lists), while emptiness and saturation are their own modes +(nil_count / max_count). Changing modes (random / one_count / max_count / +chaos) yield several cases; the rest are fully determined by one. +""" + +import hashlib +import random +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) + +import yaml + +from execution_testing.base_types.ssz import ( + SszBitlist, + SszBitvector, + SszBool, + SszByteList, + SszByteVector, + SszContainer, + SszList, + SszModel, + SszProgressiveBitlist, + SszProgressiveContainer, + SszProgressiveList, + SszType, + SszUint, + SszVector, + encode, + hash_tree_root, + spec_of, + ssz_fields, +) + +MAX_LIST_LENGTH = 10 +MAX_BYTES_LENGTH = 1000 + +RANDOM_CASE_COUNT = 30 + +_M = TypeVar("_M", bound=SszModel) + +_MODE_NAMES = ( + "random", + "zero", + "max", + "nil", + "one", + "lengthy", +) + + +class RandomizationMode(Enum): + """ + How a value's scalar and collection fields are filled. + """ + + mode_random = 0 + mode_zero = 1 + mode_max = 2 + mode_nil_count = 3 + mode_one_count = 4 + mode_max_count = 5 + + def is_changing(self) -> bool: + """ + Return whether the mode yields varying values across cases. + + True for random, one_count and max_count -- those randomize content, + so several cases are worth generating; the rest are fully determined + by a single case. + """ + return self in ( + RandomizationMode.mode_random, + RandomizationMode.mode_one_count, + RandomizationMode.mode_max_count, + ) + + def to_name(self) -> str: + """Return the canonical short name for this mode.""" + return _MODE_NAMES[self.value] + + +def deterministic_seed(*parts: object) -> int: + """ + Return a stable integer seed derived from parts. + + Uses SHA-256 over the slash-joined string parts. + """ + joined = "/".join(str(part) for part in parts) + digest = hashlib.sha256(joined.encode("utf-8")).digest() + return int.from_bytes(digest, "big") + + +@dataclass(frozen=True) +class VectorCase: + """One ssz_static case: the value, its SSZ bytes, and its root.""" + + value: Any # value.yaml + serialized: bytes # serialized.ssz + root: bytes # roots.yaml + + +def _random_bytes(rng: random.Random, length: int) -> bytes: + return bytes(rng.getrandbits(8) for _ in range(length)) + + +def _bits(rng: random.Random, length: int, mode: RandomizationMode) -> Any: + if mode == RandomizationMode.mode_zero: + return [False] * length + if mode == RandomizationMode.mode_max: + return [True] * length + return [bool(rng.getrandbits(1)) for _ in range(length)] + + +def _bitlist_length( + rng: random.Random, cap: int, mode: RandomizationMode +) -> int: + # Consensus semantics: only the *count* modes pin the length; + # zero/max keep a random length. + if mode == RandomizationMode.mode_nil_count: + return 0 + if mode == RandomizationMode.mode_one_count: + return min(1, cap) + if mode == RandomizationMode.mode_max_count: + return cap + return rng.randint(0, cap) + + +def random_value( + rng: random.Random, + spec: SszType, + mode: RandomizationMode, + *, + max_bytes_length: int = MAX_BYTES_LENGTH, + max_list_length: int = MAX_LIST_LENGTH, + chaos: bool = False, +) -> Any: + """ + Build a pydantic value of spec filled with random data per mode. + + A port of the consensus-specs get_random_ssz_object, branching on the + engine's SszType descriptors. With chaos, the mode is re-drawn at every + level of the value tree. + """ + if chaos: + mode = rng.choice(list(RandomizationMode)) + if isinstance(spec, SszByteList): + if mode == RandomizationMode.mode_nil_count: + return b"" + if mode == RandomizationMode.mode_max_count: + return _random_bytes(rng, min(max_bytes_length, spec.limit)) + if mode == RandomizationMode.mode_one_count: + return _random_bytes(rng, min(1, spec.limit)) + if mode == RandomizationMode.mode_zero: + return b"\x00" * min(1, spec.limit) + if mode == RandomizationMode.mode_max: + return b"\xff" * min(1, spec.limit) + return _random_bytes( + rng, rng.randint(0, min(max_bytes_length, spec.limit)) + ) + if isinstance(spec, SszByteVector): + # Byte vectors are fixed length; no max-bytes cap applies. + if mode == RandomizationMode.mode_zero: + return b"\x00" * spec.length + if mode == RandomizationMode.mode_max: + return b"\xff" * spec.length + return _random_bytes(rng, spec.length) + if isinstance(spec, SszUint): + if mode == RandomizationMode.mode_zero: + return 0 + if mode == RandomizationMode.mode_max: + return (1 << spec.bits) - 1 + return rng.randint(0, (1 << spec.bits) - 1) + if isinstance(spec, SszBool): + if mode == RandomizationMode.mode_zero: + return False + if mode == RandomizationMode.mode_max: + return True + return bool(rng.getrandbits(1)) + if isinstance(spec, SszBitvector): + # Bit vectors are fixed length; no cap applies. + return _bits(rng, spec.length, mode) + if isinstance(spec, SszBitlist): + # Consensus caps bit lists by the LIST cap, not the byte cap. + cap = min(max_list_length, spec.limit) + length = _bitlist_length(rng, cap, mode) + return _bits(rng, length, mode) + if isinstance(spec, SszProgressiveBitlist): + # Progressive bit lists are uncapped; the list cap bounds them. + length = _bitlist_length(rng, max_list_length, mode) + return _bits(rng, length, mode) + if isinstance(spec, (SszList, SszProgressiveList)): + # Progressive lists are uncapped; the list cap bounds them. + limit = max_list_length + if isinstance(spec, SszList) and spec.limit < limit: + limit = spec.limit + length = rng.randint(0, limit) + if mode == RandomizationMode.mode_one_count: + length = 1 + elif mode == RandomizationMode.mode_max_count: + length = limit + elif mode == RandomizationMode.mode_nil_count: + length = 0 + # Shrink the cap for nested collections, as consensus-specs does. + max_list_length = 1 << (max_list_length.bit_length() >> 1) + return [ + random_value( + rng, + spec.element, + mode, + max_bytes_length=max_bytes_length, + max_list_length=max_list_length, + chaos=chaos, + ) + for _ in range(length) + ] + if isinstance(spec, SszVector): + return [ + random_value( + rng, + spec.element, + mode, + max_bytes_length=max_bytes_length, + max_list_length=max_list_length, + chaos=chaos, + ) + for _ in range(spec.length) + ] + if isinstance(spec, (SszContainer, SszProgressiveContainer)): + return random_model( + rng, + spec.model, + mode, + max_bytes_length=max_bytes_length, + max_list_length=max_list_length, + chaos=chaos, + ) + raise TypeError(f"no random value for SSZ type {spec!r}") + + +def random_model( + rng: random.Random, + model_cls: Type[_M], + mode: RandomizationMode, + *, + fork: Optional[str] = None, + max_bytes_length: int = MAX_BYTES_LENGTH, + max_list_length: int = MAX_LIST_LENGTH, + chaos: bool = False, +) -> _M: + """ + Build a model_cls instance filled with random data per mode. + + For a fork-scoped model, fork selects which fields get values; + fields beyond that fork keep their None default. + """ + return model_cls( + **{ + name: random_value( + rng, + spec_of(model_cls, name), + mode, + max_bytes_length=max_bytes_length, + max_list_length=max_list_length, + chaos=chaos, + ) + for name in ssz_fields(model_cls, fork) + } + ) + + +def make_case(model: SszModel, fork: Optional[str] = None) -> VectorCase: + """Turn a model instance into its ssz_static case triple.""" + return VectorCase( + value=model.model_dump(mode="json", exclude_none=True), + serialized=encode(model, fork), + root=hash_tree_root(model, fork), + ) + + +def suite_name(mode: RandomizationMode, chaos: bool = False) -> str: + """Return the consensus suite name for a mode (ssz_random, ...).""" + return f"ssz_{mode.to_name()}" + ("_chaos" if chaos else "") + + +def suite_plan( + count: int = RANDOM_CASE_COUNT, +) -> List[Tuple[str, RandomizationMode, bool, int]]: + """ + Return every suite as (name, mode, chaos, case_count). + + One suite per RandomizationMode plus ssz_random_chaos; changing modes + get count cases, deterministic ones a single case. + """ + plan = [ + ( + suite_name(mode), + mode, + False, + count if mode.is_changing() else 1, + ) + for mode in RandomizationMode + ] + plan.append( + ( + suite_name(RandomizationMode.mode_random, chaos=True), + RandomizationMode.mode_random, + True, + count, + ) + ) + return plan + + +ModelSpec = Union[Type[SszModel], Tuple[Type[SszModel], str]] + + +def _normalize_models( + models: Sequence[ModelSpec], +) -> List[Tuple[Type[SszModel], Optional[str]]]: + """Normalize entries to (model, fork) and reject output collisions.""" + entries: List[Tuple[Type[SszModel], Optional[str]]] = [ + m if isinstance(m, tuple) else (m, None) for m in models + ] + seen: Dict[Tuple[str, Optional[str]], Type[SszModel]] = {} + for model_cls, fork in entries: + key = (model_cls.__name__, fork) + other = seen.setdefault(key, model_cls) + if other is not model_cls: + raise ValueError( + f"two distinct models would share vector output " + f"{key[0]!r} (fork={fork!r}); rename one" + ) + return entries + + +def generate_cases( + models: Sequence[ModelSpec], + *, + count: int = RANDOM_CASE_COUNT, + max_bytes_length: int = MAX_BYTES_LENGTH, + max_list_length: int = MAX_LIST_LENGTH, +) -> Iterator[Tuple[str, Optional[str], str, int, VectorCase]]: + """ + Yield (container_name, fork, suite, case_index, case) per vector. + + Entries are complete models or (fork-scoped model, fork) pairs. The + RNG is seeded per (container, [fork,] suite, index) so output is + fully deterministic across runs. + """ + for model_cls, fork in _normalize_models(models): + name = model_cls.__name__ + seed_head = (name, fork) if fork else (name,) + for suite, mode, chaos, n in suite_plan(count): + for i in range(n): + rng = random.Random(deterministic_seed(*seed_head, suite, i)) + model = random_model( + rng, + model_cls, + mode, + fork=fork, + max_bytes_length=max_bytes_length, + max_list_length=max_list_length, + chaos=chaos, + ) + yield name, fork, suite, i, make_case(model, fork) + + +class _HexQuotingDumper(yaml.SafeDumper): + """SafeDumper that single-quotes 0x-hex strings (see _yaml_dump).""" + + +def _represent_str(dumper: Any, data: str) -> Any: + style = "'" if data.startswith("0x") else None + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style) + + +_HexQuotingDumper.add_representer(str, _represent_str) + + +def _yaml_dump(obj: Any) -> bytes: + """ + Dump YAML with 0x-hex strings explicitly single-quoted. + + PyYAML's emitter is not consistent across interpreters about quoting + strings that look like YAML 1.1 ints (PyPy emits root: 0x... bare, which + a loader would read back as an integer). Consensus vectors always quote + them, so force the style instead of trusting the emitter. + """ + return yaml.dump(obj, Dumper=_HexQuotingDumper, sort_keys=False).encode() + + +def case_files(case: VectorCase) -> Dict[str, bytes]: + """The on-disk files for a case (bytes), mirroring the consensus layout.""" + return { + "value.yaml": _yaml_dump(case.value), + "serialized.ssz": case.serialized, + "roots.yaml": _yaml_dump({"root": "0x" + case.root.hex()}), + } + + +def case_dir( + output_dir: Path, + container_name: str, + suite: str, + case_index: int, + fork: Optional[str] = None, +) -> Path: + """Return the per-case output directory for a given case.""" + base = output_dir / container_name + if fork is not None: + base = base / fork + return base / suite / f"case_{case_index}" + + +def write_case(directory: Path, case: VectorCase) -> None: + """Write a case's value.yaml / serialized.ssz / roots.yaml.""" + directory.mkdir(parents=True, exist_ok=True) + for name, data in case_files(case).items(): + (directory / name).write_bytes(data) + + +def write_vectors( + models: Sequence[ModelSpec], + output_dir: Path, + *, + count: int = RANDOM_CASE_COUNT, +) -> int: + """Write every vector case under output_dir; return the count.""" + written = 0 + for name, fork, suite, case_index, case in generate_cases( + models, count=count + ): + write_case(case_dir(output_dir, name, suite, case_index, fork), case) + written += 1 + return written + + +__all__ = [ + "MAX_BYTES_LENGTH", + "MAX_LIST_LENGTH", + "RANDOM_CASE_COUNT", + "ModelSpec", + "RandomizationMode", + "VectorCase", + "case_dir", + "case_files", + "deterministic_seed", + "generate_cases", + "make_case", + "random_model", + "random_value", + "suite_name", + "suite_plan", + "write_case", + "write_vectors", +] diff --git a/packages/testing/src/execution_testing/tools/tests/test_ssz_vectors.py b/packages/testing/src/execution_testing/tools/tests/test_ssz_vectors.py new file mode 100644 index 00000000000..fa07717bede --- /dev/null +++ b/packages/testing/src/execution_testing/tools/tests/test_ssz_vectors.py @@ -0,0 +1,296 @@ +""" +Tests for SSZ static-vector generation (consensus-specs-style suites). + +Every generated case must be internally consistent with the engine (the +ground truth for bytes/roots), round-trip losslessly, and match pinned +known-answer values; the suites and mode semantics mirror consensus-specs' +ssz_static generator. +""" + +import random +from pathlib import Path +from typing import Annotated, List + +import pytest +import yaml + +from execution_testing.base_types import Address, Bytes, Hash +from execution_testing.base_types.ssz import ( + SszForkSchema, + SszModel, + Uint64, + Uint256, + byte_list, + decode, + encode, + hash_tree_root, + spec_of, + ssz_list, +) +from execution_testing.tools.ssz_vectors import ( + RandomizationMode, + case_files, + deterministic_seed, + generate_cases, + make_case, + random_model, + random_value, + suite_plan, + write_vectors, +) + +MAX_TX = 2**20 +MAX_BYTES_PER_TX = 2**30 +MAX_WITHDRAWALS = 16 + + +class Withdrawal(SszModel): + """A withdrawal container.""" + + index: Uint64 + validator_index: Uint64 + address: Address + amount: Uint64 + + +class Payload(SszModel): + """A container with a byte-list, a capped list, and a nested list.""" + + parent_hash: Hash + base_fee_per_gas: Uint256 + extra_data: Annotated[Bytes, byte_list(32)] + transactions: Annotated[ + List[Annotated[Bytes, byte_list(MAX_BYTES_PER_TX)]], + ssz_list(MAX_TX), + ] + withdrawals: Annotated[List[Withdrawal], ssz_list(MAX_WITHDRAWALS)] + + +class ForkedPayload(SszModel): + """A fork-scoped model, for the generator's fork axis.""" + + parent_hash: Hash + block_number: Uint64 + withdrawals: ( + Annotated[List[Withdrawal], ssz_list(MAX_WITHDRAWALS)] | None + ) = None + + __ssz_schema__ = SszForkSchema( + base_fork="Paris", + base=("parent_hash", "block_number"), + appended={"Shanghai": ("withdrawals",)}, + ) + + +def assert_roundtrip(model: SszModel) -> None: + """Reusable harness: encode -> decode reconstructs the SSZ value.""" + restored = decode(type(model), encode(model)) + assert encode(restored) == encode(model) + assert hash_tree_root(restored) == hash_tree_root(model) + + +def test_case_triple_is_consistent() -> None: + """A case's serialized/root match the engine, and the value round-trips.""" + w = Withdrawal( + index=7, + validator_index=42, + address=Address(b"\x11" * 20), + amount=32_000_000_000, + ) + case = make_case(w) + assert case.serialized == encode(w) + assert case.root == hash_tree_root(w) + assert case.value["amount"] == "0x773594000" + assert_roundtrip(w) + + +def test_suite_plan_mirrors_consensus() -> None: + """The published CL suite names; changing modes get several cases.""" + plan = suite_plan(count=30) + names = [name for name, *_rest in plan] + assert names == [ + "ssz_random", + "ssz_zero", + "ssz_max", + "ssz_nil", + "ssz_one", + "ssz_lengthy", + "ssz_random_chaos", + ] + counts = {name: n for name, _m, _c, n in plan} + # consensus-specs: cases_if_random if chaos or is_changing() else 1 + assert counts["ssz_random"] == 30 + assert counts["ssz_one"] == 30 + assert counts["ssz_lengthy"] == 30 + assert counts["ssz_random_chaos"] == 30 + assert counts["ssz_zero"] == 1 + assert counts["ssz_max"] == 1 + assert counts["ssz_nil"] == 1 + + +CONTENT_MODES = [ + ("zero", RandomizationMode.mode_zero, 0, b"\x00"), + ("max", RandomizationMode.mode_max, 2**256 - 1, b"\xff"), +] + + +@pytest.mark.parametrize( + "mode,fee,fill", + [pytest.param(m, v, b, id=name) for name, m, v, b in CONTENT_MODES], +) +def test_content_modes_pin_values_not_lengths( + mode: RandomizationMode, fee: int, fill: bytes +) -> None: + """zero/max pin scalar CONTENT; collections stay short (1 byte).""" + rng = random.Random(0) + model = random_model(rng, Payload, mode) + assert int(model.base_fee_per_gas) == fee + assert bytes(model.parent_hash) == fill * 32 + # consensus semantics: byte-lists get ONE fill byte, not emptiness + assert bytes(model.extra_data) == fill + + +COUNT_MODES = [ + ("nil", RandomizationMode.mode_nil_count, 0), + ("one", RandomizationMode.mode_one_count, 1), + ("lengthy", RandomizationMode.mode_max_count, 10), +] + + +@pytest.mark.parametrize( + "mode,length", + [pytest.param(m, n, id=name) for name, m, n in COUNT_MODES], +) +def test_count_modes_pin_lengths(mode: RandomizationMode, length: int) -> None: + """nil/one/lengthy pin list LENGTHS (up to the generator cap of 10).""" + rng = random.Random(0) + model = random_model(rng, Payload, mode) + assert len(model.transactions) == length + assert len(model.withdrawals) == length + + +def test_generate_cases_covers_models_and_suites() -> None: + """Every model x suite is emitted with the planned case counts.""" + cases = list(generate_cases([Withdrawal, Payload], count=2)) + names = {name for name, _fork, _suite, _i, _c in cases} + assert names == {"Withdrawal", "Payload"} + # 4 changing suites x 2 cases + 3 deterministic suites x 1 = 11 each + assert len(cases) == 2 * 11 + + # Every generated case is internally consistent and round-trips. + for name, _fork, _suite, _i, case in cases: + assert len(case.root) == 32 + model_cls = Withdrawal if name == "Withdrawal" else Payload + assert encode(decode(model_cls, case.serialized)) == case.serialized + + +def test_value_yaml_matches_serialized() -> None: + """ + The written value.yaml re-encodes to the written serialized.ssz. + + This is the contract an ssz_static consumer relies on: value, + serialized bytes, and root must all describe the same object. + """ + for _n, _f, _s, _i, case in generate_cases([Withdrawal], count=2): + files = case_files(case) + value = yaml.safe_load(files["value.yaml"]) + rebuilt = Withdrawal.model_validate(value) + assert encode(rebuilt) == files["serialized.ssz"] + root = yaml.safe_load(files["roots.yaml"])["root"] + assert hash_tree_root(rebuilt).hex() == root.removeprefix("0x") + + +def test_deterministic() -> None: + """Generation is deterministic across runs.""" + a = [c.root for *_h, c in generate_cases([Payload], count=2)] + b = [c.root for *_h, c in generate_cases([Payload], count=2)] + assert a == b + assert deterministic_seed("a", "b", 0) == deterministic_seed("a", "b", 0) + assert deterministic_seed("a", "b", 0) != deterministic_seed("a", "b", 1) + + +def test_chaos_redraws_modes() -> None: + """Chaos re-draws the mode per node yet still builds valid values.""" + rng = random.Random(deterministic_seed("chaos-test")) + for _ in range(5): + model = random_model( + rng, Payload, RandomizationMode.mode_random, chaos=True + ) + assert_roundtrip(model) + + +@pytest.mark.parametrize("name", list(Payload.model_fields)) +def test_random_value_covers_field_spec(name: str) -> None: + """random_value handles every SszType the test containers use.""" + rng = random.Random(1) + value = random_value( + rng, spec_of(Payload, name), RandomizationMode.mode_random + ) + assert value is not None + + +def test_case_files_layout() -> None: + """A case serializes to the consensus value/serialized/roots files.""" + w = Withdrawal( + index=1, + validator_index=2, + address=Address(b"\x00" * 20), + amount=3, + ) + files = case_files(make_case(w)) + assert set(files) == {"value.yaml", "serialized.ssz", "roots.yaml"} + assert files["serialized.ssz"] == encode(w) + # Hex strings must be single-quoted (a bare 0x... reads back as int). + assert b"root: '0x" in files["roots.yaml"] + assert b"amount: '0x3'" in files["value.yaml"] + assert b"address: '0x" in files["value.yaml"] + + +def test_write_vectors_emits_consensus_tree(tmp_path: Path) -> None: + """write_vectors lays out //case_/ triples.""" + written = write_vectors([Withdrawal], tmp_path, count=2) + assert written == 11 # 4 changing x 2 + 3 deterministic x 1 + zero_case = tmp_path / "Withdrawal" / "ssz_zero" / "case_0" + assert (zero_case / "value.yaml").is_file() + assert (zero_case / "serialized.ssz").is_file() + assert (zero_case / "roots.yaml").is_file() + # The serialized bytes reload and re-encode identically via the engine. + raw = (zero_case / "serialized.ssz").read_bytes() + assert encode(decode(Withdrawal, raw)) == raw + # Changing suites have every planned case on disk. + random_dir = tmp_path / "Withdrawal" / "ssz_random" + assert sorted(p.name for p in random_dir.iterdir()) == [ + "case_0", + "case_1", + ] + + +def test_fork_scoped_vectors(tmp_path: Path) -> None: + """(model, fork) entries emit per-fork projections under fork dirs.""" + written = write_vectors( + [(ForkedPayload, "Paris"), (ForkedPayload, "Shanghai")], + tmp_path, + count=1, + ) + assert written == 2 * 7 # all 7 suites x 1 case, per fork entry + paris_zero = tmp_path / "ForkedPayload" / "Paris" / "ssz_zero" / "case_0" + raw = (paris_zero / "serialized.ssz").read_bytes() + restored = decode(ForkedPayload, raw, fork="Paris") + assert restored.withdrawals is None # beyond-fork field absent + shanghai_zero = ( + tmp_path / "ForkedPayload" / "Shanghai" / "ssz_zero" / "case_0" + ) + assert (shanghai_zero / "roots.yaml").is_file() + + +def test_duplicate_vector_targets_rejected(tmp_path: Path) -> None: + """Two distinct same-named models cannot share an output directory.""" + + def make_dup() -> type: + class Withdrawal(SszModel): # same __name__, different class + a: Uint64 + + return Withdrawal + + with pytest.raises(ValueError, match="share vector output"): + write_vectors([Withdrawal, make_dup()], tmp_path, count=1) diff --git a/pyproject.toml b/pyproject.toml index 79341714348..23a1eff6527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -536,6 +536,11 @@ exclude = [ ] plugins = ["pydantic.mypy"] +[[tool.mypy.overrides]] +# remerkleable ships no type stubs / py.typed marker. +module = "remerkleable.*" +ignore_missing_imports = true + [tool.uv] required-version = ">=0.7.0" extra-build-dependencies = { ethash = ["setuptools", "cmake>=4.2.1,<5"] } diff --git a/uv.lock b/uv.lock index 3f44340f65a..c00273eacbf 100644 --- a/uv.lock +++ b/uv.lock @@ -777,6 +777,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028, upload-time = "2025-01-13T21:29:19.365Z" }, ] +[[package]] +name = "eth-remerkleable" +version = "0.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/ac/40fde655f67fd02f07b28e3fe4b9bb4af521388ca8aa48462d622ce4fa03/eth_remerkleable-0.1.31.tar.gz", hash = "sha256:94df4b18a50dfc46f55b2e790cfece384e64032e9cb82d185cff09224682ad1d", size = 49525, upload-time = "2026-06-11T13:23:38.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/a1/87f7c996f344c5f213c2bca657e579c3696bb99737238c0cf9f04867386a/eth_remerkleable-0.1.31-py3-none-any.whl", hash = "sha256:7e48ea1b80935977effc02300f3f9b1db19153ab87ffbcea14a6e6429bbdc56b", size = 57192, upload-time = "2026-06-11T13:23:37.208Z" }, +] + [[package]] name = "eth-typing" version = "5.2.1" @@ -1057,6 +1066,7 @@ dependencies = [ { name = "click" }, { name = "colorlog" }, { name = "eth-abi" }, + { name = "eth-remerkleable" }, { name = "ethereum-execution" }, { name = "ethereum-hive" }, { name = "ethereum-rlp" }, @@ -1110,6 +1120,7 @@ requires-dist = [ { name = "click", specifier = ">=8.1.0,<9" }, { name = "colorlog", specifier = ">=6.7.0,<7" }, { name = "eth-abi", specifier = ">=5.2.0" }, + { name = "eth-remerkleable", specifier = "==0.1.31" }, { name = "ethereum-execution", editable = "." }, { name = "ethereum-hive", specifier = ">=0.1.0a5,<1.0.0" }, { name = "ethereum-rlp", specifier = ">=0.1.6,<0.2" }, From 46364f77ab5b72376d47d1f62380f4b9901ab87e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 29 Jul 2026 02:44:38 -0600 Subject: [PATCH 23/55] refactor(ci): run geth benchmark CI only on `benchmarks/**` branches (#3249) Co-authored-by: spencer-tb --- .github/workflows/benchmark.yaml | 40 ++++--------------- .github/workflows/test.yaml | 11 +++++ Justfile | 11 ----- docs/getting_started/verifying_changes.md | 2 +- .../plugins/filler/tests/test_benchmarking.py | 22 +++++++--- tox.ini | 2 +- 6 files changed, 36 insertions(+), 52 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index c4466f8bd15..98ffa7ab187 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -3,22 +3,19 @@ name: Benchmarking on: push: branches: - - mainnet - - "forks/**" - paths: - - "tests/benchmark/**" - - "packages/testing/src/execution_testing/benchmark/**" - - "packages/testing/src/execution_testing/cli/pytest_commands/plugins/**" - - ".github/workflows/benchmark.yaml" - pull_request: - paths-ignore: + - "benchmarks/**" + paths-ignore: &non_benchmark_paths - "**.md" - "LICENSE*" - ".gitignore" - ".vscode/**" - "whitelist.txt" - - "docs/**" + - "docs/**" - "mkdocs.yml" + pull_request: + branches: + - "benchmarks/**" + paths-ignore: *non_benchmark_paths workflow_dispatch: concurrency: @@ -26,31 +23,8 @@ concurrency: cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} jobs: - unit-tests: - name: Benchmark Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - submodules: true - - - uses: ./.github/actions/setup-uv - with: - enable-cache: "false" - - - uses: ./.github/actions/build-evm-base - id: evm-builder - with: - type: benchmark - - - name: Run benchmark unit tests - run: just test-tests-bench - env: - EVM_BIN: ${{ steps.evm-builder.outputs.evm-bin }} - sanity-checks: name: ${{ matrix.name }} - needs: [unit-tests] runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5270d87a51b..9060ce9c2ea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,6 +6,7 @@ on: - master - mainnet - "forks/**" + - "benchmarks/**" paths-ignore: - "**.md" - "LICENSE*" @@ -163,10 +164,20 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-uv - uses: ./.github/actions/build-evmone + # On benchmarks/** branches, build the pinned geth so the benchmark + # plugin tests fill against a real client (via EVM_BIN). On every other + # branch this step is skipped, EVM_BIN stays empty, and those fills fall + # through to fill's in-repo EELS t8n. + - uses: ./.github/actions/build-evm-base + id: evm-builder + if: ${{ startsWith(github.ref, 'refs/heads/benchmarks/') || startsWith(github.base_ref, 'benchmarks/') }} + with: + type: benchmark - name: Run test-tests run: just test-tests env: PYTEST_XDIST_AUTO_NUM_WORKERS: auto + EVM_BIN: ${{ steps.evm-builder.outputs.evm-bin }} test-tests-pypy: runs-on: [self-hosted-ghr, size-l-x64] diff --git a/Justfile b/Justfile index 75e38c246df..377a4520762 100644 --- a/Justfile +++ b/Justfile @@ -220,7 +220,6 @@ test-tests *args: cd packages/testing && uv run pytest \ -n {{ xdist_workers }} \ --basetemp="{{ output_dir }}/test-tests/tmp" \ - --ignore=src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py \ "$@" \ src @@ -235,16 +234,6 @@ test-tests-pypy *args: "$@" \ src -# Run benchmark framework unit tests (with Python) -[group('unit tests')] -[group('benchmark tests')] -test-tests-bench *args: - @mkdir -p "{{ output_dir }}/test-tests-bench/tmp" - uv run pytest \ - --basetemp="{{ output_dir }}/test-tests-bench/tmp" \ - "$@" \ - packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py - # Run CI release script integration tests [group('unit tests')] test-ci-scripts *args: diff --git a/docs/getting_started/verifying_changes.md b/docs/getting_started/verifying_changes.md index 7bbe5e7d427..660c8d5ebc6 100644 --- a/docs/getting_started/verifying_changes.md +++ b/docs/getting_started/verifying_changes.md @@ -13,7 +13,7 @@ Some CI jobs are slow. Only run the checks relevant to your change. | Any PR (baseline) | `just static` | Lint, format, mypy, spellcheck, import isolation, workflow lint. | | Added or modified tests | `just fill tests/path/to/new/tests` | See [Filling Tests](../filling_tests/index.md). | | Framework changes (`packages/testing/`) | `just test-tests` | Framework unit tests. Mirrors the `test-tests` CI job. | -| Benchmark framework changes | `just test-tests-bench`, `just bench-gas`, `just bench-opcode`, `just bench-opcode-config` | Benchmark unit tests and sanity checks. Mirrors the benchmark CI workflow. | +| Benchmark framework changes | `just test-tests`, `just bench-gas`, `just bench-opcode`, `just bench-opcode-config` | Benchmark plugin unit tests now run within `test-tests`; the `bench-*` recipes fill/verify the suite (geth-backed on `benchmarks/**`). | | Markdown touched | `just lint-md` | Requires `markdownlint-cli2`; see [Linting Markdown](#linting-markdown). | | Docs touched | `just docs` or `just docs-fast` | `docs-fast` skips the Test Case Reference section for faster iteration. | diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py index f611ccdb4d1..0d8bacfd5bd 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py @@ -18,8 +18,18 @@ format_fork_subdir, ) -# EVM binary for fill tests; defaults to geth evm -BENCHMARK_EVM_T8N = os.environ.get("EVM_BIN", "evm") +# EVM binary for fill tests. Unset (or empty) -> the in-repo EELS t8n +# (fill's default when --evm-bin is omitted). Set EVM_BIN to fill +# against a specific binary, e.g. geth's `evm`. +BENCHMARK_EVM_T8N = os.environ.get("EVM_BIN") or None + + +def _evm_bin_args() -> List[str]: + """Return `--evm-bin` args, or none to use fill's EELS default.""" + if BENCHMARK_EVM_T8N is None: + return [] + return [f"--evm-bin={BENCHMARK_EVM_T8N}"] + test_module_dummy = textwrap.dedent( """\ @@ -321,7 +331,7 @@ def test_fixed_opcode_count_split_into_subdirs( "--no-html", "--skip-index", f"--output={output_dir}", - f"--evm-bin={BENCHMARK_EVM_T8N}", + *_evm_bin_args(), "tests/benchmark/dummy_test_module/", "-q", ) @@ -937,7 +947,7 @@ def test_fixed_opcode_count_config_file_parametrized( "--fork", "Prague", "tests/benchmark/dummy_test_module/", - f"--evm-bin={BENCHMARK_EVM_T8N}", + *_evm_bin_args(), "--fixed-opcode-count", "-v", ) @@ -1069,7 +1079,7 @@ def test_fixed_opcode_count_per_parameter_patterns( "--fork", "Prague", "tests/benchmark/dummy_test_module/", - f"--evm-bin={BENCHMARK_EVM_T8N}", + *_evm_bin_args(), "--fixed-opcode-count", "-v", ) @@ -1109,7 +1119,7 @@ def test_cli_mode_ignores_per_parameter_patterns( "Prague", "--fixed-opcode-count=1,5", "tests/benchmark/dummy_test_module/", - f"--evm-bin={BENCHMARK_EVM_T8N}", + *_evm_bin_args(), "-v", ) diff --git a/tox.ini b/tox.ini index f390f3ada51..c661849876e 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,7 @@ commands = [testenv:tests_benchmark_pytest_py3] commands = - printf '\n\033[1m execution-specs has migrated from tox to just.\033[0m\n Install just: https://just.systems/man/en/pre-built-binaries.html\n\n This tox env can be run using just via:\n \033[1mjust test-tests-bench\033[0m\n\n' + printf '\n\033[1m execution-specs has migrated from tox to just.\033[0m\n Install just: https://just.systems/man/en/pre-built-binaries.html\n\n This tox env can be run using just via:\n \033[1mjust test-tests\033[0m\n\n' false [testenv:benchmark-gas-values] From 178d9f9257fbf7ec8c9b55c3e5da3c6f20c583db Mon Sep 17 00:00:00 2001 From: spencer Date: Wed, 29 Jul 2026 12:04:09 +0200 Subject: [PATCH 24/55] chore(tests): improve EIP-7976 coverage, checklist, and ref-spec pin (#3222) --- .../eip_checklist_external_coverage.txt | 3 + .../eip_checklist_not_applicable.txt | 14 + .../spec.py | 2 +- .../test_additional_coverage.py | 2 + .../test_execution_gas.py | 3 + .../test_fork_transition.py | 317 ++++++++++++++++++ 6 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_external_coverage.txt create mode 100644 tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_not_applicable.txt create mode 100644 tests/amsterdam/eip7976_increase_calldata_floor_cost/test_fork_transition.py diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_external_coverage.txt b/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_external_coverage.txt new file mode 100644 index 00000000000..3bc37707305 --- /dev/null +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_external_coverage.txt @@ -0,0 +1,3 @@ +general/code_coverage/eels = EIP-7976 floor logic lives in calculate_intrinsic_gas_cost (transactions.py) and GasCosts (vm/gas.py); exercised end-to-end by this suite's exact-receipt and calculator cross-check tests, line coverage tracked via codecov on src/ethereum/forks/amsterdam/transactions.py +general/code_coverage/test_coverage = Suite asserts exact cumulative_gas_used and hand-derived calculator cross-checks throughout (token calculation, execution gas, validity matrices, fork transition) +general/code_coverage/missed_lines = No EIP-7976-specific lines excluded; shared intrinsic-gas infrastructure is covered by the type 0-4 validity matrices diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_not_applicable.txt b/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_not_applicable.txt new file mode 100644 index 00000000000..3fd6e4de6db --- /dev/null +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/eip_checklist_not_applicable.txt @@ -0,0 +1,14 @@ +opcode = EIP does not introduce or modify an opcode +precompile = EIP does not introduce a new precompile +removed_precompile = EIP does not remove a precompile +system_contract = EIP does not introduce a new system contract +transaction_type = EIP does not introduce a new transaction type +block_header_field = EIP does not add any new block header fields +block_body_field = EIP does not add any new block body fields +gas_cost_changes/test/out_of_gas = The raised floor cannot cause a runtime out-of-gas, it is a validity threshold plus end-of-transaction minimum billing; below-floor gas limits reject as invalid (insufficient_gas arms in test_transaction_validity.py) +gas_refunds_changes = EIP does not change refund rules, only how the billed floor interacts with refunded execution gas (covered under gas_cost_changes) +blob_count_changes = EIP does not introduce any blob count changes +execution_layer_request = EIP does not introduce an execution layer request +new_transaction_validity_constraint = The floor gas-limit reservation exists since EIP-7623, this EIP modifies the threshold (see modified_transaction_validity_constraint) +block_level_constraint = Block-level floor accounting is EIP-2780/EIP-8037 scope +general/code_coverage/second_client = Optional diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/spec.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/spec.py index df03e7071f8..5c130eeacf2 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/spec.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/spec.py @@ -12,7 +12,7 @@ class ReferenceSpec: ref_spec_7976 = ReferenceSpec( - "EIPS/eip-7976.md", "83d473b0504d316a06ce58ae581e7f03b5d54fe1" + "EIPS/eip-7976.md", "c998ef94eb16a6af8a9b8e2084f947b17ea14865" ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py index 0b745830914..8a19de64cec 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py @@ -21,6 +21,7 @@ AuthorizationTuple, Bytecode, Bytes, + EIPChecklist, Fork, Op, StateTestFiller, @@ -90,6 +91,7 @@ def to(self, pre: Alloc) -> Address: ), ], ) + @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() def test_token_calculation_verification( self, state_test: StateTestFiller, diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_execution_gas.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_execution_gas.py index a46c9d5daf6..209916b9fcf 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_execution_gas.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_execution_gas.py @@ -11,6 +11,7 @@ Alloc, AuthorizationTuple, Bytes, + EIPChecklist, Fork, Op, StateTestFiller, @@ -75,6 +76,7 @@ def to( pytest.param(0, id="exact_gas"), ], ) + @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() def test_full_gas_consumption( self, state_test: StateTestFiller, @@ -153,6 +155,7 @@ def to( pytest.param(0, id="exact_gas"), ], ) + @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() def test_gas_consumption_below_data_floor( self, state_test: StateTestFiller, diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_fork_transition.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_fork_transition.py new file mode 100644 index 00000000000..b78f996801b --- /dev/null +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_fork_transition.py @@ -0,0 +1,317 @@ +""" +Fork-transition tests for EIP-7976. + +EIP-7976 raises the calldata floor price at the Amsterdam fork +boundary: the EIP-7623 floor of 10 gas per token (10/40 per zero or +non-zero byte) becomes 16 gas per floor token with floor tokens counted +uniformly as four per calldata byte (64/64). These tests send identical +data-heavy transactions in a pre-fork block and a post-fork block and +assert that the floor changes exactly at the boundary, both as billed +gas and as the transaction-validity threshold. + +The calldata sizes sit above the crossover where the new floor exceeds +the old one: EIP-2780 lowers the floor anchor (the decomposed intrinsic +base) below the flat pre-fork 21_000, so for small calldata the new +floor is the lower of the two even though the per-byte rate rises. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + EIPChecklist, + RecipientType, + Transaction, + TransactionException, + TransactionReceipt, + TransitionFork, +) + +from .spec import ref_spec_7976 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7976.git_path +REFERENCE_SPEC_VERSION = ref_spec_7976.version + +pytestmark = pytest.mark.valid_at_transition_to("EIP7976") + +# Transition forks switch at timestamp 15_000. +PRE_FORK_TIMESTAMP = 14_999 +POST_FORK_TIMESTAMP = 15_000 + +# Calldata shapes sized above the old-floor/new-floor crossover so the +# post-fork floor is strictly larger (see module docstring). +ALL_ZERO_DATA = b"\x00" * 200 +ALL_NONZERO_DATA = b"\x01" * 400 + + +def expected_floors(fork: TransitionFork, data: bytes) -> tuple[int, int]: + """ + Hand-derive the pre- and post-fork calldata floors for `data`. + + Pre-fork (EIP-7623): 10 gas per token, one token per zero byte and + four per non-zero byte, anchored on the flat `TX_BASE`. Post-fork + (EIP-7976): 16 gas per token, four tokens per calldata byte + regardless of content, anchored on the EIP-2780 decomposed base, + which includes the recipient-access charge for a plain call. + """ + pre_costs = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP).gas_costs() + post_costs = fork.fork_at(timestamp=POST_FORK_TIMESTAMP).gas_costs() + + zero_bytes = data.count(0) + nonzero_bytes = len(data) - zero_bytes + + pre_tokens = zero_bytes + nonzero_bytes * 4 + expected_pre = int( + pre_costs.TX_BASE + pre_tokens * pre_costs.TX_DATA_TOKEN_FLOOR + ) + + post_floor_tokens = len(data) * int(post_costs.TX_DATA_TOKEN_STANDARD) + expected_post = int( + post_costs.TX_BASE + + post_costs.COLD_ACCOUNT_ACCESS + + post_floor_tokens * post_costs.TX_DATA_TOKEN_FLOOR + ) + + return expected_pre, expected_post + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +@pytest.mark.parametrize( + "data", + [ + pytest.param(ALL_ZERO_DATA, id="all_zero_bytes"), + pytest.param(ALL_NONZERO_DATA, id="all_nonzero_bytes"), + ], +) +def test_floor_cost_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + data: bytes, +) -> None: + """ + Pin the EIP-7976 floor increase across the Amsterdam boundary. + + The same data-heavy transaction to an existing EOA (no EVM + execution) is sent in a pre-fork block and a post-fork block with + the gas limit pinned to the fork-appropriate floor, so the billed + gas equals the calldata floor exactly on both sides. The zero-byte + arm discriminates the uniform token counting (zero bytes lose their + floor discount); the non-zero arm discriminates the per-token price + alone. + + The per-fork floor returned by the calculator is also checked + against a hand-derived value built from each fork's gas constants, + so a calculator regression fails here with a clear message rather + than only as a downstream balance mismatch. + """ + gas_price = 1_000_000_000 + target = pre.fund_eoa(amount=1) + + expected_pre, expected_post = expected_floors(fork, data) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_floors_per_block = [expected_pre, expected_post] + blocks = [] + post: dict[Address, Account] = {} + + for timestamp, expected_floor in zip( + timestamps, expected_floors_per_block, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + floor = sub_fork.transaction_data_floor_cost_calculator()( + data=data, + recipient_type=RecipientType.EOA, + ) + assert floor == expected_floor, ( + f"floor at timestamp {timestamp} ({sub_fork}) is {floor}, " + f"expected {expected_floor}" + ) + # The floor must dominate the standard-side intrinsic so the + # transaction is billed exactly the floor. + intrinsic = sub_fork.transaction_intrinsic_cost_calculator()( + calldata=data, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + assert floor > intrinsic, ( + f"floor {floor} does not dominate intrinsic {intrinsic} at " + f"timestamp {timestamp} ({sub_fork})" + ) + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + # The recipient is an EOA, so no EVM bytecode runs and the + # billed gas is exactly the floor; the gas limit is pinned to + # the floor, leaving no buffer. + tx = Transaction( + sender=sender, + to=target, + data=data, + gas_limit=floor, + gas_price=gas_price, + expected_receipt=TransactionReceipt(cumulative_gas_used=floor), + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + post[sender] = Account( + nonce=1, + balance=sender_initial_balance - floor * gas_price, + ) + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedBeforeFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedBeforeFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedAfterFork() +@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedAfterFork() +@pytest.mark.parametrize( + "data", + [ + pytest.param(ALL_ZERO_DATA, id="all_zero_bytes"), + pytest.param(ALL_NONZERO_DATA, id="all_nonzero_bytes"), + ], +) +@pytest.mark.parametrize( + "scenario", + [ + pytest.param("exact_floors_accepted", id="exact_floors_accepted"), + pytest.param( + "below_old_floor_rejected_before_fork", + marks=pytest.mark.exception_test, + id="below_old_floor_rejected_before_fork", + ), + pytest.param( + "old_floor_rejected_after_fork", + marks=pytest.mark.exception_test, + id="old_floor_rejected_after_fork", + ), + pytest.param( + "below_new_floor_rejected_after_fork", + marks=pytest.mark.exception_test, + id="below_new_floor_rejected_after_fork", + ), + ], +) +def test_floor_validity_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + scenario: str, + data: bytes, +) -> None: + """ + Pin the EIP-7976 validity-threshold change across the boundary. + + The gas limit must reserve the calldata floor for the transaction to + be valid. A transaction whose gas limit exactly meets the old + (EIP-7623) floor is accepted in the pre-fork block, but the + identical shape is rejected once the fork activates because the new + floor is higher for this calldata; one below the old floor is + already rejected pre-fork, one just below the new floor is rejected + post-fork, and one at the new floor is accepted post-fork. + + The zero-byte arm pins the uniform token counting on the validity + threshold itself, independently of the billed-gas path. + """ + gas_price = 1_000_000_000 + target = pre.fund_eoa(amount=1) + + old_floor, new_floor = expected_floors(fork, data) + # The old-floor-rejected-after-fork arm only exists because the new + # floor exceeds the old one for this calldata size. + assert new_floor > old_floor + + below_floor_error = TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST + sender_initial_balance = 10**18 + pre_fork_sender = pre.fund_eoa(sender_initial_balance) + post_fork_sender = pre.fund_eoa(sender_initial_balance) + + def transfer_tx( + sender: Address, gas_limit: int, valid: bool + ) -> Transaction: + return Transaction( + sender=sender, + to=target, + data=data, + gas_limit=gas_limit, + gas_price=gas_price, + error=None if valid else below_floor_error, + ) + + untouched = Account(nonce=0, balance=sender_initial_balance) + blocks: list[Block] + + if scenario == "exact_floors_accepted": + blocks = [ + Block( + timestamp=PRE_FORK_TIMESTAMP, + txs=[transfer_tx(pre_fork_sender, old_floor, valid=True)], + ), + Block( + timestamp=POST_FORK_TIMESTAMP, + txs=[transfer_tx(post_fork_sender, new_floor, valid=True)], + ), + ] + post = { + pre_fork_sender: Account( + nonce=1, + balance=sender_initial_balance - old_floor * gas_price, + ), + post_fork_sender: Account( + nonce=1, + balance=sender_initial_balance - new_floor * gas_price, + ), + } + elif scenario == "below_old_floor_rejected_before_fork": + blocks = [ + Block( + timestamp=PRE_FORK_TIMESTAMP, + txs=[transfer_tx(pre_fork_sender, old_floor - 1, valid=False)], + exception=below_floor_error, + ), + ] + post = {pre_fork_sender: untouched, post_fork_sender: untouched} + elif scenario == "old_floor_rejected_after_fork": + blocks = [ + Block( + timestamp=PRE_FORK_TIMESTAMP, + txs=[transfer_tx(pre_fork_sender, old_floor, valid=True)], + ), + # The identical gas limit that was accepted pre-fork no + # longer reserves the raised floor. + Block( + timestamp=POST_FORK_TIMESTAMP, + txs=[transfer_tx(post_fork_sender, old_floor, valid=False)], + exception=below_floor_error, + ), + ] + post = { + pre_fork_sender: Account( + nonce=1, + balance=sender_initial_balance - old_floor * gas_price, + ), + post_fork_sender: untouched, + } + else: + # One below the new floor is still rejected post-fork; together + # with the exact-floor acceptance this pins the post-fork + # threshold at exactly the new floor. + blocks = [ + Block( + timestamp=POST_FORK_TIMESTAMP, + txs=[ + transfer_tx(post_fork_sender, new_floor - 1, valid=False) + ], + exception=below_floor_error, + ), + ] + post = {pre_fork_sender: untouched, post_fork_sender: untouched} + + blockchain_test(pre=pre, blocks=blocks, post=post) From f79c4c7e9084b9b583a9232e128cc69caec71471 Mon Sep 17 00:00:00 2001 From: Aliaksei Osipau Date: Wed, 29 Jul 2026 17:40:54 +0300 Subject: [PATCH 25/55] feat(tests): EIP-7928 - cover system-address zero-tip coinbase BAL (#3239) * Add system-address zero-tip BAL fixture * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * Update tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py Co-authored-by: Mario Vega * fix(tests): apply ruff format to system-address coinbase BAL test --------- Co-authored-by: Mario Vega --- .../test_block_access_lists.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index e34f8cb13e2..b59367bfb09 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -44,6 +44,7 @@ REFERENCE_SPEC_VERSION = ref_spec_7928.version pytestmark = pytest.mark.valid_from("Amsterdam") +SYSTEM_ADDRESS = Address(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE) @EIPChecklist.BlockHeaderField.Test.ValueBehavior.Accept() @@ -1631,6 +1632,65 @@ def test_bal_coinbase_zero_tip( ) +def test_bal_system_address_coinbase_zero_tip( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, +) -> None: + """ + Ensure BAL includes SYSTEM_ADDRESS when it is the zero-tip fee recipient. + """ + bob = pre.fund_eoa(amount=0) + + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + + tx_value = 5 + alice = pre.fund_eoa() + tx = Transaction( + sender=alice, + to=bob, + value=tx_value, + gas_price=base_fee_per_gas, + ) + + block = Block( + txs=[tx], + fee_recipient=SYSTEM_ADDRESS, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1) + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(block_access_index=1, post_balance=5) + ] + ), + SYSTEM_ADDRESS: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=5), + SYSTEM_ADDRESS: Account.NONEXISTENT, + }, + genesis_environment=genesis_env, + ) + + @pytest.mark.parametrize( "value", [ From 1311ff376d2f7fbd5270e38e7bf13847c14b883e Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:51:22 +0200 Subject: [PATCH 26/55] feat(specs,amsterdam): EIP-2780 - fold transfer log cost into value transfer cost (#3214) Align the EIP-2780 implementation with the changes proposed in https://github.com/ethereum/EIPs/pull/11997 --- .../forks/forks/eips/amsterdam/eip_2780.py | 20 ++++------------ .../src/execution_testing/forks/gas_costs.py | 1 - src/ethereum/forks/amsterdam/transactions.py | 10 +++----- src/ethereum/forks/amsterdam/vm/gas.py | 3 +-- .../eip2780_reduce_intrinsic_tx_gas/spec.py | 2 +- .../test_calldata_floor.py | 12 ++++------ .../test_fork_transition.py | 11 +++------ .../test_value_moving_transactions.py | 24 +++++++------------ 8 files changed, 26 insertions(+), 57 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py index 28bbcb60b84..c001d74e6e4 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py @@ -40,8 +40,7 @@ def gas_costs(cls) -> GasCosts: return replace( parent, TX_BASE=12_000, - TRANSFER_LOG_COST=1_756, - TX_VALUE_COST=4_244, + TX_VALUE_COST=6_000, ) @classmethod @@ -75,14 +74,10 @@ def fn( # CREATE_ACCESS regular gas; TX_CREATE folds in the # NEW_ACCOUNT state gas, which the floor excludes. floor += gas_costs.TX_CREATE - gas_costs.NEW_ACCOUNT - if sends_value: - floor += gas_costs.TRANSFER_LOG_COST elif not is_self_transfer: floor += gas_costs.COLD_ACCOUNT_ACCESS if sends_value: - floor += ( - gas_costs.TRANSFER_LOG_COST + gas_costs.TX_VALUE_COST - ) + floor += gas_costs.TX_VALUE_COST return floor return fn @@ -97,9 +92,8 @@ def transaction_intrinsic_cost_calculator( Non-create, non-self targets pay ``COLD_ACCOUNT_ACCESS`` unconditionally; access lists do not warm transaction-level - accounts. Value-bearing transactions pay - ``TRANSFER_LOG_COST`` plus ``TX_VALUE_COST``; self-transfers - suppress the value-transfer charge entirely. + accounts. Value-bearing transactions pay ``TX_VALUE_COST``; + self-transfers suppress the value-transfer charge entirely. """ super_fn = super(EIP2780, cls).transaction_intrinsic_cost_calculator() gas_costs = cls.gas_costs() @@ -147,14 +141,10 @@ def fn( # remove it here, mirroring value transfer to an empty # account whose NEW_ACCOUNT is likewise top-frame. intrinsic_cost -= gas_costs.NEW_ACCOUNT - if sends_value: - intrinsic_cost += gas_costs.TRANSFER_LOG_COST elif not is_self_transfer: intrinsic_cost += gas_costs.COLD_ACCOUNT_ACCESS if sends_value: - intrinsic_cost += ( - gas_costs.TRANSFER_LOG_COST + gas_costs.TX_VALUE_COST - ) + intrinsic_cost += gas_costs.TX_VALUE_COST if return_cost_deducted_prior_execution: return intrinsic_cost diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index 0113895108f..81e660fdaa5 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -38,7 +38,6 @@ class GasCosts: NEW_ACCOUNT: int ACCOUNT_WRITE: int = 0 CREATE_ACCESS: int = 0 - TRANSFER_LOG_COST: int = 0 TX_VALUE_COST: int = 0 # Contract Creation diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index ceae8f63168..251c80472e2 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -639,8 +639,8 @@ def calculate_intrinsic_cost( call, or `CREATE_ACCESS` for a contract creation). The created account's `NEW_ACCOUNT` state gas is state-dependent and is charged at the top frame, not here. - 3. Value cost (`TRANSFER_LOG_COST`, plus `TX_VALUE_COST` for a - non-self-transfer call) when ``tx.value > 0``. + 3. Value cost (`TX_VALUE_COST` for a non-self-transfer call) when + ``tx.value > 0``. 4. Calldata cost (zero and non-zero bytes). 5. Access list entries (if applicable). 6. Authorizations (if applicable): only the state-independent base @@ -671,14 +671,10 @@ def calculate_intrinsic_cost( if is_create: recipient_regular_gas = GasCosts.CREATE_ACCESS init_code_gas = init_code_cost(ulen(tx.data)) - if tx.value > U256(0): - recipient_regular_gas += GasCosts.TRANSFER_LOG_COST elif not is_self_transfer: recipient_regular_gas = GasCosts.COLD_ACCOUNT_ACCESS if tx.value > U256(0): - recipient_regular_gas += ( - GasCosts.TRANSFER_LOG_COST + GasCosts.TX_VALUE_COST - ) + recipient_regular_gas += GasCosts.TX_VALUE_COST access_list_cost = Uint(0) tokens_in_access_list = Uint(0) diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 028b4ac4318..d4fd188946f 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -137,8 +137,7 @@ class GasCosts: # Transactions TX_BASE: Final[Uint] = Uint(12000) TX_CREATE: Final[Uint] = Uint(32000) - TX_VALUE_COST: Final[Uint] = Uint(4244) - TRANSFER_LOG_COST: Final[Uint] = Uint(1756) + TX_VALUE_COST: Final[Uint] = Uint(6000) TX_DATA_TOKEN_STANDARD: Final[Uint] = Uint(4) TX_DATA_TOKEN_FLOOR: Final[Uint] = Uint(16) TX_ACCESS_LIST_ADDRESS: Final[Uint] = COLD_ACCOUNT_ACCESS diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py index 5ac7b7e50bf..33b12ffbd2f 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py @@ -13,5 +13,5 @@ class ReferenceSpec: ref_spec_2780 = ReferenceSpec( git_path="EIPS/eip-2780.md", - version="e6d8f589d355e891c37ff479d3ce668352e5b1be", + version="04dd54c2e7ec1f408cf4a150d5c1aa43573bd025", ) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py index be3c77466ee..b0ba400f25e 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py @@ -259,13 +259,11 @@ def test_calldata_floor_contract_creation( empty code, and prices every byte as one floor token. - ``floor_binds``: ``gas_used`` pins to the floor, which anchors - on the creation regular base (``TX_BASE + CREATE_ACCESS``, plus - ``TRANSFER_LOG_COST`` when value moves) but excludes the created - account's ``NEW_ACCOUNT`` *state* charge and the init-code word - cost -- both masked by the binding floor -- while the deploy - (and any moved wei) still lands. The receipt pins the floor - exactly, so the value-bearing case sits precisely - ``TRANSFER_LOG_COST`` above the zero-value one. + on the creation regular base (``TX_BASE + CREATE_ACCESS``) + but excludes the created account's ``NEW_ACCOUNT`` *state* charge + and the init-code word cost -- both masked by the binding floor -- + while the deploy (and any moved wei) still lands. + The receipt pins the floor exactly. - ``below_floor``: a gas limit one short of the floor still covers the creation intrinsic, so the rejection is pinned to the floor, with ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST``. diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py index 80b21d0c35a..4f0cffce3c8 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py @@ -102,9 +102,7 @@ def test_intrinsic_reduction_across_amsterdam_transition( if not self_transfer: expected_post += post_gas_costs.COLD_ACCOUNT_ACCESS if value: - expected_post += ( - post_gas_costs.TRANSFER_LOG_COST + post_gas_costs.TX_VALUE_COST - ) + expected_post += post_gas_costs.TX_VALUE_COST timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] expected_intrinsics = [expected_pre, expected_post] @@ -181,9 +179,8 @@ def test_creation_tx_intrinsic_across_amsterdam_transition( block, each from a fresh sender with the gas limit pinned exactly. Pre-fork the whole cost is regular intrinsic: ``TX_BASE`` plus the flat ``TX_CREATE``. Post-fork the intrinsic keeps only the - ``CREATE_ACCESS`` regular portion of ``TX_CREATE`` (plus the - transfer-log charge when value moves), while the created account's - ``NEW_ACCOUNT`` is charged as *state* gas at the top frame — the + ``CREATE_ACCESS`` regular portion of ``TX_CREATE``, while the created + account's ``NEW_ACCOUNT`` is charged as *state* gas at the top frame — the sender-facing total is the sum of both. The per-fork costs are hand-derived from each fork's gas constants @@ -217,8 +214,6 @@ def test_creation_tx_intrinsic_across_amsterdam_transition( + (post_costs.TX_CREATE - post_costs.NEW_ACCOUNT) + init_code_terms ) - if value: - expected_post += post_costs.TRANSFER_LOG_COST expected_post_state = post_costs.NEW_ACCOUNT timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py index a329470b6ba..42d694f0c2f 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -67,11 +67,10 @@ def test_value_moving_transactions( ``NEW_ACCOUNT`` state charge when value is transferred. The EIP-7708 transfer log is asserted to fire exactly when - ``TRANSFER_LOG_COST`` is charged: for a non-self value transfer, + ``TX_VALUE_COST`` is charged: for a non-self value transfer, and never for a self-transfer (carve-out) or a zero-value tx. """ - sender_initial_balance = 10**18 - sender = pre.fund_eoa(sender_initial_balance) + sender = pre.fund_eoa() target = setup_target(pre, recipient_type, sender) target_initial_balance = ( @@ -95,13 +94,12 @@ def test_value_moving_transactions( # spills entirely into regular gas. total_gas_cost = intrinsic_gas + top_frame_gas + top_frame_state_gas - tx_gas_limit = total_gas_cost + 1000 # add a small buffer - gas_price = 1_000_000_000 + tx_gas_limit = total_gas_cost is_self_transfer = recipient_type == RecipientType.SELF # A transfer log is emitted iff value moves to a distinct account, - # which is exactly when the intrinsic includes ``TRANSFER_LOG_COST``. + # which is exactly when the intrinsic includes ``TX_VALUE_COST``. # ``logs=[]`` asserts no log fires for the carved-out cases. if value > 0 and not is_self_transfer: expected_logs = [transfer_log(sender, target, value)] @@ -113,19 +111,13 @@ def test_value_moving_transactions( to=target, value=value, gas_limit=tx_gas_limit, - gas_price=gas_price, - expected_receipt=TransactionReceipt(logs=expected_logs), - ) - - sender_value_delta = 0 if is_self_transfer else value - sender_final_balance = ( - sender_initial_balance - - sender_value_delta - - total_gas_cost * gas_price + expected_receipt=TransactionReceipt( + cumulative_gas_used=tx_gas_limit, logs=expected_logs + ), ) post: dict[Address, Account | None] = { - sender: Account(nonce=1, balance=sender_final_balance), + sender: Account(nonce=1), } if not is_self_transfer: if recipient_type == RecipientType.EMPTY_ACCOUNT and value == 0: From f1c3408bde8d5509f302eee92586cfa30a462ea4 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 29 Jul 2026 10:46:03 -0600 Subject: [PATCH 27/55] fix(tests): enhance & un-skip Amsterdam ported static tests (Pt. 1) (#3215) Co-authored-by: spencer --- .claude/commands/enhance-ported-test.md | 530 +++++++++++++++ CLAUDE.md | 2 + tests/ported_static/amsterdam_skip_list.txt | 127 +--- .../test_add_non_const.py | 103 +-- .../test_create_empty_contract.py | 88 +-- .../test_create_empty_contract_and_call_it.py | 111 ++++ ..._create_empty_contract_and_call_it_0wei.py | 92 --- ..._create_empty_contract_and_call_it_1wei.py | 95 --- ...test_create_empty_contract_with_balance.py | 79 --- .../test_create_transaction_call_data.py | 151 ++--- ...est_deleagate_call_after_value_transfer.py | 69 +- .../test_delegatecall_emptycontract.py | 57 +- .../test_raw_call_code_gas.py | 84 --- .../test_raw_call_code_gas_ask.py | 84 --- .../test_raw_call_code_gas_memory.py | 86 --- .../test_raw_call_code_gas_memory_ask.py | 86 --- .../test_raw_call_code_gas_value_transfer.py | 87 --- ...st_raw_call_code_gas_value_transfer_ask.py | 87 --- ...raw_call_code_gas_value_transfer_memory.py | 87 --- ...call_code_gas_value_transfer_memory_ask.py | 87 --- .../test_raw_call_gas.py | 184 ++++-- .../test_raw_call_gas_ask.py | 205 ++++-- .../test_raw_call_gas_value_transfer.py | 87 --- .../test_raw_call_gas_value_transfer_ask.py | 87 --- ...test_raw_call_gas_value_transfer_memory.py | 87 --- ..._raw_call_gas_value_transfer_memory_ask.py | 87 --- .../test_raw_call_memory_gas.py | 84 --- .../test_raw_call_memory_gas_ask.py | 84 --- ...test_raw_create_fail_gas_value_transfer.py | 75 --- ...est_raw_create_fail_gas_value_transfer2.py | 75 --- .../test_raw_create_gas.py | 115 ++-- .../test_raw_create_gas_memory.py | 72 -- .../test_raw_create_gas_value_transfer.py | 75 --- ...st_raw_create_gas_value_transfer_memory.py | 75 --- .../test_raw_delegate_call_gas.py | 83 --- .../test_raw_delegate_call_gas_ask.py | 85 --- .../test_raw_delegate_call_gas_memory.py | 85 --- .../test_raw_delegate_call_gas_memory_ask.py | 85 --- .../stEIP1559/test_sender_balance.py | 83 +-- .../stEIP3855_push0/test_push0_gas.py | 50 +- .../stEIP3855_push0/test_push0_gas2.py | 155 +---- .../stEIP5656_MCOPY/test_mcopy_copy_cost.py | 613 ++---------------- .../test_call_data_copy_offset.py | 97 --- .../stMemoryTest/test_code_copy_offset.py | 93 --- .../stMemoryTest/test_copy_offset.py | 77 +++ .../stNonZeroCallsTest/test_non_zero_value.py | 185 ++++++ .../test_non_zero_value_call.py | 88 --- ...test_non_zero_value_call_to_empty_paris.py | 83 --- ...ero_value_call_to_one_storage_key_paris.py | 89 --- .../test_non_zero_value_callcode.py | 88 --- ..._non_zero_value_callcode_to_empty_paris.py | 83 --- ...value_callcode_to_one_storage_key_paris.py | 89 --- .../test_non_zero_value_delegatecall.py | 87 --- ..._zero_value_delegatecall_to_empty_paris.py | 81 --- ...ue_delegatecall_to_non_non_zero_balance.py | 81 --- ...e_delegatecall_to_one_storage_key_paris.py | 87 --- .../stSpecialTest/test_make_money.py | 86 +-- ...est_static_call_value_inherit_from_call.py | 83 +-- 58 files changed, 1669 insertions(+), 4631 deletions(-) create mode 100644 .claude/commands/enhance-ported-test.md create mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it.py delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_0wei.py delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_1wei.py delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_with_balance.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer2.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_ask.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory.py delete mode 100644 tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory_ask.py delete mode 100644 tests/ported_static/stMemoryTest/test_call_data_copy_offset.py delete mode 100644 tests/ported_static/stMemoryTest/test_code_copy_offset.py create mode 100644 tests/ported_static/stMemoryTest/test_copy_offset.py create mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_empty_paris.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_one_storage_key_paris.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_empty_paris.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_one_storage_key_paris.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_empty_paris.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_non_non_zero_balance.py delete mode 100644 tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_one_storage_key_paris.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md new file mode 100644 index 00000000000..f6d3f6852e5 --- /dev/null +++ b/.claude/commands/enhance-ported-test.md @@ -0,0 +1,530 @@ +# Enhance Ported Test + +Future-proof and clean up a test under `tests/ported_static/`. These tests were +machine-ported from the legacy `ethereum/tests` static fillers (YAML/JSON) and +carry a lot of boilerplate, hardcoded values, and weak/incomplete post-state +checks. This skill is the ordered methodology for turning one into idiomatic, +robust Python. + +This skill is a **living document**: it captures the cases we have validated so +far. Real tests will hit shapes not covered here — that is expected. When you +find one, solve it, then add the new case/step to this file. + +## Goal + +The end state is a test that **passes on every fork from its `valid_from` +onward** (not just the baseline), expresses its intent explicitly, and has no +fragile hardcoded constants. "Future-proof" = a later fork that re-prices gas, +adds state costs, or changes account rules should not silently break it. + +## Core loop (subtractive) + +Most of the work is **removing** boilerplate one piece at a time and proving the +test still passes after each removal: + +1. **Baseline first.** Before touching anything, fill the test and confirm it is + green: `uv run fill --fork= -q --clean`. +2. Make **one** change. +3. Fill again (same fast command). Green → keep, move on. +4. **Red → roll back that one change and analyze.** A break is information: it + tells you the thing you removed was load-bearing. Understand *why* before + deciding whether to keep it, replace it with a dynamic equivalent, or leave + it. Never paste a new expected value just to make red go green without + understanding the change (see "Re-pinning" below). + +Do low-risk, independent removals in small batches if you like, but anything +that can plausibly interact (addresses, contracts, gas) goes **one at a time** +so a failure is attributable. + +## Verification cadence + +- **Iterating:** `--fork=` (usually Cancun) — fast. +- **Checkpoint / done:** fill the whole `valid_from` range (omit `--fork`) so all + deployed forks are exercised. +- **Probe the future fork:** explicitly `--fork Amsterdam` (or the latest fork + that enables new EIPs). A ported test listed in `amsterdam_skip_list.txt` will + always show `sss` there — to see its *real* behavior, temporarily remove its + entry from that file, fill, then restore (or, once fixed, remove it for good — + see Finishing). A gas/state-cost change there is the most likely future + breakage. +- **`fill` output:** writes to `./fixtures` (`--clean` resets it), or pass + `--output ` for a scratch location. Do **not** use `-o` — that is + pytest's `--override-ini`, not the output dir. + +## Ordered steps + +Do them roughly in this order. Earlier steps unblock later ones (notably: max +out gas *before* strengthening post-state, so added opcodes don't hit a gas +ceiling). + +### 1. Remove `env` +Delete the `Environment(...)` block, the `env=env` arg to `state_test`, and any +now-orphaned vars (`coinbase`) and the `Environment` import. The framework +supplies sensible defaults. +**Keep `env` only if** the post asserts on the coinbase/`fee_recipient` balance, +or the bytecode reads block fields (`NUMBER`, `TIMESTAMP`, `PREVRANDAO`, +`BASEFEE`, `GASLIMIT`, `COINBASE`). `fee_recipient=sender` alone is not a reason +to keep it. + +### 2. Remove `gas_limit` from the transaction (if gas is not the subject) +This is the common case and belongs early. Omitting `gas_limit` maxes out the +gas the tx receives, so the body executes fully. See `write-test.md` "Transactions". +- **Remove it** when the test is about *behavior* and just needs to run to + completion. This also lets you delete any per-fork gas band-aids (e.g. + `fork.is_eip_enabled(8037)` budget bumps) and often the `fork` param itself. +- **Keep it** only for genuinely gas-sensitive tests (OOG boundaries, + intrinsic-gas, code-deposit limits, or gas metering) — see step 10. +- **Gas-snapshot tests are gas-sensitive.** If the post asserts a stored `GAS` + reading or a `SUB(@gas_before, GAS)` delta (legacy slots `0` / `0x64`), the + test *measures gas* — handle it under step 10 (preserve via `CodeGasMeasure`), + do not just strip `gas_limit`. This is the dominant `amsterdam_skip_list.txt` + shape: the stored gas value is exactly what EIP-8037 re-prices and breaks. +- **EIP-8037 caveat:** when you omit `gas_limit` on a test that *measures* an + operation incurring **state gas** (account creation, storage writes), add + `state_gas_reservoir=0` to the tx, or that state gas is silently dropped from + the measurement on EIP-8037 forks (see step 10). Pure-execution opcodes + (e.g. `PUSH0`, arithmetic) have no state gas and do not need it. +- Do **not** add a comment explaining the absence of `gas_limit`; omission is + the default. + +### 3. Remove hardcoded contract `nonce` +Drop `nonce=0` from `pre.deploy_contract(...)`. If a `compute_create_address(..., +nonce=N)` in the post depends on it, keep them consistent. + +### 4. Remove hardcoded addresses (one contract at a time) +Two sub-cases: +- **Value discarded:** a `contract = Address(0x...)` literal that is immediately + overwritten by `pre.deploy_contract(...)` (no `address=`). Just delete the + literal; the deploy returns a `fill`-generated address. +- **Value passed to `address=`:** remove both the literal *and* the `address=` + argument, per contract, filling after each. +- **No-op case:** `to=None` creation tests often have no hardcoded address at all + (the created address is `compute_create_address(sender, nonce=0)`). Confirm by + grepping for `Address(0x` / `address=`. +- **On break:** some bytecode hardcodes that address as a CALL/CREATE target (or + the tx `to`/`data`). Thread the dynamic address through the caller and the tx + entry point instead. +- **Self-reference:** a contract that hardcodes its *own* deploy address (e.g. + `Op.BALANCE(0xF172…)` where `0xF172…` is its own `address=`). Threading a + `fill`-generated address in is impossible (chicken-and-egg), so replace the + self-reference with the opcode that yields it at runtime — `Op.BALANCE(Op. + ADDRESS)`. Don't substitute a *different* opcode that happens to be shorter + (e.g. `Op.SELFBALANCE`) if it changes what the test exercises. +- **Remove `@pytest.mark.pre_alloc_mutable`** once the test no longer hardcodes + addresses/nonces or assigns `pre[...]` directly — i.e. all allocation now goes + through `fund_eoa` / `deploy_contract` / `nonexistent_account`. Fill to confirm. + +### 5. Remove easy boilerplate values +Independent and usually safe (batchable): `pre.fund_eoa(amount=...)` → `fund_eoa()`; +tx `value`; tx `data` when it is empty (`Bytes("")`); explicit gas price fields. +Keep any of these that the post actually checks or that triggers the behavior +under test. +- **Drop opcode args that just pass their default.** Ported bytecode often spells + out zero operands that are already the default, e.g. `Op.CALL(..., args_offset=0, + args_size=0, ret_offset=0, ret_size=0)` — all four are `0` by default. Removing + them is a no-op on the assembled bytecode (verify once with + `bytes(a) == bytes(b)`) and cuts noise. Applies to any opcode arg equal to its + default. +- **Drop the hardcoded subcall `gas` operand — this is a correctness fix, not + cosmetics.** `Op.CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL` default `gas` to + `Op.GAS` (forward all remaining). Ported fillers hardcode a constant + (`gas=0xEA60`, `gas=0x186A0`) that was sized for the *old* gas schedule; once + EIP-8037 inflates the callee's state gas (e.g. a zero→non-zero SSTORE jumps to + ~97920), that fixed budget no longer covers the callee and the subcall OOGs on + Amsterdam — a common reason a pure-behavior test lands on the skip list. Omit + the operand so it forwards everything. **Caveat:** forwarding all gas via + `Op.GAS` misbehaves on **pre-EIP-150 (Homestead)** — the sweep (step 11) fails + only there, so such tests floor at **TangerineWhistle**. Keep an explicit `gas` + operand *only* when the amount forwarded is the subject (an OOG-boundary test). + **Budget vs. subject:** before dropping the operand, ask *why* the constant + has its value. A mid-sized constant (`0xEA60`) is a *budget* sized for the old + schedule — drop it. An absurd or boundary constant (`2**256 - 20`) is the + *subject*: it exercises the 63/64 clamp on an oversized ask (a client that + computed e.g. `requested + stipend` in wrapping arithmetic would forward + almost nothing and fail). Keep it, name it (`OVERSIZED_GAS_ASK`), and state + the intent in a comment. Validated on `test_make_money`. +- **A codeless / absent call target is `pre.nonexistent_account()`**, not + `pre.fund_eoa(amount=0)`. It yields an address guaranteed to hold no code and + no state, which is what "call an empty contract" tests mean. +- **Drop a stale `# noqa: F841`** on `contract = pre.deploy_contract(...)` once the + variable is actually used (in `to=` / the post); leaving it triggers `RUF100`. + +### 6. (Parametrized tests) Analyze what the `data` parameter is +Look at `tx.data` / `tx.to`: +- **Scenario A — data is a target contract address:** the tx lands in a thin + entry-point contract that just `CALL`s the address from calldata. Usually you + can **delete the entry-point** and call the target directly, and the N targets + are near-identical → replace N bytecode copies with a **dynamic generator** + parameterized by the small difference. When the targets are *gas-measurement* + contracts differing only by the measured opcode, the dedup collapses all the + way to a single `CodeGasMeasure(code=opcode)` parametrized on the opcode + (step 10) — the entry-point's `CALL` was only a delivery mechanism. Validated + on `test_push0_gas2` (PUSH0 vs PUSH1 0x00). +- **Scenario B — data is initcode:** spotted by **`to=None`**. Decide whether + running inside initcode is *required* by the test (e.g. the test is about + initcode-context behavior, per its title/docstring) or just an artifact of the + static-filler format (most common — then the logic can move to a normal + deployed contract). If required, convert the `tx_data` array into an + `initcode(d)` **generator function**: even when variants are genuinely + different programs, the function form lets each branch be labeled by intent, + surfacing the one thing that varies. + +### 7. (Parametrized tests) Simplify `expect_entries_` / `resolve_expect_post` +**First, identify which index actually discriminates — it is *not* always `d`.** +Ported tests also key on `g` (gas) or `v` (value); check both the +`expect_entries_` `indexes` (which axis is non-`-1`) and which of +`tx_data[d]`/`tx_gas[g]`/`tx_value[v]` is the list with >1 entry. The other two +indexes are pinned/wildcard. (Example: `test_add_non_const` varies `v` — +`d`/`g` are fixed at 0 and the `indexes` match on `"value"`.) +**Precondition** (to collapse to a per-case form): every entry's `network` is +implied by `valid_from` and there is no `expect_exception`. Then the post is a +pure function of the discriminating index. +- Convert `expect_entries_` into a plain **list of `result` dicts indexed by the + discriminator** — duplicating identical entries (e.g. data `[0,1]` → two + slots) is fine and preferred; an explicit flat list is easiest to reason about. +- **When the discriminator is a real quantity** (the tx `value` or `gas`), + parametrize *directly on that quantity* (`parametrize("tx_value", [0, 1])`) + rather than an opaque index, feed it straight into the `Transaction`, and + express the post as a function of it. A clean closed form is ideal — + e.g. `Account(storage={0: 2 * tx_value})` for a contract that stores + `ADD(BALANCE, BALANCE)` of a balance equal to the sent value (this is the + "encode relationships" idea from step 9 applied to the post). +- Cascade: delete the `resolve_expect_post` import, the `_exc` it returned, and + the tx's `error=_exc`. +- **Optionally merge** the data-generator and the post-list into **one + `if/elif/else` on `d`** that sets both `initcode` and `post` per case. This + co-locates each case's bytecode with its expected state — the strongest + readability win, and it tends to *reveal* incomplete verification. Use a final + `else` so every branch binds both vars; declare `initcode: Bytecode` and + `post: dict` above the switch. Prefer the array form when cases are many or + the switch would be unwieldy; this is a judgment call. +- **Clean up the `parametrize` signature.** The ported `"d, g, v"` triple is + usually overkill: drop the pinned/unused indexes from both the `parametrize` + and the function signature, keep the discriminator, and rename it to something + meaningful (and `fork` too, if no longer used). Parametrize on the renamed axis: + - **String values** (e.g. `parametrize("opcode", ["calldataload", + "calldatacopy", "codecopy"])`) read best when the cases are distinct + programs; pytest derives the test ids straight from the strings (matching the + old `id=`s), and the switch branches become `if opcode == "calldataload"`. + - **`Op` values** (e.g. `parametrize("opcode", [Op.SLOAD, Op.TLOAD])`) are + cleaner *only* when the opcode plugs directly into a shared bytecode template; + avoid forcing it when each case needs structurally different code. + - Drop the verbose `pytest.param(..., id=...)` wrapping when the bare values + already give good ids. + +### 7b. Consolidate near-identical sibling files +Ported fillers often arrive as a fan of files with near-identical names that +differ in one axis — `test_non_zero_value_{call,callcode,delegatecall}` × +`{,_to_empty,_to_one_storage_key,…}`. Once enhanced to the same shape, **join +them into one parametrized test** (`parametrize("opcode, target_kind", …)` with +ids matching the old filenames), set up the varying piece (call op, target +pre-state) from the params, and merge every source into a single `ported_from` +list. One readable file replaces N. Validated: 10 `NonZeroValue_*` files → +`test_non_zero_value.py`. + +### 8. Strengthen post-state verification +Co-locating bytecode and post (step 7) often exposes that the ported test barely +verifies anything. Improve coupling and observability: +- **Couple the expectation to the bytecode.** If a contract returns its own code + (`CODECOPY`+`RETURN`), assert `code=initcode` instead of a hand-copied + `bytes.fromhex(...)` — change the bytecode and the expectation follows. +- **Make no-op results observable.** Storing `0` is indistinguishable from not + storing (and `storage={}` already asserts "all slots zero" — see + `Storage.must_be_equal`). To genuinely prove a read returned zero, store a + derived non-zero value (e.g. `Op.ADD(Op.CALLDATALOAD(0), 1)` → assert `1`). +- **Zero source data makes offset tests vacuous.** A test that asserts an + out-of-bounds read yields zeros proves nothing if the *in-bounds* data is + also all zeros — any offset, right or wrong, reads zero. Supply non-zero + source bytes (e.g. `data=bytes(range(1, 33))` for a CALLDATACOPY test) so a + client reading from a wrong in-bounds offset produces a visible mismatch. + Ported fillers often ship all-zero calldata; the rewrite is the moment to + fix it. Validated on `test_copy_offset`. +- **Preserve every assertion the legacy filler made — count its slots.** A + ported post often pins *two* observables (e.g. the ask fillers stored both + the callee-observed gas *and* the caller's net gas, which proves unused + forwarded gas is credited back). When reframing, it is easy to carry over + the headline assertion and silently drop the second. Diff the old post's + slots against the new one and re-express each dropped slot dynamically (or + justify its removal explicitly). Validated on `test_raw_call_gas_ask` (the + caller reports its remaining gas up the stack as a second return word). +- **Add a canary.** Write a distinctive non-zero sentinel to an extra slot as the + *final* step (e.g. `Op.SSTORE(0x2, 0xC0DE)`), and assert it. If creation + reverts or the code doesn't run to completion, the slot stays zero and the + test fails loudly instead of silently passing on a coincidentally-matching + (often empty) account. +- Adding `SSTORE`s costs gas — this is why step 2 (max out gas) comes first. +- **Spot a *degraded* port and restore its stated intent.** A ported test whose + name/source promises a scenario its values don't actually exercise is a bug in + the port, not something to preserve faithfully. Classic tell: a + `*_after_value_transfer` / `*_with_value` test that sends `value=0`, so the + observable it names (a callee's `CALLVALUE`, a recipient's balance) is + vacuously zero and would pass even if the behavior were broken. Fix it by + supplying the missing ingredient (a non-zero tx `value`) and asserting the + now-meaningful result (`CALLVALUE == transferred`, recipient balance moved) — + note the restoration in the `@manually-enhanced` line. Validated on + `test_deleagate_call_after_value_transfer` (DELEGATECALL preserves the + enclosing frame's value). Read the test's *name and source comment* against + what it actually checks; the gap is the enhancement. + +### 9. Introduce variables that encode relationships +Whenever a literal carries intent or two literals are logically linked, lift them +into named variables that express the *relationship*, not just the value. E.g. +`create_value = 0xB` fed to both `Op.CREATE(value=create_value, ...)` and the tx +`value=create_value - 1` documents an intentional off-by-one (insufficient +balance) and keeps the two coupled so a future edit can't desync them. Same idea +ties a `CREATE`'s `size` operand to the memory/gas math that depends on it. +- **Post-state derived from gas/fees.** When the asserted value is a function of + the gas charge (e.g. an origin `BALANCE` read mid-execution equals + `sender_balance - gas_limit * effective_gas_price`), express it as that formula + rather than a hardcoded number. Such a test is gas-sensitive — keep an explicit + `gas_limit` (step 10), since the observable depends on it, but **derive that + `gas_limit` too** — `fork.transaction_intrinsic_cost_calculator()() + + code.gas_cost(fork) + buffer` (conservative metadata so it can't undershoot) — + so it is neither a magic number nor fork-fragile. Validated on + `test_sender_balance` (EIP-1559 effective-vs-max price). +- **But first ask whether the gas-derived value is the *subject* or just + noise.** A ported test often pins the `sender` balance to `initial − value − + gas_used * price` — pure filler bookkeeping, not what the test is about. If the + real subject is a gas-*independent* fact (a value flow `tx → caller → callee`, + a storage write, a created account), drop the `gas_limit` (step 2), drop the + fragile `sender`-balance assertion, and instead assert the gas-independent + facts, encoding them as a relationship (`caller: INITIAL + tx_value - + call_value`, `callee: INITIAL + call_value`). Only reach for the "derive the + fee formula" machinery above when the fee itself is the observable. Validated + on `test_make_money`. + +### 10. (Gas-subject / gas-snapshot tests) Replace hardcoded gas with dynamic calculation +Covers both tests that *assert* a gas amount and the dominant +`amsterdam_skip_list.txt` shape: a legacy `GAS` snapshot / `SUB(@gas_before, +GAS)` delta stored to slot `0`/`0x64`. That stored value is *why* EIP-8037 +breaks the test, but it is real coverage — **preserve and fork-robustify it, do +not drop it.** + +**The `CodeGasMeasure` workflow:** +- **Isolate** the bytecode under measurement into a variable + (`call_code = Op.CALL(...)`). This often reveals the legacy measured window + bundled extra ops — e.g. it wrapped an `SSTORE`, inflating the value by a cold + `SSTORE` (~22100). Isolating the opcode measures only it (a large but + *explainable* re-pin — see Re-pinning). +- **Wrap** it: `CodeGasMeasure(code=call_code, extra_stack_items=N, sstore_key=K)`. + It self-calibrates (subtracts its own `GAS` ops and `overhead_cost`) so the + stored value is the opcode's real cost. `extra_stack_items` = items the + measured code leaves on the stack (`CREATE`/`CALL` leave 1) — wrong value + corrupts the result. `sstore_key` = the slot the post asserts. +- **`extra_stack_items=1` silently discards a call's success flag — keep it + observable.** `CodeGasMeasure` SWAP/POPs the extra item, and gas alone + cannot replace it: a wrongly *failed* call refunds the child gas + stipend, + so it measures identically to a *success* into an empty callee, and for + `CALLCODE`/`DELEGATECALL` no balance moves either — the whole post-state is + then blind to the failure. When the measured op is a call whose success is + not otherwise observable, fold the flag into the measured window: + `store_code = Op.SSTORE(flag_slot, call_code, key_warm=False, + original_value=0, new_value=1)` with `extra_stack_items=0`, assert + `flag_slot: 1` in the post, and expect `store_code.gas_cost(fork)` (the + SSTORE's cost is now part of the measurement — and a failed call would + store 0, shifting the measured gas too, so the failure is doubly loud). + Validated on `test_non_zero_value`. +- **Apply opcode metadata from the test's context** so `gas_cost(fork)` is + correct (see `docs/writing_tests/opcode_metadata.md`). For `CALL`: + `address_warm` (is the target pre-accessed?), `value_transfer` (value > 0?), + `account_new` (target absent/empty and receiving value → created?). Use + `pre.nonexistent_account()` for a target that must stay **cold + non-existent** + so `account_new` holds — a `fund_eoa()` target already exists (warm/created) and + would change the cost. + - For `CREATE`/`CREATE2`: `new_memory_size` (the init-code window the offset/ + size operands touch, e.g. `size=0x20` → `new_memory_size=0x20`) **and** + `init_code_size` (drives the EIP-3860 per-word cost, Shanghai+). Omitting + `init_code_size` silently under-predicts by `CODE_INIT_PER_WORD * + ceil(size/32)` (2/word) — a small, easily-missed miss. `CREATE` leaves the + created address on the stack → `extra_stack_items=1`. + - **A runtime address threaded via `SLOAD`** (the create-then-call idiom: + store `CREATE`'s result, then `CALL(address=Op.SLOAD(slot))`) must mark that + `SLOAD` `key_warm=True` — the slot was just written so it is warm at runtime, + but the metadata default is cold and `gas_cost(fork)` would over-predict by + `cold − warm` (2000). An account freshly made by `CREATE` is **warm + already + existing**: `address_warm=True, account_new=False` on the following `CALL`. +- **Express the expected value dynamically** from the same metadata-bearing + variable: `call_code.gas_cost(fork)` (add `fork: Fork`). Both the bytecode and + the expectation are now fork-aware. + +**CALL value-transfer stipend.** A value-bearing `CALL` whose callee consumes +nothing (empty account / EOA) measures `gas_cost(fork) - +fork.gas_costs().CALL_STIPEND`: `gas_cost` counts the full value cost, but the +2300 stipend is forwarded to the callee and returned unused. Confirm the +`- CALL_STIPEND` holds on *every* fork (it is a fork-stable relationship, not a +coincidence). + +**EIP-8037 state-gas reservoir — critical.** Omitting `gas_limit` (step 2) on an +EIP-8037 fork *maxes the state-gas reservoir*, so state gas (e.g. account +creation) is **not** charged against what the `GAS` opcode sees — the measurement +silently loses it (observed 192921 → 9321) and only the future fork breaks. Fix: +keep `gas_limit` omitted **and** add an explicit `state_gas_reservoir=0` to the +`Transaction`. That pins the gas limit to exactly the cap (no reservoir) so state +gas is charged and measurable, and is a no-op on pre-EIP-8037 forks (a *positive* +reservoir there raises; `0` does not, and it must be set explicitly — the default +is treated as "unset"). This keeps a `CodeGasMeasure` test clean (no magic +`gas_limit`) yet correct on Amsterdam. + +**Absolute `GAS` readings are unsalvageable — convert to a delta.** A test that +stores a *raw* `GAS` value (not a `SUB(before, GAS)` delta) — e.g. `SSTORE(0, +GAS)` right after entry — pins `gas_limit - intrinsic - overhead`. Amsterdam +re-priced the **intrinsic transaction cost** (EIP-2780: base 21000 → 15000), so +that stored value shifts by a fixed amount (observed 578998 → 584998, a 6000 +jump) *independent of any state gas* — `state_gas_reservoir=0` does **not** fix +it. The only robust move is to stop storing absolute readings: wrap the measured +op in `CodeGasMeasure` (which stores the *delta* between two `GAS` reads, immune +to intrinsic) and assert `code.gas_cost(fork)`. A legacy `[[0]](GAS) … +[[100]](GAS)` snapshot pair *is* such a delta in disguise — the pair brackets one +operation (e.g. a `CREATE`); collapse it to a single `CodeGasMeasure` around that +op and drop both raw slots. Validated on the `CREATE_EmptyContract*` family. + +**Decompose the constant empirically** when no single helper applies (throwaway +script against the fork): pin each term to the known-good number, then assemble. +Map terms to fork-derived helpers: opcode base+pushes → `bytecode.gas_cost(fork)`; +memory growth → `fork.memory_expansion_gas_calculator()(new_bytes=, +previous_bytes=)`; EIP-3860 init-code words → `fork.gas_costs().CODE_INIT_PER_WORD +* ceil(size/32)`. You can also call `.gas_cost` / `.regular_cost` / `.state_cost` +on exactly the measured bytecode. + +**Nested / callee-side measurements.** When the measured op is a `CALL` whose +callee does real work, the measured cost = `call_code.gas_cost(fork) + +callee_code.gas_cost(fork)` (the CALL's own cost plus what the callee consumed). +Attach the callee's opcode metadata (e.g. SSTORE `key_warm`/`original_value`/ +`new_value`) so its `gas_cost` is right, and decompose against the callee's +*actual* bytecode rather than a reconstruction — a value supplied by `GAS` costs +2, not a `PUSH`'s 3, and that off-by-3 is a real trap. A callee-side gas snapshot +(`SSTORE(k, GAS)`) stores `forward_gas - Op.GAS.gas_cost(fork)`. Derive the +forwarded gas dynamically — `forward_gas = callee_store.gas_cost(fork) + buffer` +— rather than a magic number; under EIP-8037 a cold zero->non-zero SSTORE can +cost ~100k, so a fixed value is both fork-fragile and brittle. (Size the SSTORE +with a placeholder `new_value`: its cost depends only on the zero->non-zero +transition, not the magnitude — which also breaks the `forward_gas`/`new_value` +circularity.) Set `state_gas_reservoir=0` so the state gas is captured. +Validated on `test_raw_call_gas`. + +**Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** +Ported fillers probe "how much gas does a subcall receive when it asks for more +than is available" by pinning an absolute forwarded amount — fork-fragile, +because "available" moves with the EIP-2780 intrinsic change. Make it robust +with three moves: (1) **cap the caller frame's gas to a known budget** with an +*outer* call (`entry → CALL(gas=CALLER_GAS) → caller`); because `CALLER_GAS` is +far below the outer frame's 63/64, the caller receives exactly `CALLER_GAS` +independent of the tx gas limit. (2) **Return the observed `GAS` up the stack** +(`MSTORE(0, GAS) + RETURN(0, 32)` in the callee, `RETURN` again in the caller, +`SSTORE` only in the top frame) instead of `SSTORE`-ing in a lower frame — +avoids the EIP-8037 state-gas trap. (3) **Derive the expectation from the fork:** +``` +available = CALLER_GAS - caller_call_code.gas_cost(fork) +forwarded = available - available // 64 # NOT available * 63 // 64 +expected_gas = forwarded + stipend - Op.GAS.gas_cost(fork) +``` +where `stipend = fork.gas_costs().CALL_STIPEND` for a value-bearing call (0 +otherwise). **The `// 64` form is the trap:** `available - available // 64` and +`available * 63 // 64` differ by exactly 1 whenever `available % 64 != 0` (the +EVM uses the former). One parametrize over `(opcode, value, memory)` covers the +whole CALL/CALLCODE/DELEGATECALL family; floor **Berlin** (the call metadata). +Validated on `test_raw_call_gas_ask` (10 RawCall*GasAsk fillers). + +**Error paths charge regular gas only — assert `regular_cost(fork)`.** A failed +`CREATE`/`CALL` still charges its regular costs (base, memory, init-code words) +but creates no account, so **no state gas is charged** under EIP-8037. For a +success/failure parametrize, that is exactly the `gas_cost(fork)` vs +`regular_cost(fork)` split: success measures `code.gas_cost(fork)` (regular + +state), failure measures `code.regular_cost(fork)` (regular only). On pre-8037 +forks `state_cost` is 0 so the two coincide — one expression, correct on every +fork. Drive a `CREATE` down the balance-failure path by funding the creator one +wei short of the transferred `value` (`balance = value - 1`); the created +address is then `Account.NONEXISTENT`. Validated end-to-end on +`test_raw_create_gas` (6 RawCreate*Gas fillers consolidated). + +### 11. Lower `valid_from` to extend coverage +The ported `valid_from` (often `Cancun`) is usually higher than necessary — lower +it to widen coverage. Find the true floor empirically: temporarily delete the +`valid_from` marker and fill with no `--fork` (the framework then runs from +Frontier up); the earliest fork that *passes* is your floor. Set +`@pytest.mark.valid_from("")` — the marker is mandatory, so this is a +lowering, never a true removal. +- **Gas tests floor at the EIP that introduced their metadata.** A test using + `address_warm` / cold-access metadata + `gas_cost(fork)` is only valid from + **Berlin (EIP-2929)**: earlier forks have no warm/cold distinction, so + `gas_cost` over-predicts by `cold − flat` (2600 − 700 = 1900) and every + pre-Berlin fork fails the measurement. Same shape elsewhere — EIP-3860 + init-code metering floors at Shanghai, etc. The floor is whichever EIP the + test's behavior/metadata depends on, which the empirical sweep reveals directly. +- **Behavioral floors show up as non-gas mismatches in the sweep.** A CREATE + test asserting the created account has `nonce=1` floors at **SpuriousDragon + (EIP-161)** — earlier forks start contract nonces at 0, so Frontier/Homestead/ + TangerineWhistle fail on the nonce, not the gas. Read *what* the sweep's + earliest-passing fork is gated on; it is not always a gas-schedule change. +- **A `bad v` / `INVALID_SIGNATURE_VRS` failure is a signature floor, not a + real one — don't raise `valid_from` for it.** The default `Transaction` is + EIP-155-protected, which pre-SpuriousDragon forks reject. Instead set + `protected=fork.supports_protected_txs()` (add `fork: Fork`): it goes + unprotected on Frontier/Homestead/TangerineWhistle and protected from + SpuriousDragon on. This keeps the floor at the *behavior's* real EIP (e.g. + Homestead for `DELEGATECALL`) instead of masking it at SpuriousDragon. + Validated on `test_delegatecall_emptycontract`. + +## Re-pinning expected values + +When a measurement rewrite (step 10) or bytecode change shifts a stored value, +the workflow is: change → `fill` → read the `KeyValueMismatchError` (`want … got +…`) → update the expected value to the `got` → `fill` again. +**Sanity gate:** the shift must be *explainable* — either small (the gas of +removed framing ops) or large-but-precisely-accounted (e.g. isolating an opcode +in `CodeGasMeasure` drops a cold `SSTORE` ~22100 the legacy window had bundled). +A jump you cannot account for means the rewrite changed *what* is being measured +— stop and investigate, don't just paste the number. + +## `@manually-enhanced` markers + +A docstring `@manually-enhanced: Do not overwrite` marks a deliberate prior fix. +Respect it by default. It may be removed only when a *better* enhancement makes +the workaround it documents obsolete (e.g. maxing out gas removes a per-fork gas +budget hack) — and only under explicit direction. +**Add the marker as the closing step** once a test's enhancements are intentional +(genuinely-verifying post, dynamic addresses/gas) so future auto-porting won't +regress them; briefly state what was enhanced. Place it in the **module +docstring**, after the `Ported from:` block (blank line before), as a single +line: `@manually-enhanced: Do not overwrite. .` (keep it ≤79 +chars). + +## Known gaps (extend me) + +Not yet covered by a validated walkthrough; figure out and append when hit: +- Tests where **more than one** parametrize index varies at once (a genuine 2-D + `data` × `value`/`gas` matrix) — single-axis `d`/`g`/`v` discrimination is now + handled (step 7), but a multi-axis post is not yet exercised. +- Multi-block / `blockchain_test` ported tests. + +## Finishing + +**Remove the skip-list entry.** Once the test passes on the future fork, delete +its line from `tests/ported_static/amsterdam_skip_list.txt` and decrement both +its per-directory count header (`# stXxx (N)`) and the `# Total entries:` count. +Confirm with a full-range fill (`--fork` omitted) with the entry gone — that is +the definition of done. + +**Final sweep checklist** — each of these has been missed in practice; check +them one by one before calling the test done: +- `@pytest.mark.pre_alloc_mutable` removed if no hardcoded addresses/ + nonces/`pre[...]` remain (it silently skips the test in execute mode). +- No machine-port placeholder docstrings left (`Test_.`) — the + module and function docstrings say what the test verifies, in + imperative mood ("Verify/Measure ...", not "Gas cost of ..."). +- Docstrings re-read against the *final* architecture: collapsing a + delivery CALL or moving value onto the tx makes "inherited from the + enclosing CALL"-style prose stale. +- Inline magic operands named (`FORWARDED_GAS`, `GAS_SLOT`, ...) — + consistent with sibling files in the same directory. +- Pinned budget constants guarded: anything like + `available = BUDGET - code.gas_cost(fork)` gets an + `assert available > 0, ...` so a future repricing that outgrows the + budget fails loudly at fill time instead of producing a garbage + expectation. +- The old post's slots all accounted for (see step 8's "count its + slots"). + +When done, offer to run `/lint`. Note that pydantic coercion warnings +(`dict→Alloc/Storage`, `Bytecode→Bytes`, unfilled optional `Transaction` params) +are false positives from the type checker, not real issues. diff --git a/CLAUDE.md b/CLAUDE.md index 806edfc565b..e29e0084072 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,7 @@ When reviewing PRs that implement or test EIPs: ## When to Use Skills - Writing or modifying tests → run `/write-test` first +- Cleaning up or future-proofing a `tests/ported_static/` test → run `/enhance-ported-test` first - Writing or modifying pytester-based plugin tests → run `/pytester` first - Filling test fixtures → run `/fill-tests` first - Implementing an EIP or modifying fork code in `src/` → run `/implement-eip` first @@ -55,6 +56,7 @@ When reviewing PRs that implement or test EIPs: ## Available Skills - `/write-test` — test writing patterns, fixtures, markers, bytecode helpers +- `/enhance-ported-test` — ordered methodology to clean up & future-proof `tests/ported_static/` tests - `/pytester` — pytester execution modes, isolation, output handling for plugin tests - `/fill-tests` — `fill` CLI reference, flags, debugging, benchmark tests - `/implement-eip` — fork structure, import rules, adding opcodes/precompiles/tx types diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 4423a666f35..7aba6f38bb0 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 258 +# Total entries: 153 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -73,7 +73,7 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (40) +# stCreateTest (36) stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] @@ -90,10 +90,6 @@ stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_af stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_empty_contract.py::test_create_empty_contract[fork_Amsterdam] -stCreateTest/test_create_empty_contract_and_call_it_0wei.py::test_create_empty_contract_and_call_it_0wei[fork_Amsterdam] -stCreateTest/test_create_empty_contract_and_call_it_1wei.py::test_create_empty_contract_and_call_it_1wei[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_balance.py::test_create_empty_contract_with_balance[fork_Amsterdam] stCreateTest/test_create_empty_contract_with_storage.py::test_create_empty_contract_with_storage[fork_Amsterdam] stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py::test_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py::test_create_empty_contract_with_storage_and_call_it_1wei[fork_Amsterdam] @@ -115,12 +111,10 @@ stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_c stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v0] stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v1] -# stDelegatecallTestHomestead (6) +# stDelegatecallTestHomestead (4) stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stDelegatecallTestHomestead/test_deleagate_call_after_value_transfer.py::test_deleagate_call_after_value_transfer[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] -stDelegatecallTestHomestead/test_delegatecall_emptycontract.py::test_delegatecall_emptycontract[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] # stEIP150Specific (7) @@ -132,104 +126,13 @@ stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0 stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] -# stEIP150singleCodeGasPrices (28) +# stEIP150singleCodeGasPrices (2) stEIP150singleCodeGasPrices/test_gas_cost.py::test_gas_cost[fork_Amsterdam-d40] stEIP150singleCodeGasPrices/test_gas_cost_berlin.py::test_gas_cost_berlin[fork_Amsterdam-d40] -stEIP150singleCodeGasPrices/test_raw_call_code_gas.py::test_raw_call_code_gas[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_ask.py::test_raw_call_code_gas_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory.py::test_raw_call_code_gas_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory_ask.py::test_raw_call_code_gas_memory_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer.py::test_raw_call_code_gas_value_transfer[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_ask.py::test_raw_call_code_gas_value_transfer_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory.py::test_raw_call_code_gas_value_transfer_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory_ask.py::test_raw_call_code_gas_value_transfer_memory_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas.py::test_raw_call_gas[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas_ask.py::test_raw_call_gas_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer.py::test_raw_call_gas_value_transfer[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_ask.py::test_raw_call_gas_value_transfer_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory.py::test_raw_call_gas_value_transfer_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory_ask.py::test_raw_call_gas_value_transfer_memory_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_memory_gas.py::test_raw_call_memory_gas[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_call_memory_gas_ask.py::test_raw_call_memory_gas_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer.py::test_raw_create_fail_gas_value_transfer[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer2.py::test_raw_create_fail_gas_value_transfer2[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_gas.py::test_raw_create_gas[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_gas_memory.py::test_raw_create_gas_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer.py::test_raw_create_gas_value_transfer[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer_memory.py::test_raw_create_gas_value_transfer_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_delegate_call_gas.py::test_raw_delegate_call_gas[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_ask.py::test_raw_delegate_call_gas_ask[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory.py::test_raw_delegate_call_gas_memory[fork_Amsterdam] -stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory_ask.py::test_raw_delegate_call_gas_memory_ask[fork_Amsterdam] - -# stEIP1559 (1) -stEIP1559/test_sender_balance.py::test_sender_balance[fork_Amsterdam] # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] -# stEIP3855_push0 (3) -stEIP3855_push0/test_push0_gas.py::test_push0_gas[fork_Amsterdam] -stEIP3855_push0/test_push0_gas2.py::test_push0_gas2[fork_Amsterdam-use_push0] -stEIP3855_push0/test_push0_gas2.py::test_push0_gas2[fork_Amsterdam-use_push1_00] - -# stEIP5656_MCOPY (55) -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size0-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size0-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size1-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size1-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size31-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size31-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size32-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size32-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size33-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size33-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44767-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44767-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44768-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44768-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44769-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src0_size44769-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size0-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size0-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size1-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size1-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size31-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size31-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size32-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size32-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size33-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size33-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size44767-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size44768-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src1_size44769-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size0-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size0-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size1-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size1-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size31-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size31-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size32-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size32-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size33-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size33-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size44767-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size44768-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src31_size44769-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size0-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size0-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size1-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size1-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size31-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size31-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size32-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size32-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size33-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size33-g1] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size44767-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size44768-g0] -stEIP5656_MCOPY/test_mcopy_copy_cost.py::test_mcopy_copy_cost[fork_Amsterdam-src32_size44769-g0] - # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] @@ -248,24 +151,10 @@ stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_ stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] -# stMemoryTest (4) -stMemoryTest/test_call_data_copy_offset.py::test_call_data_copy_offset[fork_Amsterdam] -stMemoryTest/test_code_copy_offset.py::test_code_copy_offset[fork_Amsterdam] +# stMemoryTest (2) stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success14] stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success15] -# stNonZeroCallsTest (10) -stNonZeroCallsTest/test_non_zero_value_call.py::test_non_zero_value_call[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_call_to_empty_paris.py::test_non_zero_value_call_to_empty_paris[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_call_to_one_storage_key_paris.py::test_non_zero_value_call_to_one_storage_key_paris[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_callcode.py::test_non_zero_value_callcode[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_callcode_to_empty_paris.py::test_non_zero_value_callcode_to_empty_paris[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_callcode_to_one_storage_key_paris.py::test_non_zero_value_callcode_to_one_storage_key_paris[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_delegatecall.py::test_non_zero_value_delegatecall[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_delegatecall_to_empty_paris.py::test_non_zero_value_delegatecall_to_empty_paris[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_delegatecall_to_non_non_zero_balance.py::test_non_zero_value_delegatecall_to_non_non_zero_balance[fork_Amsterdam] -stNonZeroCallsTest/test_non_zero_value_delegatecall_to_one_storage_key_paris.py::test_non_zero_value_delegatecall_to_one_storage_key_paris[fork_Amsterdam] - # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] @@ -300,11 +189,7 @@ stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contrac stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] -# stSpecialTest (1) -stSpecialTest/test_make_money.py::test_make_money[fork_Amsterdam] - -# stStaticCall (4) -stStaticCall/test_static_call_value_inherit_from_call.py::test_static_call_value_inherit_from_call[fork_Amsterdam] +# stStaticCall (3) stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_create_empty_contract_and_call_it_0wei[fork_Amsterdam] stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py::test_static_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py::test_static_execute_call_that_ask_fore_gas_then_trabsaction_has[fork_Amsterdam-d0] diff --git a/tests/ported_static/stArgsZeroOneBalance/test_add_non_const.py b/tests/ported_static/stArgsZeroOneBalance/test_add_non_const.py index 0a24f5ffbc1..c0b91379c99 100644 --- a/tests/ported_static/stArgsZeroOneBalance/test_add_non_const.py +++ b/tests/ported_static/stArgsZeroOneBalance/test_add_non_const.py @@ -1,116 +1,67 @@ """ -Test_add_non_const. +Verify ADD over non-constant operands: the contract adds its own balance to +itself, where that balance equals the value sent by the transaction. Ported from: state_tests/stArgsZeroOneBalance/addNonConstFiller.yml + +@manually-enhanced: Do not overwrite. Parametrized on the transaction value +(the real discriminator), the self-referential balance reads use +`BALANCE(ADDRESS)` instead of a hardcoded address, and the post asserts the +`2 * tx_value` result directly; env/gas boilerplate removed. A canary slot +keeps the `tx_value=0` arm observable (its result slot stays zero). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY = 0xC0DE + @pytest.mark.ported_from( ["state_tests/stArgsZeroOneBalance/addNonConstFiller.yml"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Frontier") +@pytest.mark.parametrize("tx_value", [0, 1]) def test_add_non_const( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_value: int, ) -> None: - """Test_add_non_const.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) + """Add the contract's own balance to itself and store the result.""" + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - # Source: lll - # { [[ 0 ]](ADD (BALANCE ) (BALANCE )) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 + # ADD with non-constant operands: the contract's own balance added to + # itself. The balance equals the value sent by the transaction. The + # canary proves the code ran even when the stored result is zero. + target = pre.deploy_contract( code=Op.SSTORE( key=0x0, - value=Op.ADD( - Op.BALANCE(address=0xF1722FE346FA35E045DE07E47CF6AF9BAE8ADE0A), - Op.BALANCE(address=0xF1722FE346FA35E045DE07E47CF6AF9BAE8ADE0A), - ), + value=Op.ADD(Op.BALANCE(Op.ADDRESS), Op.BALANCE(Op.ADDRESS)), ) + + Op.SSTORE(key=0x1, value=CANARY) + Op.STOP, - nonce=0, - address=Address(0xF1722FE346FA35E045DE07E47CF6AF9BAE8ADE0A), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": -1, "value": 0}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 0})}, - }, - { - "indexes": {"data": -1, "gas": -1, "value": 1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 2})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [400000] - tx_value = [0, 1] + # ADD(BALANCE, BALANCE) over a balance equal to the sent value. + post = {target: Account(storage={0: 2 * tx_value, 1: CANARY})} tx = Transaction( sender=sender, to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + value=tx_value, + protected=fork.supports_protected_txs(), ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract.py b/tests/ported_static/stCreateTest/test_create_empty_contract.py index 9790df29d6c..469d1a3a521 100644 --- a/tests/ported_static/stCreateTest/test_create_empty_contract.py +++ b/tests/ported_static/stCreateTest/test_create_empty_contract.py @@ -1,17 +1,20 @@ """ -Test_create_empty_contract. +Test CREATE of an empty contract and measure the CREATE gas cost. Ported from: state_tests/stCreateTest/CREATE_EmptyContractFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithBalanceFiller.json + +@manually-enhanced: Do not overwrite. CREATE gas via CodeGasMeasure; dynamic +address + fork-derived cost; empty/with-balance folded into one parametrize. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,56 +24,61 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_SLOT = 0x64 + @pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractFiller.json"], + [ + "state_tests/stCreateTest/CREATE_EmptyContractFiller.json", + "state_tests/stCreateTest/CREATE_EmptyContractWithBalanceFiller.json", + ], +) +@pytest.mark.valid_from("SpuriousDragon") +@pytest.mark.parametrize( + "create_value", + [ + pytest.param(0, id="empty_contract"), + pytest.param(1, id="with_balance"), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_create_empty_contract( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + create_value: int, ) -> None: - """Test_create_empty_contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """CREATE an empty contract (empty init code) and measure its gas.""" + # CREATE with size=0x20 over never-written memory runs 32 zero bytes as + # init code (STOP on the first byte), depositing no code -> an empty + # account with nonce 1 (and the transferred value as balance). + create_code = Op.CREATE( + value=create_value, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, ) - - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[100]] (GAS) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=create_code, + extra_stack_items=1, + sstore_key=GAS_SLOT, + ), + balance=create_value, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, ) + created = compute_create_address(address=contract, nonce=1) post = { - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 100: 0x7ABF8, - }, + contract: Account( + storage={GAS_SLOT: create_code.gas_cost(fork)}, balance=0 ), + created: Account(nonce=1, balance=create_value), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it.py b/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it.py new file mode 100644 index 00000000000..cb9c8b21fb7 --- /dev/null +++ b/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it.py @@ -0,0 +1,111 @@ +""" +Test CREATE of an empty contract followed by a CALL to it, measuring the +CALL gas cost. + +Ported from: +state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_0weiFiller.json +state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_1weiFiller.json + +@manually-enhanced: Do not overwrite. CALL gas via CodeGasMeasure; dynamic +address (runtime SLOAD); 0wei/1wei folded into one parametrize. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + CodeGasMeasure, + Fork, + StateTestFiller, + Transaction, + compute_create_address, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +ADDRESS_SLOT = 0x1 +GAS_SLOT = 0x64 + +FORWARDED_GAS = 0xEA60 + + +@pytest.mark.ported_from( + [ + "state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_1weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "call_value", + [ + pytest.param(0, id="0wei"), + pytest.param(1, id="1wei"), + ], +) +def test_create_empty_contract_and_call_it( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_value: int, +) -> None: + """CREATE an empty contract, then CALL it and measure the CALL gas.""" + # CREATE over never-written memory deposits no code -> an empty account + # with nonce 1. Its address is stored so the CALL can target it at + # runtime (it is not known when the caller code is assembled). + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + # The created account already exists (CREATE set its nonce) and is warm + # (CREATE accessed it), so the CALL is a warm call to an existing account. + call_code = Op.CALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + value=call_value, + args_offset=0x0, + args_size=0x0, + ret_offset=0x0, + ret_size=0x0, + address_warm=True, + value_transfer=call_value > 0, + account_new=False, + ) + contract = pre.deploy_contract( + code=Op.SSTORE(key=ADDRESS_SLOT, value=create_code) + + CodeGasMeasure( + code=call_code, + extra_stack_items=1, + sstore_key=GAS_SLOT, + ), + balance=call_value, + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + # A value-bearing CALL whose empty callee consumes nothing measures + # gas_cost minus the stipend (forwarded then returned unused). + stipend = fork.gas_costs().CALL_STIPEND if call_value else 0 + created = compute_create_address(address=contract, nonce=1) + post = { + contract: Account( + storage={ + ADDRESS_SLOT: created, + GAS_SLOT: call_code.gas_cost(fork) - stipend, + }, + balance=0, + ), + # The transferred value on the 1wei case proves the CALL executed. + created: Account(nonce=1, balance=call_value), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_0wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_0wei.py deleted file mode 100644 index b8efc72b9a5..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_0wei.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Test_create_empty_contract_and_call_it_0wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_0weiFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (CALL 60000 (SLOAD 1) 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6FE6B, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_1wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_1wei.py deleted file mode 100644 index 1b35d16c1b5..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_and_call_it_1wei.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Test_create_empty_contract_and_call_it_1wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_1weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractAndCallIt_1weiFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_and_call_it_1wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_and_call_it_1wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]](CALL 60000 (SLOAD 1) 1 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6E43F, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account( - balance=1, nonce=1 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_balance.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_balance.py deleted file mode 100644 index 60862e9022b..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_balance.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Test_create_empty_contract_with_balance. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithBalanceFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractWithBalanceFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_balance( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_balance.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 1 0 32) [[100]] (GAS) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x1, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - balance=1, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 100: 0x7ABF8, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account( - balance=1 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_transaction_call_data.py b/tests/ported_static/stCreateTest/test_create_transaction_call_data.py index 7c5810b6657..3da9a7599e3 100644 --- a/tests/ported_static/stCreateTest/test_create_transaction_call_data.py +++ b/tests/ported_static/stCreateTest/test_create_transaction_call_data.py @@ -1,29 +1,27 @@ """ -Tests if CALLDATALOAD, CALLDATACOPY, CODECOPY and CODESIZE work... - -call data is always empty in initcode context and "code" is initcode. +Verify CALLDATALOAD, CALLDATACOPY, CODECOPY and CODESIZE in the initcode +context of a create transaction: call data is always empty and "code" is the +initcode itself. Ported from: state_tests/stCreateTest/CreateTransactionCallDataFiller.yml -@manually-enhanced: Do not overwrite. tx_gas was raised from 100 000 to -500 000 so the CREATE path can afford its EIP-8037 NEW_ACCOUNT state -gas on Amsterdam (post-state expectations are unchanged on all forks). +@manually-enhanced: Do not overwrite. The post-state now genuinely verifies +each case (observable +1 reads prove empty call data is zero, a slot-2 canary +guards against silent creation failure, and the CODECOPY case asserts +`code=initcode`), and gas/fork boilerplate was removed in favor of maxing out +the transaction gas. """ import pytest from execution_testing import ( Account, Alloc, - Environment, + Bytecode, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -35,99 +33,70 @@ ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="calldataload", - ), - pytest.param( - 1, - 0, - 0, - id="calldatacopy", - ), - pytest.param( - 2, - 0, - 0, - id="codecopy", - ), - ], + "opcode", + ["calldataload", "calldatacopy", "codecopy"], ) @pytest.mark.pre_alloc_mutable def test_create_transaction_call_data( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: str, ) -> None: """Tests if CALLDATALOAD, CALLDATACOPY, CODECOPY and CODESIZE work...""" - sender = pre.fund_eoa(amount=0x5AF3107A4000) - - env = Environment( - fee_recipient=sender, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) + sender = pre.fund_eoa() - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - storage={}, code=b"", nonce=1 - ), - }, - }, - { - "indexes": {"data": [2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - storage={}, - code=bytes.fromhex("3860008039386000f3"), - nonce=1, - ), - }, - }, - ] + created_contract = compute_create_address(address=sender, nonce=0) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Sentinel written to storage as the final init-code step. If creation + # reverts or the init code does not run to completion, this slot stays + # zero and the test fails instead of silently passing on an account that + # happens to match the expected (small) values. + canary = 0xC0DE - tx_data = [ - Op.SSTORE(key=0x0, value=Op.CALLDATALOAD(offset=0x0)) - + Op.SSTORE(key=0x1, value=Op.CALLDATALOAD(offset=0x21)) - + Op.STOP, - Op.CALLDATACOPY(dest_offset=Op.DUP1, offset=0x0, size=0x1) - + Op.SSTORE(key=0x0, value=Op.MLOAD(offset=0x0)) - + Op.CALLDATACOPY(dest_offset=0x0, offset=0x1, size=0x20) - + Op.SSTORE(key=0x1, value=Op.MLOAD(offset=0x0)) - + Op.STOP, - Op.CODECOPY(dest_offset=Op.DUP1, offset=0x0, size=Op.CODESIZE) - + Op.RETURN(offset=0x0, size=Op.CODESIZE), - ] - # EIP-8037 NEW_ACCOUNT + per-byte state-gas spill on Amsterdam; - # pre-EIP-8037 keeps the original 100 000 budget. - outer_tx_gas = 100_000 - if fork.is_eip_enabled(8037): - outer_tx_gas = 500_000 - tx_gas = [outer_tx_gas] + # Each case sets the init code to run and the post-state it produces. + # Call data is always empty in init code context, so the calldata reads + # resolve to zero; the only thing that varies is the opcode under test. + initcode: Bytecode + post: dict + if opcode == "calldataload": # empty data reads 0; +1 makes it visible + initcode = ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.CALLDATALOAD(offset=0x0), 1)) + + Op.SSTORE(key=0x1, value=Op.ADD(Op.CALLDATALOAD(offset=0x21), 1)) + + Op.SSTORE(key=0x2, value=canary) + + Op.STOP + ) + post = { + created_contract: Account( + storage={0: 1, 1: 1, 2: canary}, code=b"", nonce=1 + ) + } + elif opcode == "calldatacopy": # empty data reads 0; +1 makes it visible + initcode = ( + Op.CALLDATACOPY(dest_offset=Op.DUP1, offset=0x0, size=0x1) + + Op.SSTORE(key=0x0, value=Op.ADD(Op.MLOAD(offset=0x0), 1)) + + Op.CALLDATACOPY(dest_offset=0x0, offset=0x1, size=0x20) + + Op.SSTORE(key=0x1, value=Op.ADD(Op.MLOAD(offset=0x0), 1)) + + Op.SSTORE(key=0x2, value=canary) + + Op.STOP + ) + post = { + created_contract: Account( + storage={0: 1, 1: 1, 2: canary}, code=b"", nonce=1 + ) + } + else: # "codecopy": CODECOPY/CODESIZE return the init code as the code + initcode = Op.CODECOPY( + dest_offset=Op.DUP1, offset=0x0, size=Op.CODESIZE + ) + Op.RETURN(offset=0x0, size=Op.CODESIZE) + # The init code returns its own bytes, so the deployed code is the + # init code itself; assert against it directly rather than a + # hand-copied hex string. + post = {created_contract: Account(storage={}, code=initcode, nonce=1)} tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_deleagate_call_after_value_transfer.py b/tests/ported_static/stDelegatecallTestHomestead/test_deleagate_call_after_value_transfer.py index a17a34fd41e..72955299b76 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_deleagate_call_after_value_transfer.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_deleagate_call_after_value_transfer.py @@ -1,17 +1,22 @@ """ -Test_deleagate_call_after_value_transfer. +Verify DELEGATECALL propagates the caller frame's context (CALLVALUE, CALLER, +CALLDATA) into the delegate, after a value-bearing transaction. Ported from: state_tests/stDelegatecallTestHomestead/deleagateCallAfterValueTransferFiller.json + +@manually-enhanced: Do not overwrite. DELEGATECALL context propagation +(CALLVALUE/CALLER/CALLDATA) run in the caller's storage; the ported test +transferred zero value (so "after value transfer" was vacuous) -> a non-zero +tx value is now sent so the callee observes it via CALLVALUE. Dynamic +addresses, gas forwarded via the default Op.GAS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,67 +25,59 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TRANSFERRED_VALUE = 0xA + @pytest.mark.ported_from( [ "state_tests/stDelegatecallTestHomestead/deleagateCallAfterValueTransferFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("TangerineWhistle") def test_deleagate_call_after_value_transfer( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_deleagate_call_after_value_transfer.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x2386F26FC10000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - # Source: lll - # { (SSTORE 0 (CALLVALUE)) (SSTORE 1 (CALLER)) (SSTORE 2 (CALLDATALOAD 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 + """DELEGATECALL runs the callee's code in the caller's context.""" + # Delegated code records the environment it observes: it must see the + # enclosing frame's CALLVALUE (the transferred value), the original CALLER + # (the sender), and the delegate-call args as its calldata (0x1). + delegate = pre.deploy_contract( code=Op.SSTORE(key=0x0, value=Op.CALLVALUE) + Op.SSTORE(key=0x1, value=Op.CALLER) + Op.SSTORE(key=0x2, value=Op.CALLDATALOAD(offset=0x0)) + Op.STOP, - nonce=0, ) - # Source: lll - # { (MSTORE 0 0x01) (DELEGATECALL 100000 0 64 0 64) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 + caller = pre.deploy_contract( code=Op.MSTORE(offset=0x0, value=0x1) + Op.DELEGATECALL( - gas=0x186A0, - address=addr, + address=delegate, args_offset=0x0, args_size=0x40, ret_offset=0x0, ret_size=0x40, ) + Op.STOP, - balance=0x10C8E0, - nonce=0, ) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=453081, + to=caller, + value=TRANSFERRED_VALUE, + protected=fork.supports_protected_txs(), ) post = { - target: Account(storage={0: 0, 1: sender, 2: 1}), - addr: Account(storage={0: 0, 1: 0, 2: 0}), + # DELEGATECALL preserves the enclosing frame's value, so the callee + # sees CALLVALUE == the transferred value; its writes land in the + # caller's storage, not the callee's. + caller: Account( + balance=TRANSFERRED_VALUE, + storage={0: TRANSFERRED_VALUE, 1: sender, 2: 1}, + ), + delegate: Account(storage={0: 0, 1: 0, 2: 0}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_emptycontract.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_emptycontract.py index 60b0410f6c4..90c1b8b72bb 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_emptycontract.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_emptycontract.py @@ -1,17 +1,19 @@ """ -Test_delegatecall_emptycontract. +Verify a DELEGATECALL to a codeless, nonexistent account succeeds without +creating or touching the target. Ported from: state_tests/stDelegatecallTestHomestead/delegatecallEmptycontractFiller.json + +@manually-enhanced: Do not overwrite. DELEGATECALL to a codeless account +returns success; dynamic addresses, gas maxed out. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -26,33 +28,20 @@ "state_tests/stDelegatecallTestHomestead/delegatecallEmptycontractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("TangerineWhistle") def test_delegatecall_emptycontract( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_delegatecall_emptycontract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x10C8E0) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - # Source: lll - # { [[ 0 ]] (DELEGATECALL 50000 0x945304eb96065b2a98b57a48a06ae28d285a71b5 0 64 0 64 )} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 + """DELEGATECALL to a codeless account succeeds (returns 1).""" + # A DELEGATECALL to an account with no code runs nothing and returns 1. + empty = pre.nonexistent_account() + caller = pre.deploy_contract( code=Op.SSTORE( key=0x0, value=Op.DELEGATECALL( - gas=0xC350, - address=0x945304EB96065B2A98B57A48A06AE28D285A71B5, + address=empty, args_offset=0x0, args_size=0x40, ret_offset=0x0, @@ -60,17 +49,21 @@ def test_delegatecall_emptycontract( ), ) + Op.STOP, - balance=1000, - nonce=0, ) + # DELEGATECALL predates EIP-155, so the tx must go unprotected on + # pre-SpuriousDragon forks or it fails signature validation. tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=105044, + sender=pre.fund_eoa(), + to=caller, + protected=fork.supports_protected_txs(), ) - post = {target: Account(storage={0: 1})} + # DELEGATECALL carries no value, so it must not create (or even touch) + # the target account. + post = { + caller: Account(storage={0: 1}), + empty: Account.NONEXISTENT, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas.py deleted file mode 100644 index ce267ffe0b9..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_raw_call_code_gas. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 30000 0 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x7530, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 24739, 2: 29998}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_ask.py deleted file mode 100644 index 4850de47a79..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_ask.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_raw_call_code_gas_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasAskFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 3000000 0 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x2DC6C0, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 24739, 2: 0x727BB}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory.py deleted file mode 100644 index d5eecdff81e..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Test_raw_call_code_gas_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 30000 0 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x7530, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 25608, 2: 29998}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory_ask.py deleted file mode 100644 index dbed9f97250..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_memory_ask.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Test_raw_call_code_gas_memory_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_memory_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_memory_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 3000000 0 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x2DC6C0, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 25608, 2: 0x72464}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer.py deleted file mode 100644 index 8082a69671c..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_code_gas_value_transfer. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_value_transfer( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_value_transfer.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 30000 10 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x7530, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 31439, 2: 32298}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_ask.py deleted file mode 100644 index d69e9eec044..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_ask.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_code_gas_value_transfer_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_value_transfer_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_value_transfer_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 3000000 10 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x2DC6C0, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 31439, 2: 0x70E1C}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory.py deleted file mode 100644 index adc2b040768..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_code_gas_value_transfer_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_value_transfer_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_value_transfer_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 30000 10 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x7530, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 32308, 2: 32298}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory_ask.py deleted file mode 100644 index db089d6d3fc..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_code_gas_value_transfer_memory_ask.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_code_gas_value_transfer_memory_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_code_gas_value_transfer_memory_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_code_gas_value_transfer_memory_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALLCODE 3000000 10 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALLCODE( - gas=0x2DC6C0, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 32308, 2: 0x70AC4}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas.py index 97bc8524d66..6fe0cbca6de 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas.py @@ -1,17 +1,29 @@ """ -Test_raw_call_gas. +Measure the gas cost of CALL / CALLCODE / DELEGATECALL with CodeGasMeasure, +across value-transfer and memory-expansion variants. The callee records the +gas it was forwarded. Ported from: state_tests/stEIP150singleCodeGasPrices/RawCallGasFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryFiller.json +state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasFiller.json +state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryFiller.json + +@manually-enhanced: Do not overwrite. Nested call gas via CodeGasMeasure. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) @@ -20,65 +32,143 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FORWARD_BUFFER = 100 # margin forwarded beyond the callee's own gas cost +MEMORY_SIZE = 0x1F40 # args/ret buffer size for memory variants +CALL_VALUE = 0xA +CALLER_BALANCE = 100 + @pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallGasFiller.json"], + [ + "state_tests/stEIP150singleCodeGasPrices/RawCallGasFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "opcode, value, memory", + [ + pytest.param(Op.CALL, 0, False, id="raw_call_gas"), + pytest.param( + Op.CALL, CALL_VALUE, False, id="raw_call_gas_value_transfer" + ), + pytest.param(Op.CALL, 0, True, id="raw_call_memory_gas"), + pytest.param( + Op.CALL, CALL_VALUE, True, id="raw_call_gas_value_transfer_memory" + ), + pytest.param(Op.CALLCODE, 0, False, id="raw_call_code_gas"), + pytest.param( + Op.CALLCODE, + CALL_VALUE, + False, + id="raw_call_code_gas_value_transfer", + ), + pytest.param(Op.CALLCODE, 0, True, id="raw_call_code_gas_memory"), + pytest.param( + Op.CALLCODE, + CALL_VALUE, + True, + id="raw_call_code_gas_value_transfer_memory", + ), + pytest.param(Op.DELEGATECALL, 0, False, id="raw_delegate_call_gas"), + pytest.param( + Op.DELEGATECALL, 0, True, id="raw_delegate_call_gas_memory" + ), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_raw_call_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + opcode: Op, + value: int, + memory: bool, ) -> None: - """Test_raw_call_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """Measure call-family gas, with the callee recording forwarded gas.""" + stipend = fork.gas_costs().CALL_STIPEND if value else 0 + mem = MEMORY_SIZE if memory else 0 - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The callee writes a cold (zero->non-zero) slot; SSTORE cost depends only + # on that transition, not the value, so a placeholder new_value suffices to + # size the gas to forward (large under EIP-8037 state gas). + callee_store = Op.SSTORE( + key=0x2, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) + forward_gas = callee_store.gas_cost(fork) + FORWARD_BUFFER + callee = pre.deploy_contract(code=callee_store + Op.STOP) - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 30000 0 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x7530, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + # Callee records the gas it received: forwarded gas plus the value-transfer + # stipend, minus the GAS opcode it executes. + callee_gas_seen = forward_gas + stipend - Op.GAS.gas_cost(fork) + + if opcode == Op.DELEGATECALL: + call_code = Op.DELEGATECALL( + gas=forward_gas, + address=callee, + args_offset=0x0, + args_size=mem, + ret_offset=0x0, + ret_size=mem, + address_warm=False, + new_memory_size=mem, + ) + else: + call_code = opcode( + gas=forward_gas, + address=callee, + value=value, + args_offset=0x0, + args_size=mem, + ret_offset=0x0, + ret_size=mem, + address_warm=False, + value_transfer=value > 0, + account_new=False, + new_memory_size=mem, ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, + caller = pre.deploy_contract( + code=CodeGasMeasure( + code=call_code, + extra_stack_items=1, + sstore_key=0x1, + ), + balance=CALLER_BALANCE, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, + sender=pre.fund_eoa(), + to=caller, + state_gas_reservoir=0, ) + # Measured cost = the call's own cost plus the callee's consumption; the + # value-transfer stipend is forwarded free, not charged to the caller. + call_gas = call_code.gas_cost(fork) + callee_store.gas_cost(fork) - stipend + + # CALL runs the callee in its own context (slot 2 in the callee); CALLCODE + # and DELEGATECALL run it in the caller's context (slot 2 in the caller). + if opcode == Op.CALL: + callee_storage = {0x2: callee_gas_seen} + caller_storage = {0x1: call_gas} + else: + callee_storage = {} + caller_storage = {0x1: call_gas, 0x2: callee_gas_seen} + post = { - addr: Account(storage={2: 29998}), - target: Account(storage={1: 24739}), + callee: Account(storage=callee_storage), + caller: Account(storage=caller_storage), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_ask.py index 6935ff2a0d7..e16c5b50880 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_ask.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_ask.py @@ -1,17 +1,35 @@ """ -Test_raw_call_gas_ask. +Verify the EIP-150 "all but one 64th" rule: a subcall asking for more gas +than is available receives 63/64 of it, across CALL / CALLCODE / DELEGATECALL +and their value-transfer and memory-expansion variants. Ported from: state_tests/stEIP150singleCodeGasPrices/RawCallGasAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasAskFiller.json +state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryAskFiller.json + +@manually-enhanced: Do not overwrite. The ported fillers pinned the forwarded +gas as an absolute number tied to the tx gas limit (fork-fragile via the +intrinsic). Reframed so an outer call caps the caller frame at a known gas +budget, the callee returns its observed GAS up to the top frame (no lower-frame +SSTORE state-gas trap), and the expected value is derived from the fork: +`all_but_one_64th(caller_gas - call.gas_cost(fork))`. The caller also reports +its remaining gas after the subcall, preserving the ported fillers' second +assertion that unused forwarded gas is credited back. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,65 +38,156 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALLER_GAS = 100_000 +CALL_VALUE = 0xA +MEMORY_SIZE = 0x1F40 # 8000-byte args/ret buffer for the memory variants + @pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallGasAskFiller.json"], + [ + "state_tests/stEIP150singleCodeGasPrices/RawCallGasAskFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasAskFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasMemoryAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCallCodeGasValueTransferMemoryAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasAskFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryAskFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "opcode, value, memory", + [ + pytest.param(Op.CALL, 0, False, id="raw_call_gas_ask"), + pytest.param( + Op.CALL, CALL_VALUE, False, id="raw_call_gas_value_transfer_ask" + ), + pytest.param(Op.CALL, 0, True, id="raw_call_memory_gas_ask"), + pytest.param( + Op.CALL, + CALL_VALUE, + True, + id="raw_call_gas_value_transfer_memory_ask", + ), + pytest.param(Op.CALLCODE, 0, False, id="raw_call_code_gas_ask"), + pytest.param(Op.CALLCODE, 0, True, id="raw_call_code_gas_memory_ask"), + pytest.param( + Op.CALLCODE, + CALL_VALUE, + False, + id="raw_call_code_gas_value_transfer_ask", + ), + pytest.param( + Op.CALLCODE, + CALL_VALUE, + True, + id="raw_call_code_gas_value_transfer_memory_ask", + ), + pytest.param( + Op.DELEGATECALL, 0, False, id="raw_delegate_call_gas_ask" + ), + pytest.param( + Op.DELEGATECALL, 0, True, id="raw_delegate_call_gas_memory_ask" + ), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_raw_call_gas_ask( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + opcode: Op, + value: int, + memory: bool, ) -> None: - """Test_raw_call_gas_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A subcall asking for more gas than available receives 63/64 of it.""" + sender = pre.fund_eoa() + + # Callee returns the gas it observed on entry back to the caller. + gas_return_code = Op.MSTORE(0, Op.GAS, new_memory_size=32) + Op.RETURN( + 0, 32 ) + gas_return_contract = pre.deploy_contract(code=gas_return_code) + + mem = MEMORY_SIZE if memory else 0 + ret_size = MEMORY_SIZE if memory else 32 # must fit the 32-byte GAS return + new_memory_size = MEMORY_SIZE if memory else 32 - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, + # The caller asks for "all" gas (the default Op.GAS operand), which exceeds + # what remains after the call's own cost, so the 63/64 cap kicks in. + if opcode == Op.DELEGATECALL: + caller_call_code = Op.DELEGATECALL( + address=gas_return_contract, + args_offset=0, + args_size=mem, + ret_offset=0, + ret_size=ret_size, + address_warm=False, + new_memory_size=new_memory_size, + ) + else: + caller_call_code = opcode( + address=gas_return_contract, + value=value, + args_offset=0, + args_size=mem, + ret_offset=0, + ret_size=ret_size, + address_warm=False, + value_transfer=value > 0, + account_new=False, + new_memory_size=new_memory_size, + ) + # After the subcall returns, the caller appends its own remaining gas to + # the return data, so the top frame can also assert that the unused part + # of the 63/64-forwarded grant was credited back to the caller. + caller = pre.deploy_contract( + code=caller_call_code + Op.MSTORE(32, Op.GAS) + Op.RETURN(0, 64), + balance=value, ) - # Source: lll - # { [0] (GAS) (CALL 3000000 0 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x2DC6C0, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + + # An outer call pins the caller frame's gas to a known budget, so the + # forwarded amount does not depend on the tx gas limit. + entry = pre.deploy_contract( + code=Op.SSTORE(0, 1) + + Op.CALL( + gas=CALLER_GAS, + address=caller, + value=0, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=64, ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, + + Op.SSTORE(1, Op.MLOAD(0)) + + Op.SSTORE(2, Op.MLOAD(32)), ) - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, + # EIP-150 forwards "all but one 64th" of the gas left after the call's own + # cost; a value-bearing call additionally hands the callee the stipend. + stipend = fork.gas_costs().CALL_STIPEND if value else 0 + available = CALLER_GAS - caller_call_code.gas_cost(fork) + assert available > 0, "CALLER_GAS must exceed the call's own cost" + forwarded = available - available // 64 + expected_gas = forwarded + stipend - Op.GAS.gas_cost(fork) + + # The callee's unconsumed gas returns to the caller: what the caller sees + # after the subcall is its budget minus the call's own cost and the + # callee's consumption (the stipend nets out on value-bearing calls). + expected_caller_gas = ( + CALLER_GAS + - caller_call_code.gas_cost(fork) + + stipend + - gas_return_code.gas_cost(fork) + - Op.GAS.gas_cost(fork) ) + tx = Transaction(sender=sender, to=entry) + post = { - addr: Account(storage={2: 0x727BB}), - target: Account(storage={1: 24739}), + entry: Account(storage={0: 1, 1: expected_gas, 2: expected_caller_gas}) } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer.py deleted file mode 100644 index c94dd98e65a..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_gas_value_transfer. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_gas_value_transfer( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_gas_value_transfer.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 30000 10 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x7530, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={2: 32298}), - target: Account(storage={1: 31439}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_ask.py deleted file mode 100644 index 46240d39693..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_ask.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_gas_value_transfer_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_gas_value_transfer_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_gas_value_transfer_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 3000000 10 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x2DC6C0, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={2: 0x70E1C}), - target: Account(storage={1: 31439}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory.py deleted file mode 100644 index cd1f9f68c18..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_gas_value_transfer_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_gas_value_transfer_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_gas_value_transfer_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 30000 10 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x7530, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={2: 32298}), - target: Account(storage={1: 32308}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory_ask.py deleted file mode 100644 index 0939cc07c2a..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_gas_value_transfer_memory_ask.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_raw_call_gas_value_transfer_memory_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCallGasValueTransferMemoryAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_gas_value_transfer_memory_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_gas_value_transfer_memory_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 3000000 10 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x2DC6C0, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - addr: Account(storage={2: 0x70AC4}), - target: Account(storage={1: 32308}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas.py deleted file mode 100644 index 2626ed1afa2..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_raw_call_memory_gas. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_memory_gas( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_memory_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 30000 0 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x7530, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={2: 29998}), - target: Account(storage={1: 25608}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas_ask.py deleted file mode 100644 index 1ea4c6eab02..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_call_memory_gas_ask.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_raw_call_memory_gas_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCallMemoryGasAskFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_call_memory_gas_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_call_memory_gas_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 3000000 0 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x2DC6C0, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={2: 0x72464}), - target: Account(storage={1: 25608}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer.py deleted file mode 100644 index b1926166116..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Test_raw_create_fail_gas_value_transfer. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransferFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransferFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_create_fail_gas_value_transfer( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_create_fail_gas_value_transfer.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [0] (GAS) (CREATE 11 0 0) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0xB, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - contract_0: Account(storage={1: 32022}), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer2.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer2.py deleted file mode 100644 index cff6a4c875c..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_fail_gas_value_transfer2.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Test_raw_create_fail_gas_value_transfer2. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransfer2Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransfer2Filler.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_create_fail_gas_value_transfer2( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_create_fail_gas_value_transfer2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [0] (GAS) (CREATE 11 0 8000) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0xB, offset=0x0, size=0x1F40)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - contract_0: Account(storage={1: 33391}), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py index cfcdaf8e86f..e7e0e5962af 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py @@ -1,17 +1,25 @@ """ -Test_raw_create_gas. +Measure the gas cost of CREATE with CodeGasMeasure, across value-transfer, +memory-expansion, and insufficient-balance (failure) variants. Ported from: state_tests/stEIP150singleCodeGasPrices/RawCreateGasFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCreateGasMemoryFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferMemoryFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransferFiller.json +state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransfer2Filler.json + +@manually-enhanced: Do not overwrite. Six RawCreate*Gas fillers folded into one +CodeGasMeasure parametrize; failure path charges regular_cost (no state gas). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,52 +29,85 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_SLOT = 0x1 +MEMORY_SIZE = 0x1F40 # 8000-byte init-code window for the memory variants + @pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCreateGasFiller.json"], + [ + "state_tests/stEIP150singleCodeGasPrices/RawCreateGasFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCreateGasMemoryFiller.json", + "state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferMemoryFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransferFiller.json", # noqa: E501 + "state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransfer2Filler.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("SpuriousDragon") +@pytest.mark.parametrize( + "create_value, size, fails", + [ + pytest.param(0x0, 0x0, False, id="raw_create_gas"), + pytest.param(0x0, MEMORY_SIZE, False, id="raw_create_gas_memory"), + pytest.param(0xA, 0x0, False, id="raw_create_gas_value_transfer"), + pytest.param( + 0xA, MEMORY_SIZE, False, id="raw_create_gas_value_transfer_memory" + ), + pytest.param(0xB, 0x0, True, id="raw_create_fail_gas_value_transfer"), + pytest.param( + 0xB, MEMORY_SIZE, True, id="raw_create_fail_gas_value_transfer2" + ), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_raw_create_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + create_value: int, + size: int, + fails: bool, ) -> None: - """Test_raw_create_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Measure CREATE gas; a balance-failure path is cheaper (no state gas).""" + # Init code is never written, so it is `size` zero bytes: the created + # contract STOPs immediately and deposits no code. + create_code = Op.CREATE( + value=create_value, + offset=0x0, + size=size, + new_memory_size=size, + init_code_size=size, ) - - # Source: lll - # { [0] (GAS) (CREATE 0 0 0) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, + # Fund the creator one wei short of `create_value` on the failure cases so + # the CREATE aborts on the balance check; otherwise give it exactly enough. + balance = create_value - 1 if fails else create_value + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=create_code, + extra_stack_items=1, + sstore_key=GAS_SLOT, + ), + balance=balance, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, ) + created = compute_create_address(address=contract, nonce=1) + if fails: + # A balance-check failure runs no init code and creates no account, so + # only the regular (execution) gas is charged, never state gas. + expected_gas = create_code.regular_cost(fork) + created_account = Account.NONEXISTENT + else: + expected_gas = create_code.gas_cost(fork) + created_account = Account(balance=create_value) + post = { - contract_0: Account(storage={1: 32022}), - compute_create_address(address=contract_0, nonce=0): Account( - balance=0 - ), + contract: Account(storage={GAS_SLOT: expected_gas}), + created: created_account, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_memory.py deleted file mode 100644 index 78b9d6682e6..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_memory.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Test_raw_create_gas_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCreateGasMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawCreateGasMemoryFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_create_gas_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_create_gas_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [0] (GAS) (CREATE 0 0 8000) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x1F40)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - contract_0: Account(storage={1: 33391}), - compute_create_address(address=contract_0, nonce=0): Account( - balance=0 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer.py deleted file mode 100644 index 355d74d1dfd..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Test_raw_create_gas_value_transfer. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_create_gas_value_transfer( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_create_gas_value_transfer.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [0] (GAS) (CREATE 10 0 0) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0xA, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - contract_0: Account(storage={1: 32022}), - compute_create_address(address=contract_0, nonce=0): Account( - balance=10 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer_memory.py deleted file mode 100644 index 52578c5b4b9..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas_value_transfer_memory.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Test_raw_create_gas_value_transfer_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawCreateGasValueTransferMemoryFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_create_gas_value_transfer_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_create_gas_value_transfer_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [0] (GAS) (CREATE 10 0 8000) [[1]] (SUB @0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP(Op.CREATE(value=0xA, offset=0x0, size=0x1F40)) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=500000, - value=10, - ) - - post = { - contract_0: Account(storage={1: 33391}), - compute_create_address(address=contract_0, nonce=0): Account( - balance=10 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas.py deleted file mode 100644 index 9ff87ffdc2a..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Test_raw_delegate_call_gas. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_delegate_call_gas( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_delegate_call_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (DELEGATECALL 30000 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.DELEGATECALL( - gas=0x7530, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 24736, 2: 29998}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_ask.py deleted file mode 100644 index 64eeacabb25..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_ask.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Test_raw_delegate_call_gas_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_delegate_call_gas_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_delegate_call_gas_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (DELEGATECALL 3000000 0 0 0 0) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.DELEGATECALL( - gas=0x2DC6C0, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 24736, 2: 0x727BE}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory.py deleted file mode 100644 index 3db2620bb5a..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Test_raw_delegate_call_gas_memory. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_delegate_call_gas_memory( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_delegate_call_gas_memory.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (DELEGATECALL 30000 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.DELEGATECALL( - gas=0x7530, - address=addr, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 25605, 2: 29998}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory_ask.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory_ask.py deleted file mode 100644 index 37333476bf2..00000000000 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_delegate_call_gas_memory_ask.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Test_raw_delegate_call_gas_memory_ask. - -Ported from: -state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryAskFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stEIP150singleCodeGasPrices/RawDelegateCallGasMemoryAskFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_raw_delegate_call_gas_memory_ask( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_raw_delegate_call_gas_memory_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[2]] (GAS) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=Op.GAS) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (DELEGATECALL 3000000 0 8000 0 8000) [[1]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.DELEGATECALL( - gas=0x2DC6C0, - address=addr, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=500000, - ) - - post = { - addr: Account(storage={}), - target: Account(storage={1: 25605, 2: 0x72467}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP1559/test_sender_balance.py b/tests/ported_static/stEIP1559/test_sender_balance.py index c248718b4df..7ea2c909564 100644 --- a/tests/ported_static/stEIP1559/test_sender_balance.py +++ b/tests/ported_static/stEIP1559/test_sender_balance.py @@ -1,22 +1,20 @@ """ -The execution records the EIP-1559 transaction origin balance to make... - -properly computed based on the effective gas price (not the maximum gas price -as in -the transaction validity check). +The origin balance seen during execution of an EIP-1559 transaction is +computed from the effective gas price, not the maximum gas price used in the +transaction validity check. Ported from: state_tests/stEIP1559/senderBalanceFiller.yml + +@manually-enhanced: Do not overwrite. Balance derived from gas/fee inputs. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,49 +27,58 @@ @pytest.mark.ported_from( ["state_tests/stEIP1559/senderBalanceFiller.yml"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_sender_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """The execution records the EIP-1559 transaction origin balance to...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0xE04D1AC7DDDA0C98397D56A0B501E960D4CD325A39286919AC23C1A07009A869 - ) + """Origin balance during execution reflects the effective gas price.""" + base_fee = 11 + priority_fee = 100 + max_fee = 1000 + sender_balance = 0xDE0B6B3A7640000 + + # The effective gas price is base + priority (kept below max_fee, so the + # validity check would reserve more — the point of the test). + effective_gas_price = base_fee + priority_fee + + env = Environment(base_fee_per_gas=base_fee) + sender = pre.fund_eoa(amount=sender_balance) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=11, - gas_limit=30000000, + # Source: yul: { sstore(0, balance(caller())) } + target_code = ( + Op.SSTORE( + key=0x0, + value=Op.BALANCE(address=Op.CALLER, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.STOP ) + target = pre.deploy_contract(code=target_code) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: yul - # london - # { - # sstore(0, balance(caller())) - # } - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.BALANCE(address=Op.CALLER)) + Op.STOP, - nonce=0, - address=Address(0x420132F96200BA8E5C98298A85633C35C4F052EF), # noqa: E501 + # Size the gas limit to the work done, so the upfront charge (and thus the + # observed balance) tracks the fork's costs rather than a magic number. + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()() + + target_code.gas_cost(fork) + + 1000 ) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=60000, - max_fee_per_gas=1000, - max_priority_fee_per_gas=100, - access_list=[], + gas_limit=gas_limit, + max_fee_per_gas=max_fee, + max_priority_fee_per_gas=priority_fee, ) - post = {target: Account(storage={0: 0xDE0B6B3A6FE6060})} + post = { + target: Account( + storage={0: sender_balance - gas_limit * effective_gas_price} + ) + } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP3855_push0/test_push0_gas.py b/tests/ported_static/stEIP3855_push0/test_push0_gas.py index e9a870b6600..a3314b55996 100644 --- a/tests/ported_static/stEIP3855_push0/test_push0_gas.py +++ b/tests/ported_static/stEIP3855_push0/test_push0_gas.py @@ -1,17 +1,18 @@ """ -Test_push0_gas. +Measure the gas cost of the PUSH0 instruction. Ported from: state_tests/Shanghai/stEIP3855_push0/push0GasFiller.yml + +@manually-enhanced: Do not overwrite. PUSH0 gas via CodeGasMeasure. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) @@ -24,42 +25,29 @@ @pytest.mark.ported_from( ["state_tests/Shanghai/stEIP3855_push0/push0GasFiller.yml"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Shanghai") def test_push0_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_push0_gas.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=89128960, - ) - - # Source: raw - # 0x5a6000555f5a6000540360015500 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.PUSH0 - + Op.SSTORE(key=0x1, value=Op.SUB(Op.SLOAD(key=0x0), Op.GAS)) - + Op.STOP, - nonce=0, + """Measure PUSH0's gas cost against the fork-derived expectation.""" + sender = pre.fund_eoa() + + push0_code = Op.PUSH0 + target = pre.deploy_contract( + code=CodeGasMeasure( + code=push0_code, + extra_stack_items=1, + sstore_key=0x1, + ), ) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, ) - post = {target: Account(storage={0: 0x13496, 1: 22107})} + post = {target: Account(storage={0x1: push0_code.gas_cost(fork)})} - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP3855_push0/test_push0_gas2.py b/tests/ported_static/stEIP3855_push0/test_push0_gas2.py index 7405bce15a3..85e6c5140be 100644 --- a/tests/ported_static/stEIP3855_push0/test_push0_gas2.py +++ b/tests/ported_static/stEIP3855_push0/test_push0_gas2.py @@ -1,24 +1,23 @@ """ -Test_push0_gas2. +Measure the gas cost of PUSH0 and of PUSH1 0x00: each case asserts its own +fork-derived cost, which together demonstrate PUSH0 is the cheaper encoding. Ported from: state_tests/Shanghai/stEIP3855_push0/push0Gas2Filler.yml + +@manually-enhanced: Do not overwrite. Opcode gas via CodeGasMeasure. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Bytecode, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -28,138 +27,34 @@ @pytest.mark.ported_from( ["state_tests/Shanghai/stEIP3855_push0/push0Gas2Filler.yml"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Shanghai") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="use_push0", - ), - pytest.param( - 1, - 0, - 0, - id="use_push1_00", - ), - ], + "opcode", + [Op.PUSH0, Op.PUSH1[0x00]], + ids=["use_push0", "use_push1_00"], ) -@pytest.mark.pre_alloc_mutable def test_push0_gas2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + opcode: Bytecode, ) -> None: - """Test_push0_gas2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x0000000000000000000000000000000000001000) - contract_2 = Address(0x0000000000000000000000000000000000000200) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=89128960, - ) - - pre[sender] = Account(balance=0x989680) - # Source: yul - # berlin - # { - # sstore(0, call(100000, shr(96, calldataload(0)), 0, 0, 0, 0, 0)) - # sstore(1, 1) - # } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x186A0, - address=Op.SHR(0x60, Op.CALLDATALOAD(offset=Op.DUP1)), - value=Op.DUP1, - args_offset=Op.DUP1, - args_size=Op.DUP1, - ret_offset=Op.DUP1, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=Op.DUP1, value=0x1) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: raw - # 0x5a5f5a9091039055 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.GAS - + Op.PUSH0 - + Op.GAS - + Op.SWAP1 - + Op.SWAP2 - + Op.SUB - + Op.SWAP1 - + Op.SSTORE, - nonce=0, - address=Address(0x0000000000000000000000000000000000001000), # noqa: E501 - ) - # Source: raw - # 0x5a60005a9091039055 - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.GAS - + Op.PUSH1[0x0] - + Op.GAS - + Op.SWAP1 - + Op.SWAP2 - + Op.SUB - + Op.SWAP1 - + Op.SSTORE, - nonce=0, - address=Address(0x0000000000000000000000000000000000000200), # noqa: E501 + """Measure the parametrized push encoding's exact gas cost.""" + sender = pre.fund_eoa() + + measured = pre.deploy_contract( + code=CodeGasMeasure( + code=opcode, + extra_stack_items=1, + sstore_key=0x0, + ), ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_1: Account(storage={0: 4}, balance=0), - contract_0: Account(storage={0: 1, 1: 1}), - }, - }, - { - "indexes": {"data": [1], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_2: Account(storage={0: 5}, balance=0), - contract_0: Account(storage={0: 1, 1: 1}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - contract_1, - contract_2, - ] - tx_gas = [300000] - tx = Transaction( sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + to=measured, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = {measured: Account(storage={0x0: opcode.gas_cost(fork)})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP5656_MCOPY/test_mcopy_copy_cost.py b/tests/ported_static/stEIP5656_MCOPY/test_mcopy_copy_cost.py index 8cd67806eee..12340a9259e 100644 --- a/tests/ported_static/stEIP5656_MCOPY/test_mcopy_copy_cost.py +++ b/tests/ported_static/stEIP5656_MCOPY/test_mcopy_copy_cost.py @@ -3,604 +3,73 @@ Ported from: state_tests/Cancun/stEIP5656_MCOPY/MCOPY_copy_costFiller.yml + +@manually-enhanced: Do not overwrite. The ported filler probed MCOPY cost via a +tight OOG gas boundary (55697); EIP-8037 reprices the instrumentation SSTORE +into state gas, breaking that boundary. Reframed to measure the MCOPY copy cost +directly with CodeGasMeasure over a pre-expanded memory (so no expansion is +charged), asserting the fork-derived `mcopy.gas_cost(fork)`. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_SLOT = 0x0 +# MSTORE at this offset grows memory to PREEXPANDED bytes, covering every +# (src, size) copy region below so the measured MCOPY never expands memory. +PREEXPAND_OFFSET = 0xAF00 +PREEXPANDED = PREEXPAND_OFFSET + 0x20 # 44832 bytes = 1401 words + +SRCS = [0x0, 0x1, 0x1F, 0x20] +SIZES = [0x0, 0x1, 0x1F, 0x20, 0x21, 0xAEDF, 0xAEE0, 0xAEE1] + @pytest.mark.ported_from( ["state_tests/Cancun/stEIP5656_MCOPY/MCOPY_copy_costFiller.yml"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="src0_size0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="src0_size0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="src0_size1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="src0_size1-g1", - ), - pytest.param( - 2, - 0, - 0, - id="src0_size31-g0", - ), - pytest.param( - 2, - 1, - 0, - id="src0_size31-g1", - ), - pytest.param( - 3, - 0, - 0, - id="src0_size32-g0", - ), - pytest.param( - 3, - 1, - 0, - id="src0_size32-g1", - ), - pytest.param( - 4, - 0, - 0, - id="src0_size33-g0", - ), - pytest.param( - 4, - 1, - 0, - id="src0_size33-g1", - ), - pytest.param( - 5, - 0, - 0, - id="src0_size44767-g0", - ), - pytest.param( - 5, - 1, - 0, - id="src0_size44767-g1", - ), - pytest.param( - 6, - 0, - 0, - id="src0_size44768-g0", - ), - pytest.param( - 6, - 1, - 0, - id="src0_size44768-g1", - ), - pytest.param( - 7, - 0, - 0, - id="src0_size44769-g0", - ), - pytest.param( - 7, - 1, - 0, - id="src0_size44769-g1", - ), - pytest.param( - 8, - 0, - 0, - id="src1_size0-g0", - ), - pytest.param( - 8, - 1, - 0, - id="src1_size0-g1", - ), - pytest.param( - 9, - 0, - 0, - id="src1_size1-g0", - ), - pytest.param( - 9, - 1, - 0, - id="src1_size1-g1", - ), - pytest.param( - 10, - 0, - 0, - id="src1_size31-g0", - ), - pytest.param( - 10, - 1, - 0, - id="src1_size31-g1", - ), - pytest.param( - 11, - 0, - 0, - id="src1_size32-g0", - ), - pytest.param( - 11, - 1, - 0, - id="src1_size32-g1", - ), - pytest.param( - 12, - 0, - 0, - id="src1_size33-g0", - ), - pytest.param( - 12, - 1, - 0, - id="src1_size33-g1", - ), - pytest.param( - 13, - 0, - 0, - id="src1_size44767-g0", - ), - pytest.param( - 13, - 1, - 0, - id="src1_size44767-g1", - ), - pytest.param( - 14, - 0, - 0, - id="src1_size44768-g0", - ), - pytest.param( - 14, - 1, - 0, - id="src1_size44768-g1", - ), - pytest.param( - 15, - 0, - 0, - id="src1_size44769-g0", - ), - pytest.param( - 15, - 1, - 0, - id="src1_size44769-g1", - ), - pytest.param( - 16, - 0, - 0, - id="src31_size0-g0", - ), - pytest.param( - 16, - 1, - 0, - id="src31_size0-g1", - ), - pytest.param( - 17, - 0, - 0, - id="src31_size1-g0", - ), - pytest.param( - 17, - 1, - 0, - id="src31_size1-g1", - ), - pytest.param( - 18, - 0, - 0, - id="src31_size31-g0", - ), - pytest.param( - 18, - 1, - 0, - id="src31_size31-g1", - ), - pytest.param( - 19, - 0, - 0, - id="src31_size32-g0", - ), - pytest.param( - 19, - 1, - 0, - id="src31_size32-g1", - ), - pytest.param( - 20, - 0, - 0, - id="src31_size33-g0", - ), - pytest.param( - 20, - 1, - 0, - id="src31_size33-g1", - ), - pytest.param( - 21, - 0, - 0, - id="src31_size44767-g0", - ), - pytest.param( - 21, - 1, - 0, - id="src31_size44767-g1", - ), - pytest.param( - 22, - 0, - 0, - id="src31_size44768-g0", - ), - pytest.param( - 22, - 1, - 0, - id="src31_size44768-g1", - ), - pytest.param( - 23, - 0, - 0, - id="src31_size44769-g0", - ), - pytest.param( - 23, - 1, - 0, - id="src31_size44769-g1", - ), - pytest.param( - 24, - 0, - 0, - id="src32_size0-g0", - ), - pytest.param( - 24, - 1, - 0, - id="src32_size0-g1", - ), - pytest.param( - 25, - 0, - 0, - id="src32_size1-g0", - ), - pytest.param( - 25, - 1, - 0, - id="src32_size1-g1", - ), - pytest.param( - 26, - 0, - 0, - id="src32_size31-g0", - ), - pytest.param( - 26, - 1, - 0, - id="src32_size31-g1", - ), - pytest.param( - 27, - 0, - 0, - id="src32_size32-g0", - ), - pytest.param( - 27, - 1, - 0, - id="src32_size32-g1", - ), - pytest.param( - 28, - 0, - 0, - id="src32_size33-g0", - ), - pytest.param( - 28, - 1, - 0, - id="src32_size33-g1", - ), - pytest.param( - 29, - 0, - 0, - id="src32_size44767-g0", - ), - pytest.param( - 29, - 1, - 0, - id="src32_size44767-g1", - ), - pytest.param( - 30, - 0, - 0, - id="src32_size44768-g0", - ), - pytest.param( - 30, - 1, - 0, - id="src32_size44768-g1", - ), - pytest.param( - 31, - 0, - 0, - id="src32_size44769-g0", - ), - pytest.param( - 31, - 1, - 0, - id="src32_size44769-g1", - ), - ], -) +@pytest.mark.parametrize("size", SIZES, ids=lambda s: f"size{s}") +@pytest.mark.parametrize("src", SRCS, ids=lambda s: f"src{s}") def test_mcopy_copy_cost( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + src: int, + size: int, ) -> None: - """Test cases for the cost of memory copy in the MCOPY instruction.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x3B9ACA00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1687174231, - prev_randao=0x20000, - base_fee_per_gas=10, + """Measure the MCOPY copy cost (linear in size, independent of source).""" + # Memory is pre-expanded past the largest copy region, so the measured + # MCOPY charges only its base + per-word copy cost, never expansion. + mcopy = Op.MCOPY( + dest_offset=0x0, + offset=src, + size=size, + data_size=size, + old_memory_size=PREEXPANDED, + new_memory_size=PREEXPANDED, ) - - # Source: yul - # shanghai optimise { - # function mcopy(dst, src, size) { verbatim_3i_0o(hex"5e", dst, src, size) } # noqa: E501 - # - # // Put a flag in storage indicating successful execution (will be reverted in case of OOG). # noqa: E501 - # sstore(0, 1) - # - # // Expand memory to cover memory expansion cost before MCOPY. - # // The test uses up to 1400 memory words. - # mstore(44800, 1) - # - # // MCOPY using src and size from CALLDATA to 0 destination. - # mcopy(0, calldataload(0), calldataload(32)) - # } - target = pre.deploy_contract( # noqa: F841 - code=Op.JUMP(pc=0xC) - + Op.JUMPDEST - + Op.MCOPY(dest_offset=Op.DUP3, offset=Op.DUP3, size=Op.DUP3) - + Op.POP * 3 - + Op.JUMP - + Op.JUMPDEST - + Op.SSTORE(key=Op.PUSH0, value=0x1) - + Op.MSTORE(offset=0xAF00, value=0x1) - + Op.PUSH1[0x22] - + Op.CALLDATALOAD(offset=0x20) - + Op.CALLDATALOAD(offset=Op.PUSH0) - + Op.PUSH0 - + Op.JUMP(pc=0x3) - + Op.JUMPDEST, - nonce=1, + contract = pre.deploy_contract( + code=Op.MSTORE(offset=PREEXPAND_OFFSET, value=0x1) + + CodeGasMeasure( + code=mcopy, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), ) - expect_entries_: list[dict] = [ - { - "indexes": { - "data": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - ], - "gas": 0, - "value": -1, - }, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 1})}, - }, - { - "indexes": { - "data": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 16, - 17, - 18, - 19, - 20, - 24, - 25, - 26, - 27, - 28, - ], - "gas": 1, - "value": -1, - }, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 1})}, - }, - { - "indexes": { - "data": [13, 14, 15, 21, 22, 23, 29, 30, 31], - "gas": 1, - "value": -1, - }, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 0})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + tx = Transaction(sender=pre.fund_eoa(), to=contract) - tx_data = [ - Hash(0x0) + Hash(0x0), - Hash(0x0) + Hash(0x1), - Hash(0x0) + Hash(0x1F), - Hash(0x0) + Hash(0x20), - Hash(0x0) + Hash(0x21), - Hash(0x0) + Hash(0xAEDF), - Hash(0x0) + Hash(0xAEE0), - Hash(0x0) + Hash(0xAEE1), - Hash(0x1) + Hash(0x0), - Hash(0x1) + Hash(0x1), - Hash(0x1) + Hash(0x1F), - Hash(0x1) + Hash(0x20), - Hash(0x1) + Hash(0x21), - Hash(0x1) + Hash(0xAEDF), - Hash(0x1) + Hash(0xAEE0), - Hash(0x1) + Hash(0xAEE1), - Hash(0x1F) + Hash(0x0), - Hash(0x1F) + Hash(0x1), - Hash(0x1F) + Hash(0x1F), - Hash(0x1F) + Hash(0x20), - Hash(0x1F) + Hash(0x21), - Hash(0x1F) + Hash(0xAEDF), - Hash(0x1F) + Hash(0xAEE0), - Hash(0x1F) + Hash(0xAEE1), - Hash(0x20) + Hash(0x0), - Hash(0x20) + Hash(0x1), - Hash(0x20) + Hash(0x1F), - Hash(0x20) + Hash(0x20), - Hash(0x20) + Hash(0x21), - Hash(0x20) + Hash(0xAEDF), - Hash(0x20) + Hash(0xAEE0), - Hash(0x20) + Hash(0xAEE1), - ] - tx_gas = [100000, 55697] - - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, - ) + post = {contract: Account(storage={GAS_SLOT: mcopy.gas_cost(fork)})} - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemoryTest/test_call_data_copy_offset.py b/tests/ported_static/stMemoryTest/test_call_data_copy_offset.py deleted file mode 100644 index 2cd7a467e4b..00000000000 --- a/tests/ported_static/stMemoryTest/test_call_data_copy_offset.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Test_call_data_copy_offset. - -Ported from: -state_tests/stMemoryTest/callDataCopyOffsetFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stMemoryTest/callDataCopyOffsetFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_call_data_copy_offset( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_call_data_copy_offset.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE) - contract_1 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (MSTORE 0x00 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) (CALLDATACOPY 0x00 0xffff 0x10) (SSTORE 0x00 (MLOAD 0x00)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - ) - + Op.CALLDATACOPY(dest_offset=0x0, offset=0xFFFF, size=0x10) - + Op.SSTORE(key=0x0, value=Op.MLOAD(offset=0x0)) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=1, - address=Address(0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE), # noqa: E501 - ) - # Source: yul - # berlin { mstore(0, 0x0123456789abcdef) pop(call(0xffff,0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee,0, 0,0x0f, 0,0)) } # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x123456789ABCDEF) - + Op.CALL( - gas=0xFFFF, - address=contract_0, - value=Op.DUP1, - args_offset=Op.DUP2, - args_size=0xF, - ret_offset=Op.DUP1, - ret_size=0x0, - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=1, - address=Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_1, - data=Bytes(""), - gas_limit=400000, - value=0x186A0, - ) - - post = { - contract_0: Account(storage={0: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF}) - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemoryTest/test_code_copy_offset.py b/tests/ported_static/stMemoryTest/test_code_copy_offset.py deleted file mode 100644 index 36b096523aa..00000000000 --- a/tests/ported_static/stMemoryTest/test_code_copy_offset.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Test_code_copy_offset. - -Ported from: -state_tests/stMemoryTest/codeCopyOffsetFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stMemoryTest/codeCopyOffsetFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_code_copy_offset( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_code_copy_offset.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0xB1F4CBC3A50042184425A6F9E996D0910F7BA879457CE5DAC5C71E498AD3C005 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (MSTORE 0x00 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) (CODECOPY 0x00 0xffff 0x10) (SSTORE 0x00 (MLOAD 0x00)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - ) - + Op.CODECOPY(dest_offset=0x0, offset=0xFFFF, size=0x10) - + Op.SSTORE(key=0x0, value=Op.MLOAD(offset=0x0)) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=1, - address=Address(0x27D16E1D3CC862149F1E7162E612635FCAEF9FF4), # noqa: E501 - ) - # Source: yul - # berlin { mstore(0, 0x0123456789abcdef) pop(call(0xffff, , 0, 0, 0x0f, 0, 0)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x123456789ABCDEF) - + Op.CALL( - gas=0xFFFF, - address=addr, - value=Op.DUP1, - args_offset=Op.DUP2, - args_size=0xF, - ret_offset=Op.DUP1, - ret_size=0x0, - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=1, - address=Address(0xAF89A7504341A87E1CFDFFD483A00A4688469B3D), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=400000, - value=0x186A0, - ) - - post = {addr: Account(storage={0: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF})} - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemoryTest/test_copy_offset.py b/tests/ported_static/stMemoryTest/test_copy_offset.py new file mode 100644 index 00000000000..26f32b060a9 --- /dev/null +++ b/tests/ported_static/stMemoryTest/test_copy_offset.py @@ -0,0 +1,77 @@ +""" +Test CODECOPY / CALLDATACOPY reading from an out-of-bounds source offset, +which yields zeros. + +Ported from: +state_tests/stMemoryTest/codeCopyOffsetFiller.json +state_tests/stMemoryTest/callDataCopyOffsetFiller.json + +@manually-enhanced: Do not overwrite. CODECOPY/CALLDATACOPY OOB-offset +zero-fill folded into one parametrize; delivery-CALL dropped; dynamic +addresses; nonzero tx calldata so a wrong in-bounds offset is observable. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + StateTestFiller, + Transaction, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +# Copy 16 bytes from a source offset far past the end of code/calldata; the +# out-of-bounds region reads as zeros, which overwrite memory bytes 0..15 +# (the most-significant half of the word MLOAD reads back), leaving only the +# low 128 bits of the pre-filled word set to 0xFF. +OOB_OFFSET = 0xFFFF +COPY_SIZE = 0x10 +EXPECTED = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +# Nonzero calldata makes the CALLDATACOPY arm discriminate a wrong (in-bounds) +# source offset from the correct out-of-bounds zero-fill; with empty calldata +# every offset would read zeros and the assertion would be vacuous. +TX_DATA = bytes(range(1, 33)) + + +@pytest.mark.ported_from( + [ + "state_tests/stMemoryTest/codeCopyOffsetFiller.json", + "state_tests/stMemoryTest/callDataCopyOffsetFiller.json", + ], +) +@pytest.mark.valid_from("Frontier") +@pytest.mark.parametrize( + "copy_op", + [ + pytest.param(Op.CODECOPY, id="code_copy_offset"), + pytest.param(Op.CALLDATACOPY, id="call_data_copy_offset"), + ], +) +def test_copy_offset( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + copy_op: Op, +) -> None: + """Copying from an out-of-bounds source offset yields zeros.""" + contract = pre.deploy_contract( + code=Op.MSTORE(offset=0x0, value=(1 << 256) - 1) + + copy_op(dest_offset=0x0, offset=OOB_OFFSET, size=COPY_SIZE) + + Op.SSTORE(key=0x0, value=Op.MLOAD(offset=0x0)) + + Op.STOP, + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + data=TX_DATA, + protected=fork.supports_protected_txs(), + ) + + post = {contract: Account(storage={0: EXPECTED})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value.py new file mode 100644 index 00000000000..27f734dbb65 --- /dev/null +++ b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value.py @@ -0,0 +1,185 @@ +""" +Measure the gas cost of CALL / CALLCODE / DELEGATECALL carrying non-zero +value to targets in various pre-states, using CodeGasMeasure. + +Ported from: +state_tests/stNonZeroCallsTest/NonZeroValue_CALLFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToEmpty_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToOneStorageKey_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODEFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToEmpty_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToOneStorageKey_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALLFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToEmpty_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToOneStorageKey_ParisFiller.json +state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToNonNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. Call gas via CodeGasMeasure; the call +success flag is stored inside the measured window (a wrongly failed call is +gas-identical to success against an empty callee, so gas alone cannot +discriminate). +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + CodeGasMeasure, + Fork, + StateTestFiller, + Transaction, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +CONTRACT_BALANCE = 100 +CALL_VALUE = 1 +EXISTING_BALANCE = 10 +NONZERO_BALANCE = 100 +FORWARDED_GAS = 0xEA60 +GAS_SLOT = 0x64 +SUCCESS_SLOT = 0x1 + + +@pytest.mark.ported_from( + [ + "state_tests/stNonZeroCallsTest/NonZeroValue_CALLFiller.json", + "state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToEmpty_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToOneStorageKey_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODEFiller.json", + "state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToEmpty_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToOneStorageKey_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALLFiller.json", + "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToEmpty_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToOneStorageKey_ParisFiller.json", # noqa: E501 + "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToNonNonZeroBalanceFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "opcode, target_kind", + [ + pytest.param(Op.CALL, "nonexistent", id="call"), + pytest.param(Op.CALL, "empty", id="call_to_empty"), + pytest.param(Op.CALL, "one_storage_key", id="call_to_one_storage_key"), + pytest.param(Op.CALLCODE, "nonexistent", id="callcode"), + pytest.param(Op.CALLCODE, "empty", id="callcode_to_empty"), + pytest.param( + Op.CALLCODE, "one_storage_key", id="callcode_to_one_storage_key" + ), + pytest.param(Op.DELEGATECALL, "nonexistent", id="delegatecall"), + pytest.param(Op.DELEGATECALL, "empty", id="delegatecall_to_empty"), + pytest.param( + Op.DELEGATECALL, + "one_storage_key", + id="delegatecall_to_one_storage_key", + ), + pytest.param( + Op.DELEGATECALL, + "nonzero_balance", + id="delegatecall_to_nonzero_balance", + ), + ], +) +def test_non_zero_value( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + opcode: Op, + target_kind: str, +) -> None: + """Measure call-family gas to a cold target of each pre-state.""" + transfers_value = opcode != Op.DELEGATECALL + + # Set up the target account in the requested pre-state. + if target_kind == "nonexistent": + call_target = pre.nonexistent_account() + target_balance = 0 + target_storage: dict = {} + elif target_kind == "one_storage_key": + target_balance = EXISTING_BALANCE + target_storage = {0x0: 0x1} + call_target = pre.deploy_contract( + code=b"", balance=target_balance, storage=target_storage + ) + else: + target_balance = ( + NONZERO_BALANCE + if target_kind == "nonzero_balance" + else EXISTING_BALANCE + ) + target_storage = {} + call_target = pre.fund_eoa(amount=target_balance) + + # Only a plain CALL forwards value to the target (and can create it); + # CALLCODE keeps value in the caller's context, DELEGATECALL has no value. + account_new = opcode == Op.CALL and target_kind == "nonexistent" + received = CALL_VALUE if opcode == Op.CALL else 0 + + if opcode == Op.DELEGATECALL: + call_code = Op.DELEGATECALL( + gas=FORWARDED_GAS, + address=call_target, + address_warm=False, + ) + else: + call_code = opcode( + gas=FORWARDED_GAS, + address=call_target, + value=CALL_VALUE, + address_warm=False, + value_transfer=True, + account_new=account_new, + ) + + # Store the call's success flag inside the measured window: a wrongly + # failed call is otherwise indistinguishable from a success into empty + # code (same gas, balances, and storage for CALLCODE/DELEGATECALL). + store_code = Op.SSTORE( + SUCCESS_SLOT, + call_code, + key_warm=False, + original_value=0, + new_value=1, + ) + + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=store_code, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), + balance=CONTRACT_BALANCE, + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + # A value-bearing call whose callee consumes nothing returns the stipend. + measured = store_code.gas_cost(fork) + if transfers_value: + measured -= fork.gas_costs().CALL_STIPEND + + if target_kind == "nonexistent": + target_account = ( + Account(balance=CALL_VALUE) if account_new else Account.NONEXISTENT + ) + else: + target_account = Account( + balance=target_balance + received, storage=target_storage + ) + + post = { + contract: Account( + storage={GAS_SLOT: measured, SUCCESS_SLOT: 1}, + balance=CONTRACT_BALANCE - received, + ), + call_target: target_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call.py deleted file mode 100644 index aae4b4903c5..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Test_non_zero_value_call. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALLFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stNonZeroCallsTest/NonZeroValue_CALLFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_call( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_call.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [0](GAS) [[1]] (CALL 60000 0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=0xEA60, - address=0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=100, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account(storage={1: 1, 100: 56435}, balance=99), - Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B): Account( - balance=1 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_empty_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_empty_paris.py deleted file mode 100644 index fcfb431e5b8..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_empty_paris.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Test_non_zero_value_call_to_empty_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToEmpty_ParisFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToEmpty_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_call_to_empty_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_call_to_empty_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - addr = pre.fund_eoa(amount=10) # noqa: F841 - # Source: lll - # { [0](GAS) [[1]] (CALL 60000 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=0xEA60, - address=addr, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=1000, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(balance=11), - target: Account(storage={1: 1, 100: 31435}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_one_storage_key_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_one_storage_key_paris.py deleted file mode 100644 index 76dd7cdb2ba..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_one_storage_key_paris.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Test_non_zero_value_call_to_one_storage_key_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToOneStorageKey_ParisFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToOneStorageKey_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_call_to_one_storage_key_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_call_to_one_storage_key_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - addr = Address(0x4757608F18B70777AE788DD4056EEED52F7AA68F) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - pre[addr] = Account(balance=10, storage={0: 1}) - # Source: lll - # { [0](GAS) [[1]] (CALL 60000 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=0xEA60, - address=0x4757608F18B70777AE788DD4056EEED52F7AA68F, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=1000, - nonce=0, - address=Address(0xF6029618CF51CA5236AFC14EAD1FBE0739573C23), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(storage={0: 1}, balance=11), - target: Account(storage={1: 1, 100: 31435}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode.py deleted file mode 100644 index 55b2e219e75..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Test_non_zero_value_callcode. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODEFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODEFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_callcode( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_callcode.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [0](GAS) [[1]] (CALLCODE 60000 0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xEA60, - address=0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=100, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account(storage={1: 1, 100: 31435}), - Address( - 0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B - ): Account.NONEXISTENT, - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_empty_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_empty_paris.py deleted file mode 100644 index 36f7f5f9c28..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_empty_paris.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Test_non_zero_value_callcode_to_empty_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToEmpty_ParisFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToEmpty_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_callcode_to_empty_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_callcode_to_empty_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - addr = pre.fund_eoa(amount=10) # noqa: F841 - # Source: lll - # { [0](GAS) [[1]] (CALLCODE 60000 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xEA60, - address=addr, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=100, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(storage={}, code=b"", balance=10, nonce=0), - target: Account(storage={1: 1, 100: 31435}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_one_storage_key_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_one_storage_key_paris.py deleted file mode 100644 index 24406f1746d..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_one_storage_key_paris.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Test_non_zero_value_callcode_to_one_storage_key_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToOneStorageKey_ParisFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToOneStorageKey_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_callcode_to_one_storage_key_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_callcode_to_one_storage_key_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - addr = Address(0x4757608F18B70777AE788DD4056EEED52F7AA68F) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - pre[addr] = Account(balance=10, storage={0: 1}) - # Source: lll - # { [0](GAS) [[1]] (CALLCODE 60000 1 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xEA60, - address=0x4757608F18B70777AE788DD4056EEED52F7AA68F, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=1000, - nonce=0, - address=Address(0xB7BB61C75BE691459CEF9A8FD7EC074933FA1D1F), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(storage={0: 1}, balance=10), - target: Account(storage={1: 1, 100: 31435}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall.py deleted file mode 100644 index ca0b9e66a79..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_non_zero_value_delegatecall. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALLFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALLFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_delegatecall( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_delegatecall.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [0](GAS) [[1]] (DELEGATECALL 60000 0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=0xEA60, - address=0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account(storage={1: 1, 100: 24732}), - Address( - 0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B - ): Account.NONEXISTENT, - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_empty_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_empty_paris.py deleted file mode 100644 index 1c2a832d499..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_empty_paris.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Test_non_zero_value_delegatecall_to_empty_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToEmpty_ParisFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToEmpty_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_delegatecall_to_empty_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_delegatecall_to_empty_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - addr = pre.fund_eoa(amount=10) # noqa: F841 - # Source: lll - # { [0](GAS) [[1]] (DELEGATECALL 60000 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=0xEA60, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(storage={}, code=b"", balance=10, nonce=0), - target: Account(storage={1: 1, 100: 24732}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_non_non_zero_balance.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_non_non_zero_balance.py deleted file mode 100644 index f4716bb2707..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_non_non_zero_balance.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Test_non_zero_value_delegatecall_to_non_non_zero_balance. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToNonNonZeroBalanceFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToNonNonZeroBalanceFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_delegatecall_to_non_non_zero_balance( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_delegatecall_to_non_non_zero_balance.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - addr = pre.fund_eoa(amount=100) # noqa: F841 - # Source: lll - # { [0](GAS) [[1]] (DELEGATECALL 60000 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=0xEA60, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(balance=100), - target: Account(storage={1: 1, 100: 24732}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_one_storage_key_paris.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_one_storage_key_paris.py deleted file mode 100644 index 59f545e4ace..00000000000 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_delegatecall_to_one_storage_key_paris.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Test_non_zero_value_delegatecall_to_one_storage_key_paris. - -Ported from: -state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToOneStorageKey_ParisFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stNonZeroCallsTest/NonZeroValue_DELEGATECALL_ToOneStorageKey_ParisFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_non_zero_value_delegatecall_to_one_storage_key_paris( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_non_zero_value_delegatecall_to_one_storage_key_paris.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - addr = Address(0x4757608F18B70777AE788DD4056EEED52F7AA68F) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - pre[addr] = Account(balance=10, storage={0: 1}) - # Source: lll - # { [0](GAS) [[1]] (DELEGATECALL 60000 0 0 0 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=0xEA60, - address=0x4757608F18B70777AE788DD4056EEED52F7AA68F, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - address=Address(0x9C1470E9F035F5D8F34D7C0FF2650F9F89DE43FE), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - addr: Account(storage={0: 1}, balance=10), - target: Account(storage={1: 1, 100: 24732}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSpecialTest/test_make_money.py b/tests/ported_static/stSpecialTest/test_make_money.py index 9f0678d1dc0..26fd61f9de2 100644 --- a/tests/ported_static/stSpecialTest/test_make_money.py +++ b/tests/ported_static/stSpecialTest/test_make_money.py @@ -1,17 +1,21 @@ """ -Test_make_money. +Verify value flows tx -> caller -> callee when the CALL asks for an absurdly +oversized gas amount (near 2^256), which the EIP-150 63/64 cap must clamp. Ported from: state_tests/stSpecialTest/makeMoneyFiller.json + +@manually-enhanced: Do not overwrite. Value flow tx->caller->callee expressed +as a relationship; dynamic addresses. The oversized CALL gas operand is the +original filler's point (clamping, not wrapping, of a near-2^256 ask) and +must stay explicit. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,70 +24,52 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 0xDE0B6B3A7640000 +TX_VALUE = 10 +CALL_VALUE = 0x17 +# The ported filler asks for nearly 2^256 gas: a client computing e.g. +# `requested + stipend` in wrapping arithmetic would forward almost nothing +# and OOG the callee, so the 63/64 clamp itself is under test. +OVERSIZED_GAS_ASK = 2**256 - 20 + @pytest.mark.ported_from( ["state_tests/stSpecialTest/makeMoneyFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("TangerineWhistle") def test_make_money( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_make_money.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x3B9ACA00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - # Source: raw - # 0x600160015532600255 - addr = pre.deploy_contract( # noqa: F841 + """Value forwards tx -> caller -> callee; the callee records ORIGIN.""" + # Callee stores a sentinel and the transaction origin, proving its code + # ran (not merely that value was transferred). + callee = pre.deploy_contract( code=Op.SSTORE(key=0x1, value=0x1) + Op.SSTORE(key=0x2, value=Op.ORIGIN), - balance=0xDE0B6B3A7640000, - nonce=0, + balance=INITIAL_BALANCE, ) - # Source: lll - # { (MSTORE 0 0x601080600c6000396000f20060003554156009570060203560003555) (CALL 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec 23 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x601080600C6000396000F20060003554156009570060203560003555, - ) - + Op.CALL( - gas=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC, # noqa: E501 - address=addr, - value=0x17, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + caller = pre.deploy_contract( + code=Op.CALL(gas=OVERSIZED_GAS_ASK, address=callee, value=CALL_VALUE) + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, + balance=INITIAL_BALANCE, ) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=228500, - value=10, + to=caller, + value=TX_VALUE, + protected=fork.supports_protected_txs(), ) post = { - target: Account(balance=0xDE0B6B3A763FFF3), - sender: Account(balance=0x3B8F6A16), - addr: Account(balance=0xDE0B6B3A7640017), + caller: Account(balance=INITIAL_BALANCE + TX_VALUE - CALL_VALUE), + callee: Account( + balance=INITIAL_BALANCE + CALL_VALUE, + storage={1: 1, 2: sender}, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_call_value_inherit_from_call.py b/tests/ported_static/stStaticCall/test_static_call_value_inherit_from_call.py index 930f7670065..7686c35c607 100644 --- a/tests/ported_static/stStaticCall/test_static_call_value_inherit_from_call.py +++ b/tests/ported_static/stStaticCall/test_static_call_value_inherit_from_call.py @@ -1,17 +1,21 @@ """ -Test_static_call_value_inherit_from_call. +Verify a STATICCALL callee observes CALLVALUE 0, never inheriting the +enclosing frame's non-zero value (delivered here by the transaction). Ported from: state_tests/stStaticCall/static_call_value_inherit_from_callFiller.json + +@manually-enhanced: Do not overwrite. STATICCALL sees CALLVALUE 0 (never +inherited from the enclosing value-bearing frame — the ported filler's +delivery CALL is collapsed into the transaction's own value); dynamic +addresses, gas forwarded via the default Op.GAS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,49 +24,33 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALL_VALUE = 0xA + @pytest.mark.ported_from( [ "state_tests/stStaticCall/static_call_value_inherit_from_callFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Byzantium") def test_static_call_value_inherit_from_call( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_static_call_value_inherit_from_call.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { (MSTORE 0 (CALLVALUE)) (RETURN 0 32) } - addr_2 = pre.deploy_contract( # noqa: F841 + """A STATICCALL callee observes CALLVALUE 0, not the caller's value.""" + # Callee returns whatever CALLVALUE it sees; under STATICCALL that is 0. + callee = pre.deploy_contract( code=Op.MSTORE(offset=0x0, value=Op.CALLVALUE) - + Op.RETURN(offset=0x0, size=0x20) - + Op.STOP, - balance=1, - nonce=0, + + Op.RETURN(offset=0x0, size=0x20), ) - # Source: lll - # { [[0]] (STATICCALL 50000 0 0 0 32) [[1]] (MLOAD 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 + # The tx delivers CALL_VALUE to this contract, so its own CALLVALUE is + # non-zero; the STATICCALL must still hand the callee a CALLVALUE of 0. + caller = pre.deploy_contract( code=Op.SSTORE( key=0x0, value=Op.STATICCALL( - gas=0xC350, - address=addr_2, + address=callee, args_offset=0x0, args_size=0x0, ret_offset=0x0, @@ -72,33 +60,16 @@ def test_static_call_value_inherit_from_call( + Op.SSTORE(key=0x1, value=Op.MLOAD(offset=0x0)) + Op.STOP, storage={1: 1}, - balance=1, - nonce=0, - ) - # Source: lll - # { (CALL 100000 10 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0x186A0, - address=addr, - value=0xA, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=460000, - value=10, + sender=pre.fund_eoa(), + to=caller, + value=CALL_VALUE, + protected=fork.supports_protected_txs(), ) - post = {addr: Account(storage={0: 1, 1: 0})} + # slot 0: STATICCALL succeeded (1). slot 1: the returned CALLVALUE (0). + post = {caller: Account(storage={0: 1, 1: 0})} - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From d3baec819ff3f83cefc29a69a2fa5b141ea36cb1 Mon Sep 17 00:00:00 2001 From: spencer Date: Wed, 29 Jul 2026 18:56:58 +0200 Subject: [PATCH 28/55] fix(test-execute): prune fork-less items before evaluating filter_combinations (#3259) --- .../plugins/execute/execute.py | 5 ++ .../plugins/execute/tests/test_execute.py | 53 +++++++++++++++++++ .../pytest_commands/plugins/forks/forks.py | 11 +++- .../forks/tests/test_covariant_markers.py | 19 +++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py index a10cb1ba5c1..679b05405d0 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py @@ -504,12 +504,17 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: ) +@pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems( items: List[pytest.Item], ) -> None: """ Remove transition tests and add the appropriate execute markers to the test. + + Runs tryfirst so that items collected without a fork parametrization + (tests not valid for the session's fork) are removed before other + plugins inspect item params, as in the filler plugin. """ items_for_removal = [] for i, item in enumerate(items): diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py index 2f606ad21c8..2b6c183747d 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py @@ -3,6 +3,8 @@ from types import SimpleNamespace from typing import Any +import pytest + from execution_testing.test_types.block_types import EnvironmentDefaults from ..execute import pytest_configure @@ -55,3 +57,54 @@ def test_pytest_configure_applies_explicit_transaction_gas_limit() -> None: assert config.engine_rpc_supported is False EnvironmentDefaults.gas_limit = original_gas_limit + + +EXECUTE_COLLECTION_PLUGINS = [ + "execution_testing.cli.pytest_commands.plugins.shared.execute_fill", + "execution_testing.cli.pytest_commands.plugins.shared.live_client_flags", + "execution_testing.cli.pytest_commands.plugins.execute.execute", + "execution_testing.cli.pytest_commands.plugins.forks.forks", +] + + +def test_forkless_items_pruned_before_filter_combinations( + pytester: pytest.Pytester, +) -> None: + """ + Collect a test that is not valid for the session's fork in execute mode. + + Such a test is collected without a fork parametrization and hence + without its covariant params; it must be pruned before the forks + plugin evaluates filter_combinations predicates, which would + otherwise fail with a TypeError and abort the whole session. + """ + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", [1, 2]) + @pytest.mark.with_all_refund_types() + @pytest.mark.filter_combinations( + lambda refund_type, a, **_: True, + reason="requires the covariant refund_type param", + ) + @pytest.mark.valid_from("Amsterdam") + def test_case(state_test, refund_type, a): + pass + """ + ) + plugin_args = [ + arg for name in EXECUTE_COLLECTION_PLUGINS for arg in ("-p", name) + ] + result = pytester.runpytest( + *plugin_args, + "--fork=Osaka", + "--collect-only", + "-q", + ) + output = "\n".join(result.outlines + result.errlines) + assert "INTERNALERROR" not in output + assert result.ret in ( + pytest.ExitCode.OK, + pytest.ExitCode.NO_TESTS_COLLECTED, + ) 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 d927c6b1637..df04e8d4a14 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 @@ -1584,7 +1584,16 @@ def _combination_filter_reason( f"{predicate!r}", returncode=pytest.ExitCode.USAGE_ERROR, ) - if not predicate(**params): + try: + keep = predicate(**params) + except TypeError as e: + pytest.exit( + f"filter_combinations predicate for " + f"'{item.nodeid}' cannot be called with the " + f"item's params: {e}", + returncode=pytest.ExitCode.USAGE_ERROR, + ) + if not keep: return marker.kwargs.get( "reason", "rejected by filter_combinations" ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_covariant_markers.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_covariant_markers.py index 3aa22dd1674..b58e9b5eb1e 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_covariant_markers.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_covariant_markers.py @@ -617,6 +617,25 @@ def test_case(state_test, a): "filter_combinations deselected all", id="filter_combinations_empty_set_error", ), + pytest.param( + """ + import pytest + + @pytest.mark.parametrize("a", [1, 2]) + @pytest.mark.filter_combinations( + lambda nonexistent_param, **_: True, + reason="predicate names a parameter that does not exist", + ) + @pytest.mark.valid_from("Cancun") + @pytest.mark.valid_until("Cancun") + @pytest.mark.state_test_only + def test_case(state_test, a): + pass + """, + {}, + "cannot be called with the item's params", + id="filter_combinations_bad_predicate_signature_error", + ), ], ) def test_filter_combinations( From 593078295684ff46f686607726d5a839d50b6205 Mon Sep 17 00:00:00 2001 From: spencer Date: Wed, 29 Jul 2026 19:47:14 +0200 Subject: [PATCH 29/55] refactor(spec-specs,tests): rename EIP-8037 regular gas to execution gas (#3238) --- .claude/commands/implement-eip.md | 4 +- .claude/commands/write-test.md | 2 +- docs/writing_tests/fork_methods.md | 2 +- docs/writing_tests/opcode_metadata.md | 2 +- .../plugins/execute/pre_alloc.py | 65 ++-- .../src/execution_testing/forks/base_fork.py | 18 +- .../forks/forks/eips/amsterdam/eip_2780.py | 14 +- .../forks/forks/eips/amsterdam/eip_8037.py | 38 +-- .../forks/forks/eips/amsterdam/eip_8038.py | 13 +- .../src/execution_testing/forks/gas_costs.py | 2 +- .../src/execution_testing/specs/base.py | 2 +- .../src/execution_testing/specs/blockchain.py | 2 +- .../tools/tests/test_iterating_bytecode.py | 68 ++-- .../tools/tools_code/generators.py | 88 +++--- .../src/execution_testing/vm/bytecode.py | 20 +- src/ethereum/forks/amsterdam/fork.py | 18 +- src/ethereum/forks/amsterdam/fork_types.py | 2 +- src/ethereum/forks/amsterdam/transactions.py | 42 +-- src/ethereum/forks/amsterdam/vm/__init__.py | 3 +- .../forks/amsterdam/vm/eoa_delegation.py | 2 +- src/ethereum/forks/amsterdam/vm/gas.py | 82 ++--- .../amsterdam/vm/instructions/storage.py | 4 +- .../forks/amsterdam/vm/instructions/system.py | 22 +- .../forks/amsterdam/vm/interpreter.py | 2 +- .../helpers.py | 6 +- .../test_authorization_charges.py | 6 +- .../test_authorization_oog.py | 122 +++---- .../test_calldata_floor.py | 2 +- .../test_fork_transition.py | 10 +- .../test_intrinsic_gas_boundary.py | 2 +- .../test_top_frame_charges.py | 86 ++--- .../test_value_moving_transactions.py | 6 +- .../test_value_moving_with_tx_delegation.py | 16 +- .../test_gas_accounting.py | 50 +-- .../test_block_access_lists.py | 2 +- .../test_block_access_lists_cross_index.py | 2 +- .../test_block_access_lists_eip7702.py | 8 +- .../test_block_access_lists_opcodes.py | 4 +- .../test_max_code_size.py | 2 +- .../test_additional_coverage.py | 14 +- .../test_floor_boundary_exact_balance.py | 4 +- .../test_floor_boundary_exact_balance.py | 4 +- .../spec.py | 6 +- .../test_block_2d_gas_accounting.py | 158 +++++----- .../test_state_gas_call.py | 52 +-- .../test_state_gas_calldata_floor.py | 58 ++-- .../test_state_gas_create.py | 268 ++++++++-------- .../test_state_gas_fork_transition.py | 4 +- .../test_state_gas_multi_block.py | 2 +- .../test_state_gas_ordering.py | 28 +- .../test_state_gas_pricing.py | 92 +++--- .../test_state_gas_reservoir.py | 163 +++++----- .../test_state_gas_selfdestruct.py | 58 ++-- .../test_state_gas_set_code.py | 298 +++++++++--------- .../test_state_gas_sstore.py | 96 +++--- .../test_access_list_gas.py | 6 +- .../test_call_gas.py | 54 ++-- .../test_create_gas.py | 70 ++-- .../test_fork_transition.py | 24 +- .../test_selfdestruct_gas.py | 76 ++--- .../test_set_code_auth_gas.py | 58 ++-- .../test_set_code_auth_refunds.py | 18 +- .../test_sstore_gas.py | 32 +- .../test_sstore_refunds.py | 20 +- .../test_transient_storage_regression.py | 2 +- .../compute/instruction/test_system.py | 4 +- tests/benchmark/helper/contract_factory.py | 22 +- .../stateful/bloatnet/test_sstore.py | 3 +- .../test_raw_create_gas.py | 4 +- 69 files changed, 1284 insertions(+), 1255 deletions(-) diff --git a/.claude/commands/implement-eip.md b/.claude/commands/implement-eip.md index 4465317f1e3..f42d9361f25 100644 --- a/.claude/commands/implement-eip.md +++ b/.claude/commands/implement-eip.md @@ -34,11 +34,11 @@ Each fork lives at `src/ethereum/forks//`. Explore the latest fork di ## Gas Handling -Recent forks meter two gas dimensions: regular gas and state gas (for durable state growth). Key rules: +Recent forks meter two gas dimensions: execution gas and state gas (for durable state growth). Key rules: 1. Gas constants and calculations go in `vm/gas.py`; a frame's mutable gas state lives on `Evm.gas_meter`. 2. Extend the named helper vocabulary (`charge_*`, `credit_*`, `restore_*`, `withhold_*`, ...) instead of doing gas arithmetic by hand at call sites; encode each helper's invariant as an assert. -3. State gas is charged by the frame whose opcode causes the creation, before the child's regular-gas share is withheld; the whole reservoir passes to the child. +3. State gas is charged by the frame whose opcode causes the creation, before the child's execution-gas share is withheld; the whole reservoir passes to the child. 4. A failing frame settles its own meter before returning, so parents incorporate children unconditionally. 5. Opcodes that touch state use labeled stages, with all charging before the operation: `GAS (STATE-INDEPENDENT)` → `STATE ACCESS (STATE-DEPENDENT GAS)` → `STATE GAS` → `CHILD GRANT` → `OPERATION`. Simple opcodes keep the bare `GAS` marker. `generic_call`/`generic_create` contain no pricing; they run the child lifecycle: `PREFLIGHT` → `DESTINATION ACCESS` → `CHILD GRANT` → `DISPATCH` → `OUTCOME`. 6. Avoid "frame" in gas identifiers (a future EIP claims the term); when a name diverges from the spec's variable name, cross-reference the spec name in the docstring. diff --git a/.claude/commands/write-test.md b/.claude/commands/write-test.md index bf6e8eee42a..1cda7e433ec 100644 --- a/.claude/commands/write-test.md +++ b/.claude/commands/write-test.md @@ -50,7 +50,7 @@ Conventions and patterns for writing consensus tests. Run this skill before writ Never hand-reconstruct a gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule duplicates the framework's own calculation and silently breaks when a future fork reprices. Instead: -- **Read the cost off the bytecode under test.** Set the relevant opcode metadata (`account_new`, `value_transfer`, `address_warm`, `key_warm`/`original_value`/`current_value`/`new_value`, `init_code_size`, `code_deposit_size`, `new_memory_size`, ...) and use `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior — e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **Read the cost off the bytecode under test.** Set the relevant opcode metadata (`account_new`, `value_transfer`, `address_warm`, `key_warm`/`original_value`/`current_value`/`new_value`, `init_code_size`, `code_deposit_size`, `new_memory_size`, ...) and use `bytecode.gas_cost(fork)` (execution + state), `.execution_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior — e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. - **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`; `fork.transaction_top_frame_state_gas(contract_creation=True)` for the created account's `NEW_ACCOUNT` (under EIP-2780 it is NOT part of the intrinsic — never subtract it from the intrinsic); `fork.transaction_data_floor_cost_calculator()`; `fork.call_value_stipend()`. - **A single bare opcode/schedule cost** (e.g. an account-access constant) comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)`. - **Fork-transition / cross-fork comparisons:** evaluate the same bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare `before` vs `after` costs — do not compare raw schedule constants. diff --git a/docs/writing_tests/fork_methods.md b/docs/writing_tests/fork_methods.md index 6f29d6f3a0c..6edd8abe4a8 100644 --- a/docs/writing_tests/fork_methods.md +++ b/docs/writing_tests/fork_methods.md @@ -117,7 +117,7 @@ fork.transaction_intrinsic_cost_calculator() # Returns a callable ``` !!! warning "Do not reconstruct expected gas from `gas_costs()` constants" - `fork.gas_costs()` exposes the raw schedule for framework internals. When a test needs an *expected* gas amount, derive it from a cost construct that tracks the live schedule (`bytecode.gas_cost(fork)` / `.regular_cost(fork)` / `.state_cost(fork)` / `.refund(fork)`, opcode metadata, the intrinsic/top-frame/data-floor calculators, `fork.call_value_stipend()`) rather than hand-summing constants — hand-built expectations silently break when a fork reprices. See [Opcode Metadata and Gas Calculations](opcode_metadata.md#do-not-hand-reconstruct-gas-from-constants). + `fork.gas_costs()` exposes the raw schedule for framework internals. When a test needs an *expected* gas amount, derive it from a cost construct that tracks the live schedule (`bytecode.gas_cost(fork)` / `.execution_cost(fork)` / `.state_cost(fork)` / `.refund(fork)`, opcode metadata, the intrinsic/top-frame/data-floor calculators, `fork.call_value_stipend()`) rather than hand-summing constants — hand-built expectations silently break when a fork reprices. See [Opcode Metadata and Gas Calculations](opcode_metadata.md#do-not-hand-reconstruct-gas-from-constants). ### Transaction Types diff --git a/docs/writing_tests/opcode_metadata.md b/docs/writing_tests/opcode_metadata.md index 5149fb9ea0b..49753e7eb23 100644 --- a/docs/writing_tests/opcode_metadata.md +++ b/docs/writing_tests/opcode_metadata.md @@ -13,7 +13,7 @@ The execution testing package provides capabilities to calculate gas costs and r Never build an expected gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule by hand duplicates the framework's own calculation and silently breaks when a future fork reprices or restructures a cost. Always derive the expectation from a framework construct that tracks the live schedule: -- **The bytecode/opcode under test:** set the relevant metadata (see below) and read `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior, e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **The bytecode/opcode under test:** set the relevant metadata (see below) and read `bytecode.gas_cost(fork)` (execution + state), `.execution_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior, e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. - **A single bare opcode/schedule cost** comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)` yields the cold account-access cost with no operand pushes. - **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`, `fork.transaction_top_frame_state_gas(contract_creation=True)` (the created account's new-account state gas — on recent forks it is charged at the top frame, *not* in the intrinsic, so never subtract it from the intrinsic), `fork.transaction_data_floor_cost_calculator()`, and `fork.call_value_stipend()`. - **Cross-fork / fork-transition comparisons:** evaluate the *same* bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare the resulting costs — do not compare raw schedule constants. diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py index 5f248d6811f..fccb7ca45cd 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py @@ -232,12 +232,12 @@ def _compute_deploy_gas_limit( storage_slots: int = 0, ) -> Tuple[int, int]: """ - Compute the deploy transaction gas limit, returning both the regular - gas portion bound by the EIP 7825 cap and the total regular plus + Compute the deploy transaction gas limit, returning both the execution + gas portion bound by the EIP 7825 cap and the total execution plus state gas used as the transaction gas field. Under EIP 8037 the cap - binds only the regular portion while state gas comes from the block + binds only the execution portion while state gas comes from the block reservoir and may push the total above the cap, and before Amsterdam - the state gas is zero so the total equals the regular gas. The regular + the state gas is zero so the total equals the execution gas. The execution portion is doubled as a safety buffer since gas estimation is approximate while the state portion is exact. """ @@ -247,44 +247,45 @@ def _compute_deploy_gas_limit( sstore = Op.SSTORE(new_value=1) sstore_state_gas = sstore.state_cost(fork) - sstore_regular_gas = sstore.gas_cost(fork) - sstore_state_gas + sstore_execution_gas = sstore.gas_cost(fork) - sstore_state_gas - # The intrinsic cost is now regular-only: the created account's + # The intrinsic cost is now execution-only: the created account's # NEW_ACCOUNT state gas is charged at the top frame, not folded in. - intrinsic_regular_gas = intrinsic_gas_calculator( + intrinsic_execution_gas = intrinsic_gas_calculator( calldata=initcode, contract_creation=True ) - # Regular portion, bound by the gas cap. - regular_gas = intrinsic_regular_gas + # Execution portion, bound by the gas cap. + execution_gas = intrinsic_execution_gas if fork.state_gas_reservoir_enabled(): - regular_gas += gas_costs.OPCODE_KECCAK256_PER_WORD * ( + execution_gas += gas_costs.OPCODE_KECCAK256_PER_WORD * ( (deploy_code_size + 31) // 32 ) else: - regular_gas += deploy_code_size * gas_costs.CODE_DEPOSIT_PER_BYTE - regular_gas += memory_expansion_gas_calculator( + execution_gas += deploy_code_size * gas_costs.CODE_DEPOSIT_PER_BYTE + execution_gas += memory_expansion_gas_calculator( new_bytes=len(bytes(initcode)) ) - regular_gas += storage_slots * sstore_regular_gas + execution_gas += storage_slots * sstore_execution_gas # Double as a safety buffer since gas estimation is approximate. The buffer # must not, by itself, push a contract that genuinely deploys within the - # EIP-7825 regular-gas cap over it: when the unbuffered estimate still fits + # EIP-7825 execution-gas cap over it: when the unbuffered estimate + # still fits # the cap, clamp the limit to the cap instead. The deploy then runs with a - # cap-sized regular limit and consumes only its (smaller) actual gas. + # cap-sized execution limit and consumes only its (smaller) actual gas. # Only a contract whose unbuffered estimate exceeds the cap is truly # undeployable (the caller raises on that). - buffered_regular_gas = regular_gas * 2 + buffered_execution_gas = execution_gas * 2 tx_gas_limit_cap = fork.transaction_gas_limit_cap() if ( tx_gas_limit_cap is not None - and buffered_regular_gas > tx_gas_limit_cap - and regular_gas <= tx_gas_limit_cap + and buffered_execution_gas > tx_gas_limit_cap + and execution_gas <= tx_gas_limit_cap ): - regular_gas = tx_gas_limit_cap + execution_gas = tx_gas_limit_cap else: - regular_gas = buffered_regular_gas + execution_gas = buffered_execution_gas # State portion, from the block reservoir. The created account's # NEW_ACCOUNT is charged at the top frame for create transactions @@ -293,8 +294,8 @@ def _compute_deploy_gas_limit( state_gas += fork.transaction_top_frame_state_gas(contract_creation=True) state_gas += storage_slots * sstore_state_gas - deploy_gas_limit = regular_gas + state_gas - return regular_gas, deploy_gas_limit + deploy_gas_limit = execution_gas + state_gas + return execution_gas, deploy_gas_limit class Alloc(SharedAlloc): @@ -426,18 +427,18 @@ def _deterministic_deploy_contract( raise ValueError( f"initcode too large {len(initcode)} > {max_initcode_size}" ) - regular_gas, deploy_gas_limit = _compute_deploy_gas_limit( + execution_gas, deploy_gas_limit = _compute_deploy_gas_limit( fork, deploy_code_size=len(deploy_code), initcode=initcode, ) # Per EIP-8037, the per-tx 2^24 cap (EIP-7825) binds only the - # regular-gas portion; state gas is drawn from the block reservoir. + # execution-gas portion; state gas is drawn from the block reservoir. tx_gas_limit_cap = fork.transaction_gas_limit_cap() - if tx_gas_limit_cap and regular_gas > tx_gas_limit_cap: + if tx_gas_limit_cap and execution_gas > tx_gas_limit_cap: raise ValueError( - f"deterministic deploy regular gas exceeds the transaction " - f"gas limit cap: {regular_gas} > {tx_gas_limit_cap}" + f"deterministic deploy execution gas exceeds the transaction " + f"gas limit cap: {execution_gas} > {tx_gas_limit_cap}" ) # Defer the on-chain check; the deploy tx (if needed) and the @@ -541,19 +542,19 @@ def _deploy_contract( f"initcode too large {initcode_len} > {max_initcode_size}" ) - regular_gas, deploy_gas_limit = _compute_deploy_gas_limit( + execution_gas, deploy_gas_limit = _compute_deploy_gas_limit( fork, deploy_code_size=len(code), initcode=prepared_initcode, storage_slots=len(storage.root), ) # Per EIP-8037, the per-tx 2^24 cap (EIP-7825) binds only the - # regular-gas portion; state gas is drawn from the block reservoir. + # execution-gas portion; state gas is drawn from the block reservoir. tx_gas_limit_cap = fork.transaction_gas_limit_cap() - if tx_gas_limit_cap and regular_gas > tx_gas_limit_cap: + if tx_gas_limit_cap and execution_gas > tx_gas_limit_cap: raise ValueError( - f"deploy regular gas exceeds the transaction gas limit cap: " - f"{regular_gas} > {tx_gas_limit_cap}" + f"deploy execution gas exceeds the transaction gas limit cap: " + f"{execution_gas} > {tx_gas_limit_cap}" ) deploy_tx = self._add_pending_tx( diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f24bc228d77..08305f5eefe 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -177,11 +177,11 @@ class AuthorizationGasInfo(Protocol): class TopFrameGasCalculator(Protocol): """ - A protocol to calculate the additional regular gas charged at the + A protocol to calculate the additional execution gas charged at the top-level transaction frame, after intrinsic gas is deducted but before EVM execution begins. - Returns only the regular-gas portion of the post-intrinsic + Returns only the execution-gas portion of the post-intrinsic state-aware preparation (e.g. the delegated-recipient access charge). The state-gas portion is exposed separately by ``BaseFork.transaction_top_frame_state_gas`` so tests can model the @@ -201,7 +201,7 @@ def __call__( authorizations: Sequence[AuthorizationGasInfo] = (), ) -> int: """ - Return the regular gas consumed by top-frame preparation for a + Return the execution gas consumed by top-frame preparation for a transaction at this fork. Args: @@ -217,9 +217,9 @@ def __call__( target is already warm, charging warm rather than cold access. authorizations: The transaction's EIP-7702 authorizations; - each contributes its top-frame regular gas. + each contributes its top-frame execution gas. - Returns: Regular gas added by top-frame preparation. + Returns: Execution gas added by top-frame preparation. """ pass @@ -782,7 +782,7 @@ def transaction_top_frame_gas_calculator( cls, ) -> TopFrameGasCalculator: """ - Return a callable that calculates the additional regular gas + Return a callable that calculates the additional execution gas charged at the top-level transaction frame, after intrinsic gas is deducted but before EVM execution begins. @@ -818,7 +818,7 @@ def transaction_top_frame_state_gas( frame, after intrinsic gas is deducted but before EVM execution begins. Companion to ``transaction_top_frame_gas_calculator``; tests targeting the spillover boundary feed this through - ``oog_budget_lift`` to get the equivalent regular-gas budget. + ``oog_budget_lift`` to get the equivalent execution-gas budget. Defaults to 0 for forks that do not perform such post-intrinsic preparation. @@ -1012,9 +1012,9 @@ def oog_budget_lift( deploy_code_size: int = 0, ) -> int: """ - Return the extra regular gas an out of gas budget needs to + Return the extra execution gas an out of gas budget needs to stop at the same point on this fork: the state gas EIP-8037 - spills into regular gas for the given SSTOREs, CREATEs, and + spills into execution gas for the given SSTOREs, CREATEs, and deployed bytes. Zero before EIP-8037, so no fork guard needed. """ return ( diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py index c001d74e6e4..13342b15abd 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py @@ -48,7 +48,7 @@ def transaction_data_floor_cost_calculator( cls, ) -> TransactionDataFloorCostCalculator: """ - Anchor the calldata floor on the decomposed regular-gas intrinsic + Anchor the calldata floor on the decomposed execution-gas intrinsic base (EIP-2780). The inherited floor base is ``TX_BASE`` alone; add the recipient @@ -71,7 +71,7 @@ def fn( floor = super_fn(data=data, access_list=access_list) is_self_transfer = recipient_type == RecipientType.SELF if contract_creation: - # CREATE_ACCESS regular gas; TX_CREATE folds in the + # CREATE_ACCESS execution gas; TX_CREATE folds in the # NEW_ACCOUNT state gas, which the floor excludes. floor += gas_costs.TX_CREATE - gas_costs.NEW_ACCOUNT elif not is_self_transfer: @@ -170,7 +170,7 @@ def transaction_top_frame_gas_calculator( cls, ) -> TopFrameGasCalculator: """ - Return the additional regular gas charged at the top-level + Return the additional execution gas charged at the top-level transaction frame, after intrinsic gas is deducted but before the EVM dispatches. @@ -197,17 +197,17 @@ def fn( if contract_creation: return 0 - regular = 0 + execution = 0 if recipient_type == RecipientType.DELEGATION_7702: - regular += ( + execution += ( gas_costs.WARM_ACCESS if delegation_warm else gas_costs.COLD_ACCOUNT_ACCESS ) for auth in authorizations: if auth.first_write: - regular += gas_costs.ACCOUNT_WRITE - return regular + execution += gas_costs.ACCOUNT_WRITE + return execution return fn diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py index f3428f7ec10..030b8dd4db8 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py @@ -118,11 +118,11 @@ def fn(opcode: OpcodeBase) -> int: gas_cost_or_calculator = opcode_gas_map[opcode] if callable(gas_cost_or_calculator): - regular_gas = gas_cost_or_calculator(opcode) + execution_gas = gas_cost_or_calculator(opcode) else: - regular_gas = gas_cost_or_calculator + execution_gas = gas_cost_or_calculator - return regular_gas + opcode_state_calculator(opcode) + return execution_gas + opcode_state_calculator(opcode) return fn @@ -189,11 +189,11 @@ def fn(opcode: OpcodeBase) -> int: refund_or_calculator = opcode_refund_map[opcode] if callable(refund_or_calculator): - regular_refund = refund_or_calculator(opcode) + execution_refund = refund_or_calculator(opcode) else: - regular_refund = refund_or_calculator + execution_refund = refund_or_calculator - return regular_refund + state_refund + return execution_refund + state_refund return fn @@ -324,7 +324,7 @@ def _calculate_return_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular RETURN gas cost: the code hash gas + Calculate the execution RETURN gas cost: the code hash gas (keccak256 of the deployed bytecode). The per byte code deposit cost moves to state gas, returned by `_calculate_return_state_gas`. """ @@ -361,8 +361,8 @@ def _calculate_create_state_gas( Calculate the CREATE and CREATE2 state gas cost, which is `NEW_ACCOUNT` (if the account did not exist before). Before EIP-8037 this was folded into `OPCODE_CREATE_BASE`. Under - EIP-8037 it is exposed here so that `OPCODE_CREATE_BASE` stays regular - only and matches the spec EVM constant. + EIP-8037 it is exposed here so that `OPCODE_CREATE_BASE` stays + execution-only and matches the spec EVM constant. """ if opcode.metadata["account_new"]: return gas_costs.NEW_ACCOUNT @@ -375,9 +375,9 @@ def _calculate_selfdestruct_state_gas( """ Calculate the SELFDESTRUCT state gas cost: `NEW_ACCOUNT` when a positive balance funds a new account. Before EIP-8037 this was - folded into the regular SELFDESTRUCT cost; under EIP-8037 it is + folded into the execution SELFDESTRUCT cost; under EIP-8037 it is exposed here as state gas (mirroring `_calculate_create_state_gas`) - so the regular cost matches the spec EVM + so the execution cost matches the spec EVM (`OPCODE_SELFDESTRUCT_BASE` + account access + the EIP-8038 `ACCOUNT_WRITE` surcharge). """ @@ -390,14 +390,14 @@ def _calculate_selfdestruct_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular SELFDESTRUCT gas cost. The Frontier base - calculation folds `NEW_ACCOUNT` into the regular cost when a + Calculate the execution SELFDESTRUCT gas cost. The Frontier base + calculation folds `NEW_ACCOUNT` into the execution cost when a positive balance funds a new account; EIP-8038 (the mixin between the base and EIP-8037 in the MRO) adds only the `ACCOUNT_WRITE` surcharge. EIP-8037 moves that funding cost to the state-gas dimension (see `_calculate_selfdestruct_state_gas`), so this - subtracts the `NEW_ACCOUNT` term back out of the inherited regular - cost; the EIP-8038 `ACCOUNT_WRITE` surcharge stays in regular gas. + subtracts the `NEW_ACCOUNT` term back out of the inherited execution + cost; the EIP-8038 `ACCOUNT_WRITE` surcharge stays in execution gas. """ gas_cost = super()._calculate_selfdestruct_gas(opcode, gas_costs) if opcode.metadata["account_new"]: @@ -411,7 +411,7 @@ def _calculate_call_state_gas( """ Calculate the CALL state gas cost: `NEW_ACCOUNT` when a value transfer funds a new account. Before EIP-8037 this was folded - into the regular CALL cost (EIP-161); under EIP-8037 it is + into the execution CALL cost (EIP-161); under EIP-8037 it is exposed here as state gas, mirroring `_calculate_selfdestruct_state_gas`. """ @@ -426,12 +426,12 @@ def _calculate_call_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular CALL gas cost. The EIP-161 base - calculation folds `NEW_ACCOUNT` into the regular cost when a + Calculate the execution CALL gas cost. The EIP-161 base + calculation folds `NEW_ACCOUNT` into the execution cost when a value transfer funds a new account; EIP-8037 moves that charge to the state-gas dimension (see `_calculate_call_state_gas`), so this subtracts the `NEW_ACCOUNT` term back out of the - inherited regular cost. + inherited execution cost. """ gas_cost = super()._calculate_call_gas(opcode, gas_costs) metadata = opcode.metadata diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py index 0e120edbc91..26beca8e120 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py @@ -53,7 +53,7 @@ def gas_costs(cls) -> GasCosts: account_write = 8_000 create_access = 11_000 # ecRecover stays PRECOMPILE_ECRECOVER (3000) until EIP-7904 lands. - regular_per_auth_base_cost = ( + execution_per_auth_base_cost = ( 1_616 + 3_000 + cold_account_access + 2 * warm_access ) @@ -73,8 +73,9 @@ def gas_costs(cls) -> GasCosts: STORAGE_SET=storage_write, OPCODE_CREATE_BASE=create_access, TX_CREATE=create_access, - AUTH_PER_EMPTY_ACCOUNT=account_write + regular_per_auth_base_cost, - REGULAR_PER_AUTH_BASE_COST=regular_per_auth_base_cost, + AUTH_PER_EMPTY_ACCOUNT=account_write + + execution_per_auth_base_cost, + REGULAR_PER_AUTH_BASE_COST=execution_per_auth_base_cost, ) @classmethod @@ -109,7 +110,7 @@ def _calculate_selfdestruct_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular SELFDESTRUCT gas cost. EIP-8038 adds + Calculate the execution SELFDESTRUCT gas cost. EIP-8038 adds `ACCOUNT_WRITE` when a positive balance is sent to an empty account, on top of the inherited cost (where `NEW_ACCOUNT` holds the EIP-8037 state-gas portion). @@ -126,7 +127,7 @@ def _calculate_sstore_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular SSTORE gas cost. The state portion is + Calculate the execution SSTORE gas cost. The state portion is returned separately by `_calculate_sstore_state_gas`. Under EIP-8038 the access cost (`COLD_STORAGE_ACCESS` when cold, else `WARM_SLOAD`) is always charged, and a first-time change to the @@ -159,7 +160,7 @@ def _calculate_sstore_refund( cls, opcode: OpcodeBase, gas_costs: GasCosts ) -> int: """ - Calculate the regular SSTORE gas refund. The state portion is + Calculate the execution SSTORE gas refund. The state portion is returned separately by `_calculate_sstore_state_refund`. """ metadata = opcode.metadata diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index 81e660fdaa5..261540cd590 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -49,7 +49,7 @@ class GasCosts: # State gas for writing a net-new EIP-7702 delegation indicator; # 0 before the state-creation repricing introduces it. AUTH_BASE: int = 0 - # State-independent regular gas charged per EIP-7702 authorization + # State-independent execution gas charged per EIP-7702 authorization # tuple; 0 before the state-access repricing introduces it. REGULAR_PER_AUTH_BASE_COST: int = 0 diff --git a/packages/testing/src/execution_testing/specs/base.py b/packages/testing/src/execution_testing/specs/base.py index 27278494403..bf0585396f8 100644 --- a/packages/testing/src/execution_testing/specs/base.py +++ b/packages/testing/src/execution_testing/specs/base.py @@ -303,7 +303,7 @@ def validate_benchmark_gas( ) # No single gas dimension may exceed the block gas limit. The # block-header gas is the max across dimensions; the combined - # regular+state gas may exceed the target under EIP-8037, so the + # execution+state gas may exceed the target under EIP-8037, so the # ceiling is checked against the header value when available. block_gas_used = ( benchmark_block_gas_used diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 72145a35e4f..ec3bd0d88e2 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -481,7 +481,7 @@ def block_gas_used(self) -> int: Return the block-header gas used. Under EIP-8037 this is the maximum across the independent gas - dimensions (regular vs state), i.e. the value that counts against the + dimensions (execution vs state), i.e. the value that counts against the block gas limit, as opposed to ``cumulative_gas_used`` which is their combined sum. """ diff --git a/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py b/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py index a0d3ac643cd..22e7144db5d 100644 --- a/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py +++ b/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py @@ -99,7 +99,7 @@ def test_iterating_bytecode_gas_cost( iterating_bytecode: IteratingBytecode, iterations: int, expected_cost: int ) -> None: """Test the gas cost calculating function of an iterating bytecode.""" - calculated_cost = iterating_bytecode.regular_gas_cost_by_iteration_count( + calculated_cost = iterating_bytecode.execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=iterations ) assert calculated_cost == expected_cost, ( @@ -142,15 +142,15 @@ def test_iterating_subcall_reserve_includes_state_gas() -> None: """ The 63/64 reserve covers the subcall's state gas too: once the state reservoir is exhausted, the child pays its state charges (e.g. the - EIP-8037 per-byte code deposit) from forwarded regular gas. + EIP-8037 per-byte code deposit) from forwarded execution gas. """ - # Initcode depositing 2 bytes: tiny regular cost, 2 * 1530 state gas. + # Initcode depositing 2 bytes: tiny execution cost, 2 * 1530 state gas. initcode = Op.RETURN(0, 2, code_deposit_size=2) bytecode = IteratingBytecode( iterating=Op.CREATE2(offset=0, size=2, salt=0), iterating_subcall=initcode, ) - combined = initcode.regular_cost(fork=Amsterdam) + initcode.state_cost( + combined = initcode.execution_cost(fork=Amsterdam) + initcode.state_cost( fork=Amsterdam ) assert initcode.state_cost(fork=Amsterdam) == 2 * 1530 @@ -172,7 +172,7 @@ def test_with_fixed_iteration_count() -> None: assert fixed.iteration_count == 10 assert fixed.gas_cost( Osaka - ) == iterating_bytecode.regular_gas_cost_by_iteration_count( + ) == iterating_bytecode.execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=10 ) @@ -184,13 +184,13 @@ def test_tx_gas_cost_by_iteration_count() -> None: ) intrinsic_gas_cost_calc = Osaka.transaction_intrinsic_cost_calculator() - tx_gas = bytecode.tx_regular_gas_cost_by_iteration_count( + tx_gas = bytecode.tx_execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=5, ) expected = ( - bytecode.regular_gas_cost_by_iteration_count( + bytecode.execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=5 ) + intrinsic_gas_cost_calc() @@ -198,12 +198,12 @@ def test_tx_gas_cost_by_iteration_count() -> None: assert tx_gas == expected # With calldata - tx_gas = bytecode.tx_regular_gas_cost_by_iteration_count( + tx_gas = bytecode.tx_execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=5, calldata=b"hello", ) - expected = bytecode.regular_gas_cost_by_iteration_count( + expected = bytecode.execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=5 ) + intrinsic_gas_cost_calc( calldata=b"hello", return_cost_deducted_prior_execution=True @@ -223,13 +223,13 @@ def test_tx_gas_limit_by_iteration_count() -> None: iteration_count=5, include_state_gas_reservoir=True, ) - tx_gas_cost = bytecode.tx_regular_gas_cost_by_iteration_count( + tx_gas_cost = bytecode.tx_execution_gas_cost_by_iteration_count( fork=Osaka, iteration_count=5, ) reserve = bytecode.iterating_subcall_reserve(fork=Osaka) - # Osaka has no state-gas reservoir, so the limit is regular + reserve. + # Osaka has no state-gas reservoir, so the limit is execution + reserve. assert tx_gas_limit == tx_gas_cost + reserve @@ -393,12 +393,12 @@ def test_tx_gas_limit_includes_state_gas_reservoir() -> None: """ Under EIP-8037 ``include_state_gas_reservoir`` adds the per-iteration state gas to the transaction gas limit; otherwise the limit is the - regular gas plus the 63/64 subcall reserve only. + execution gas plus the 63/64 subcall reserve only. """ # SSTORE of a fresh slot from zero charges STORAGE_SET state gas. bytecode = IteratingBytecode(iterating=Op.SSTORE(0, 1)) - regular = bytecode.tx_regular_gas_cost_by_iteration_count( + execution = bytecode.tx_execution_gas_cost_by_iteration_count( fork=Amsterdam, iteration_count=5 ) state = bytecode.state_gas_cost_by_iteration_count( @@ -418,13 +418,13 @@ def test_tx_gas_limit_includes_state_gas_reservoir() -> None: include_state_gas_reservoir=True, ) - assert without_state == regular + reserve - assert with_state == regular + reserve + state + assert without_state == execution + reserve + assert with_state == execution + reserve + state -def test_state_reservoir_lets_tx_gas_exceed_regular_gas_limit_cap() -> None: +def test_state_reservoir_lets_tx_gas_exceed_execution_gas_limit_cap() -> None: """ - Under EIP-8037 the EIP-7825 transaction gas limit cap binds regular gas + Under EIP-8037 the EIP-7825 transaction gas limit cap binds execution gas only. A state-heavy transaction can therefore pack more iterations than that cap alone would allow, because its state gas draws from a separate reservoir and the combined ``tx.gas`` grows past the cap. @@ -433,18 +433,18 @@ def test_state_reservoir_lets_tx_gas_exceed_regular_gas_limit_cap() -> None: fork = CustomAmsterdam.with_tx_gas_limit_cap(cap) bytecode = IteratingBytecode(iterating=Op.SSTORE(0, 1)) - total_iterations = (cap // Op.SSTORE(0, 1).regular_cost(fork=fork)) - 1 + total_iterations = (cap // Op.SSTORE(0, 1).execution_cost(fork=fork)) - 1 counts = list( bytecode.tx_iterations_by_total_iteration_count( fork=fork, total_iterations=total_iterations ) ) - # Regular gas stays under the cap, so all iterations fit in one tx even - # though their combined (regular + state) gas far exceeds the cap. + # Execution gas stays under the cap, so all iterations fit in one tx even + # though their combined (execution + state) gas far exceeds the cap. assert counts == [total_iterations] - regular = bytecode.tx_regular_gas_cost_by_iteration_count( + execution = bytecode.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=total_iterations ) combined = bytecode.tx_gas_limit_by_iteration_count( @@ -452,7 +452,7 @@ def test_state_reservoir_lets_tx_gas_exceed_regular_gas_limit_cap() -> None: iteration_count=total_iterations, include_state_gas_reservoir=True, ) - assert regular <= cap, "regular gas must respect the EIP-7825 cap" + assert execution <= cap, "execution gas must respect the EIP-7825 cap" assert combined > cap, ( "combined tx.gas exceeds the cap via state reservoir" ) @@ -471,12 +471,12 @@ def test_transaction_with_cost_billing_by_outcome( ) -> None: """ Billed gas and block-header contribution follow the expected outcome: - combined regular + state on success, regular only on revert (state gas + combined execution + state on success, execution only on revert (state gas is refunded), and the whole gas limit on an exceptional halt. """ tx = TransactionWithCost( gas_limit=150_000, - regular_cost=60_000, + execution_cost=60_000, state_cost=40_000, outcome=outcome, ) @@ -487,21 +487,21 @@ def test_transaction_with_cost_billing_by_outcome( def test_tx_iterations_by_gas_limit_outcome_packing() -> None: """ The block budget is consumed according to the expected outcome: the - max-dimension gas on success, the regular gas only on revert, and the + max-dimension gas on success, the execution gas only on revert, and the whole gas limit (including the subcall reserve, without any state allowance) on out-of-gas. """ budget = 1_000_000 fork = CustomAmsterdam.with_tx_gas_limit_cap(16_777_216) - # SSTORE of a fresh slot from zero: state gas dominates regular gas. + # SSTORE of a fresh slot from zero: state gas dominates execution gas. bytecode = IteratingBytecode( iterating=Op.SSTORE(0, 1), iterating_subcall=6300 ) reserve = bytecode.iterating_subcall_reserve(fork=fork) assert reserve > 0 - def regular(iterations: int) -> int: - return bytecode.tx_regular_gas_cost_by_iteration_count( + def execution(iterations: int) -> int: + return bytecode.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iterations ) @@ -525,17 +525,17 @@ def state(iterations: int) -> int: ) # Success packing is bound by the dominant (state) dimension. - assert sum(max(regular(i), state(i)) for i in success) <= budget + assert sum(max(execution(i), state(i)) for i in success) <= budget assert state(sum(success) + 1) > budget, ( "one more iteration should overflow the state dimension" ) - # Revert packing bills regular gas only, so far more iterations fit. + # Revert packing bills execution gas only, so far more iterations fit. assert sum(revert) > sum(success) - assert sum(regular(i) for i in revert) <= budget + assert sum(execution(i) for i in revert) <= budget # Out-of-gas packing counts the whole gas limit, reserve included. - assert sum(regular(i) + reserve for i in out_of_gas) <= budget - assert regular(sum(out_of_gas) + 1) + reserve > budget, ( - "one more iteration should overflow the regular budget" + assert sum(execution(i) + reserve for i in out_of_gas) <= budget + assert execution(sum(out_of_gas) + 1) + reserve > budget, ( + "one more iteration should overflow the execution budget" ) diff --git a/packages/testing/src/execution_testing/tools/tools_code/generators.py b/packages/testing/src/execution_testing/tools/tools_code/generators.py index 75ef1a354b0..0ccad4b9075 100644 --- a/packages/testing/src/execution_testing/tools/tools_code/generators.py +++ b/packages/testing/src/execution_testing/tools/tools_code/generators.py @@ -112,7 +112,7 @@ def __new__( return instance - def execution_gas(self, fork: Type[ForkOpcodeInterface]) -> int: + def evm_gas(self, fork: Type[ForkOpcodeInterface]) -> int: """ Gas cost of executing the initcode, charged before the code deposit fee. @@ -773,9 +773,9 @@ class TxOutcome(Enum): Expected outcome of a generated transaction. Under EIP-8037 the outcome decides how gas is billed: on success the - sender pays regular plus state gas, on revert the runtime state gas is + sender pays execution plus state gas, on revert the runtime state gas is rolled back into the reservoir and refunded, and on an exceptional halt - the whole declared gas limit burns in the regular dimension. + the whole declared gas limit burns in the execution dimension. """ SUCCESS = auto() @@ -786,7 +786,7 @@ class TxOutcome(Enum): class TransactionWithCost(Transaction): """Transaction object that can include the expected gas to be consumed.""" - regular_cost: int = Field(..., exclude=True) + execution_cost: int = Field(..., exclude=True) state_cost: int = Field(..., exclude=True) outcome: TxOutcome = Field(TxOutcome.SUCCESS, exclude=True) @@ -797,8 +797,8 @@ def gas_cost(self) -> int: `cumulativeGasUsed` reflects. Use for `expected_benchmark_gas_used`. - On success this is the combined regular + state gas. On revert only - the regular gas is billed (runtime state gas is refunded; intrinsic + On success this is the combined execution + state gas. On revert only + the execution gas is billed (runtime state gas is refunded; intrinsic state gas, e.g. for authorizations, is not modeled here). On an exceptional halt the whole gas limit burns: the generators size out-of-gas transactions below the EIP-7825 cap, where the state @@ -806,11 +806,11 @@ def gas_cost(self) -> int: """ match self.outcome: case TxOutcome.REVERT: - return self.regular_cost + return self.execution_cost case TxOutcome.OUT_OF_GAS: return int(self.gas_limit) case _: - return self.regular_cost + self.state_cost + return self.execution_cost + self.state_cost @property def block_gas_cost(self) -> int: @@ -818,24 +818,24 @@ def block_gas_cost(self) -> int: Return the gas this transaction contributes to the block-header gas. The block-header gas is the maximum across the independent gas - dimensions (EIP-8037: `max(regular, state)`), not their sum, so this + dimensions (EIP-8037: `max(execution, state)`), not their sum, so this is the right per-transaction quantity for block-fitting decisions (e.g. how many transactions fit under a gas target). On revert only - the regular gas lands; on an exceptional halt the whole gas limit - lands in the regular dimension. + the execution gas lands; on an exceptional halt the whole gas limit + lands in the execution dimension. Summing this over a block is exact only when a single dimension dominates every transaction uniformly (the common benchmark shape); for a mixed block the exact occupancy is - `max(sum(regular_cost), sum(state_cost))`. + `max(sum(execution_cost), sum(state_cost))`. """ match self.outcome: case TxOutcome.REVERT: - return self.regular_cost + return self.execution_cost case TxOutcome.OUT_OF_GAS: return int(self.gas_limit) case _: - return max(self.regular_cost, self.state_cost) + return max(self.execution_cost, self.state_cost) @dataclass(kw_only=True, slots=True) @@ -844,7 +844,7 @@ class GasCaps: Small helper class to represent multidimensional gas caps. """ - regular: int + execution: int state: int | None gas_limit: int | None @@ -953,7 +953,7 @@ def iterating_subcall_gas_cost( """Return the gas cost of the iterating subcall.""" if isinstance(self.iterating_subcall, int): return self.iterating_subcall - return self.iterating_subcall.regular_cost(fork=fork) + return self.iterating_subcall.execution_cost(fork=fork) def iterating_subcall_state_gas_cost( self, *, fork: Type[ForkOpcodeInterface] @@ -980,16 +980,16 @@ def iterating_subcall_reserve( iterating_subcall_gas_cost * 64 // 63 ) - iterating_subcall_gas_cost - def regular_gas_cost_by_iteration_count( + def execution_gas_cost_by_iteration_count( self, *, fork: Type[ForkOpcodeInterface], iteration_count: int ) -> int: """Return the cost of iterating through the bytecode N times.""" loop_gas_cost = 0 if iteration_count > 0: # Cold cost is just charged for the first iteration - loop_gas_cost = self.iterating.regular_cost(fork=fork) + loop_gas_cost = self.iterating.execution_cost(fork=fork) # Warm cost is charged for all iterations except the first - loop_gas_cost += self.warm_iterating.regular_cost(fork=fork) * ( + loop_gas_cost += self.warm_iterating.execution_cost(fork=fork) * ( iteration_count - 1 ) # Subcall cost is charged for all iterations. @@ -997,9 +997,9 @@ def regular_gas_cost_by_iteration_count( self.iterating_subcall_gas_cost(fork=fork) * iteration_count ) return ( - self.setup.regular_cost(fork=fork) + self.setup.execution_cost(fork=fork) + loop_gas_cost - + self.cleanup.regular_cost(fork=fork) + + self.cleanup.execution_cost(fork=fork) ) def state_gas_cost_by_iteration_count( @@ -1043,7 +1043,7 @@ def with_fixed_iteration_count( # Methods to calculate transactions that call a contract containing the # iterating bytecode. - def tx_regular_gas_cost_by_iteration_count( + def tx_execution_gas_cost_by_iteration_count( self, *, fork: Fork, @@ -1088,7 +1088,7 @@ def tx_regular_gas_cost_by_iteration_count( } ) return ( - self.regular_gas_cost_by_iteration_count( + self.execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iteration_count ) + intrinsic_gas_cost_calc(**intrinsic_cost_kwargs) @@ -1111,7 +1111,7 @@ def tx_gas_limit_by_iteration_count( The gas limit is calculated by adding the required extra gas for the last iteration due to the 63/64 rule. """ - tx_gas_limit = self.tx_regular_gas_cost_by_iteration_count( + tx_gas_limit = self.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iteration_count, start_iteration=start_iteration, @@ -1135,18 +1135,18 @@ def _iteration_count_exceeds_caps( """ Evaluate whether the iteration count exceeds any of the constraints. """ - tx_regular_gas_cost = self.tx_regular_gas_cost_by_iteration_count( + tx_execution_gas_cost = self.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iteration_count, start_iteration=start_iteration, **intrinsic_cost_kwargs, ) - if tx_regular_gas_cost > caps.regular: + if tx_execution_gas_cost > caps.execution: return True if caps.gas_limit is not None and ( - self.iterating_subcall_reserve(fork=fork) + tx_regular_gas_cost + self.iterating_subcall_reserve(fork=fork) + tx_execution_gas_cost > caps.gas_limit ): return True @@ -1169,7 +1169,7 @@ def _binary_search_iterations( **intrinsic_cost_kwargs: Any, ) -> Tuple[int, int, int]: """ - Binary search for the maximum iterations that fit within the regular + Binary search for the maximum iterations that fit within the execution gas, state gas and gas limit cap constraints. """ if self._iteration_count_exceeds_caps( @@ -1212,8 +1212,8 @@ def _binary_search_iterations( low = mid + 1 best_iterations = low - 1 - best_iterations_regular_gas = ( - self.tx_regular_gas_cost_by_iteration_count( + best_iterations_execution_gas = ( + self.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=best_iterations, start_iteration=start_iteration, @@ -1225,7 +1225,7 @@ def _binary_search_iterations( ) return ( best_iterations, - best_iterations_regular_gas, + best_iterations_execution_gas, best_iterations_state_gas, ) @@ -1252,7 +1252,7 @@ def tx_iterations_by_gas_limit( The gas each transaction counts against the budget follows its expected outcome (see `TransactionWithCost.block_gas_cost`): the - max-dimension gas on success, the regular gas only on revert (state + max-dimension gas on success, the execution gas only on revert (state gas is refunded), and the whole gas limit including the subcall reserve on out-of-gas. """ @@ -1269,7 +1269,7 @@ def tx_iterations_by_gas_limit( def current_caps() -> GasCaps: return GasCaps( - regular=remaining_gas - reserve, + execution=remaining_gas - reserve, # State gas only counts against the block budget when the # transaction succeeds; on revert or halt it is refunded. state=( @@ -1289,7 +1289,7 @@ def current_caps() -> GasCaps: # within remaining_gas ( best_iterations, - best_iterations_regular_gas, + best_iterations_execution_gas, best_iterations_state_gas, ) = self._binary_search_iterations( fork=fork, @@ -1300,12 +1300,12 @@ def current_caps() -> GasCaps: yield best_iterations match outcome: case TxOutcome.REVERT: - remaining_gas -= best_iterations_regular_gas + remaining_gas -= best_iterations_execution_gas case TxOutcome.OUT_OF_GAS: - remaining_gas -= best_iterations_regular_gas + reserve + remaining_gas -= best_iterations_execution_gas + reserve case _: remaining_gas -= max( - best_iterations_regular_gas, + best_iterations_execution_gas, best_iterations_state_gas, ) start_iteration += best_iterations @@ -1351,7 +1351,7 @@ def tx_iterations_by_total_iteration_count( best_iterations, _, _ = self._binary_search_iterations( fork=fork, caps=GasCaps( - regular=gas_limit_cap, + execution=gas_limit_cap, state=None, gas_limit=gas_limit_cap, ), @@ -1399,7 +1399,7 @@ def transactions_by_gas_limit( according to `outcome`. Out-of-gas transactions are sized without the state gas allowance, - so the whole gas limit burns as regular gas and the billed amount is + so the whole gas limit burns as execution gas and the billed amount is exact; the caller must still make the bytecode inexhaustible (e.g. with a negative `tx_gas_limit_delta` or a loop with no exit). """ @@ -1427,7 +1427,7 @@ def transactions_by_gas_limit( ), **intrinsic_cost_kwargs, ) - tx_regular_cost = self.tx_regular_gas_cost_by_iteration_count( + tx_execution_cost = self.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iteration_count, start_iteration=start_iteration, @@ -1448,7 +1448,7 @@ def transactions_by_gas_limit( to=to, gas_limit=tx_gas_limit + tx_gas_limit_delta, sender=sender, - regular_cost=tx_regular_cost, + execution_cost=tx_execution_cost, state_cost=tx_state_cost, outcome=outcome, **current_tx_kwargs, @@ -1503,7 +1503,7 @@ def transactions_by_total_iteration_count( include_state_gas_reservoir=True, **intrinsic_cost_kwargs, ) - tx_regular_cost = self.tx_regular_gas_cost_by_iteration_count( + tx_execution_cost = self.tx_execution_gas_cost_by_iteration_count( fork=fork, iteration_count=iteration_count, start_iteration=start_iteration, @@ -1524,7 +1524,7 @@ def transactions_by_total_iteration_count( to=to, gas_limit=tx_gas_limit + tx_gas_limit_delta, sender=sender, - regular_cost=tx_regular_cost, + execution_cost=tx_execution_cost, state_cost=tx_state_cost, **current_tx_kwargs, ) @@ -1591,7 +1591,7 @@ def __new__( def gas_cost(self, fork: Type[ForkOpcodeInterface]) -> int: """Return the cost of iterating through the bytecode N times.""" - return self.regular_gas_cost_by_iteration_count( + return self.execution_gas_cost_by_iteration_count( fork=fork, iteration_count=self.iteration_count, ) + self.state_gas_cost_by_iteration_count( diff --git a/packages/testing/src/execution_testing/vm/bytecode.py b/packages/testing/src/execution_testing/vm/bytecode.py index 1a28cd18f09..fdb73d2fa0c 100644 --- a/packages/testing/src/execution_testing/vm/bytecode.py +++ b/packages/testing/src/execution_testing/vm/bytecode.py @@ -38,8 +38,8 @@ class Bytecode: _gas_cost_fork_: Type[ForkOpcodeInterface] | None = None _state_cost_: int | None = None _state_cost_fork_: Type[ForkOpcodeInterface] | None = None - _regular_cost_: int | None = None - _regular_cost_fork_: Type[ForkOpcodeInterface] | None = None + _execution_cost_: int | None = None + _execution_cost_fork_: Type[ForkOpcodeInterface] | None = None _refund_: int | None = None _refund_fork_: Type[ForkOpcodeInterface] | None = None _state_refund_: int | None = None @@ -321,19 +321,19 @@ def state_cost(self, fork: Type[ForkOpcodeInterface]) -> int: self._state_cost_ += opcode_state_calculator(opcode) return self._state_cost_ - def regular_cost(self, fork: Type[ForkOpcodeInterface]) -> int: + def execution_cost(self, fork: Type[ForkOpcodeInterface]) -> int: """ - Use a fork object to calculate the regular gas used by this + Use a fork object to calculate the execution gas used by this bytecode (i.e. excluding the state-gas portion under EIP-8037). - Useful for OOG-boundary tests that need to land at the regular - gas charge of an opcode rather than its combined regular + state + Useful for OOG-boundary tests that need to land at the execution + gas charge of an opcode rather than its combined execution + state cost. """ - if self._regular_cost_ is None or self._regular_cost_fork_ != fork: - self._regular_cost_fork_ = fork - self._regular_cost_ = self.gas_cost(fork) - self.state_cost(fork) - return self._regular_cost_ + if self._execution_cost_ is None or self._execution_cost_fork_ != fork: + self._execution_cost_fork_ = fork + self._execution_cost_ = self.gas_cost(fork) - self.state_cost(fork) + return self._execution_cost_ def refund(self, fork: Type[ForkOpcodeInterface]) -> int: """Use a fork object to calculate the gas refund from this bytecode.""" diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index d76b92a24cc..9ffddf833ae 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -98,7 +98,7 @@ from .vm.gas import ( GasCosts, StateGasCosts, - allocate_execution_gas, + allocate_evm_gas, calculate_blob_gas_price, calculate_data_fee, calculate_excess_blob_gas, @@ -561,7 +561,7 @@ def check_transaction( is empty. """ - regular_gas_available = ( + execution_gas_available = ( block_env.block_gas_limit - block_output.block_gas_used ) state_gas_available = ( @@ -570,8 +570,8 @@ def check_transaction( blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used # EIP-8037 per-dimension inclusion check. - if min(TX_MAX_GAS_LIMIT, tx.gas) > regular_gas_available: - raise GasUsedExceedsLimitError("regular gas used exceeds limit") + if min(TX_MAX_GAS_LIMIT, tx.gas) > execution_gas_available: + raise GasUsedExceedsLimitError("execution gas used exceeds limit") if tx.gas > state_gas_available: raise GasUsedExceedsLimitError("state gas used exceeds limit") @@ -1048,9 +1048,9 @@ def process_transaction( effective_gas_fee = tx.gas * effective_gas_price - # Split execution gas into a regular grant (capped by the remaining - # regular-gas budget) and a state gas reservoir. - allocation = allocate_execution_gas(tx.gas, intrinsic) + # Split the EVM gas into an execution-gas grant (capped by the + # remaining execution-gas budget) and a state gas reservoir. + allocation = allocate_evm_gas(tx.gas, intrinsic) increment_nonce(tx_state, sender) @@ -1077,7 +1077,7 @@ def process_transaction( recipient=tx.to, value=tx.value, gas_price=effective_gas_price, - gas=allocation.regular_gas, + gas=allocation.execution_gas, state_gas_reservoir=allocation.state_gas_reservoir, access_list_addresses=access_list_addresses, access_list_storage_keys=access_list_storage_keys, @@ -1113,7 +1113,7 @@ def process_transaction( # transfer miner fees create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) - block_output.block_gas_used += settlement.regular_gas_used + block_output.block_gas_used += settlement.execution_gas_used block_output.block_state_gas_used += settlement.state_gas_used block_output.blob_gas_used += tx_blob_gas_used diff --git a/src/ethereum/forks/amsterdam/fork_types.py b/src/ethereum/forks/amsterdam/fork_types.py index 52f0c8f8234..a058a019aea 100644 --- a/src/ethereum/forks/amsterdam/fork_types.py +++ b/src/ethereum/forks/amsterdam/fork_types.py @@ -34,7 +34,7 @@ Bloom = Bytes256 -RegularGas = NewType("RegularGas", Uint) +ExecutionGas = NewType("ExecutionGas", Uint) StateGas = NewType("StateGas", Uint) diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index 251c80472e2..d83754306db 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -25,7 +25,7 @@ InitCodeTooLargeError, TransactionTypeError, ) -from .fork_types import Authorization, RegularGas, VersionedHash +from .fork_types import Authorization, ExecutionGas, VersionedHash @final @@ -33,10 +33,10 @@ class IntrinsicGasCost: """Intrinsic gas costs for a transaction, split by gas type.""" - regular: RegularGas - """Regular execution gas (calldata, base cost, access list, etc.).""" + execution: ExecutionGas + """Execution gas (calldata, base cost, access list, etc.).""" - calldata_floor: RegularGas + calldata_floor: ExecutionGas """ Minimum gas cost based on calldata size per [EIP-7623]. @@ -597,16 +597,16 @@ def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic = calculate_intrinsic_cost(tx, sender) - intrinsic_gas = Uint(intrinsic.regular) + intrinsic_gas = Uint(intrinsic.execution) if intrinsic_gas > tx.gas: raise InsufficientTransactionGasError("Insufficient intrinsic gas") if intrinsic.calldata_floor > tx.gas: raise InsufficientTransactionGasError("Insufficient calldata floor") if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: raise InitCodeTooLargeError("Code size too large") - if intrinsic.regular > TX_MAX_GAS_LIMIT: + if intrinsic.execution > TX_MAX_GAS_LIMIT: raise InsufficientTransactionGasError( - "Intrinsic regular gas exceeds TX_MAX_GAS_LIMIT" + "Intrinsic execution gas exceeds TX_MAX_GAS_LIMIT" ) if intrinsic.calldata_floor > TX_MAX_GAS_LIMIT: raise InsufficientTransactionGasError( @@ -652,9 +652,9 @@ def calculate_intrinsic_cost( charges. This function takes a transaction and its sender as parameters and - returns the intrinsic regular gas cost and the minimum (floor) gas - cost based on the calldata size. The floor is anchored on the - regular-gas portion of items 1 to 3 above rather than `TX_BASE` + returns the intrinsic execution gas cost and the minimum (floor) + gas cost based on the calldata size. The floor is anchored on the + execution-gas portion of items 1 to 3 above rather than `TX_BASE` alone, so it never undercuts the transaction's own intrinsic base. """ from .vm.gas import GasCosts, init_code_cost @@ -666,15 +666,15 @@ def calculate_intrinsic_cost( is_create = tx.to == Bytes0(b"") is_self_transfer = tx.to == sender - recipient_regular_gas = Uint(0) + recipient_execution_gas = Uint(0) init_code_gas = Uint(0) if is_create: - recipient_regular_gas = GasCosts.CREATE_ACCESS + recipient_execution_gas = GasCosts.CREATE_ACCESS init_code_gas = init_code_cost(ulen(tx.data)) elif not is_self_transfer: - recipient_regular_gas = GasCosts.COLD_ACCOUNT_ACCESS + recipient_execution_gas = GasCosts.COLD_ACCOUNT_ACCESS if tx.value > U256(0): - recipient_regular_gas += GasCosts.TX_VALUE_COST + recipient_execution_gas += GasCosts.TX_VALUE_COST access_list_cost = Uint(0) tokens_in_access_list = Uint(0) @@ -704,24 +704,24 @@ def calculate_intrinsic_cost( # Total floor tokens. total_floor_tokens = floor_tokens_in_calldata + tokens_in_access_list - # Decomposed regular-gas intrinsic base (EIP-2780), which also anchors - # the calldata floor. - base_regular_gas = GasCosts.TX_BASE + recipient_regular_gas + # Decomposed execution-gas intrinsic base (EIP-2780), which also + # anchors the calldata floor. + base_execution_gas = GasCosts.TX_BASE + recipient_execution_gas # Floor gas cost (EIP-7623: minimum gas for data-heavy transactions). data_floor_gas_cost = ( - total_floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + base_regular_gas + total_floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + base_execution_gas ) return IntrinsicGasCost( - regular=RegularGas( - base_regular_gas + execution=ExecutionGas( + base_execution_gas + init_code_gas + data_cost + access_list_cost + auth_cost ), - calldata_floor=RegularGas(data_floor_gas_cost), + calldata_floor=ExecutionGas(data_floor_gas_cost), ) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index c7563ab5fd2..a2395a96ff9 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -70,7 +70,8 @@ class BlockOutput: Contains the following: block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. + Execution gas used for executing all transactions. EIP-8037 + names this counter `block_execution_gas_used`. block_state_gas_used : `ethereum.base_types.Uint` State gas used for executing all transactions. cumulative_gas_used : `ethereum.base_types.Uint` diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index f2c69d5e7d8..28639cb7544 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -206,7 +206,7 @@ def set_delegation(evm: Evm) -> None: - ``StateGasCosts.NEW_ACCOUNT`` (state) when the authority's account leaf does not yet exist. - - ``GasCosts.ACCOUNT_WRITE`` (regular) when applying the + - ``GasCosts.ACCOUNT_WRITE`` (execution) when applying the authorization is the transaction's first write to the authority's leaf. Writes the transaction already prices elsewhere are exempt: the sender's, covered by ``TX_BASE``, and, for a diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index d4fd188946f..358418b7545 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -254,8 +254,9 @@ class GasMeter: gas_left: Uint """ - Gas still available from the frame's regular grant. Pays regular - charges, and state charges as [spill] once the reservoir empties. + Gas still available from the frame's execution-gas grant. Pays + execution-gas charges, and state charges as [spill] once the + reservoir empties. [spill]: ref:ethereum.forks.amsterdam.vm.gas.GasMeter.state_gas_spilled """ @@ -280,7 +281,7 @@ class GasMeter: state_gas_spilled: Uint = Uint(0) """ - Regular gas spent covering state charges after the reservoir + Execution gas spent covering state charges after the reservoir emptied. Credited back to `gas_left` first, in LIFO order, on a refund or failure. [EIP-8037] names this quantity `state_gas_from_gas_left`. @@ -358,14 +359,14 @@ def check_gas(evm: "Evm", amount: Uint) -> None: def charge_gas(evm: "Evm", amount: Uint) -> None: """ - Subtracts `amount` from `gas_left` (regular gas). + Subtracts `amount` from `gas_left` (execution gas). Parameters ---------- evm : The current EVM. amount : - The amount of regular gas the current operation requires. + The amount of execution gas the current operation requires. """ evm_trace(evm, GasAndRefund(int(amount))) @@ -561,7 +562,7 @@ def credit_state_gas_refund(gas_meter: GasMeter, amount: StateGas) -> None: def forfeit_remaining_gas(gas_meter: GasMeter) -> None: """ - Consume all remaining regular gas on an exceptional halt. + Consume all remaining execution gas on an exceptional halt. Parameters ---------- @@ -580,7 +581,7 @@ def withhold_create_gas(gas_meter: GasMeter) -> Uint: Withhold and return the gas made available to a `CREATE*` child. Deduct the all-but-one-64th share from the frame's `gas_left` and - return it as the child frame's regular gas grant. + return it as the child frame's execution-gas grant. Parameters ---------- @@ -590,7 +591,7 @@ def withhold_create_gas(gas_meter: GasMeter) -> Uint: Returns ------- child_gas : `ethereum.base_types.Uint` - The regular gas granted to the child frame. + The execution gas granted to the child frame. """ child_gas = max_message_call_gas(gas_meter.gas_left) @@ -629,15 +630,15 @@ def restore_child_gas( Return a child frame's unused gas grant to the parent. Used when the child frame is never entered (for example, a stack - depth or balance check fails): the withheld regular gas and drained - reservoir are returned untouched. + depth or balance check fails): the withheld execution gas and + drained reservoir are returned untouched. Parameters ---------- gas_meter : The parent frame's gas meter. gas : - The regular gas grant to return. + The execution gas grant to return. state_gas_reservoir : The state gas reservoir to return. @@ -910,28 +911,28 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: @final @dataclass -class ExecutionGasAllocation: +class EvmGasAllocation: """ - Split of a transaction's execution gas across the two dimensions. + Split of a transaction's EVM gas across the two dimensions. """ - regular_gas: Uint - """Regular gas granted to the top frame, capped by the budget.""" + execution_gas: Uint + """Execution gas granted to the top frame, capped by the budget.""" state_gas_reservoir: Uint """State gas set aside for the top frame's reservoir.""" -def allocate_execution_gas( +def allocate_evm_gas( tx_gas: Uint, intrinsic: IntrinsicGasCost -) -> ExecutionGasAllocation: +) -> EvmGasAllocation: """ - Split execution gas into a regular grant and a state reservoir. + Split EVM gas into an execution-gas grant and a state reservoir. - After the intrinsic cost is removed, the remaining execution gas is - divided into regular gas -- capped by the regular-gas budget that - remains below `TX_MAX_GAS_LIMIT` -- and a state gas reservoir that - holds whatever exceeds that cap. + After the intrinsic cost is removed, the remaining EVM gas is + divided into execution gas -- capped by the execution-gas budget + that remains below `TX_MAX_GAS_LIMIT` -- and a state gas reservoir + that holds whatever exceeds that cap. Only valid once `validate_transaction` has confirmed the transaction can afford its intrinsic cost, which guarantees the subtractions @@ -946,15 +947,15 @@ def allocate_execution_gas( Returns ------- - allocation : `ExecutionGasAllocation` - The regular gas grant and state gas reservoir. + allocation : `EvmGasAllocation` + The execution gas grant and state gas reservoir. """ - execution_gas = tx_gas - Uint(intrinsic.regular) - regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic.regular - regular_gas = min(regular_gas_budget, execution_gas) - state_gas_reservoir = Uint(execution_gas - regular_gas) - return ExecutionGasAllocation(regular_gas, state_gas_reservoir) + evm_gas = tx_gas - Uint(intrinsic.execution) + execution_gas_budget = TX_MAX_GAS_LIMIT - intrinsic.execution + execution_gas = min(execution_gas_budget, evm_gas) + state_gas_reservoir = Uint(evm_gas - execution_gas) + return EvmGasAllocation(execution_gas, state_gas_reservoir) @final @@ -973,8 +974,8 @@ class TransactionGasSettlement: gas_left: Uint """Gas returned to the sender, priced at the effective gas price.""" - regular_gas_used: Uint - """Regular gas the transaction contributes to the block total.""" + execution_gas_used: Uint + """Execution gas the transaction contributes to the block total.""" state_gas_used: Uint """State gas the transaction contributes to the block total.""" @@ -993,16 +994,17 @@ def settle_transaction_gas( Compute, in order: - - the gas used before refunds, from the gas limit less the regular - gas and reservoir the top frame returned; + - the gas used before refunds, from the gas limit less the + execution gas and reservoir the top frame returned; - the refund, capped at one fifth of that pre-refund usage; - the gas used, taken as the larger of the post-refund usage and the calldata floor, so a transaction never pays below the floor; and - the per-dimension block amounts: the state gas used (clamped to - zero, since refunds can drive it negative) and the regular gas - used, which carries the floor because the floor binds the regular - dimension. Unlike the sender-facing `gas_used`, it ignores - refunds: block accounting counts pre-refund gas ([EIP-7778]). + zero, since refunds can drive it negative) and the execution gas + used, which carries the floor because the floor binds the + execution dimension. Unlike the sender-facing `gas_used`, it + ignores refunds: block accounting counts pre-refund gas + ([EIP-7778]). Parameters ---------- @@ -1011,7 +1013,7 @@ def settle_transaction_gas( intrinsic : The transaction's intrinsic gas cost. gas_left : - Regular gas the top frame returned. + Execution gas the top frame returned. state_gas_left : State gas reservoir the top frame returned. refund_counter : @@ -1033,13 +1035,13 @@ def settle_transaction_gas( gas_used = max(gas_used_after_refund, intrinsic.calldata_floor) settled_state_gas_used = Uint(max(0, state_gas_used)) - regular_gas_used = max( + execution_gas_used = max( gas_used_before_refund - settled_state_gas_used, intrinsic.calldata_floor, ) return TransactionGasSettlement( gas_used=gas_used, gas_left=tx_gas - gas_used, - regular_gas_used=regular_gas_used, + execution_gas_used=execution_gas_used, state_gas_used=settled_state_gas_used, ) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index b63a8ea2a42..b54b3821fd2 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -149,8 +149,8 @@ def sstore(evm: Evm) -> None: # Slot set then cleared: refund the state gas charge. credit_state_gas_refund(evm.gas_meter, StateGasCosts.STORAGE_SET) - # Charge regular gas before state gas so that a regular-gas OOG - # does not consume state gas that would inflate the parent's + # Charge execution gas before state gas so that an execution-gas + # OOG does not consume state gas that would inflate the parent's # reservoir on frame failure. charge_gas(evm, gas_cost) charge_state_gas(evm, state_gas) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 7d5a23a0173..3f3afafe53d 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -115,10 +115,10 @@ def generic_create( charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) # CHILD GRANT - # Withhold all but one 64th of the regular gas. + # Withhold all but one 64th of the execution gas. create_message_gas = withhold_create_gas(evm.gas_meter) - # On a collision the child's regular grant is consumed and no + # On a collision the child's execution-gas grant is consumed and no # account is created; a storage-only collision target is # non-existent: charged above, refilled here. if not account_deployable(tx_state, contract_address): @@ -484,7 +484,7 @@ def call(evm: Evm) -> None: # STATE ACCESS (STATE-DEPENDENT GAS) # Perform the accesses and complete the state-dependent pricing -- - # a delegation adds its access cost -- then charge the regular + # a delegation adds its access cost -- then charge the execution # gas. tx_state = evm.message.tx_env.state if is_cold_access: @@ -610,7 +610,7 @@ def callcode(evm: Evm) -> None: # STATE ACCESS (STATE-DEPENDENT GAS) # Perform the accesses and complete the state-dependent pricing -- - # a delegation adds its access cost; the regular gas is charged + # a delegation adds its access cost; the execution gas is charged # with the child grant. tx_state = evm.message.tx_env.state if is_cold_access: @@ -634,7 +634,7 @@ def callcode(evm: Evm) -> None: code = get_code(tx_state, code_hash) # CHILD GRANT - # Charge the call's cost and withhold the child's regular gas + # Charge the call's cost and withhold the child's execution gas # share in one step. The whole reservoir rides along (no 63/64 # rule for state gas). message_call_gas = calculate_message_call_gas( @@ -724,8 +724,8 @@ def selfdestruct(evm: Evm) -> None: state_gas = StateGasCosts.NEW_ACCOUNT account_write_gas = GasCosts.ACCOUNT_WRITE - # Charge regular gas before state gas so that a regular-gas OOG - # does not consume state gas that would inflate the parent's + # Charge execution gas before state gas so that an execution-gas + # OOG does not consume state gas that would inflate the parent's # reservoir on frame failure. charge_gas(evm, gas_cost + account_write_gas) charge_state_gas(evm, state_gas) @@ -791,7 +791,7 @@ def delegatecall(evm: Evm) -> None: # STATE ACCESS (STATE-DEPENDENT GAS) # Perform the accesses and complete the state-dependent pricing -- - # a delegation adds its access cost; the regular gas is charged + # a delegation adds its access cost; the execution gas is charged # with the child grant. if is_cold_access: evm.accessed_addresses.add(code_address) @@ -815,7 +815,7 @@ def delegatecall(evm: Evm) -> None: code = get_code(tx_state, code_hash) # CHILD GRANT - # Charge the call's cost and withhold the child's regular gas + # Charge the call's cost and withhold the child's execution gas # share in one step. The whole reservoir rides along (no 63/64 # rule for state gas). message_call_gas = calculate_message_call_gas( @@ -894,7 +894,7 @@ def staticcall(evm: Evm) -> None: # STATE ACCESS (STATE-DEPENDENT GAS) # Perform the accesses and complete the state-dependent pricing -- - # a delegation adds its access cost; the regular gas is charged + # a delegation adds its access cost; the execution gas is charged # with the child grant. if is_cold_access: evm.accessed_addresses.add(to) @@ -918,7 +918,7 @@ def staticcall(evm: Evm) -> None: code = get_code(tx_state, code_hash) # CHILD GRANT - # Charge the call's cost and withhold the child's regular gas + # Charge the call's cost and withhold the child's execution gas # share in one step. The whole reservoir rides along (no 63/64 # rule for state gas). message_call_gas = calculate_message_call_gas( diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 8d0a3fe26ce..b1810361f3a 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -426,7 +426,7 @@ def process_message(message: Message) -> Evm: except ExceptionalHalt as error: evm_trace(evm, OpException(error)) # Frame settlement: refill state gas to the baseline, then - # forfeit -- a halted frame returns no regular gas to its + # forfeit -- a halted frame returns no execution gas to its # parent. After these handlers the meter states exactly what # the frame gives back, so parents absorb unconditionally. restore_state_gas(evm.gas_meter) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py index 6a61ddbb6b4..3b158cfc374 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py @@ -194,7 +194,7 @@ def authorization_transaction_cost( The recipient is a ``CONTRACT`` that runs no code, so no recipient top-frame charge applies and the cost reduces to the intrinsic plus - the authorizations' own top-frame regular and state charges. Each + the authorizations' own top-frame execution and state charges. Each authorization's charge is driven by its ``creates_account`` / ``writes_delegation`` / ``first_write`` annotations. """ @@ -203,7 +203,7 @@ def authorization_transaction_cost( authorization_list_or_count=authorization_list, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) @@ -211,7 +211,7 @@ def authorization_transaction_cost( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) - return intrinsic_gas + top_frame_regular + top_frame_state + return intrinsic_gas + top_frame_execution + top_frame_state def setup_target( diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_charges.py index 152d1c45d7f..d4a2b496fe3 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_charges.py @@ -201,7 +201,7 @@ def _intrinsic_gas( recipient_type: RecipientType = RecipientType.CONTRACT, sends_value: bool = False, ) -> int: - """Return the regular intrinsic gas deducted before execution.""" + """Return the execution intrinsic gas deducted before execution.""" return fork.transaction_intrinsic_cost_calculator()( recipient_type=recipient_type, sends_value=sends_value, @@ -377,7 +377,7 @@ def test_account_write_authority_is_recipient( # delegation. recipient_type = RecipientType.DELEGATION_7702 authorizations = [authorization] - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=recipient_type, authorizations=authorizations, ) @@ -392,7 +392,7 @@ def test_account_write_authority_is_recipient( recipient_type=recipient_type, sends_value=bool(value), ) - + top_frame_regular + + top_frame_execution + top_frame_state ) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_oog.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_oog.py index 2b7065f6363..eb9b707b739 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_oog.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_oog.py @@ -13,7 +13,7 @@ ``NEW_ACCOUNT``, or on the delegation-resolution access. The whole preparation shares one snapshot, so every authorization applied so far is rolled back and the frame halts without dispatching. The - transaction is still included and consumes its full regular budget; + transaction is still included and consumes its full execution budget; a state-gas reservoir, whose charges are refilled with the rollback, is returned to the sender in full. The sender nonce (bumped at inclusion, before the snapshot) is not rolled back. @@ -73,7 +73,7 @@ def _auth_top_frame_charges(fork: Fork, authorizations: list) -> int: """ - Return the top-frame regular + state gas attributable to the + Return the top-frame execution + state gas attributable to the authorizations alone. Computed against a ``CONTRACT`` recipient, which contributes no @@ -82,7 +82,7 @@ def _auth_top_frame_charges(fork: Fork, authorizations: list) -> int: ``AUTH_BASE``. Under the zero state reservoir these all draw from ``gas_left``. """ - regular = fork.transaction_top_frame_gas_calculator()( + execution = fork.transaction_top_frame_gas_calculator()( recipient_type=RecipientType.CONTRACT, authorizations=authorizations, ) @@ -90,17 +90,17 @@ def _auth_top_frame_charges(fork: Fork, authorizations: list) -> int: recipient_type=RecipientType.CONTRACT, authorizations=authorizations, ) - return regular + state + return execution + state -def _intrinsic_regular( +def _intrinsic_execution( fork: Fork, authorization_list: list, *, recipient_type: RecipientType, sends_value: bool = False, ) -> int: - """Return the regular intrinsic gas deducted before execution.""" + """Return the execution intrinsic gas deducted before execution.""" return fork.transaction_intrinsic_cost_calculator()( recipient_type=recipient_type, sends_value=sends_value, @@ -157,7 +157,7 @@ def test_set_delegation_oog_charge_point( - ``new_account``: the second (a creation) runs out at its opening ``NEW_ACCOUNT`` state charge. - ``account_write``: the second covers ``NEW_ACCOUNT`` but runs out - at the following ``ACCOUNT_WRITE`` regular charge. + at the following ``ACCOUNT_WRITE`` execution charge. - ``auth_base``: the second (a delegation on an existing empty EOA) covers its first-write ``ACCOUNT_WRITE`` but runs out at the following ``AUTH_BASE`` state charge. @@ -193,12 +193,12 @@ def test_set_delegation_oog_charge_point( authorization_list = [first.authorization, second.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) first_auth_charges = _auth_top_frame_charges(fork, [first.authorization]) - # gas_left entering set_delegation is gas_limit - intrinsic_regular + # gas_left entering set_delegation is gas_limit - intrinsic_execution # (the state reservoir is zero). The first authorization is applied # in full; the second is starved by one gas at the target charge, # after covering any charges that precede it within that same @@ -214,7 +214,7 @@ def test_set_delegation_oog_charge_point( shortfall_charge = gas_costs.AUTH_BASE gas_limit = ( - intrinsic_regular + first_auth_charges + preceding + shortfall_charge + intrinsic_execution + first_auth_charges + preceding + shortfall_charge ) if outcome != "succeeds": gas_limit -= 1 @@ -317,7 +317,7 @@ def test_set_delegation_oog_rolls_back_first_auth( second = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT) authorization_list = [first.authorization, second.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) first_auth_charges = _auth_top_frame_charges(fork, [first.authorization]) @@ -326,7 +326,7 @@ def test_set_delegation_oog_rolls_back_first_auth( # runs out at its opening NEW_ACCOUNT state charge, rolling back the # whole authorization phase. gas_limit = ( - intrinsic_regular + first_auth_charges + gas_costs.NEW_ACCOUNT - 1 + intrinsic_execution + first_auth_charges + gas_costs.NEW_ACCOUNT - 1 ) tx = Transaction( @@ -455,7 +455,7 @@ def test_recipient_charge_oog_rolls_back_delegations( delegated_to: BalAccountExpectation.empty() if succeeds else None } - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=recipient_type, @@ -466,7 +466,7 @@ def test_recipient_charge_oog_rolls_back_delegations( # is starved by one gas -- or, with ``succeeds``, covered exactly. # The charge shares the preparation snapshot, so its out-of-gas # rolls the applied delegations back. - gas_limit = intrinsic_regular + auth_charges + recipient_charge_gas + gas_limit = intrinsic_execution + auth_charges + recipient_charge_gas if not succeeds: gas_limit -= 1 @@ -538,7 +538,7 @@ def test_reservoir_settlement_by_failure_point( at each point along the top frame, settles four different ways. A non-zero reservoir requires ``gas_limit`` above the EIP-7825 cap, - which also hands the frame the *full* regular budget -- so starving + which also hands the frame the *full* execution budget -- so starving the preparation is only reachable when its demand exceeds the cap plus the reservoir. Account-creating authorizations are the one charge dense enough to get there: each demands ~234,606 gas @@ -564,7 +564,7 @@ def test_reservoir_settlement_by_failure_point( leaves the reservoir empty and the halt burns the rest: ``gas_used == gas_limit``, the full amount. - ``execution_revert``: as above, but ``REVERT`` returns the unused - regular budget: ``gas_used`` is exactly the intrinsic cost plus + execution budget: ``gas_used`` is exactly the intrinsic cost plus every preparation charge plus the reverting code's own gas. Together the four pin that the reservoir's fate follows the state @@ -608,11 +608,11 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: probe_authority = pre.fund_eoa(amount=0) probe = creation_authorization(probe_authority) - base_intrinsic = _intrinsic_regular( + base_intrinsic = _intrinsic_execution( fork, [], recipient_type=RecipientType.DELEGATION_7702 ) per_auth_intrinsic = ( - _intrinsic_regular( + _intrinsic_execution( fork, [probe], recipient_type=RecipientType.DELEGATION_7702 ) - base_intrinsic @@ -634,7 +634,7 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: creation_authorization(authority) for authority in authorities[1:] ] - intrinsic_regular = base_intrinsic + auth_count * per_auth_intrinsic + intrinsic_execution = base_intrinsic + auth_count * per_auth_intrinsic auth_charges = auth_count * per_auth_charges dispatch_charge = gas_costs.COLD_ACCOUNT_ACCESS auth_state_total = fork.transaction_top_frame_state_gas( @@ -644,30 +644,32 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: if failure_point == "set_delegation_oog": # The final authorization's closing AUTH_BASE is starved by one. - gas_limit = intrinsic_regular + auth_charges - 1 + gas_limit = intrinsic_execution + auth_charges - 1 expected_gas_used = cap delegations_persist = False elif failure_point == "dispatch_charge_oog": # All authorizations apply; the recipient's cold # delegation-resolution access is starved by one. - gas_limit = intrinsic_regular + auth_charges + dispatch_charge - 1 + gas_limit = intrinsic_execution + auth_charges + dispatch_charge - 1 expected_gas_used = cap delegations_persist = False elif failure_point == "execution_halt": - gas_limit = intrinsic_regular + auth_charges + dispatch_charge + 10_000 + gas_limit = ( + intrinsic_execution + auth_charges + dispatch_charge + 10_000 + ) expected_gas_used = gas_limit delegations_persist = True else: # execution_revert exec_gas = recipient_code.gas_cost(fork) gas_limit = ( - intrinsic_regular + intrinsic_execution + auth_charges + dispatch_charge + exec_gas + 10_000 ) expected_gas_used = ( - intrinsic_regular + auth_charges + dispatch_charge + exec_gas + intrinsic_execution + auth_charges + dispatch_charge + exec_gas ) delegations_persist = True @@ -828,14 +830,14 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: probe_authority = pre.fund_eoa(amount=0) probe = creation_authorization(probe_authority) - base_intrinsic = _intrinsic_regular( + base_intrinsic = _intrinsic_execution( fork, [], recipient_type=RecipientType.EMPTY_ACCOUNT, sends_value=True, ) per_auth_intrinsic = ( - _intrinsic_regular( + _intrinsic_execution( fork, [probe], recipient_type=RecipientType.EMPTY_ACCOUNT, @@ -859,7 +861,7 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: creation_authorization(authority) for authority in authorities[1:] ] - intrinsic_regular = base_intrinsic + auth_count * per_auth_intrinsic + intrinsic_execution = base_intrinsic + auth_count * per_auth_intrinsic auth_charges = auth_count * per_auth_charges auth_state_total = fork.transaction_top_frame_state_gas( recipient_type=RecipientType.CONTRACT, @@ -869,14 +871,14 @@ def creation_authorization(authority: EOA) -> AuthorizationTuple: data = b"" if failure_point == "set_delegation_oog": # The final authorization's closing AUTH_BASE is starved by one. - gas_limit = intrinsic_regular + auth_charges - 1 + gas_limit = intrinsic_execution + auth_charges - 1 expected_gas_used = cap delegations_persist = False elif failure_point == "dispatch_charge_oog": # All authorizations apply; the recipient's NEW_ACCOUNT state # charge is starved by one. gas_limit = ( - intrinsic_regular + auth_charges + gas_costs.NEW_ACCOUNT - 1 + intrinsic_execution + auth_charges + gas_costs.NEW_ACCOUNT - 1 ) expected_gas_used = cap delegations_persist = False @@ -1012,7 +1014,7 @@ def test_delegation_persists_on_execution_oog( authorization_list = [auth_a.authorization, auth_b.authorization] auth_charges = _auth_top_frame_charges(fork, authorization_list) - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT, @@ -1023,7 +1025,7 @@ def test_delegation_persists_on_execution_oog( # execution and then runs out on the second, consuming all gas. recipient_code = Op.PUSH1(0) + Op.PUSH1(0) one_opcode = Op.PUSH1(0).gas_cost(fork) - gas_limit = intrinsic_regular + auth_charges + one_opcode + gas_limit = intrinsic_execution + auth_charges + one_opcode recipient = pre.deploy_contract(code=recipient_code) @@ -1104,16 +1106,16 @@ def test_auth_state_charges_survive_dispatch_revert( auth = build_authorization(pre, auth_action) authorization_list = [auth.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) auth_charges = _auth_top_frame_charges(fork, authorization_list) revert_exec_gas = revert_code.gas_cost(fork) - # The authorization's regular and state charges and the two PUSH + # The authorization's execution and state charges and the two PUSH # opcodes feeding the REVERT stay paid; only the unused execution # budget returns. - gas_used = intrinsic_regular + auth_charges + revert_exec_gas + gas_used = intrinsic_execution + auth_charges + revert_exec_gas tx = Transaction( sender=sender, @@ -1158,10 +1160,10 @@ def test_auth_state_charges_survive_dispatch_halt_with_reservoir( With an ordinary gas limit the reservoir is zero and a halt consumes all of ``gas_left`` anyway, masking any wrongly-refilled state gas. Here the gas limit exceeds the EIP-7825 cap (allowed -- - the cap binds only the regular dimension), so the excess forms a + the cap binds only the execution dimension), so the excess forms a state-gas reservoir that covers the authorization's ``NEW_ACCOUNT`` + ``AUTH_BASE``. The dispatched call hits ``INVALID``, consuming - all regular gas; the *unused* reservoir returns to the sender, but + all execution gas; the *unused* reservoir returns to the sender, but the portion consumed for the persisting delegation must not. A regression that refills the authorization's state gas with the @@ -1192,7 +1194,7 @@ def test_auth_state_charges_survive_dispatch_halt_with_reservoir( # into gas_left. reservoir = auth_state_gas + 50_000 - # The halt consumes the full regular budget (the cap); of the + # The halt consumes the full execution budget (the cap); of the # reservoir, only the authorization's state gas is consumed -- its # delegation persists -- and the unused remainder returns. gas_used = cap + auth_state_gas @@ -1225,9 +1227,9 @@ def test_auth_state_gas_in_header_on_dispatch_revert( The state gas of an applied authorization is counted in the block's state dimension when the dispatched call reverts. - The header ``gas_used`` is ``max(block_regular_gas, + The header ``gas_used`` is ``max(block_execution_gas, block_state_gas)``. The authorization creates and delegates a fresh - authority (218,790 state gas), which dominates the small regular + authority (218,790 state gas), which dominates the small execution side (intrinsic + ``ACCOUNT_WRITE`` + the pre-revert execution), so a correct accounting yields ``gas_used == 218,790`` even though the dispatched call reverts -- the delegation, and the state it grew, @@ -1235,7 +1237,7 @@ def test_auth_state_gas_in_header_on_dispatch_revert( A regression that refills the authorization's state gas on the frame's rollback collapses ``tx_state_gas`` to zero and the header - to the small regular sum, which balance-only state tests cannot + to the small execution sum, which balance-only state tests cannot distinguish from a correctly-split total. """ sender = pre.fund_eoa() @@ -1246,10 +1248,10 @@ def test_auth_state_gas_in_header_on_dispatch_revert( auth = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT) authorization_list = [auth.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) - auth_regular = fork.transaction_top_frame_gas_calculator()( + auth_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) @@ -1259,11 +1261,11 @@ def test_auth_state_gas_in_header_on_dispatch_revert( ) revert_exec_gas = revert_code.gas_cost(fork) - regular_total = intrinsic_regular + auth_regular + revert_exec_gas - assert auth_state > regular_total, ( + execution_total = intrinsic_execution + auth_execution + revert_exec_gas + assert auth_state > execution_total, ( "the state dimension must dominate for the header to pin it" ) - expected_gas_used = max(regular_total, auth_state) + expected_gas_used = max(execution_total, auth_state) tx = Transaction( sender=sender, @@ -1314,7 +1316,7 @@ def test_reverted_dispatch_state_gas_counts_toward_block_limit( beyond it (``exceeded``: the per-transaction state check fires and the block is correctly rejected). - The regular dimension is asserted to have room either way, pinning + The execution dimension is asserted to have room either way, pinning the rejection to the state dimension. An implementation that drops a reverted transaction's persisting state gas from the block's state total would accept the ``exceeded`` block and fork. @@ -1330,10 +1332,10 @@ def test_reverted_dispatch_state_gas_counts_toward_block_limit( auth = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT) authorization_list = [auth.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) - auth_regular = fork.transaction_top_frame_gas_calculator()( + auth_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) @@ -1343,13 +1345,13 @@ def test_reverted_dispatch_state_gas_counts_toward_block_limit( ) revert_exec_gas = revert_code.gas_cost(fork) - first_tx_regular = intrinsic_regular + auth_regular + revert_exec_gas + first_tx_execution = intrinsic_execution + auth_execution + revert_exec_gas first_tx = Transaction( sender=pre.fund_eoa(), to=recipient, value=0, authorization_list=authorization_list, - gas_limit=first_tx_regular + auth_state, + gas_limit=first_tx_execution + auth_state, ) # The last transaction's worst-case state contribution is its full @@ -1360,10 +1362,10 @@ def test_reverted_dispatch_state_gas_counts_toward_block_limit( last_tx_gas = state_available + delta # Pin the rejection (when delta > 0) to the state check: the - # regular check must not fire. - regular_available = block_gas_limit - first_tx_regular - assert min(cap, last_tx_gas) < regular_available, ( - "the last tx would fail the regular check instead of the state check" + # execution check must not fire. + execution_available = block_gas_limit - first_tx_execution + assert min(cap, last_tx_gas) < execution_available, ( + "the last tx would fail the execution check instead of the state check" ) last_tx_error = ( @@ -1413,7 +1415,7 @@ def test_recipient_new_account_refilled_on_dispatch_halt_with_reservoir( refills. The gas limit exceeds the EIP-7825 cap so the charge draws from a - state-gas reservoir; the halt consumes the full regular budget (the + state-gas reservoir; the halt consumes the full execution budget (the cap) but the *entire* reservoir returns, pinning the refill in the receipt's gas used. This is the counterpart of ``test_auth_state_charges_survive_dispatch_halt_with_reservoir``, @@ -1436,7 +1438,7 @@ def test_recipient_new_account_refilled_on_dispatch_halt_with_reservoir( reservoir = new_account_state_gas + 50_000 - # The halt consumes the full regular budget; the NEW_ACCOUNT drawn + # The halt consumes the full execution budget; the NEW_ACCOUNT drawn # from the reservoir is refilled (the account creation rolled # back), so the whole reservoir returns to the sender. gas_used = cap @@ -1491,11 +1493,11 @@ def test_dispatched_frame_state_gas_still_refills_on_revert( auth = build_authorization(pre, AuthorizationAction.SETS_NEW_DELEGATION) authorization_list = [auth.authorization] - intrinsic_regular = _intrinsic_regular( + intrinsic_execution = _intrinsic_execution( fork, authorization_list, recipient_type=RecipientType.CONTRACT ) auth_charges = _auth_top_frame_charges(fork, authorization_list) - exec_regular = sstore_revert_code.regular_cost(fork) + evm_execution = sstore_revert_code.execution_cost(fork) exec_state = sstore_revert_code.state_cost(fork) assert exec_state > 0, ( "the dispatched SSTORE must carry a state-gas charge" @@ -1503,8 +1505,8 @@ def test_dispatched_frame_state_gas_still_refills_on_revert( # The SSTORE's state gas is charged and then refilled by the # revert (the slot rolls back), so the sender pays only the - # authorization charges and the regular execution gas. - gas_used = intrinsic_regular + auth_charges + exec_regular + # authorization charges and the execution gas. + gas_used = intrinsic_execution + auth_charges + evm_execution tx = Transaction( sender=sender, diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py index b0ba400f25e..c1a13931d54 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py @@ -259,7 +259,7 @@ def test_calldata_floor_contract_creation( empty code, and prices every byte as one floor token. - ``floor_binds``: ``gas_used`` pins to the floor, which anchors - on the creation regular base (``TX_BASE + CREATE_ACCESS``) + on the creation execution base (``TX_BASE + CREATE_ACCESS``) but excludes the created account's ``NEW_ACCOUNT`` *state* charge and the init-code word cost -- both masked by the binding floor -- while the deploy (and any moved wei) still lands. diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py index 4f0cffce3c8..18885366818 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py @@ -18,7 +18,7 @@ lowered ``TX_BASE`` with no recipient or value-transfer charge, regardless of value, the largest reduction. - A contract creation splits the flat pre-fork ``TX_CREATE`` into the - ``CREATE_ACCESS`` regular intrinsic and a top-frame ``NEW_ACCOUNT`` + ``CREATE_ACCESS`` execution intrinsic and a top-frame ``NEW_ACCOUNT`` state charge. """ @@ -177,9 +177,9 @@ def test_creation_tx_intrinsic_across_amsterdam_transition( The same creation transaction (``to=None``, ``STOP`` init code that deploys empty code) is sent in a pre-fork block and a post-fork block, each from a fresh sender with the gas limit pinned exactly. - Pre-fork the whole cost is regular intrinsic: ``TX_BASE`` plus the + Pre-fork the whole cost is execution intrinsic: ``TX_BASE`` plus the flat ``TX_CREATE``. Post-fork the intrinsic keeps only the - ``CREATE_ACCESS`` regular portion of ``TX_CREATE``, while the created + ``CREATE_ACCESS`` execution portion of ``TX_CREATE``, while the created account's ``NEW_ACCOUNT`` is charged as *state* gas at the top frame — the sender-facing total is the sum of both. @@ -204,11 +204,11 @@ def test_creation_tx_intrinsic_across_amsterdam_transition( ) init_code_terms = pre_costs.TX_DATA_TOKEN_STANDARD + 2 - # Pre-fork: flat regular intrinsic, no top-frame charge. + # Pre-fork: flat execution intrinsic, no top-frame charge. expected_pre = pre_costs.TX_BASE + pre_costs.TX_CREATE + init_code_terms # Post-fork: EIP-8037 folds ``NEW_ACCOUNT`` into ``TX_CREATE``; # EIP-2780 moves that state portion to the top frame, leaving the - # ``CREATE_ACCESS`` regular remainder in the intrinsic. + # ``CREATE_ACCESS`` execution remainder in the intrinsic. expected_post = ( post_costs.TX_BASE + (post_costs.TX_CREATE - post_costs.NEW_ACCOUNT) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py index 2292edae54e..6ecaf65362a 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py @@ -95,7 +95,7 @@ def test_intrinsic_gas_floor_boundary_contract_creation( A creation tx's intrinsic includes the ``NEW_ACCOUNT`` state gas, so the pre-execution check rejects against the combined - ``regular + state`` intrinsic. The init code never runs. + ``execution + state`` intrinsic. The init code never runs. """ sender = pre.fund_eoa(10**18) init_code = Op.STOP diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index 9d59d9d7be8..ca889f3f525 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -8,12 +8,12 @@ - ``NEW_ACCOUNT`` (state gas) when the recipient is empty and the transaction transfers value, or when a creation transaction's target leaf did not exist before the transaction. -- ``COLD_ACCOUNT_ACCESS`` (regular gas) when the recipient holds an +- ``COLD_ACCOUNT_ACCESS`` (execution gas) when the recipient holds an EIP-7702 delegation. Each test parametrizes over the interesting outcomes for that charge: running out of gas at the boundary, succeeding through the charge and -into the EVM, and (for the regular charge) succeeding through the +into the EVM, and (for the execution charge) succeeding through the charge but reverting from the delegated code. For creation transactions, the charge keys on the *transaction pre-state* being empty, and — being consumed on any successful halt — survives the @@ -179,14 +179,14 @@ def test_top_frame_new_account_charged_as_state_gas( ) -> None: """ The top-frame ``NEW_ACCOUNT`` charge for a value transfer to an - empty recipient is *state* gas, not regular gas. This pins the + empty recipient is *state* gas, not execution gas. This pins the dimension via the block header ``gas_used``, which the spec - computes as ``max(block_regular_gas, block_state_gas)``. + computes as ``max(block_execution_gas, block_state_gas)``. Correctly attributed, the ``NEW_ACCOUNT`` state gas dominates the - small regular intrinsic, so ``gas_used == NEW_ACCOUNT``. A - regression mis-classifying the charge as regular gas would instead - yield ``intrinsic_regular + NEW_ACCOUNT``. + small execution intrinsic, so ``gas_used == NEW_ACCOUNT``. A + regression mis-classifying the charge as execution gas would instead + yield ``intrinsic_execution + NEW_ACCOUNT``. ``state_test``-based balance assertions (e.g. ``test_top_frame_state_charge``) only observe the *sum* of the two @@ -197,7 +197,7 @@ def test_top_frame_new_account_charged_as_state_gas( target = pre.fund_eoa(amount=0) value = 1 - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( sends_value=True, recipient_type=RecipientType.EMPTY_ACCOUNT, return_cost_deducted_prior_execution=True, @@ -206,26 +206,26 @@ def test_top_frame_new_account_charged_as_state_gas( sends_value=True, recipient_type=RecipientType.EMPTY_ACCOUNT, ) - # The state charge must dominate the regular intrinsic for the - # header ``gas_used`` to distinguish a state vs regular + # The state charge must dominate the execution intrinsic for the + # header ``gas_used`` to distinguish a state vs execution # mis-classification. - assert new_account_state_gas > intrinsic_regular, ( + assert new_account_state_gas > intrinsic_execution, ( "test only distinguishes the dimension when NEW_ACCOUNT " - f"({new_account_state_gas}) dominates the regular intrinsic " - f"({intrinsic_regular})" + f"({new_account_state_gas}) dominates the execution intrinsic " + f"({intrinsic_execution})" ) - # No EVM bytecode runs (empty recipient), so the only regular gas + # No EVM bytecode runs (empty recipient), so the only execution gas # is the intrinsic and the only state gas is the top-frame # ``NEW_ACCOUNT`` charge. - expected_gas_used = max(intrinsic_regular, new_account_state_gas) + expected_gas_used = max(intrinsic_execution, new_account_state_gas) gas_price = 1_000_000_000 tx = Transaction( sender=sender, to=target, value=value, - gas_limit=intrinsic_regular + new_account_state_gas + 1000, + gas_limit=intrinsic_execution + new_account_state_gas + 1000, gas_price=gas_price, ) @@ -439,7 +439,7 @@ def test_top_frame_new_account_skipped_for_create_target_funded_same_block( The funding transaction pays its own top-frame ``NEW_ACCOUNT`` for materializing the leaf, which the block header pins as the block's - entire state-gas dimension: ``gas_used = max(regular, state)`` must + entire state-gas dimension: ``gas_used = max(execution, state)`` must equal exactly one ``NEW_ACCOUNT``. """ funder = pre.fund_eoa() @@ -503,11 +503,11 @@ def test_top_frame_new_account_skipped_for_create_target_funded_same_block( ) # Header pin: the block's state dimension is exactly the funding - # transaction's ``NEW_ACCOUNT``; both regular intrinsics sit at or + # transaction's ``NEW_ACCOUNT``; both execution intrinsics sit at or # above their calldata floors, so no floor term enters the block's - # regular dimension either. - block_regular = fund_intrinsic + create_total - assert fund_state_gas > block_regular, ( + # execution dimension either. + block_execution = fund_intrinsic + create_total + assert fund_state_gas > block_execution, ( "the state dimension must dominate for the header to pin it" ) @@ -537,7 +537,7 @@ def test_top_frame_new_account_skipped_for_create_target_funded_same_block( pytest.param(1, id="non-zero_value"), ], ) -def test_top_frame_regular_charge( +def test_top_frame_execution_charge( fork: Fork, pre: Alloc, state_test: StateTestFiller, @@ -546,15 +546,15 @@ def test_top_frame_regular_charge( ) -> None: """ Recipient is an existing EIP-7702 delegation, so the top-frame - fires the ``COLD_ACCOUNT_ACCESS`` regular-gas charge regardless of + fires the ``COLD_ACCOUNT_ACCESS`` execution-gas charge regardless of whether the transaction transfers value. - - ``oog``: gas limit is one short of covering the regular charge + - ``oog``: gas limit is one short of covering the execution charge (plus the value-transfer charge when ``value > 0``). The transaction OOGs at ``charge_gas(COLD_ACCOUNT_ACCESS)`` before the delegated code runs. The sender pays the full ``gas_limit`` and the recipient keeps its pre-tx state. - - ``success``: gas limit covers the regular charge; the delegated + - ``success``: gas limit covers the execution charge; the delegated code is a ``STOP`` and the transaction lands the value transfer. - ``evm_reverts``: the delegated code reverts immediately. The top-frame charge is consumed before dispatch and the two @@ -584,7 +584,7 @@ def test_top_frame_regular_charge( recipient_type=RecipientType.DELEGATION_7702, ) assert top_frame_gas > 0, ( - "top-frame regular gas must be non-zero for this scenario" + "top-frame execution gas must be non-zero for this scenario" ) gas_price = 1_000_000_000 @@ -691,12 +691,12 @@ def test_initcode_selfdestruct_keeps_top_frame_state_charge( beneficiary = pre.nonexistent_account() # Sweeping a non-zero balance into a non-existent leaf creates # the beneficiary, paying NEW_ACCOUNT (state) and ACCOUNT_WRITE - # (regular) at the opcode. + # (execution) at the opcode. init_code = Op.SELFDESTRUCT.with_metadata( address_warm=False, account_new=bool(value) )(beneficiary) - # Combined regular + state execution gas, including any sweep + # Combined execution + state execution gas, including any sweep # charges modeled by the metadata above. exec_gas = init_code.gas_cost(fork) @@ -756,14 +756,14 @@ def test_initcode_selfdestruct_state_gas_in_header( dimensions, so the sibling ``test_initcode_selfdestruct_keeps_top_frame_state_charge`` cannot distinguish which dimension the surviving charge settled into. The - block header can: ``gas_used = max(block_regular, block_state)``, + block header can: ``gas_used = max(block_execution, block_state)``, and with a zero endowment and a self beneficiary the whole created account vanishes while the state side (one ``NEW_ACCOUNT``, - dominating the small regular side) must still show in the header. + dominating the small execution side) must still show in the header. Bug signatures: a refill regression collapses the header to the - small regular sum; a regular-gas mis-classification raises it to - ``regular + NEW_ACCOUNT``. + small execution sum; an execution-gas mis-classification raises it to + ``execution + NEW_ACCOUNT``. """ sender = pre.fund_eoa() created = compute_create_address(address=sender, nonce=sender.nonce) @@ -771,7 +771,7 @@ def test_initcode_selfdestruct_state_gas_in_header( init_code = Op.SELFDESTRUCT.with_metadata( address_warm=True, account_new=False )(Op.ADDRESS) - exec_regular = init_code.regular_cost(fork) + evm_execution = init_code.execution_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( calldata=init_code, @@ -785,14 +785,14 @@ def test_initcode_selfdestruct_state_gas_in_header( data=init_code, contract_creation=True, ) - # Block accounting carries the calldata floor in the regular + # Block accounting carries the calldata floor in the execution # dimension. - regular_side = max(intrinsic_gas + exec_regular, calldata_floor) - assert state_side > regular_side, ( + execution_side = max(intrinsic_gas + evm_execution, calldata_floor) + assert state_side > execution_side, ( "the state dimension must dominate for the header to pin it" ) - total_gas = intrinsic_gas + state_side + exec_regular + total_gas = intrinsic_gas + state_side + evm_execution tx = Transaction( sender=sender, to=None, @@ -821,7 +821,7 @@ class TopFrameFailureMode(Enum): CREATE_STATE_OOG = auto() NEW_ACCOUNT_STATE_OOG = auto() - DELEGATED_REGULAR_OOG = auto() + DELEGATED_EXECUTION_OOG = auto() @pytest.mark.parametrize( @@ -836,8 +836,8 @@ class TopFrameFailureMode(Enum): id="new_account_state_oog", ), pytest.param( - TopFrameFailureMode.DELEGATED_REGULAR_OOG, - id="delegated_regular_oog", + TopFrameFailureMode.DELEGATED_EXECUTION_OOG, + id="delegated_execution_oog", ), ], ) @@ -873,8 +873,8 @@ def test_receipt_status_top_frame_oog_between_successful_txs( - ``new_account_state_oog``: value transfer to an empty recipient; the ``NEW_ACCOUNT`` state charge fires and the gas limit is one short. - - ``delegated_regular_oog``: recipient holds an EIP-7702 - delegation; the ``COLD_ACCOUNT_ACCESS`` regular charge fires and + - ``delegated_execution_oog``: recipient holds an EIP-7702 + delegation; the ``COLD_ACCOUNT_ACCESS`` execution charge fires and the gas limit is one short. The failing transaction burns its full gas limit, bumps the sender @@ -929,7 +929,7 @@ def test_receipt_status_top_frame_oog_between_successful_txs( # The rolled-back transfer must not bring the recipient into # existence. fail_target_post = None - elif failure_mode is TopFrameFailureMode.DELEGATED_REGULAR_OOG: + elif failure_mode is TopFrameFailureMode.DELEGATED_EXECUTION_OOG: delegated_to = pre.deploy_contract(code=Op.STOP) target_code = Spec7702.delegation_designation(delegated_to) fail_to = pre.deploy_contract(code=target_code) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py index 42d694f0c2f..05fd5d9aef1 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -91,7 +91,7 @@ def test_value_moving_transactions( recipient_type=recipient_type, ) # Under the default zero state-gas reservoir, top-frame state gas - # spills entirely into regular gas. + # spills entirely into execution gas. total_gas_cost = intrinsic_gas + top_frame_gas + top_frame_state_gas tx_gas_limit = total_gas_cost @@ -159,7 +159,7 @@ def test_value_contract_creation_tx( When the init code reverts, the deploy is rolled back: no code is set, the value transfer is reversed, and the top-frame ``NEW_ACCOUNT`` state-gas charge for the created account is - refilled. The sender therefore pays only the regular intrinsic + refilled. The sender therefore pays only the execution intrinsic plus the few EVM gas units spent before the revert -- the ``NEW_ACCOUNT`` charge does not appear on the receipt. """ @@ -194,7 +194,7 @@ def test_value_contract_creation_tx( # charge is refilled and does not appear on the receipt. gas_used = intrinsic_gas + execution_gas # A tiny init code can leave the decomposed calldata floor above - # the regular gas actually consumed; gas_used then pins to the + # the execution gas actually consumed; gas_used then pins to the # floor, which EIP-2780 anchors on the create intrinsic base. gas_used = max( gas_used, diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py index 14e9d4a1fee..71b3d790b6a 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py @@ -6,7 +6,7 @@ Each authorization pays, on top of the state-independent ``REGULAR_PER_AUTH_BASE_COST`` charged in the intrinsic: -- ``NEW_ACCOUNT`` (state) + ``ACCOUNT_WRITE`` (regular) when the +- ``NEW_ACCOUNT`` (state) + ``ACCOUNT_WRITE`` (execution) when the authority's account leaf does not yet exist, and - ``AUTH_BASE`` (state) when a net-new delegation indicator is written. @@ -106,7 +106,7 @@ def test_tx_installs_delegation_on_funded_recipient( authorization_list_or_count=authorization_list, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( sends_value=bool(value), recipient_type=RecipientType.DELEGATION_7702, delegation_warm=False, @@ -119,8 +119,8 @@ def test_tx_installs_delegation_on_funded_recipient( ) # Costs are charged exactly (no refund); under the default zero - # state-gas reservoir the state gas spills into regular gas. - total_gas_cost = intrinsic_gas + top_frame_regular + top_frame_state + # state-gas reservoir the state gas spills into execution gas. + total_gas_cost = intrinsic_gas + top_frame_execution + top_frame_state tx_gas_limit = total_gas_cost + 1000 gas_price = 1_000_000_000 @@ -203,7 +203,7 @@ def test_tx_installs_delegation_on_empty_recipient( authorization_list_or_count=authorization_list, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( sends_value=bool(value), recipient_type=RecipientType.DELEGATION_7702, delegation_warm=False, @@ -215,7 +215,7 @@ def test_tx_installs_delegation_on_empty_recipient( authorizations=authorization_list, ) - total_gas_cost = intrinsic_gas + top_frame_regular + top_frame_state + total_gas_cost = intrinsic_gas + top_frame_execution + top_frame_state tx_gas_limit = total_gas_cost + 1000 gas_price = 1_000_000_000 @@ -333,7 +333,7 @@ def test_tx_installs_delegation_on_sender( authorization_list_or_count=authorization_list, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( sends_value=bool(value), recipient_type=top_frame_recipient_type, delegation_warm=False, @@ -345,7 +345,7 @@ def test_tx_installs_delegation_on_sender( authorizations=authorization_list, ) - total_gas_cost = intrinsic_gas + top_frame_regular + top_frame_state + total_gas_cost = intrinsic_gas + top_frame_execution + top_frame_state tx_gas_limit = total_gas_cost + 1000 gas_price = 1_000_000_000 diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py index 987ecf51784..21c1c67cbca 100644 --- a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py @@ -88,14 +88,14 @@ def build_refund_tx( storage=dict.fromkeys(storage_slots, 1), ) - # Combined gas (regular + state) from intrinsic cost calculator + # Combined gas (execution + state) from intrinsic cost calculator combined_gas_used = intrinsic_cost_calc( calldata=call_data, return_cost_deducted_prior_execution=True, authorization_list_or_count=authorization_list, ) + code.gas_cost(fork) - # EIP-8037: block gas_used only counts regular gas + # EIP-8037: block gas_used only counts execution gas gas_used_pre_refund = combined_gas_used # Calculate refund (still applied to user's balance) @@ -108,7 +108,7 @@ def build_refund_tx( remaining_state_gas = 0 # In the spec, the refund cap uses tx_gas_used_before_refund which is - # tx.gas - gas_left - state_gas_left (combined regular + remaining + # tx.gas - gas_left - state_gas_left (combined execution + remaining # state). combined_before_refund = gas_used_pre_refund + remaining_state_gas @@ -123,7 +123,7 @@ def build_refund_tx( gas_used_post_refund = receipt_gas_used refund_tx_gas_used = max(call_data_floor_cost, gas_used_post_refund) - # gas_limit must cover combined gas (regular + state) + # gas_limit must cover combined gas (execution + state) refund_tx_gas_limit = ( max(call_data_floor_cost, combined_gas_used) + refund_tx_extra_gas ) @@ -217,10 +217,10 @@ def test_simple_gas_accounting( refund_tx_reverts=refund_tx_reverts, ) - # EIP-8037: block gas_used = max(block_regular_gas, block_state_gas), - # with the calldata floor binding the regular dimension. - block_regular = max(gas_used_pre_refund, call_data_floor_cost) - refund_tx_block_gas_used = max(block_regular, tx_state_gas) + # EIP-8037: block gas_used = max(block_execution_gas, block_state_gas), + # with the calldata floor binding the execution dimension. + block_execution = max(gas_used_pre_refund, call_data_floor_cost) + refund_tx_block_gas_used = max(block_execution, tx_state_gas) blockchain_test( pre=pre, @@ -318,7 +318,7 @@ def test_multi_transaction_gas_accounting( extra_tx_intrinsic_gas_cost = intrinsic_cost_calc( calldata=extra_tx_calldata ) - # Block regular gas applies the calldata floor to the actual charge. + # Block execution gas applies the calldata floor to the actual charge. extra_tx_block_gas = max( intrinsic_cost_calc( calldata=extra_tx_calldata, @@ -342,13 +342,13 @@ def test_multi_transaction_gas_accounting( ), ) - # EIP-8037: block_gas_used = max(sum_regular, sum_state) + # EIP-8037: block_gas_used = max(sum_execution, sum_state) # Extra tx has no state gas, so its state gas contribution = 0 - block_regular = gas_used_pre_refund + extra_tx_block_gas + block_execution = gas_used_pre_refund + extra_tx_block_gas block_state = tx_state_gas - total_block_gas_used = max(block_regular, block_state) + total_block_gas_used = max(block_execution, block_state) # The block gas_limit must accommodate extra_tx's full gas_limit - # (floor-inclusive, like its block-regular charge). For + # (floor-inclusive, like its block-execution charge). For # exceed_block_gas_limit=True we set the limit below # total_block_gas_used to test that the extra_tx fails. if exceed_block_gas_limit: @@ -513,10 +513,10 @@ def test_varying_calldata_costs( f"Could not find the call_data with {num_iterations} iterations." ) - # EIP-8037: block gas_used = max(block_regular_gas, block_state_gas), - # with the calldata floor binding the regular dimension. - block_regular = max(gas_used_pre_refund, call_data_floor_cost) - refund_tx_block_gas_used = max(block_regular, tx_state_gas) + # EIP-8037: block gas_used = max(block_execution_gas, block_state_gas), + # with the calldata floor binding the execution dimension. + block_execution = max(gas_used_pre_refund, call_data_floor_cost) + refund_tx_block_gas_used = max(block_execution, tx_state_gas) blockchain_test( pre=pre, @@ -566,10 +566,10 @@ def test_multiple_refund_types_in_one_tx( refund_tx_reverts=refund_tx_reverts, ) - # EIP-8037: block gas_used = max(block_regular_gas, block_state_gas), - # with the calldata floor binding the regular dimension. - block_regular = max(gas_used_pre_refund, call_data_floor_cost) - refund_tx_block_gas_used = max(block_regular, tx_state_gas) + # EIP-8037: block gas_used = max(block_execution_gas, block_state_gas), + # with the calldata floor binding the execution dimension. + block_execution = max(gas_used_pre_refund, call_data_floor_cost) + refund_tx_block_gas_used = max(block_execution, tx_state_gas) blockchain_test( pre=pre, @@ -599,7 +599,7 @@ def test_mixed_gas_regimes( tx3: 1000 zero-byte calldata to STOP (floor binds fee and block gas). The floor binds the tx-level fee (tx_gas_used = max(post_refund, - floor)) and the block's regular dimension (max(pre_refund gas minus + floor)) and the block's execution dimension (max(pre_refund gas minus state gas, floor)) alike. Per-tx sender balance is also asserted to lock in that the floor-binding tx pays `floor * gas_price`, not `pre_refund * gas_price`. @@ -615,7 +615,7 @@ def test_mixed_gas_regimes( tx1_target = pre.deploy_contract(code=tx1_code) tx1_sender = pre.fund_eoa(initial_fund) tx1_data = b"" - # Full intrinsic + execution gas (regular + state) sizes the gas limit + # Full intrinsic + execution gas (execution + state) sizes the gas limit # and the balance charged to the sender. tx1_pre_refund = intrinsic_cost_calc( calldata=tx1_data, @@ -624,7 +624,7 @@ def test_mixed_gas_regimes( tx1_floor = data_floor_calc(data=tx1_data) assert tx1_pre_refund > tx1_floor, "tx1: pre_refund must exceed floor" tx1_contribution = max(tx1_pre_refund, tx1_floor) - # EIP-8037: block gas_used counts only regular gas; the SSTORE-set + # EIP-8037: block gas_used counts only execution gas; the SSTORE-set # state gas lives in the separate state dimension, so the block-level # contribution excludes it. tx1_block_contribution = max( @@ -678,7 +678,7 @@ def test_mixed_gas_regimes( tx3_floor = data_floor_calc(data=tx3_data) assert tx3_floor > tx3_pre_refund, "tx3: floor must bind upward" tx3_fee_gas = max(tx3_pre_refund, tx3_floor) - # The floor binds the block's regular dimension as well as the fee. + # The floor binds the block's execution dimension as well as the fee. tx3_block_contribution = max(tx3_pre_refund, tx3_floor) tx3 = Transaction( to=tx3_target, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index b59367bfb09..d9b50654535 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -3060,7 +3060,7 @@ def test_bal_cross_tx_funding_chain( # to recipients that begin empty, so each pays the value-transfer # intrinsic surcharges plus the top-frame ``NEW_ACCOUNT`` state # charge that fires under EIP-2780. With the default zero - # state-gas reservoir the latter spills entirely into regular gas. + # state-gas reservoir the latter spills entirely into execution gas. forwarding_intrinsic = intrinsic_calc( sends_value=True, recipient_type=RecipientType.EMPTY_ACCOUNT, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py index d5b6f59bbd4..5ece8d98570 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py @@ -2,7 +2,7 @@ Tests for EIP-7928 BAL cross-index tracking. Tests that state changes are correctly tracked across different block indices: -- Index 1..N: Regular transactions +- Index 1..N: Execution transactions - Index N+1: Post-execution system operations Includes tests for system contracts (withdrawal/consolidation) cross-index diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index fd8a11c1fff..cc0f6fa9aaa 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -581,7 +581,7 @@ def test_bal_7702_recipient_excluded_on_authorization_oog( auth = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT) authorization_list = [auth.authorization] - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( recipient_type=RecipientType.CONTRACT, authorization_list_or_count=authorization_list, return_cost_deducted_prior_execution=True, @@ -592,12 +592,12 @@ def test_bal_7702_recipient_excluded_on_authorization_oog( if outcome == "oog": # The authorization runs out at its opening NEW_ACCOUNT state # charge, drawn from gas_left under the zero state reservoir. - gas_limit = intrinsic_regular + fork.gas_costs().NEW_ACCOUNT - 1 + gas_limit = intrinsic_execution + fork.gas_costs().NEW_ACCOUNT - 1 recipient_expectation = None authority_expectation = BalAccountExpectation.empty() expected_authority = auth.original_account else: - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) @@ -605,7 +605,7 @@ def test_bal_7702_recipient_excluded_on_authorization_oog( recipient_type=RecipientType.CONTRACT, authorizations=authorization_list, ) - gas_limit = intrinsic_regular + top_frame_regular + top_frame_state + gas_limit = intrinsic_execution + top_frame_execution + top_frame_state recipient_expectation = BalAccountExpectation.empty() authority_expectation = BalAccountExpectation( nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)], diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py index 3392b80ff0a..e6ca49ddbd1 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py @@ -368,7 +368,7 @@ def test_bal_account_touch_system_address( access_opcode: Callable[[Address], Bytecode], ) -> None: """ - Ensure a regular transaction that explicitly touches SYSTEM_ADDRESS via + Ensure a normal transaction that explicitly touches SYSTEM_ADDRESS via an account-accessing opcode includes SYSTEM_ADDRESS as an account-only BAL entry. @@ -3092,7 +3092,7 @@ def test_bal_transient_storage_not_tracked( """ alice = pre.fund_eoa() - # Contract that uses transient storage then persists to regular storage + # Contract that uses transient storage then persists to execution storage contract_code = ( # TSTORE slot 0x01 with value 0x42 (transient storage) Op.TSTORE(0x01, 0x42) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py index 808d911d3cd..d6b52684e67 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py @@ -162,7 +162,7 @@ def test_max_code_size_deposit_gas( gas_limit=( intrinsic_gas + top_frame_state_gas - + initcode.execution_gas(fork) + + initcode.evm_gas(fork) + initcode.deployment_gas(fork) - gas_shortfall ), diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py index 8a19de64cec..7392728dc57 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py @@ -702,16 +702,16 @@ def test_authorization_list_intrinsic_gas( Verify the authorization-list intrinsic cost under EIP-2780. Each authorization adds exactly ``REGULAR_PER_AUTH_BASE_COST`` to - the (regular) intrinsic; the state-dependent authorization costs + the (execution) intrinsic; the state-dependent authorization costs moved to the top frame. Measured on the *raw* intrinsic (before the EIP-7623 calldata floor is applied) the per-authorization delta is exactly ``num_authorizations * REGULAR_PER_AUTH_BASE_COST`` -- even when the floor would otherwise mask it (e.g. a single authorization whose base cost stays below the floor). Each existing authority then pays the - first-write ``ACCOUNT_WRITE`` (regular) and ``AUTH_BASE`` + first-write ``ACCOUNT_WRITE`` (execution) and ``AUTH_BASE`` (state) at the top frame, so with a STOP recipient the receipt - is ``max(intrinsic_regular + num_authorizations * + is ``max(intrinsic_execution + num_authorizations * (ACCOUNT_WRITE + AUTH_BASE), floor_cost)``. """ gas_costs = fork.gas_costs() @@ -762,17 +762,17 @@ def test_authorization_list_intrinsic_gas( data=calldata ) # Existing authorities pay the first-write ACCOUNT_WRITE - # (regular) and AUTH_BASE (state) each at the top frame; the + # (execution) and AUTH_BASE (state) each at the top frame; the # STOP recipient does no execution, so the receipt is - # max(regular + state, floor). - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + # max(execution + state, floor). + top_frame_execution = fork.transaction_top_frame_gas_calculator()( authorizations=authorization_list, ) top_frame_state = fork.transaction_top_frame_state_gas( authorizations=authorization_list, ) expected_gas = max( - intrinsic_with_auth + top_frame_regular + top_frame_state, + intrinsic_with_auth + top_frame_execution + top_frame_state, floor_cost, ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py index 1dddc735d39..8b2be28ff78 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py @@ -49,7 +49,7 @@ def test_below_amsterdam_floor_with_exact_balance_sender( `test_transaction_validity.py`. """ tx_data = Bytes(b"\x00" * zero_bytes) - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( calldata=tx_data, return_cost_deducted_prior_execution=True, ) @@ -62,7 +62,7 @@ def test_below_amsterdam_floor_with_exact_balance_sender( # (zero/nonzero both weighted by 4). prague_floor = 21000 + Spec7623.TX_DATA_TOKEN_FLOOR * zero_bytes gas_limit = (prague_floor + amsterdam_floor) // 2 - assert intrinsic_regular <= gas_limit + assert intrinsic_execution <= gas_limit assert prague_floor <= gas_limit < amsterdam_floor gas_price = 10 diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py index a6ddbcc7747..60eafcd0efb 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py @@ -57,7 +57,7 @@ def test_below_amsterdam_floor_with_access_list_exact_balance( ) ] tx_data = Bytes(b"\x01" * nonzero_bytes) - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( calldata=tx_data, access_list=access_list, return_cost_deducted_prior_execution=True, @@ -68,7 +68,7 @@ def test_below_amsterdam_floor_with_access_list_exact_balance( # Pin gas_limit inside the access-list-byte uplift gap so an # implementation that omits this term from its floor accepts. gas_limit = (amsterdam_floor_no_al + amsterdam_floor) // 2 - assert intrinsic_regular <= gas_limit < amsterdam_floor + assert intrinsic_execution <= gas_limit < amsterdam_floor assert gas_limit >= amsterdam_floor_no_al gas_price = 10 diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py index e807c34ebd4..ced4110a55e 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py @@ -47,10 +47,10 @@ class Spec: STATE_BYTES_PER_STORAGE_SET = 64 STATE_BYTES_PER_AUTH_BASE = 23 - # Regular gas constants. EIP-8037 separated state from regular gas; + # Execution gas constants. EIP-8037 separated state from execution gas; # EIP-8038 then repriced them. - REGULAR_GAS_CREATE = 11000 - # Total regular intrinsic per EIP-7702 authorization: + EXECUTION_GAS_CREATE = 11000 + # Total execution intrinsic per EIP-7702 authorization: # ACCOUNT_WRITE (8000) + REGULAR_PER_AUTH_BASE_COST (7816). PER_AUTH_BASE_COST = 15816 GAS_COLD_STORAGE_WRITE = 13000 diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py index 662af4980f2..c1d358afc6a 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py @@ -2,7 +2,7 @@ Test block-level two-dimensional gas accounting under EIP-8037. Verify that the block header gas_used equals -max(block_regular_gas_used, block_state_gas_used) across +max(block_execution_gas_used, block_state_gas_used) across single-block, multi-block, and mixed-transaction scenarios. Tests for [EIP-8037: State Creation Gas Cost Increase] @@ -39,9 +39,9 @@ def sstore_tx_gas(fork: Fork, num_sstores: int = 1) -> tuple[int, int]: - """Return (regular, state) gas for a tx with N cold SSTOREs.""" + """Return (execution, state) gas for a tx with N cold SSTOREs.""" intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() - evm_total = num_sstores * Op.SSTORE(0, 1).regular_cost(fork) + evm_total = num_sstores * Op.SSTORE(0, 1).execution_cost(fork) state = num_sstores * Op.SSTORE(new_value=1).state_cost(fork) return intrinsic_gas + evm_total, state @@ -112,20 +112,20 @@ def test_block_gas_used_state_dominates( num_sstores: int, ) -> None: """ - Verify block.gas_used = block_state_gas when state > regular. + Verify block.gas_used = block_state_gas when state > execution. Each tx performs zero-to-nonzero SSTOREs. Since state gas per - SSTORE exceeds regular gas, block_state_gas exceeds - block_regular_gas and becomes the header gas_used. + SSTORE exceeds execution gas, block_state_gas exceeds + block_execution_gas and becomes the header gas_used. The spillover variant provides reservoir for only one SSTORE per tx; the remaining state gas spills into gas_left. Block-level accounting must still separate the two dimensions. """ - tx_regular, tx_state = sstore_tx_gas(fork, num_sstores) - block_regular = num_txs * tx_regular + tx_execution, tx_state = sstore_tx_gas(fork, num_sstores) + block_execution = num_txs * tx_execution block_state = num_txs * tx_state - assert block_state > block_regular + assert block_state > block_execution txs, post = sstore_txs( pre, @@ -146,18 +146,18 @@ def test_block_gas_used_state_dominates( @pytest.mark.valid_from("EIP8037") -def test_block_gas_used_regular_dominates( +def test_block_gas_used_execution_dominates( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - Verify block.gas_used = block_regular_gas when state gas is zero. + Verify block.gas_used = block_execution_gas when state gas is zero. A block containing only STOP transactions to existing contracts produces no state gas. The block header gas_used must equal the - sum of regular gas across all transactions, since - max(regular, 0) = regular. + sum of execution gas across all transactions, since + max(execution, 0) = execution. """ num_txs = 3 intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() @@ -194,16 +194,18 @@ def test_block_gas_used_mixed_txs( """ Verify block.gas_used with mixed STOP and SSTORE transactions. - STOP txs contribute only regular gas; SSTORE txs contribute both. + STOP txs contribute only execution gas; SSTORE txs contribute both. The interleaved variant alternates SSTORE/STOP to test that non-contiguous state gas contributions accumulate correctly. """ intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() - tx_regular_sstore, tx_state_sstore = sstore_tx_gas(fork) + tx_execution_sstore, tx_state_sstore = sstore_tx_gas(fork) - block_regular = num_stop * intrinsic_gas + num_sstore * tx_regular_sstore + block_execution = ( + num_stop * intrinsic_gas + num_sstore * tx_execution_sstore + ) block_state = num_sstore * tx_state_sstore - expected = max(block_regular, block_state) + expected = max(block_execution, block_state) txs_sstore, post = sstore_txs(pre, fork, num_sstore) txs_stop = stop_txs(pre, fork, num_stop) @@ -239,7 +241,7 @@ def test_block_gas_refund_eip7778_no_block_reduction( """ Verify block gas accounting for SSTORE 0→x→0 refund paths. - Regular gas refund via `refund_counter` does NOT reduce block gas + Execution gas refund via `refund_counter` does NOT reduce block gas (EIP-7778). State gas refund goes to the reservoir and DOES reduce `block_state_gas_used` (net zero state growth). """ @@ -254,8 +256,8 @@ def test_block_gas_refund_eip7778_no_block_reduction( current_value=1, new_value=0, )(0, 0) - tx_regular = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas - expected = num_txs * tx_regular + tx_execution = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas + expected = num_txs * tx_execution txs = [] for _ in range(num_txs): contract = pre.deploy_contract(code=code) @@ -297,9 +299,9 @@ def test_block_2d_gas_boundary_exact_fit( num_sstores: int, ) -> None: """ - Verify a block is valid when state gas dominates regular gas. + Verify a block is valid when state gas dominates execution gas. - Clients that sum regular + state will reject this valid block. + Clients that sum execution + state will reject this valid block. """ block_gas_limit = 30_000_000 while True: @@ -311,17 +313,17 @@ def test_block_2d_gas_boundary_exact_fit( env = Environment( gas_limit=block_gas_limit, ) - tx_regular, tx_state = sstore_tx_gas(fork, num_sstores) - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()() + tx_execution, tx_state = sstore_tx_gas(fork, num_sstores) + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()() - tx_limit = tx_regular + tx_state + tx_regular // 10 + tx_limit = tx_execution + tx_state + tx_execution // 10 - # Per-tx worst-case state contribution: tx.gas - intrinsic_regular. + # Per-tx worst-case state contribution: tx.gas - intrinsic_execution. # The block_gas_limit must leave enough state budget for every tx. - worst_state_per_tx = tx_limit - intrinsic_regular + worst_state_per_tx = tx_limit - intrinsic_execution minimum_block_gas_limit = max( - # Regular dimension: last tx must fit. - (num_txs - 1) * tx_regular + tx_limit, + # Execution dimension: last tx must fit. + (num_txs - 1) * tx_execution + tx_limit, # State dimension: cumulative worst-case must fit. num_txs * worst_state_per_tx, ) @@ -329,9 +331,9 @@ def test_block_2d_gas_boundary_exact_fit( break block_gas_limit += 1_000_000 - block_regular = num_txs * tx_regular + block_execution = num_txs * tx_execution block_state = num_txs * tx_state - expected_gas_used = max(block_regular, block_state) + expected_gas_used = max(block_execution, block_state) txs, post = sstore_txs( pre, @@ -416,16 +418,16 @@ def test_block_gas_used_create_tx( create_state_gas = fork.create_state_gas(code_size=0) init_code = bytes(Op.STOP) - create_regular = ( + create_execution = ( intrinsic_calc( calldata=init_code, contract_creation=True, ) - create_state_gas ) - stop_regular = intrinsic_calc() + stop_execution = intrinsic_calc() - expected = max(create_regular + stop_regular, create_state_gas) + expected = max(create_execution + stop_execution, create_state_gas) txs = [ Transaction( @@ -457,13 +459,13 @@ def test_multi_block_dimension_flip( """ Verify gas_used across blocks where dominant dimension flips. - Block 1: STOP txs only (regular dominates). + Block 1: STOP txs only (execution dominates). Block 2: SSTORE txs only (state dominates). Each block independently computes its own 2D max. """ n = 3 intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() - tx_regular, tx_state = sstore_tx_gas(fork) + tx_execution, tx_state = sstore_tx_gas(fork) block_1 = stop_txs(pre, fork, n) block_2, post_2 = sstore_txs(pre, fork, n) @@ -478,7 +480,7 @@ def test_multi_block_dimension_flip( Block( txs=block_2, header_verify=Header( - gas_used=max(n * tx_regular, n * tx_state), + gas_used=max(n * tx_execution, n * tx_state), ), ), ], @@ -538,7 +540,7 @@ def test_tx_gas_limit_block_boundary( Reject tx whose ``gas_limit`` exceeds the block ``gas_limit``. EIP-8037 inclusion rule: ``min(TX_MAX_GAS_LIMIT, tx.gas) <= - regular_gas_available`` and ``tx.gas <= state_gas_available``. + execution_gas_available`` and ``tx.gas <= state_gas_available``. At block start both budgets equal ``block_gas_limit``. """ gas_limit = block_gas_limit + tx_gas_delta @@ -612,16 +614,16 @@ def test_tx_gas_limit_block_boundary( # EIP-8037 novelty. Floor is Osaka only because the gas-cap guard # below relies on EIP-7825's transaction_gas_limit_cap(). @pytest.mark.valid_from("Osaka") -def test_tx_inclusion_at_regular_gas_block_limit_small( +def test_tx_inclusion_at_execution_gas_block_limit_small( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, delta: int, ) -> None: """ - Probe the regular-gas inclusion boundary with a small-gas tx. + Probe the execution-gas inclusion boundary with a small-gas tx. - The second tx's ``gas_limit`` is the remaining regular budget + The second tx's ``gas_limit`` is the remaining execution budget plus ``delta``. The inclusion check uses strict ``>``, so ``delta=0`` must pass and ``delta=1`` must reject with ``GAS_ALLOWANCE_EXCEEDED``. Catches an off-by-one ``>=`` bug. @@ -683,7 +685,7 @@ def test_tx_inclusion_at_regular_gas_block_limit_small( ], ) @pytest.mark.valid_from("EIP8037") -def test_block_2d_gas_tx_gas_limit_exceeds_regular_remaining( +def test_block_2d_gas_tx_gas_limit_exceeds_execution_remaining( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, @@ -691,7 +693,7 @@ def test_block_2d_gas_tx_gas_limit_exceeds_regular_remaining( ) -> None: """ Verify a block is valid when a later tx's gas_limit exceeds the - regular budget remaining but its capped regular contribution fits. + execution budget remaining but its capped execution contribution fits. """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None @@ -714,9 +716,9 @@ def test_block_2d_gas_tx_gas_limit_exceeds_regular_remaining( code=Op.SSTORE(storage.store_next(1), 1), ) - tx1_regular = intrinsic_gas - tx2_regular, tx2_state = sstore_tx_gas(fork) - expected_gas_used = max(tx1_regular + tx2_regular, tx2_state) + tx1_execution = intrinsic_gas + tx2_execution, tx2_state = sstore_tx_gas(fork) + expected_gas_used = max(tx1_execution + tx2_execution, tx2_state) blockchain_test( pre=pre, @@ -751,11 +753,11 @@ def test_receipt_cumulative_differs_from_header_gas_used( Verify receipt cumulative_gas_used can diverge from header gas_used under 2D accounting when state gas dominates. """ - tx_regular, tx_state = sstore_tx_gas(fork) + tx_execution, tx_state = sstore_tx_gas(fork) num_txs = 3 sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - per_tx_gas_used = tx_regular + tx_state + per_tx_gas_used = tx_execution + tx_state txs: list[Transaction] = [] post: dict = {} @@ -776,11 +778,11 @@ def test_receipt_cumulative_differs_from_header_gas_used( ) post[contract] = Account(storage=storage) - block_regular = num_txs * tx_regular + block_execution = num_txs * tx_execution block_state = num_txs * tx_state - header_gas_used = max(block_regular, block_state) + header_gas_used = max(block_execution, block_state) - assert block_state > block_regular + assert block_state > block_execution assert header_gas_used < num_txs * per_tx_gas_used blockchain_test( @@ -795,7 +797,7 @@ def test_receipt_cumulative_differs_from_header_gas_used( ) -@pytest.mark.parametrize("dominant_dimension", ["state", "regular"]) +@pytest.mark.parametrize("dominant_dimension", ["state", "execution"]) @pytest.mark.parametrize( "single_tx", [ @@ -815,8 +817,8 @@ def test_base_fee_per_gas_follows_dominant_dimension( Verify the child block's base fee follows the bottleneck dimension. Block 1 exceeds the gas target on one dimension only: state, via - SSTORE-set txs that spill, or regular, via STOP/MSTORE txs. Its header - gas_used = max(regular, state) is then set by that dimension alone, + SSTORE-set txs that spill, or execution, via STOP/MSTORE txs. Its header + gas_used = max(execution, state) is then set by that dimension alone, which lifts empty block 2's base fee under the EIP-1559 update. """ genesis_base_fee = 10**9 @@ -831,36 +833,40 @@ def test_base_fee_per_gas_follows_dominant_dimension( if single_tx: num_txs = 1 num_sstores = target // sstore_tx_gas(fork, num_sstores=1)[1] + 1 - tx_regular, tx_state = sstore_tx_gas(fork, num_sstores=num_sstores) + tx_execution, tx_state = sstore_tx_gas( + fork, num_sstores=num_sstores + ) else: num_sstores = 1 - tx_regular, tx_state = sstore_tx_gas(fork, num_sstores=num_sstores) - while tx_regular >= tx_state: + tx_execution, tx_state = sstore_tx_gas( + fork, num_sstores=num_sstores + ) + while tx_execution >= tx_state: num_sstores += 1 - tx_regular, tx_state = sstore_tx_gas( + tx_execution, tx_state = sstore_tx_gas( fork, num_sstores=num_sstores ) num_txs = target // tx_state + 1 - block_regular = num_txs * tx_regular + block_execution = num_txs * tx_execution block_state = num_txs * tx_state - tx_gas_limit = tx_regular + tx_state - assert block_state > target > block_regular + tx_gas_limit = tx_execution + tx_state + assert block_state > target > block_execution else: if single_tx: num_txs = 1 # Just consume all gas - regular_contract = pre.deploy_contract( + execution_contract = pre.deploy_contract( code=Op.MSTORE(offset=2**256 - 1, value=1) + Op.STOP ) tx_gas_limit = target + 1 else: tx_gas_limit = fork.transaction_intrinsic_cost_calculator()() - # Enough STOP txs that regular gas alone clears the target. - regular_contract = pre.deploy_contract(code=Op.STOP) + # Enough STOP txs that execution gas alone clears the target. + execution_contract = pre.deploy_contract(code=Op.STOP) num_txs = target // tx_gas_limit + 1 - block_regular = num_txs * tx_gas_limit + block_execution = num_txs * tx_gas_limit block_state = 0 - assert block_regular > target > block_state + assert block_execution > target > block_state for _ in range(num_txs): if dominant_dimension == "state": @@ -872,7 +878,7 @@ def test_base_fee_per_gas_follows_dominant_dimension( contract = pre.deploy_contract(code=code) post[contract] = Account(storage=storage) else: - contract = regular_contract + contract = execution_contract txs.append( Transaction( to=contract, @@ -883,7 +889,7 @@ def test_base_fee_per_gas_follows_dominant_dimension( ) ) - block_1_gas_used = max(block_regular, block_state) + block_1_gas_used = max(block_execution, block_state) assert block_1_gas_used < gas_limit, ( "test needs update: gas_limit reached by usage, simply raise the " "anchored gas_limit value" @@ -950,7 +956,7 @@ def test_cumulative_block_state_gas_boundary( state gas reaches block_state_gas_used only via spillover, and its gas_limit exactly fills the block. tx2's gas_limit is the remaining state budget plus delta, below both the per-tx cap and the remaining - regular budget, so only the state gate can reject it: delta=0 must + execution budget, so only the state gate can reject it: delta=0 must be accepted (strict >) and delta=1 rejected. test_block_state_gas_limit_boundary covers this gate with a reservoir-funded tx1 and an above-cap tx2. @@ -960,13 +966,13 @@ def test_cumulative_block_state_gas_boundary( sstore_code = ( sum((Op.SSTORE(i, 1) for i in range(n)), Bytecode()) + Op.STOP ) - tx1_regular = intrinsic + sstore_code.regular_cost(fork) + tx1_execution = intrinsic + sstore_code.execution_cost(fork) tx1_state = sstore_code.state_cost(fork) - # tx1 exactly fills the block; the leftover state budget is tx1_regular. - block_gas_limit = tx1_regular + tx1_state - # tx2 stays within the remaining regular budget, so only the state + # tx1 exactly fills the block; the leftover state budget is tx1_execution. + block_gas_limit = tx1_execution + tx1_state + # tx2 stays within the remaining execution budget, so only the state # dimension can reject it. - assert tx1_regular + 1 <= block_gas_limit - tx1_regular + assert tx1_execution + 1 <= block_gas_limit - tx1_execution sstore_contract = pre.deploy_contract(code=sstore_code) stop_contract = pre.deploy_contract(code=Op.STOP) @@ -977,7 +983,7 @@ def test_cumulative_block_state_gas_boundary( ) tx2 = Transaction( to=stop_contract, - gas_limit=tx1_regular + delta, + gas_limit=tx1_execution + delta, sender=pre.fund_eoa(), error=error, ) @@ -987,7 +993,7 @@ def test_cumulative_block_state_gas_boundary( if not delta: post = {sstore_contract: Account(storage=dict.fromkeys(range(n), 1))} header_verify = Header( - gas_used=max(tx1_regular + intrinsic, tx1_state) + gas_used=max(tx1_execution + intrinsic, tx1_state) ) blockchain_test( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py index 170217fa7cf..79e81ad7deb 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py @@ -7,7 +7,7 @@ that spilled into `gas_left` returns there and the reservoir-funded portion restores the reservoir. An exceptional halt likewise resets the reservoir to its start-of-frame value, but the spilled portion stays -consumed as regular gas with the rest of `gas_left`. +consumed as execution gas with the rest of `gas_left`. All CALL-family opcodes (CALL, DELEGATECALL, STATICCALL) pass the full reservoir to child frames. @@ -171,7 +171,7 @@ def test_reservoir_returned_on_oog( """ Test state gas reservoir is returned to parent on child OOG. - The child runs out of regular gas. The parent recovers the + The child runs out of execution gas. The parent recovers the reservoir and can use it for its own state operations. """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -260,7 +260,7 @@ def test_reservoir_restored_after_child_spill_and_halt( reservoir and spilling into `gas_left`, then hits INVALID causing an exceptional halt. The child's halt resets its frame to (0, R0_child) — only the reservoir-portion is returned to the - parent; the spilled gas stays burned (re-classified as regular). + parent; the spilled gas stays burned (re-classified as execution). The parent does two SSTOREs: the first drains the recovered reservoir, the second spills from the parent's own `gas_left`. """ @@ -665,7 +665,7 @@ def test_gas_opcode_excludes_reservoir( ) # Verify: slot 0 should hold a value <= TX_MAX_GAS_LIMIT - # (gas_left is capped by TX_MAX_GAS_LIMIT - intrinsic.regular) + # (gas_left is capped by TX_MAX_GAS_LIMIT - intrinsic.execution) # We can't check the exact value, but we verify the SSTORE # succeeded and the contract executed correctly post = {contract: Account(storage=storage)} @@ -820,14 +820,14 @@ def test_call_pre_charged_costs_excluded_from_forwarding( child_code = Op.SSTORE(child_storage.store_next(1, "child_ran"), 1) child = pre.deploy_contract(child_code) - child_regular_gas = child_code.regular_cost(fork) + child_execution_gas = child_code.execution_cost(fork) # Memory expansion triggered by ret_size on the wrapper's CALL ret_size = 512 * 32 # 512 words memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) # Wrapper: CALL child requesting max gas with memory expansion. The - # memory metadata makes `wrapper_code.regular_cost(fork)` fold the + # memory metadata makes `wrapper_code.execution_cost(fork)` fold the # cold access, the 7 argument pushes and the memory expansion. wrapper_code = Op.CALL( gas=0xFFFFFFFF, @@ -844,9 +844,9 @@ def test_call_pre_charged_costs_excluded_from_forwarding( # After the up-front pre-charge, the wrapper has gas_remaining left. # The 63/64 rule should forward gas_remaining * 63/64 to the child — # just enough for its SSTORE. - gas_remaining = child_regular_gas * 64 // 63 + memory_cost // 2 + gas_remaining = child_execution_gas * 64 // 63 + memory_cost // 2 - wrapper_gas = wrapper_code.regular_cost(fork) + gas_remaining + wrapper_gas = wrapper_code.execution_cost(fork) + gas_remaining caller = pre.deploy_contract( Op.POP(Op.CALL(gas=wrapper_gas, address=wrapper)) @@ -877,7 +877,7 @@ def test_call_new_account_header_gas_used( A contract CALLs a non-existent address with value, charging GAS_NEW_ACCOUNT state gas. The block must be accepted with - correct 2D max(regular, state) accounting in the header. + correct 2D max(execution, state) accounting in the header. """ target = pre.fund_eoa(amount=0) @@ -1207,7 +1207,7 @@ def test_call_value_to_pre_existing_selfdestructed_account( new account creation gate does not fire. Several cold SSTOREs after the CALLs make block state gas - dominate the block regular gas component, so the block header + dominate the block execution gas component, so the block header reflects exactly `num_probes * sstore_state_gas`. A spurious new account charge on the value bearing CALL would push the header up by that charge, breaking the assertion. @@ -1215,7 +1215,7 @@ def test_call_value_to_pre_existing_selfdestructed_account( sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Enough probes that the combined probe state gas dominates the - # transaction's regular gas component and the header reflects + # transaction's execution gas component and the header reflects # block state gas alone. num_probes = 6 probe_state_gas = num_probes * sstore_state_gas @@ -1400,7 +1400,7 @@ def test_create_oog_during_state_gas_charge( """ Verify the parent reservoir is refunded when a child's CREATE OOGs while charging account-creation state gas. The grandchild - SSTORE is forwarded only its regular stipend, so it succeeds + SSTORE is forwarded only its execution stipend, so it succeeds only if the refund landed in the reservoir (not in `gas_left`). """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -1426,7 +1426,7 @@ def test_create_oog_during_state_gas_charge( grandchild_code = Op.SSTORE(grandchild_storage.store_next(1, "ran"), 1) grandchild = pre.deploy_contract(code=grandchild_code) - grandchild_stipend = grandchild_code.regular_cost(fork) + grandchild_stipend = grandchild_code.execution_cost(fork) parent = pre.deploy_contract( code=( @@ -1449,14 +1449,14 @@ def test_create_oog_during_state_gas_charge( @pytest.mark.valid_from("EIP8037") -def test_call_new_account_no_regular_account_creation_cost( +def test_call_new_account_no_execution_account_creation_cost( state_test: StateTestFiller, pre: Alloc, fork: Fork, ) -> None: """ Verify CALL with value to a non-existent account does not - charge a regular account-creation cost on top of state gas. + charge an execution-gas account-creation cost on top of state gas. """ target = pre.fund_eoa(amount=0) @@ -1474,8 +1474,8 @@ def test_call_new_account_no_regular_account_creation_cost( ) caller = pre.deploy_contract(code=caller_code, balance=1) - # Tight budget: slack is less than the old pre-Amsterdam regular - # account-creation cost, so any extra regular draw would OOG. + # Tight budget: slack is less than the old pre-Amsterdam execution + # account-creation cost, so any extra execution draw would OOG. intrinsic = fork.transaction_intrinsic_cost_calculator()() tx = Transaction( to=caller, @@ -1563,7 +1563,7 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( """ Verify state gas from a failing child is restored to the reservoir, so a sibling probe SSTORE can draw from it under a - tight regular stipend. Covers SSTORE and CALL-value (new + tight execution stipend. Covers SSTORE and CALL-value (new account) state-gas charge paths. """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -1590,7 +1590,7 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( child = pre.deploy_contract(code=child_code, balance=child_balance) probe = pre.deploy_contract(probe_code) - probe_stipend = probe_code.regular_cost(fork) + probe_stipend = probe_code.execution_cost(fork) parent = pre.deploy_contract( code=( @@ -1641,7 +1641,7 @@ def test_call_insufficient_balance_refunds_new_account_state_gas( probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) probe = pre.deploy_contract(probe_code) - probe_stipend = probe_code.regular_cost(fork) + probe_stipend = probe_code.execution_cost(fork) non_existent_account = pre.nonexistent_account() @@ -1690,7 +1690,7 @@ def test_call_value_precompile_halt_refunds_new_account_state_gas( probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) probe = pre.deploy_contract(probe_code) - probe_stipend = probe_code.regular_cost(fork) + probe_stipend = probe_code.execution_cost(fork) ecpairing = 0x08 @@ -1740,7 +1740,7 @@ def test_call_value_new_account_state_gas_consumed_on_caller_halt( in the child and the charge is refilled to `gas_left` in LIFO order. The caller then hits `INVALID`; the halt burns all of `gas_left`, including the spilled charge, and resets the reservoir to its start-of-frame value. - The sender pays the full regular budget: the whole `gas_limit` in-cap, or + The sender pays the full execution budget: the whole `gas_limit` in-cap, or the EIP-7825 gas cap over-cap (the restored reservoir is refunded). The value transfer is rolled back, leaving `target` absent and the caller balance intact. @@ -1812,7 +1812,7 @@ def test_call_value_new_account_state_gas_returned_on_caller_revert( caller ends with `REVERT`. A revert refills the frame state gas in LIFO order: the spilled portion returns to `gas_left` and the reservoir-funded portion restores the reservoir, both refunded to the sender. The sender - pays only the regular execution gas, the same value in-cap and over-cap, + pays only the execution gas, the same value in-cap and over-cap, and the value transfer is rolled back. """ value = 1 @@ -1827,14 +1827,14 @@ def test_call_value_new_account_state_gas_returned_on_caller_revert( caller = pre.deploy_contract(code=caller_code, balance=value) sender = pre.fund_eoa() - # Only regular execution is billed: the spilled and reservoir-funded + # Only execution gas is billed: the spilled and reservoir-funded # parts of the NEW_ACCOUNT charge are both refunded, so the cost - # matches in-cap and over-cap. `regular_cost` covers the pushes, cold + # matches in-cap and over-cap. `execution_cost` covers the pushes, cold # access and the value transfer (NEW_ACCOUNT lands in the state # dimension); the empty child returns its stipend unused. expected_gas_used = ( fork.transaction_intrinsic_cost_calculator()() - + caller_code.regular_cost(fork) + + caller_code.execution_cost(fork) - fork.call_value_stipend() ) receipt = TransactionReceipt(cumulative_gas_used=expected_gas_used) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py index 2a58f8e6d55..08ab7d6cd23 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py @@ -1,11 +1,11 @@ """ Test EIP-7623 calldata floor interaction with EIP-8037 state gas. -The calldata floor applies to the regular gas dimension only. It +The calldata floor applies to the execution gas dimension only. It does not affect state gas. Block gas accounting applies the floor to -the regular dimension (``max(pre_refund_gas - state_gas, floor)``), +the execution dimension (``max(pre_refund_gas - state_gas, floor)``), so a transaction contributes at least the floor to the block's -regular gas while state gas is tracked separately. +execution gas while state gas is tracked separately. Tests for [EIP-8037: State Creation Gas Cost Increase] (https://eips.ethereum.org/EIPS/eip-8037). @@ -44,7 +44,7 @@ def test_calldata_floor_with_sstore( Test calldata floor does not affect state gas charging. A transaction with large calldata triggers the calldata floor for - regular gas, but state gas for SSTORE is charged independently. + execution gas, but state gas for SSTORE is charged independently. """ storage = Storage() contract = pre.deploy_contract( @@ -71,7 +71,7 @@ def test_calldata_floor_independent_of_state_gas( pre: Alloc, ) -> None: """ - Test calldata floor applies only to regular gas dimension. + Test calldata floor applies only to execution gas dimension. The calldata floor applies only to the sender's bill and does not affect the state gas dimension. A transaction with high calldata @@ -102,7 +102,7 @@ def test_calldata_floor_higher_than_execution_with_state_ops( """ Test state gas is tracked separately when calldata floor dominates. - Even when calldata floor > actual regular gas used, state gas for + Even when calldata floor > actual execution gas used, state gas for SSTORE is charged normally from the reservoir or gas_left. """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -112,7 +112,7 @@ def test_calldata_floor_higher_than_execution_with_state_ops( code=Op.SSTORE(storage.store_next(1), 1), ) - # Large calldata so floor dominates regular gas + # Large calldata so floor dominates execution gas calldata = b"\x01" * 1024 tx = Transaction( @@ -144,9 +144,9 @@ def test_calldata_floor_exceeding_tx_gas_limit_cap( Reject a transaction whose calldata floor exceeds the cap, isolating the cap check from the sufficiency check. - EIP-8037 caps ``max(intrinsic_regular, calldata_floor)`` at + EIP-8037 caps ``max(intrinsic_execution, calldata_floor)`` at ``TX_MAX_GAS_LIMIT``. When the EIP-7976 calldata floor crosses the cap - the transaction must be rejected even though the regular intrinsic gas + the transaction must be rejected even though the execution intrinsic gas is within the cap. For the rejection case ``gas_limit`` is set above the floor so the sufficiency check ``max(intrinsic_total, floor) <= tx.gas`` passes and the cap is the only reason for rejection — the exact shape a @@ -186,12 +186,12 @@ def floor_fits(num_bytes: int) -> bool: if exceeds_cap: intrinsic = fork.transaction_intrinsic_cost_calculator() - regular = intrinsic( + execution = intrinsic( calldata=calldata, return_cost_deducted_prior_execution=True, ) assert floor > cap, "calldata floor must exceed the cap" - assert regular < cap, "regular intrinsic must stay below the cap" + assert execution < cap, "execution intrinsic must stay below the cap" # Fund the floor in full so the sufficiency check cannot reject the # transaction first; only the cap check can. gas_limit = floor + 1_000_000 @@ -265,28 +265,28 @@ def test_calldata_floor_binds_with_reservoir( Large calldata makes the EIP-7976 floor the sender's bill, while an over-cap `gas_limit` puts the SSTORE-set state charge in the - reservoir. The floor binds the receipt and the block's regular + reservoir. The floor binds the receipt and the block's execution dimension alike, so the header gas_used is the floor (not the state dimension). """ storage = Storage() code = Op.SSTORE(storage.store_next(1), 1, new_value=1) state_cost = code.state_cost(fork) - regular_cost = code.regular_cost(fork) + execution_cost = code.execution_cost(fork) - # Sized so the floor binds while block-regular stays under storage_set. + # Sized so the floor binds while block-execution stays under storage_set. calldata = b"\x00" * 5000 floor = fork.transaction_data_floor_cost_calculator()(data=calldata) intrinsic = fork.transaction_intrinsic_cost_calculator()( calldata=calldata, return_cost_deducted_prior_execution=True, ) - tx_regular = intrinsic + regular_cost - assert floor > tx_regular + state_cost, ( + tx_execution = intrinsic + execution_cost + assert floor > tx_execution + state_cost, ( "calldata floor must exceed the sender's pre-floor bill" ) - assert tx_regular < state_cost, ( - "block-regular must stay under the state dimension" + assert tx_execution < state_cost, ( + "block-execution must stay under the state dimension" ) contract = pre.deploy_contract(code=code) @@ -313,10 +313,10 @@ def test_calldata_floor_counts_toward_block_gas( fork: Fork, ) -> None: """ - Verify the calldata floor is charged to the block's regular gas. + Verify the calldata floor is charged to the block's execution gas. With a STOP callee and large zero-byte calldata the floor exceeds - the actual regular gas charge, so the transaction contributes the + the actual execution gas charge, so the transaction contributes the floor (not the pre-floor charge) to the header gas_used. """ calldata = b"\x00" * 1024 @@ -353,10 +353,10 @@ def test_calldata_floor_not_discounted_by_state_gas( Verify state gas spending does not discount the block-level floor. Calldata is sized so the floor sits between the transaction's - regular-gas portion and its total gas used - (``tx_regular < floor < tx_regular + state``). The sender's bill is - the pre-floor total, yet the block's regular dimension must still - charge the full floor: the floor is compared against the regular + execution-gas portion and its total gas used + (``tx_execution < floor < tx_execution + state``). The sender's bill is + the pre-floor total, yet the block's execution dimension must still + charge the full floor: the floor is compared against the execution portion alone, so state gas cannot absorb it. An implementation that instead floors the transaction total before deducting state gas (or skips the floor entirely) would report the state dimension @@ -365,7 +365,7 @@ def test_calldata_floor_not_discounted_by_state_gas( storage = Storage() code = Op.SSTORE(storage.store_next(1), 1, new_value=1) state_cost = code.state_cost(fork) - regular_cost = code.regular_cost(fork) + execution_cost = code.execution_cost(fork) floor_cost = fork.transaction_data_floor_cost_calculator() # Smallest zero-byte calldata whose floor exceeds the state @@ -380,10 +380,10 @@ def test_calldata_floor_not_discounted_by_state_gas( calldata=calldata, return_cost_deducted_prior_execution=True, ) - tx_regular = intrinsic + regular_cost - tx_total = tx_regular + state_cost - assert tx_regular < floor < tx_total, ( - "floor must bind the regular portion but not the total" + tx_execution = intrinsic + execution_cost + tx_total = tx_execution + state_cost + assert tx_execution < floor < tx_total, ( + "floor must bind the execution portion but not the total" ) contract = pre.deploy_contract(code=code) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index fe8a624fc16..d403d5f8b4b 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -2,7 +2,7 @@ Test CREATE and CREATE2 state gas charging under EIP-8037. Contract creation charges state gas for the new account and for -code deposit. Regular gas for CREATE is charged separately. +code deposit. Execution gas for CREATE is charged separately. Tests for [EIP-8037: State Creation Gas Cost Increase] (https://eips.ethereum.org/EIPS/eip-8037). @@ -271,7 +271,7 @@ def test_code_deposit_state_gas_exact_fit_boundary( 0, code_size, code_deposit_size=code_size, new_memory_size=code_size ) - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( calldata=bytes(init_code), contract_creation=True, return_cost_deducted_prior_execution=True, @@ -281,7 +281,7 @@ def test_code_deposit_state_gas_exact_fit_boundary( # folds the memory expansion, code-hash keccak and code-deposit state # gas into `init_code`'s own cost. exact_fit_gas = ( - intrinsic_regular + intrinsic_execution + fork.transaction_top_frame_state_gas(contract_creation=True) + init_code.gas_cost(fork) ) @@ -300,7 +300,7 @@ def test_code_deposit_state_gas_exact_fit_boundary( post = {created: Account(code=b"\x00" * code_size)} else: # reservoir: the deposit OOG refills the reservoir, so the sender - # pays the regular cap. spill: the refilled NEW_ACCOUNT lands in + # pays the execution cap. spill: the refilled NEW_ACCOUNT lands in # gas_left and is burned, so the sender pays the full gas_limit. receipt_gas_used = cap if funding == "reservoir" else gas_limit post = {created: Account.NONEXISTENT} @@ -456,7 +456,7 @@ def test_create_insufficient_state_gas( """ Test CREATE OOGs when state gas is insufficient. - Provide enough gas for CREATE's regular gas cost but not enough + Provide enough gas for CREATE's execution gas cost but not enough to cover the new-account state gas. The CREATE should fail, returning 0. """ @@ -478,10 +478,10 @@ def test_create_insufficient_state_gas( ), ) - # Tight gas — enough for intrinsic + CREATE regular gas but not + # Tight gas — enough for intrinsic + CREATE execution gas but not # enough for the new account state gas intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - gas_limit = intrinsic_cost() + create_call.regular_cost(fork) + 10_000 + gas_limit = intrinsic_cost() + create_call.execution_cost(fork) + 10_000 tx = Transaction( to=contract, @@ -564,7 +564,7 @@ def test_create_tx_intrinsic_gas_boundary( Test CREATE tx intrinsic gas boundary includes state component. The intrinsic gas for a contract-creating transaction includes - both regular gas and state gas. A transaction with gas_limit + both execution gas and state gas. A transaction with gas_limit exactly at the boundary succeeds; one gas below is rejected. """ intrinsic_cost = fork.transaction_intrinsic_cost_calculator() @@ -602,11 +602,11 @@ def test_create_tx_below_total_intrinsic( initcode: Bytecode, ) -> None: """ - Reject a creation tx one gas below the (now regular-only) intrinsic. + Reject a creation tx one gas below the (now execution-only) intrinsic. Under EIP-2780 the created account's ``NEW_ACCOUNT`` cost moved out of the transaction intrinsic and into the top frame, so the creation - intrinsic is entirely regular: + intrinsic is entirely execution: ``fork.transaction_intrinsic_cost_calculator()(contract_creation=True, calldata=initcode)``. Pinning ``gas_limit`` at ``intrinsic - 1`` must be rejected as intrinsic-gas-too-low, mirroring the set_code case in @@ -614,7 +614,7 @@ def test_create_tx_below_total_intrinsic( This now overlaps ``test_create_tx_intrinsic_gas_boundary`` (``gas_delta=-1``), but additionally sweeps the initcode so the - per-word init-code cost folded into the regular intrinsic is + per-word init-code cost folded into the execution intrinsic is exercised. """ intrinsic = fork.transaction_intrinsic_cost_calculator()( @@ -661,7 +661,7 @@ def test_code_deposit_oog_preserves_parent_reservoir( size=len(init_code), ) - # Limited regular gas forwarded to the factory. After CREATE + # Limited execution gas forwarded to the factory. After CREATE # takes 63/64, the factory retains ~23 K for its SSTOREs. child_gas = 1_500_000 @@ -759,7 +759,7 @@ def test_parent_state_gas_after_child_failure( factory_storage = Storage() # Split the factory into the CREATE run (memory setup + CREATE, whose # result is left on the stack) and the post-CREATE stores, so each - # step's regular gas is read off `.regular_cost(fork)` rather than + # step's execution gas is read off `.execution_cost(fork)` rather than # rebuilt from constants. factory_create_code = ( Op.MSTORE(0, Op.PUSH32(bytes(initcode)), new_memory_size=32) @@ -791,7 +791,7 @@ def test_parent_state_gas_after_child_failure( if failure_op == Op.INVALID: # Simulate runtime gas for HALT under EIP-8037 LIFO refills: - # 1. Regular pool capped by transaction_gas_limit_cap. The + # 1. Execution pool capped by transaction_gas_limit_cap. The # remainder forms the state reservoir. # 2. CREATE charges new_account state gas, reservoir first # then spilled to gas_left and tracked. @@ -806,13 +806,13 @@ def test_parent_state_gas_after_child_failure( # 7. Factory post-CREATE SSTORE charges sstore_state_gas, # reservoir first then spilled to gas_left. execution_gas = gas_limit - intrinsic_cost - regular_budget = gas_limit_cap - intrinsic_cost - sim_gas_left = min(regular_budget, execution_gas) + execution_budget = gas_limit_cap - intrinsic_cost + sim_gas_left = min(execution_budget, execution_gas) sim_state_gas_left = execution_gas - sim_gas_left - # Memory setup, the CREATE arg pushes and the CREATE regular + # Memory setup, the CREATE arg pushes and the CREATE execution # cost are all consumed before the 63/64 split. - sim_gas_left -= factory_create_code.regular_cost(fork) + sim_gas_left -= factory_create_code.execution_cost(fork) # CREATE new_account state gas: reservoir first, spill tracked. new_account_from_reservoir = min( @@ -836,7 +836,7 @@ def test_parent_state_gas_after_child_failure( sim_gas_left += new_account_spill sim_state_gas_left += new_account_from_reservoir - sim_gas_left -= factory_post_create_code.regular_cost(fork) + sim_gas_left -= factory_post_create_code.execution_cost(fork) # Factory post-CREATE SSTORE: reservoir first, spill otherwise. if sim_state_gas_left >= sstore_state_gas: @@ -852,9 +852,9 @@ def test_parent_state_gas_after_child_failure( # factory's own post-CREATE SSTORE consumes net state gas. expected_cumulative = ( intrinsic_cost - + factory_create_code.regular_cost(fork) - + factory_post_create_code.regular_cost(fork) - + initcode.regular_cost(fork) + + factory_create_code.execution_cost(fork) + + factory_post_create_code.execution_cost(fork) + + initcode.execution_cost(fork) + sstore_state_gas ) @@ -884,7 +884,7 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( Test nested CREATE code deposit does not borrow parent gas. Provide just enough gas for CREATE to start (new account state - gas + regular gas) but not enough for the child frame to cover + gas + execution gas) but not enough for the child frame to cover code deposit after init code runs. The CREATE increments the factory nonce but code deposit fails, so no contract is deployed. """ @@ -907,19 +907,19 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( # Init code child execution: PUSH1 + PUSH1 + RETURN's mem_exp. # Code deposit (keccak + state) is charged AFTER the child returns. - init_cost = init_code.regular_cost(fork) + init_cost = init_code.execution_cost(fork) # Target child: enough for init, not enough for code deposit state. target_child = (init_cost + code_deposit_state) // 2 # Invert EIP-150 63/64ths rule: ceil(target_child * 64 / 63). factory_remaining = (target_child * 64 + 62) // 63 # NEW_ACCOUNT state gas spills into gas_left (no reservoir at the - # top level), so it must be funded out of the regular budget. + # top level), so it must be funded out of the execution budget. intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() gas_limit = ( intrinsic_cost - + factory_mstore.regular_cost(fork) - + factory_create.regular_cost(fork) + + factory_mstore.execution_cost(fork) + + factory_create.execution_cost(fork) + factory_create.state_cost(fork) + factory_remaining ) @@ -952,16 +952,16 @@ def test_sstore_oog_no_reservoir_inflation( gas_shortfall: int, ) -> None: """ - Verify SSTORE state gas is not charged when regular gas OOGs. + Verify SSTORE state gas is not charged when execution gas OOGs. With zero reservoir, all state gas spills into gas_left. A child frame does CREATE (charging state gas from gas_left) followed by SSTORE. When the factory is 1 gas short, SSTORE OOGs. If state - gas is incorrectly charged before regular gas, the extra state gas + gas is incorrectly charged before execution gas, the extra state gas inflates the parent's reservoir on frame failure, changing the transaction's effective gas consumption. - Regression test for SSTORE gas ordering: regular gas must be + Regression test for SSTORE gas ordering: execution gas must be checked before state gas. """ initcode = Initcode(deploy_code=Op.STOP) @@ -985,15 +985,15 @@ def test_sstore_oog_no_reservoir_inflation( factory = pre.deploy_contract(factory_code) create_address = compute_create_address(address=factory, nonce=1) - # Total gas includes both regular and state components since + # Total gas includes both execution and state components since # reservoir is zero — all state gas comes from gas_left. factory_gas = ( factory_code.gas_cost(fork) - + initcode.execution_gas(fork) + + initcode.evm_gas(fork) + initcode.deployment_gas(fork) ) - # Caller forwards total gas (regular + state) through CALL. + # Caller forwards total gas (execution + state) through CALL. # With zero reservoir, the CALL gas parameter is the only source. caller = pre.deploy_contract( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) @@ -1046,9 +1046,9 @@ def test_max_initcode_size_gas_metering_via_create( """ Verify 2D gas metering for CREATE with max initcode size. - A caller contract forwards exact regular gas to a factory via CALL. + A caller contract forwards exact execution gas to a factory via CALL. State gas is supplied through the reservoir (tx.gas_limit above the - cap). With short_one_gas, the factory is 1 regular gas short and + cap). With short_one_gas, the factory is 1 execution gas short and all state changes revert. """ initcode = Initcode( @@ -1096,22 +1096,22 @@ def test_max_initcode_size_gas_metering_via_create( opcode=create_opcode, ) - # Split gas into regular and state components. + # Split gas into execution and state components. # CALL gas only feeds gas_left; state gas must come from the reservoir. factory_gas = ( factory_code.gas_cost(fork) - + initcode.execution_gas(fork) + + initcode.evm_gas(fork) + initcode.deployment_gas(fork) ) factory_state_gas = fork.create_state_gas( code_size=len(initcode.deploy_code) ) + Op.SSTORE(new_value=1).state_cost(fork) - factory_regular_gas = factory_gas - factory_state_gas + factory_execution_gas = factory_gas - factory_state_gas caller = pre.deploy_contract( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.CALL( - gas=factory_regular_gas - gas_shortfall, + gas=factory_execution_gas - gas_shortfall, address=factory, value=0, args_offset=0, @@ -1149,9 +1149,9 @@ def test_create_no_double_charge_new_account( """ Verify CREATE does not double-charge new-account gas. - CREATE charges REGULAR_GAS_CREATE as regular gas and new-account + CREATE charges EXECUTION_GAS_CREATE as execution gas and new-account state gas separately. Provide exactly enough gas for both — if - GAS_NEW_ACCOUNT were charged twice (once in regular, once in + GAS_NEW_ACCOUNT were charged twice (once in execution, once in state), the CREATE would OOG. """ create_state_gas = fork.create_state_gas(code_size=0) @@ -1163,19 +1163,19 @@ def test_create_no_double_charge_new_account( # Compute exact gas: child bytecode + CREATE child frame. # The child frame is empty (size=0) so only the CREATE opcode - # charges matter: regular (REGULAR_GAS_CREATE) + state (new account). + # charges matter: execution (EXECUTION_GAS_CREATE) + state (new account). child_total = child_code.gas_cost(fork) create_address = compute_create_address(address=child, nonce=1) - # Caller forwards exact regular gas via CALL. State gas for + # Caller forwards exact execution gas via CALL. State gas for # new account comes from the reservoir (gas_limit above the cap). caller_storage = Storage() - regular_gas = child_total - create_state_gas + execution_gas = child_total - create_state_gas caller = pre.deploy_contract( Op.SSTORE( caller_storage.store_next(1, "create_succeeds"), - Op.CALL(gas=regular_gas, address=child), + Op.CALL(gas=execution_gas, address=child), ) ) @@ -1229,7 +1229,7 @@ def test_code_deposit_halt_discards_initcode_state_gas( state changes including the new account. The reverted GAS_NEW_ACCOUNT must NOT count in block_state_gas_used, which determines the block header gas_used via - max(block_regular_gas, block_state_gas). + max(block_execution_gas, block_state_gas). """ subcall_forwarded_value = 1 entry_account_value = 1 @@ -1300,12 +1300,12 @@ def test_create_tx_header_gas_used( actual consumed gas. For a fresh target the top-frame NEW_ACCOUNT state gas is charged and - dominates the regular gas, so gas_used == NEW_ACCOUNT. For a + dominates the execution gas, so gas_used == NEW_ACCOUNT. For a pre-existing balance-only leaf the target is not EMPTY pre-tx, so the top-frame NEW_ACCOUNT is never charged: net state gas is zero and only - the regular dimension remains. The block-level calldata floor tops up - that regular remainder, so the expected value is the greater of the - regular intrinsic and the floor, and fails if a stray NEW_ACCOUNT is + the execution dimension remains. The block-level calldata floor tops up + that execution remainder, so the expected value is the greater of the + execution intrinsic and the floor, and fails if a stray NEW_ACCOUNT is charged. """ initcode = Op.STOP @@ -1328,15 +1328,15 @@ def test_create_tx_header_gas_used( sender=sender, ) - # block_gas_used = max(block_regular, block_state) + # block_gas_used = max(block_execution, block_state) if target == "existing": intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - # Regular-only creation intrinsic; STOP initcode deploys empty + # Execution-only creation intrinsic; STOP initcode deploys empty # code (zero deposit) and the pre-existing target adds no state - # gas. The block-level calldata floor tops up this small regular + # gas. The block-level calldata floor tops up this small execution # remainder and, being the larger of the two, is what the header - # reflects (the floor applies to block-level regular gas). - regular_intrinsic = intrinsic_cost( + # reflects (the floor applies to block-level execution gas). + execution_intrinsic = intrinsic_cost( calldata=bytes(initcode), contract_creation=True, return_cost_deducted_prior_execution=True, @@ -1344,13 +1344,13 @@ def test_create_tx_header_gas_used( floor = fork.transaction_data_floor_cost_calculator()( data=bytes(initcode), contract_creation=True ) - assert floor > regular_intrinsic, ( + assert floor > execution_intrinsic, ( "the floor must bind for this arm to pin floor-in-header" ) - expected_gas_used = max(regular_intrinsic, floor) + expected_gas_used = max(execution_intrinsic, floor) else: # For a minimal CREATE tx deploying Op.STOP (1 byte), - # state gas (new account) dominates regular gas. + # state gas (new account) dominates execution gas. expected_gas_used = fork.transaction_top_frame_state_gas( contract_creation=True ) @@ -1403,12 +1403,12 @@ def test_create_initcode_halt_no_code_deposit_state_gas( ) # On exceptional halt all gas_left is consumed. - # block_gas_used = max(block_regular, block_state) + # block_gas_used = max(block_execution, block_state) # block_state = intrinsic_state_gas (new account only, no deposit) - # block_regular = gas_limit - intrinsic_state_gas (all remaining) - tx_regular = gas_limit - intrinsic_state_gas + # block_execution = gas_limit - intrinsic_state_gas (all remaining) + tx_execution = gas_limit - intrinsic_state_gas tx_state = intrinsic_state_gas - expected_gas_used = max(tx_regular, tx_state) + expected_gas_used = max(tx_execution, tx_state) blockchain_test( pre=pre, @@ -1443,7 +1443,7 @@ def test_state_gas_spill_header_gas_used( intrinsic_gas = intrinsic_cost() sstore_state_gas = sstore_code.state_cost(fork) - evm_regular = sstore_code.regular_cost(fork) + evm_execution = sstore_code.execution_cost(fork) # Reservoir = half the SSTORE state gas, rest spills to gas_left reservoir = sstore_state_gas // 2 @@ -1454,9 +1454,9 @@ def test_state_gas_spill_header_gas_used( sender=pre.fund_eoa(), ) - tx_regular = intrinsic_gas + evm_regular + tx_execution = intrinsic_gas + evm_execution tx_state = sstore_state_gas - expected_gas_used = max(tx_regular, tx_state) + expected_gas_used = max(tx_execution, tx_state) blockchain_test( pre=pre, @@ -1578,10 +1578,10 @@ def test_create_silent_failure_refunds_state_gas( # CREATE's GAS_NEW_ACCOUNT is refunded (silent failure, no child # spawned). SSTORE's state portion is tracked separately in - # tx_state, so only the regular dimension remains here. - tx_regular = intrinsic_cost + factory_code.regular_cost(fork) + # tx_state, so only the execution dimension remains here. + tx_execution = intrinsic_cost + factory_code.execution_cost(fork) tx_state = sstore_state_gas - expected = max(tx_regular, tx_state) + expected = max(tx_execution, tx_state) blockchain_test( pre=pre, blocks=[Block(txs=[tx], header_verify=Header(gas_used=expected))], @@ -1649,16 +1649,16 @@ def test_create_child_revert_refunds_state_gas( ) # CREATE's GAS_NEW_ACCOUNT is refunded on child REVERT. SSTORE's - # state portion is tracked separately. Child REVERT regular + # state portion is tracked separately. Child REVERT execution # (init_code execution) is propagated via # incorporate_child_on_error. - tx_regular = ( + tx_execution = ( intrinsic_cost - + factory_code.regular_cost(fork) + + factory_code.execution_cost(fork) + init_code.gas_cost(fork) ) tx_state = sstore_state_gas - expected = max(tx_regular, tx_state) + expected = max(tx_execution, tx_state) blockchain_test( pre=pre, blocks=[Block(txs=[tx], header_verify=Header(gas_used=expected))], @@ -1686,10 +1686,10 @@ def test_create_child_halt_refunds_state_gas( Verify CREATE/CREATE2 child halt refunds parent's account gas. Exceptional halts (invalid opcode, EIP-3541 invalid prefix) - consume all forwarded regular gas, so block accounting cannot + consume all forwarded execution gas, so block accounting cannot strictly discriminate via header gas. Tight gas tuning via a caller wrapper leaves the factory with just - enough `gas_left` to pay the probe SSTORE's regular portion + enough `gas_left` to pay the probe SSTORE's execution portion but not enough to spill the state portion, so the probe SSTORE can only succeed via the refunded reservoir. """ @@ -1719,19 +1719,19 @@ def test_create_child_halt_refunds_state_gas( ), ) - # Tight gas tuning: child halt consumes all forwarded regular + # Tight gas tuning: child halt consumes all forwarded execution # gas. Factory retains - # ~(forwarded - pre_sstore_regular) / 64 after CREATE. Target - # the discrimination window `(probe_regular, - # probe_regular + sstore_state_gas)` so the probe SSTORE - # regular fits but state gas spillover from `gas_left` under + # ~(forwarded - pre_sstore_execution) / 64 after CREATE. Target + # the discrimination window `(probe_execution, + # probe_execution + sstore_state_gas)` so the probe SSTORE + # execution fits but state gas spillover from `gas_left` under # the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.regular_cost(fork) + pre_sstore_execution = pre_sstore_code.execution_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.regular_cost(fork) - target_gas_left = probe_regular + sstore_state_gas // 2 - forwarded_gas = target_gas_left * 64 + pre_sstore_regular + probe_execution = probe_code.execution_cost(fork) + target_gas_left = probe_execution + sstore_state_gas // 2 + forwarded_gas = target_gas_left * 64 + pre_sstore_execution # Reservoir sized for CREATE charge only — SSTORE must pull # from the refunded reservoir, not from spill. caller = pre.deploy_contract( @@ -1782,12 +1782,12 @@ def call(size: int, salt: int) -> Bytecode: # STOP deploys empty code, so only GAS_NEW_ACCOUNT counts for # the successful CREATE, and the failed CREATE is refunded. block_state = create_account_state_gas - tx_regular = ( + tx_execution = ( intrinsic_gas + factory_code.gas_cost(fork) - 2 * create_account_state_gas ) - expected = max(tx_regular, block_state) + expected = max(tx_execution, block_state) tx = Transaction( to=factory, @@ -1815,7 +1815,7 @@ def test_create_collision_refunds_state_gas( Verify CREATE/CREATE2 address collision refunds account state gas. The collision path increments the factory nonce and burns the - forwarded regular gas (consumed by the never-spawned child), but + forwarded execution gas (consumed by the never-spawned child), but still refunds `GAS_NEW_ACCOUNT` to the reservoir. Tight gas tuning limits the factory's post-collision `gas_left` so the probe SSTORE can only succeed via the refunded reservoir, not @@ -1850,17 +1850,17 @@ def test_create_collision_refunds_state_gas( pre.deploy_contract(code=Op.STOP, address=collision_target) # Tight gas tuning: factory retains - # ~(forwarded - pre_sstore_regular) / 64 after collision burns - # `max_message_call_gas` as regular. Target the discrimination - # window `(probe_regular, probe_regular + sstore_state_gas)` so - # the probe SSTORE regular fits but state gas spillover from + # ~(forwarded - pre_sstore_execution) / 64 after collision burns + # `max_message_call_gas` as execution. Target the discrimination + # window `(probe_execution, probe_execution + sstore_state_gas)` so + # the probe SSTORE execution fits but state gas spillover from # `gas_left` under the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.regular_cost(fork) + pre_sstore_execution = pre_sstore_code.execution_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.regular_cost(fork) - target_gas_left = probe_regular + sstore_state_gas // 2 - forwarded_gas = target_gas_left * 64 + pre_sstore_regular + probe_execution = probe_code.execution_cost(fork) + target_gas_left = probe_execution + sstore_state_gas // 2 + forwarded_gas = target_gas_left * 64 + pre_sstore_execution # Reservoir sized for CREATE charge only — SSTORE must pull from # the refunded reservoir, not from spill. caller = pre.deploy_contract( @@ -1916,15 +1916,15 @@ def test_create_code_deposit_oog_refunds_state_gas( ) # Child halt consumes all forwarded gas; factory retains only - # ~(forwarded - pre_sstore_regular) / 64. Target the - # discrimination window so SSTORE regular fits but state gas + # ~(forwarded - pre_sstore_execution) / 64. Target the + # discrimination window so SSTORE execution fits but state gas # spillover fails. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.regular_cost(fork) + pre_sstore_execution = pre_sstore_code.execution_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.regular_cost(fork) - target_gas_left = probe_regular + sstore_state_gas // 2 - forwarded_gas = target_gas_left * 64 + pre_sstore_regular + probe_execution = probe_code.execution_cost(fork) + target_gas_left = probe_execution + sstore_state_gas // 2 + forwarded_gas = target_gas_left * 64 + pre_sstore_execution caller = pre.deploy_contract( code=Op.CALL(gas=forwarded_gas, address=factory) ) @@ -2035,7 +2035,7 @@ def test_create_account_charge_reduces_child_gas( # Burn the middle of `(reduced_share, full_share]` for robustness. target_burn = (full_share + reduced_share) // 2 - # Init code burns `target_burn` regular gas via one MSTORE memory + # Init code burns `target_burn` execution gas via one MSTORE memory # expansion, then deploys empty code (zero code deposit). Invert # `words * MEMORY_PER_WORD + words ** 2 // 512 = target_mem` to size # the sink offset from gas rather than a magic number. @@ -2084,11 +2084,13 @@ def test_create_account_charge_reduces_child_gas( create_address = compute_create_address(address=factory, nonce=1) pre.fund_address(create_address, amount=1) - # Regular gas the factory spends before the NEW_ACCOUNT charge: the - # initcode setup MSTORE plus the create opcode's regular portion. + # Execution gas the factory spends before the NEW_ACCOUNT charge: the + # initcode setup MSTORE plus the create opcode's execution portion. setup = Op.MSTORE(0, mstore_value) - pre_charge_regular = setup.gas_cost(fork) + create_call.regular_cost(fork) - forwarded_gas = gas_at_charge + pre_charge_regular + pre_charge_execution = setup.gas_cost(fork) + create_call.execution_cost( + fork + ) + forwarded_gas = gas_at_charge + pre_charge_execution caller = pre.deploy_contract( code=Op.CALL(gas=forwarded_gas, address=factory) ) @@ -2133,8 +2135,8 @@ def test_failed_create_tx_refills_top_frame_new_account( * REVERT preserves ``gas_left`` and ``restore_state_gas`` returns the spilled ``NEW_ACCOUNT`` to it, so the state block nets to zero - and only the regular consumption counts as work. The calldata floor - tops up the billed amount and the block-level regular gas alike, so + and only the execution consumption counts as work. The calldata floor + tops up the billed amount and the block-level execution gas alike, so receipt and header agree at the greater of consumption and floor: the memory expansion keeps ``revert`` above the floor, while the bare ``revert_floor_bound`` pins the floor in both. @@ -2143,17 +2145,17 @@ def test_failed_create_tx_refills_top_frame_new_account( """ intrinsic_calc = fork.transaction_intrinsic_cost_calculator() - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(init_code), contract_creation=True, return_cost_deducted_prior_execution=True, ) # gas_limit must cover the top-frame NEW_ACCOUNT and the initcode's own - # regular execution so the initcode runs to completion. + # execution gas so the initcode runs to completion. gas_limit = ( - intrinsic_regular + intrinsic_execution + fork.transaction_top_frame_state_gas(contract_creation=True) - + init_code.regular_cost(fork) + + init_code.execution_cost(fork) + 1000 ) @@ -2163,17 +2165,19 @@ def test_failed_create_tx_refills_top_frame_new_account( expected_gas_used = gas_limit else: # REVERT refills the spilled NEW_ACCOUNT, netting the state block - # to zero, so only the regular consumption counts as work. The + # to zero, so only the execution consumption counts as work. The # calldata floor binds the billed amount and the block-level - # regular gas alike, so receipt and header agree either way. - regular_consumed = intrinsic_regular + init_code.regular_cost(fork) + # execution gas alike, so receipt and header agree either way. + execution_consumed = intrinsic_execution + init_code.execution_cost( + fork + ) floor = fork.transaction_data_floor_cost_calculator()( data=bytes(init_code), contract_creation=True ) - assert (floor > regular_consumed) == floor_binds, ( + assert (floor > execution_consumed) == floor_binds, ( "init code lands on the wrong side of the floor" ) - expected_gas_used = max(regular_consumed, floor) + expected_gas_used = max(execution_consumed, floor) sender = pre.fund_eoa() created = compute_create_address(address=sender, nonce=0) @@ -2210,23 +2214,23 @@ def test_create_tx_collision_no_new_account_charge( charge, but on an address collision the target already exists pre-tx, the create path returns ``AddressCollision`` before the top frame is prepared, and no ``NEW_ACCOUNT`` is ever charged. The full - forwarded gas is burned as regular (no initcode runs) and block + forwarded gas is burned as execution (no initcode runs) and block state-gas is zero, so header ``gas_used`` equals the whole ``gas_limit``. """ intrinsic_calc = fork.transaction_intrinsic_cost_calculator() init_code = Op.STOP - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) - gas_limit = intrinsic_regular + 1000 + gas_limit = intrinsic_execution + 1000 sender = pre.fund_eoa() collision_target = compute_create_address(address=sender, nonce=0) pre[collision_target] = Account(nonce=1) - # Collision burns the full forwarded gas as regular; state block is + # Collision burns the full forwarded gas as execution; state block is # zero (no NEW_ACCOUNT charged). expected_gas_used = gas_limit @@ -2261,11 +2265,11 @@ def test_create_tx_collision_refunds_reservoir( Verify the state-gas reservoir is refunded on a depth-0 CREATE-tx address collision when `gas_limit > TX_MAX_GAS_LIMIT`. - EIP-8037 splits `gas_limit` into the capped regular budget and a - state-gas reservoir. On collision the inner regular gas is burnt + EIP-8037 splits `gas_limit` into the capped execution budget and a + state-gas reservoir. On collision the inner execution gas is burnt and `intrinsic_state_gas` is refunded; the reservoir must also be refunded to the sender. `header.gas_used` is fixed at the - regular cap regardless of reservoir handling, so the sender's + execution cap regardless of reservoir handling, so the sender's post-balance is the primary discriminating assertion. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -2492,13 +2496,13 @@ def test_selfdestruct_in_create_tx_initcode( create_state_gas = fork.create_state_gas(code_size=0) beneficiary = 0xDEAD - # `account_new` folds the beneficiary's `ACCOUNT_WRITE` regular + # `account_new` folds the beneficiary's `ACCOUNT_WRITE` execution # cost and account-creation state gas into `gas_cost`. initcode = Op.SELFDESTRUCT(beneficiary, account_new=True) sender = pre.fund_eoa() intrinsic_calc = fork.transaction_intrinsic_cost_calculator() - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(initcode), contract_creation=True, sends_value=True ) @@ -2507,7 +2511,7 @@ def test_selfdestruct_in_create_tx_initcode( expected_state = create_state_gas + initcode.state_cost(fork) initcode_gas = initcode.gas_cost(fork) - gas_limit = intrinsic_regular + create_state_gas + initcode_gas + 1000 + gas_limit = intrinsic_execution + create_state_gas + initcode_gas + 1000 tx = Transaction( sender=sender, @@ -2593,7 +2597,7 @@ def test_inner_create_succeeds_code_deposit_state_gas( ) if outer_outcome == "halts": - initcode_gas = initcode.regular_cost(fork) + initcode_gas = initcode.execution_cost(fork) else: initcode_gas = initcode.gas_cost(fork) # The outer created account's NEW_ACCOUNT is a top-frame state charge @@ -2841,7 +2845,7 @@ def test_inner_create_fail_refunds_in_creation_tx( @pytest.mark.pre_alloc_mutable @pytest.mark.with_all_create_opcodes() @pytest.mark.valid_from("EIP8037") -def test_create_collision_burned_gas_counted_in_block_regular( +def test_create_collision_burned_gas_counted_in_block_execution( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, @@ -2849,7 +2853,7 @@ def test_create_collision_burned_gas_counted_in_block_regular( ) -> None: """ Verify gas burned by a CREATE/CREATE2 address collision counts - toward block regular gas used in the header. + toward block execution gas used in the header. """ init_code = Op.STOP mstore_value, size = init_code_at_high_bytes(init_code) @@ -2871,7 +2875,7 @@ def test_create_collision_burned_gas_counted_in_block_regular( # CPSB-agnostic baseline: block_state_gas is zero for this tx (the # existent collision target is not charged), so header.gas_used - # equals the regular-gas total. Decompose the parent + inner frame + # equals the execution-gas total. Decompose the parent + inner frame # accounting from fork APIs so the baseline tracks future cost # changes automatically. gas_used_until_collision = ( @@ -2885,7 +2889,7 @@ def test_create_collision_burned_gas_counted_in_block_regular( gas_at_create = gas_limit - gas_used_until_collision # Inner burns 63/64 of the available gas on collision; the parent # retains 1/64. Post-CREATE consumes from the retained pool. A - # mutation that drops the burned forwarded gas from regular + # mutation that drops the burned forwarded gas from execution # accounting would reduce this baseline. retained = gas_at_create // 64 gas_post_create = factory_post_create_code.gas_cost(fork) @@ -2996,7 +3000,7 @@ def test_no_account_charge_on_existing_account( Verify the create opcode is not charged NEW_ACCOUNT when the target account already exists in the trie. - The factory is forwarded exactly the create's regular gas, with no + The factory is forwarded exactly the create's execution gas, with no NEW_ACCOUNT included. Because the target is pre-funded (alive), that budget is sufficient and the create succeeds, deploying empty code (created nonce 1). With one gas less it runs out of gas at the diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_fork_transition.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_fork_transition.py index 2b0c379d36f..255fd52c042 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_fork_transition.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_fork_transition.py @@ -47,7 +47,7 @@ def test_sstore_state_gas_at_transition( Test SSTORE state gas activates at the EIP-8037 fork boundary. Before the fork, an SSTORE zero-to-nonzero succeeds with only - regular gas (no state gas dimension). After the fork, the same + execution gas (no state gas dimension). After the fork, the same operation requires state gas. Both blocks use TX_MAX_GAS_LIMIT which provides enough gas in either regime. """ @@ -59,7 +59,7 @@ def test_sstore_state_gas_at_transition( ) blocks = [ - # Before fork: SSTORE succeeds with regular gas only + # Before fork: SSTORE succeeds with execution gas only Block( timestamp=14_999, txs=[ diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py index fecc4871775..6b26bd0bb5d 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py @@ -59,7 +59,7 @@ def test_exact_coinbase_fee_simple_sstore( sstore_contract = pre.deploy_contract(code=sstore_code) # tx 1 gas used: the intrinsic (TX_BASE plus the EIP-2780 - # recipient-access charge) plus the SSTORE code's own regular and + # recipient-access charge) plus the SSTORE code's own execution and # state cost. tx1_gas_used = ( fork.transaction_intrinsic_cost_calculator()() diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py index b9b1f8c5074..03fc036f747 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py @@ -1,8 +1,8 @@ """ Test state gas consumption ordering under EIP-8037. -When an opcode charges both regular gas and state gas, regular gas MUST -be charged first. If regular gas OOGs, state gas is not consumed. This +When an opcode charges both execution gas and state gas, execution gas MUST +be charged first. If execution gas OOGs, state gas is not consumed. This prevents the parent's reservoir from being inflated on frame failure. Each test gives a child frame exactly 1 gas less than needed, then uses @@ -61,7 +61,7 @@ def test_sstore_oog_reservoir_inflation_detection( that need more total state gas than the correct reservoir but less than the inflated one. - With correct ordering (regular gas first): probe OOGs on 4th SSTORE. + With correct ordering (execution gas first): probe OOGs on 4th SSTORE. With wrong ordering (state gas first): reservoir is inflated, probe succeeds. """ @@ -87,7 +87,7 @@ def test_sstore_oog_reservoir_inflation_detection( factory_gas = ( factory_code.gas_cost(fork) - + initcode.execution_gas(fork) + + initcode.evm_gas(fork) + initcode.deployment_gas(fork) ) @@ -98,15 +98,15 @@ def test_sstore_oog_reservoir_inflation_detection( Op.SSTORE(0, 1) + Op.SSTORE(1, 1) + Op.SSTORE(2, 1) + Op.SSTORE(3, 1) ) - # Compute probe gas: enough for 4 SSTOREs' regular gas + pushes, - # but after 4th regular charge, gas_left < the state gas spill. + # Compute probe gas: enough for 4 SSTOREs' execution gas + pushes, + # but after 4th execution charge, gas_left < the state gas spill. sstore_state = Op.SSTORE(new_value=1).state_cost(fork) - sstore_regular = Op.SSTORE(0, 1).regular_cost(fork) + sstore_execution = Op.SSTORE(0, 1).execution_cost(fork) create_state_gas = fork.create_state_gas( code_size=len(initcode.deploy_code) ) spill = 4 * sstore_state - create_state_gas - probe_gas = 4 * sstore_regular + spill // 2 + probe_gas = 4 * sstore_execution + spill // 2 caller_storage = Storage() caller = pre.deploy_contract( @@ -153,7 +153,7 @@ def test_call_oog_reservoir_inflation_detection( Detect CALL state gas ordering via reservoir inflation. A child does CALL(value=1) to a dead address with gas tuned so - the regular gas charge OOGs by 1. If state gas (new account) is + the execution gas charge OOGs by 1. If state gas (new account) is incorrectly charged first, the parent's reservoir is inflated. A single-SSTORE probe detects the inflation: with correct reservoir @@ -171,7 +171,7 @@ def test_call_oog_reservoir_inflation_detection( value_transfer=True, account_new=True, ) - # One gas short of the CALL's full cost (regular plus the NEW_ACCOUNT + # One gas short of the CALL's full cost (execution plus the NEW_ACCOUNT # state charge), so it OOGs on the account-creation charge. child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) @@ -209,14 +209,14 @@ def test_selfdestruct_oog_reservoir_inflation_detection( Detect SELFDESTRUCT state gas ordering via reservoir inflation. A child with non-zero balance does SELFDESTRUCT(dead_beneficiary) - with gas tuned so the regular gas charge OOGs by 1. If state gas + with gas tuned so the execution gas charge OOGs by 1. If state gas is incorrectly charged first, the parent's reservoir is inflated. Single-SSTORE probe detects the inflation. """ dead_beneficiary = 0xBEEF child_code = Op.SELFDESTRUCT(dead_beneficiary, account_new=True) - # One gas short of the SELFDESTRUCT's full cost (regular plus the + # One gas short of the SELFDESTRUCT's full cost (execution plus the # NEW_ACCOUNT state charge), so it OOGs on the account-creation charge. child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code, balance=1) @@ -289,7 +289,7 @@ def test_create_oog_reservoir_inflation_detection( else: child_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op - # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # One gas short of the CREATE's full cost (execution plus the NEW_ACCOUNT # state charge), so it OOGs on the account-creation charge. child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) @@ -361,7 +361,7 @@ def test_create_oog_full_burn_no_state_credit( factory_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op factory = pre.deploy_contract(factory_code) - # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # One gas short of the CREATE's full cost (execution plus the NEW_ACCOUNT # state charge), so it OOGs on the account-creation charge. body_gas = factory_code.gas_cost(fork) - 1 diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py index 99af67b5a2c..df91061a30d 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py @@ -99,7 +99,7 @@ def test_charge_draws_entirely_from_reservoir( When the reservoir has enough gas for the SSTORE state cost, gas_left should not be reduced by the state charge. Verify by - performing a regular-gas-heavy computation after the SSTORE. + performing an execution-gas-heavy computation after the SSTORE. """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -108,10 +108,10 @@ def test_charge_draws_entirely_from_reservoir( code=( # SSTORE draws state gas from reservoir Op.SSTORE(storage.store_next(1), 1) - # Remaining gas_left is available for regular ops + # Remaining gas_left is available for execution ops + Op.SSTORE( storage.store_next(1), - Op.ADD(1, 0), # Cheap regular-gas op + Op.ADD(1, 0), # Cheap execution-gas op ) ), ) @@ -183,9 +183,9 @@ def test_charge_spill_boundary( contract = pre.deploy_contract(code=code) intrinsic = fork.transaction_intrinsic_cost_calculator()() - regular = code.regular_cost(fork) + execution = code.execution_cost(fork) sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - gas_limit = intrinsic + regular + sstore_state_gas + gas_delta + gas_limit = intrinsic + execution + sstore_state_gas + gas_delta tx = Transaction( to=contract, @@ -194,7 +194,7 @@ def test_charge_spill_boundary( ) header = Header( - gas_used=max(intrinsic + regular, sstore_state_gas) + gas_used=max(intrinsic + execution, sstore_state_gas) if gas_delta == 0 else gas_limit ) @@ -217,7 +217,7 @@ def test_refund_cap_includes_state_gas( When state gas is drawn from gas_left (no reservoir), it counts toward tx_gas_used_before_refund. The 1/5 refund cap applies to - the combined total of regular + state gas consumed. This test + the combined total of execution + state gas consumed. This test performs an SSTORE zero-to-nonzero-to-zero sequence to generate a refund and verifies the transaction succeeds. """ @@ -248,7 +248,7 @@ def test_refund_with_reservoir_state_gas( Test refund when state gas is drawn from reservoir. When state gas comes from the reservoir, the refund still applies. - The refund_counter accumulates state + regular gas refunds, and + The refund_counter accumulates state + execution gas refunds, and the 1/5 cap uses tx_gas_used_before_refund which accounts for both dimensions. An SSTORE zero-to-nonzero-to-zero sequence should refund correctly. @@ -270,30 +270,32 @@ def test_refund_with_reservoir_state_gas( state_test(pre=pre, post=post, tx=tx) -def _access_list_over_regular_cap( +def _access_list_over_execution_cap( fork: Fork, cap: int, *, margin_num: int = 1, margin_den: int = 1 ) -> list[AccessList]: """ - Build an access list whose intrinsic *regular* gas exceeds ``cap`` by + Build an access list whose intrinsic *execution* gas exceeds ``cap`` by roughly the factor ``margin_num / margin_den``. - Each access-list address adds a fixed amount to the regular intrinsic + Each access-list address adds a fixed amount to the execution intrinsic (the EIP-2930 address cost plus the EIP-7981 floor-token surcharge) and a much smaller amount to the calldata floor, so the list raises the - regular operand of ``max(intrinsic_regular, calldata_floor)`` over the + execution operand of ``max(intrinsic_execution, calldata_floor)`` over the cap while the floor stays below it. No state gas is incurred. """ intrinsic = fork.transaction_intrinsic_cost_calculator() - base_regular = intrinsic(return_cost_deducted_prior_execution=True) - per_address_regular = ( + base_execution = intrinsic(return_cost_deducted_prior_execution=True) + per_address_execution = ( intrinsic( access_list=[AccessList(address=Address(0x100), storage_keys=[])], return_cost_deducted_prior_execution=True, ) - - base_regular + - base_execution ) - assert per_address_regular > 0 - num_entries = (cap * margin_num) // (per_address_regular * margin_den) + 1 + assert per_address_execution > 0 + num_entries = (cap * margin_num) // ( + per_address_execution * margin_den + ) + 1 return [ AccessList(address=Address(0x10000 + i), storage_keys=[]) for i in range(num_entries) @@ -302,18 +304,18 @@ def _access_list_over_regular_cap( @pytest.mark.exception_test @pytest.mark.valid_from("EIP8037") -def test_intrinsic_regular_gas_exceeds_cap( +def test_intrinsic_execution_gas_exceeds_cap( state_test: StateTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - Reject a transaction whose intrinsic *regular* gas exceeds the cap. + Reject a transaction whose intrinsic *execution* gas exceeds the cap. - EIP-8037 enforces ``max(intrinsic_regular, calldata_floor) <= + EIP-8037 enforces ``max(intrinsic_execution, calldata_floor) <= TX_MAX_GAS_LIMIT`` after the separate sufficiency check ``max(intrinsic_total, calldata_floor) <= tx.gas``. A large access list - raises the regular intrinsic over the cap while adding no state gas and + raises the execution intrinsic over the cap while adding no state gas and keeping the calldata floor below the cap. ``gas_limit`` is set above the total intrinsic so the sufficiency check passes and the cap is the only reason the transaction is rejected; a client that compares the intrinsic @@ -324,16 +326,16 @@ def test_intrinsic_regular_gas_exceeds_cap( floor_cost = fork.transaction_data_floor_cost_calculator() intrinsic = fork.transaction_intrinsic_cost_calculator() - access_list = _access_list_over_regular_cap(fork, cap) - regular = intrinsic( + access_list = _access_list_over_execution_cap(fork, cap) + execution = intrinsic( access_list=access_list, return_cost_deducted_prior_execution=True, ) floor = floor_cost(data=b"", access_list=access_list) - tx_gas = regular + 1_000_000 + tx_gas = execution + 1_000_000 - assert max(regular, floor) > cap, "cap check must fire" - assert regular <= tx_gas, "sufficiency check must not fire" + assert max(execution, floor) > cap, "cap check must fire" + assert execution <= tx_gas, "sufficiency check must not fire" assert floor <= tx_gas tx = Transaction( @@ -349,21 +351,21 @@ def test_intrinsic_regular_gas_exceeds_cap( @pytest.mark.exception_test @pytest.mark.valid_from("EIP8037") -def test_intrinsic_regular_gas_exceeds_cap_with_floor_below_cap( +def test_intrinsic_execution_gas_exceeds_cap_with_floor_below_cap( state_test: StateTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - Reject when intrinsic *regular* gas exceeds the cap while the calldata - floor stays below it, isolating the regular operand of - ``max(intrinsic_regular, calldata_floor)``. + Reject when intrinsic *execution* gas exceeds the cap while the calldata + floor stays below it, isolating the execution operand of + ``max(intrinsic_execution, calldata_floor)``. - A large access list with no calldata pushes the regular intrinsic over + A large access list with no calldata pushes the execution intrinsic over the cap while the floor stays well below it, and ``gas_limit`` covers the total intrinsic so the sufficiency check passes. The explicit ``floor < cap`` assertion guarantees the rejection comes from the - regular operand, so a client that compares only the calldata floor + execution operand, so a client that compares only the calldata floor against the cap would wrongly accept the transaction. """ cap = fork.transaction_gas_limit_cap() @@ -371,19 +373,19 @@ def test_intrinsic_regular_gas_exceeds_cap_with_floor_below_cap( floor_cost = fork.transaction_data_floor_cost_calculator() intrinsic = fork.transaction_intrinsic_cost_calculator() - access_list = _access_list_over_regular_cap( + access_list = _access_list_over_execution_cap( fork, cap, margin_num=5, margin_den=4 ) - regular = intrinsic( + execution = intrinsic( access_list=access_list, return_cost_deducted_prior_execution=True, ) floor = floor_cost(data=b"", access_list=access_list) - tx_gas = regular + 1_000_000 + tx_gas = execution + 1_000_000 - assert regular > cap, "regular operand must exceed the cap" + assert execution > cap, "execution operand must exceed the cap" assert floor < cap, "calldata floor must stay below the cap" - assert regular <= tx_gas, "sufficiency check must not fire" + assert execution <= tx_gas, "sufficiency check must not fire" tx = Transaction( ty=1, @@ -407,7 +409,7 @@ def test_intrinsic_within_cap_gas_limit_above_cap( intrinsic operands stay below it. EIP-8037 relaxes the EIP-7825 cap on ``tx.gas`` itself; only - ``max(intrinsic_regular, calldata_floor)`` is capped. This positive + ``max(intrinsic_execution, calldata_floor)`` is capped. This positive control sets ``gas_limit`` above the cap with a small access list so both operands are far below it, and the transaction must execute. It is the accepting counterpart to the cap-rejection tests above. @@ -421,12 +423,12 @@ def test_intrinsic_within_cap_gas_limit_above_cap( AccessList(address=Address(0x10000 + i), storage_keys=[]) for i in range(16) ] - regular = intrinsic( + execution = intrinsic( access_list=access_list, return_cost_deducted_prior_execution=True, ) floor = floor_cost(data=b"", access_list=access_list) - assert regular <= cap + assert execution <= cap assert floor <= cap storage = Storage() @@ -466,26 +468,26 @@ def test_calldata_floor_enforced_with_state_gas( Test EIP-7623 calldata floor is enforced when EIP-8037 is active. Send 100 non-zero calldata bytes to a call transaction so the - regular intrinsic cost is below the calldata floor. A gas_limit + execution intrinsic cost is below the calldata floor. A gas_limit at the floor succeeds; one below the floor is rejected. """ calldata = b"\x01" * 100 intrinsic_cost = fork.transaction_intrinsic_cost_calculator() floor_cost = fork.transaction_data_floor_cost_calculator() - regular_gas = intrinsic_cost( + execution_gas = intrinsic_cost( calldata=calldata, return_cost_deducted_prior_execution=True, ) floor_gas = floor_cost(data=calldata) - assert floor_gas > regular_gas, "floor must exceed regular for test" + assert floor_gas > execution_gas, "floor must exceed execution for test" if above_floor: gas_limit = floor_gas error = None else: - # Between regular and floor: satisfies regular but not floor - gas_limit = (regular_gas + floor_gas) // 2 + # Between execution and floor: satisfies execution but not floor + gas_limit = (execution_gas + floor_gas) // 2 error = TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST tx = Transaction( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py index 5ac3ba9226c..a75ac6136e5 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py @@ -3,11 +3,12 @@ EIP-7825 TX_MAX_GAS_LIMIT cap. EIP-8037 splits execution gas into two pools: -- `gas_left` (regular gas): capped at `TX_MAX_GAS_LIMIT - intrinsic.regular` -- `state_gas_reservoir`: the overflow beyond the regular gas cap +- `gas_left` (execution gas): capped at + `TX_MAX_GAS_LIMIT - intrinsic.execution` +- `state_gas_reservoir`: the overflow beyond the execution gas cap State gas charges draw from the reservoir first, then spill into gas_left. -Regular gas charges draw only from gas_left. +Execution gas charges draw only from gas_left. Tests for [EIP-8037: State Creation Gas Cost Increase] (https://eips.ethereum.org/EIPS/eip-8037). @@ -170,17 +171,17 @@ def test_insufficient_gas_for_sstore_state_cost( """ Test that execution OOGs when gas is insufficient for SSTORE state cost. - Provide just enough gas for intrinsic costs plus the SSTORE regular + Provide just enough gas for intrinsic costs plus the SSTORE execution gas, but not enough to also cover the SSTORE state gas. The SSTORE should OOG, leaving storage slot 0 unchanged at zero. """ contract_code = Op.SSTORE(0, 1) contract = pre.deploy_contract(code=contract_code) - # Enough for intrinsic + warm SSTORE regular gas, but not the + # Enough for intrinsic + warm SSTORE execution gas, but not the # state gas cost for zero-to-nonzero transition intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - gas_limit = intrinsic_cost() + contract_code.regular_cost(fork) + gas_limit = intrinsic_cost() + contract_code.execution_cost(fork) tx = Transaction( to=contract, @@ -201,16 +202,16 @@ def test_insufficient_gas_for_sstore_state_cost( ], ) @pytest.mark.valid_from("EIP8037") -def test_block_regular_gas_limit( +def test_block_execution_gas_limit( blockchain_test: BlockchainTestFiller, pre: Alloc, exceed_block_gas_limit: bool, fork: Fork, ) -> None: """ - Test check_transaction enforcement of regular gas against block limit. + Test check_transaction enforcement of execution gas against block limit. - The regular gas check uses min(TX_MAX_GAS_LIMIT, tx.gas). + The execution gas check uses min(TX_MAX_GAS_LIMIT, tx.gas). Fill the block with transactions at TX_MAX_GAS_LIMIT and verify the last one is accepted or rejected based on remaining capacity. """ @@ -268,7 +269,7 @@ def test_block_state_gas_limit_boundary( (delta=0, accepted because the check is strict `>`) or exceeds it by 1 (delta=1, rejected with `GAS_ALLOWANCE_EXCEEDED`). - The regular check is asserted to pass so rejection on delta=1 is + The execution check is asserted to pass so rejection on delta=1 is pinned to the state dimension. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -285,7 +286,7 @@ def test_block_state_gas_limit_boundary( tx1_contract = pre.deploy_contract(code=tx1_code) tx1_state = tx1_code.state_cost(fork) - tx1_regular = intrinsic_cost() + tx1_code.gas_cost(fork) - tx1_state + tx1_execution = intrinsic_cost() + tx1_code.gas_cost(fork) - tx1_state tx1_gas = gas_limit_cap + tx1_state # tx2: worst-case state contribution = tx.gas (strict EIP rule). @@ -294,10 +295,10 @@ def test_block_state_gas_limit_boundary( tx2_gas = state_available + delta # Pin the rejection (when delta > 0) to the state check: the - # regular check must not fire. - regular_available = block_gas_limit - tx1_regular - assert min(gas_limit_cap, tx2_gas) < regular_available, ( - "tx2 would fail the regular check instead of the state check" + # execution check must not fire. + execution_available = block_gas_limit - tx1_execution + assert min(gas_limit_cap, tx2_gas) < execution_available, ( + "tx2 would fail the execution check instead of the state check" ) tx2_error = ( @@ -333,57 +334,57 @@ def test_block_state_gas_limit_boundary( @pytest.mark.exception_test @pytest.mark.valid_from("EIP8037") -def test_creation_tx_regular_check_uses_full_tx_gas( +def test_creation_tx_execution_check_uses_full_tx_gas( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - Verify the regular check uses the full `tx.gas` (no subtraction). + Verify the execution check uses the full `tx.gas` (no subtraction). - The EIP regular check is `min(TX_MAX, tx.gas) > regular_available`. + The EIP execution check is `min(TX_MAX, tx.gas) > execution_available`. Under EIP-2780 a creation tx has `intrinsic.state == 0` (the created account's `NEW_ACCOUNT` moved to the top frame), so its intrinsic is - regular-only. This test sizes a creation tx whose full `tx.gas` - exceeds the remaining regular budget by one — it must be rejected. A + execution-only. This test sizes a creation tx whose full `tx.gas` + exceeds the remaining execution budget by one — it must be rejected. A formula that instead used the execution gas - (`tx.gas - intrinsic_regular`) would have wrongly accepted. + (`tx.gas - intrinsic_execution`) would have wrongly accepted. """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - # The creation intrinsic is regular-only and cpsb-free - # (GAS_TX_BASE + REGULAR_GAS_CREATE + init_code_cost), giving a stable + # The creation intrinsic is execution-only and cpsb-free + # (GAS_TX_BASE + EXECUTION_GAS_CREATE + init_code_cost), giving a stable # `block_gas_limit` independent of cpsb. - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( contract_creation=True ) # Tight boundary: after the filler consumes gas_limit_cap, exactly - # `intrinsic_regular + 1` regular gas remains in the block. - block_gas_limit = gas_limit_cap + intrinsic_regular + 1 + # `intrinsic_execution + 1` execution gas remains in the block. + block_gas_limit = gas_limit_cap + intrinsic_execution + 1 - # Ask for one more than the remaining regular budget: min(TX_MAX, - # tx.gas) == tx.gas exceeds `remaining_regular` by one, so the strict + # Ask for one more than the remaining execution budget: min(TX_MAX, + # tx.gas) == tx.gas exceeds `remaining_execution` by one, so the strict # check rejects. The tx still carries more than its own intrinsic, so - # it is a valid creation tx on its own — only the block-level regular + # it is a valid creation tx on its own — only the block-level execution # check fails. - remaining_regular = block_gas_limit - gas_limit_cap - create_tx_gas = remaining_regular + 1 + remaining_execution = block_gas_limit - gas_limit_cap + create_tx_gas = remaining_execution + 1 - # Filler consumes the full regular cap (OOG on INVALID). + # Filler consumes the full execution cap (OOG on INVALID). filler = pre.deploy_contract(code=Op.INVALID) assert create_tx_gas <= gas_limit_cap, ( "min(TX_MAX, tx.gas) must be tx.gas for this boundary" ) - assert create_tx_gas > intrinsic_regular, ( + assert create_tx_gas > intrinsic_execution, ( "tx must carry more than its own intrinsic" ) - assert min(gas_limit_cap, create_tx_gas) > remaining_regular, ( - "strict formula must reject: full tx.gas exceeds remaining regular" + assert min(gas_limit_cap, create_tx_gas) > remaining_execution, ( + "strict formula must reject: full tx.gas exceeds remaining execution" ) - assert create_tx_gas - intrinsic_regular <= remaining_regular, ( + assert create_tx_gas - intrinsic_execution <= remaining_execution, ( "a formula using execution gas would have accepted" ) @@ -468,7 +469,7 @@ def test_creation_tx_state_check_exceeded( A creation tx (`to=None`) goes through the per-dimension inclusion check like any other tx. A filler tx consumes state budget; the creation tx's `tx.gas` then exceeds the remaining state budget by - one while its regular contribution still fits, pinning the + one while its execution contribution still fits, pinning the rejection to the state dimension. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -485,16 +486,16 @@ def test_creation_tx_state_check_exceeded( tx1_contract = pre.deploy_contract(code=tx1_code) tx1_state = tx1_code.state_cost(fork) - tx1_regular = intrinsic_cost() + tx1_code.gas_cost(fork) - tx1_state + tx1_execution = intrinsic_cost() + tx1_code.gas_cost(fork) - tx1_state tx1_gas = gas_limit_cap + tx1_state state_available = block_gas_limit - tx1_state # tx2: full tx.gas exceeds state_available by 1, so rejected. tx2_gas = state_available + 1 - # Regular check must pass so rejection is pinned to state. - regular_available = block_gas_limit - tx1_regular - assert min(gas_limit_cap, tx2_gas) < regular_available + # Execution check must pass so rejection is pinned to state. + execution_available = block_gas_limit - tx1_execution + assert min(gas_limit_cap, tx2_gas) < execution_available tx1 = Transaction( to=tx1_contract, @@ -529,10 +530,10 @@ def test_block_gas_used_no_state_ops( fork: Fork, ) -> None: """ - Test block gas_used when regular gas dominates (no state operations). + Test block gas_used when execution gas dominates (no state operations). With no state-creating operations, state gas is 0 and block gas_used - should equal regular gas used. + should equal execution gas used. """ contract = pre.deploy_contract(code=Op.STOP) @@ -576,9 +577,9 @@ def test_block_gas_used_with_state_ops( ) intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - block_regular_gas = intrinsic_cost() + code.regular_cost(fork) + block_execution_gas = intrinsic_cost() + code.execution_cost(fork) block_state_gas = code.state_cost(fork) - assert block_state_gas > block_regular_gas + assert block_state_gas > block_execution_gas blockchain_test( pre=pre, @@ -603,7 +604,7 @@ def test_block_2d_gas_valid_when_cumulative_exceeds_limit( """ Verify block validity under 2D gas when sum(txGasUsed) > gas_limit. - EIP-8037 block validity: max(regular, state) <= gas_limit. + EIP-8037 block validity: max(execution, state) <= gas_limit. Receipt cumulative_gas_used sums both dimensions per-tx, so it can legitimately exceed gas_limit. Clients must not use the 1D cumulative check for block validation. @@ -613,21 +614,21 @@ def test_block_2d_gas_valid_when_cumulative_exceeds_limit( sstore_code = Op.SSTORE(0, 1, new_value=1) sstore_state_gas = sstore_code.state_cost(fork) - tx_regular = ( - sstore_code.regular_cost(fork) + tx_execution = ( + sstore_code.execution_cost(fork) + fork.transaction_intrinsic_cost_calculator()() ) tx_state = sstore_state_gas - tx_gas_used = tx_regular + tx_state + tx_gas_used = tx_execution + tx_state - assert tx_state > tx_regular + assert tx_state > tx_execution block_gas_used = tx_state env = Environment(gas_limit=block_gas_limit) tx_limit = tx_gas_used + 1000 # Strict rule counts full `tx.gas` per dimension; state is the - # binding one (tx_state > tx_regular), so every `tx_limit` must + # binding one (tx_state > tx_execution), so every `tx_limit` must # fit the remaining state gas. num_txs = (block_gas_limit - tx_limit) // tx_state + 1 two_d_bound = num_txs * block_gas_used @@ -796,7 +797,7 @@ def test_top_level_failure_zeros_block_state_gas( With `state_gas_used` zeroed on failure, `block_state_gas_used` excludes any state gas consumed during the failed transaction and - the block header `gas_used` falls back to the regular gas + the block header `gas_used` falls back to the execution gas component alone. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -820,19 +821,19 @@ def test_top_level_failure_zeros_block_state_gas( ) if failure_mode == "revert": - expected_block_regular = ( + expected_block_execution = ( intrinsic_cost + code.gas_cost(fork) - sstore_state_gas ) else: # Exceptional halt and out of gas zero gas_left. - expected_block_regular = tx_gas - sstore_state_gas + expected_block_execution = tx_gas - sstore_state_gas blockchain_test( pre=pre, blocks=[ Block( txs=[tx], - header_verify=Header(gas_used=expected_block_regular), + header_verify=Header(gas_used=expected_block_execution), ), ], post={contract: Account(storage={})}, @@ -851,7 +852,7 @@ def test_creation_tx_failure_preserves_intrinsic_state_gas( A creation tx (to=None) whose initcode halts exercises both the intrinsic state gas for the new account and the top level failure refund of execution state gas. The test asserts the block header - `gas_used` equals `max(block_regular, intrinsic_state_gas)`, + `gas_used` equals `max(block_execution, intrinsic_state_gas)`, guarding that the failure path does not raise and that block accounting does not underflow when the refund is applied. """ @@ -871,8 +872,8 @@ def test_creation_tx_failure_preserves_intrinsic_state_gas( sender=pre.fund_eoa(), ) - block_regular = tx_gas - create_intrinsic_state - sstore_state_gas - expected_gas_used = max(block_regular, create_intrinsic_state) + block_execution = tx_gas - create_intrinsic_state - sstore_state_gas + expected_gas_used = max(block_execution, create_intrinsic_state) blockchain_test( pre=pre, @@ -918,7 +919,7 @@ def test_subcall_failure_does_not_zero_top_level_state_gas( sender=pre.fund_eoa(), ) - # Parent's SSTORE state gas dominates tx_regular and surfaces in + # Parent's SSTORE state gas dominates tx_execution and surfaces in # the block header, proving the top level refund is scoped to # top level failures and not child reverts. blockchain_test( @@ -968,7 +969,7 @@ def test_top_level_failure_spilled_state_gas( `gas_left` and only the reservoir-funded portion to the reservoir. - REVERT preserves `gas_left`, so all state gas is refunded and the - sender pays only the regular component. + sender pays only the execution component. - Halt refills LIFO then zeros `gas_left`, so the spill is burned and only the start reservoir survives. """ @@ -1001,7 +1002,7 @@ def test_top_level_failure_spilled_state_gas( if failure_mode == "revert": # gas_left preserved, all state gas refunded, so the sender - # pays only the regular component. + # pays only the execution component. expected_cumulative = ( intrinsic_cost + parent_code.gas_cost(fork) - total_state ) @@ -1231,7 +1232,7 @@ def test_nested_failure_resets_to_tx_reservoir( Refunds are LIFO. On REVERT every state gas charge (body charges, spilled portions, and CREATE pre-charges) is refilled, the spill - landing back in `gas_left`, so the user pays only regular charges + landing back in `gas_left`, so the user pays only execution charges plus intrinsic. On HALT the LIFO refill returns spilled state gas to `gas_left`, which is then zeroed, so only the start reservoir survives and the user pays `tx_gas - reservoir = gas_limit_cap`, @@ -1240,7 +1241,7 @@ def test_nested_failure_resets_to_tx_reservoir( Two assertions cross-check the gas accounting: - `cumulative_gas_used` (receipt) pins `tx.gas - gas_left - state_gas_left`, catching bugs in the leftover split. - - `header.gas_used` pins `max(block_regular, block_state)` via + - `header.gas_used` pins `max(block_execution, block_state)` via the block accumulators. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -1270,7 +1271,7 @@ def test_nested_failure_resets_to_tx_reservoir( else: top, frame_codes = _build_create_chain(pre, frame_bodies, terminator) - sum_regular = sum(code.regular_cost(fork) for code in frame_codes) + sum_execution = sum(code.execution_cost(fork) for code in frame_codes) if failure_mode == "halt": # LIFO refill returns spilled state gas (and spilled CREATE # pre-charges) to gas_left, which halt then zeros. Only the @@ -1278,17 +1279,17 @@ def test_nested_failure_resets_to_tx_reservoir( expected_cumulative = tx_gas - reservoir assert expected_cumulative == gas_limit_cap # Header: all gas_left (including the refilled spill) is - # consumed as regular. Block state gas is zero for plain + # consumed as execution. Block state gas is zero for plain # frames. expected_header_gas_used = gas_limit_cap elif failure_mode == "revert": # Revert preserves gas_left, full state gas refund, so the - # user pays only regular costs plus intrinsic. - expected_cumulative = intrinsic_cost + sum_regular - # Header reflects the regular-vs-state attribution directly: + # user pays only execution costs plus intrinsic. + expected_cumulative = intrinsic_cost + sum_execution + # Header reflects the execution-vs-state attribution directly: # state_gas_used is zeroed by the tx error handler, so only - # regular gas usage shows up. - expected_header_gas_used = intrinsic_cost + sum_regular + # execution gas usage shows up. + expected_header_gas_used = intrinsic_cost + sum_execution else: raise ValueError("Invariant, unreachable code.") @@ -1461,7 +1462,7 @@ def test_top_level_opcode_oog_before_frame_end_does_not_refund_state_gas( unsettled state gas. The transaction has enough gas for the SSTORE and all preceding - regular work, but is one gas short of the MCOPY regular cost. The + execution work, but is one gas short of the MCOPY execution cost. The frame halts before frame-end settlement runs, so the earlier SSTORE never contributes execution state gas to refund. """ @@ -1478,7 +1479,7 @@ def test_top_level_opcode_oog_before_frame_end_does_not_refund_state_gas( ) contract = pre.deploy_contract(code=code) - # One gas short of the regular-gas portion of successful execution. + # One gas short of the execution-gas portion of successful execution. tx_gas = intrinsic_cost + code.gas_cost(fork) - sstore_state_gas - 1 tx = Transaction( @@ -1508,14 +1509,14 @@ def test_top_level_opcode_oog_before_frame_end_does_not_refund_state_gas( ], ) @pytest.mark.valid_from("EIP8037") -def test_access_list_gas_is_regular_not_state( +def test_access_list_gas_is_execution_not_state( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, num_access_list_entries: int, slots_per_entry: int, ) -> None: - """Verify EIP-2930 access list gas counts as regular, not state.""" + """Verify EIP-2930 access list gas counts as execution, not state.""" contract = pre.deploy_contract(code=Op.STOP) access_list = [] @@ -1549,12 +1550,12 @@ def test_access_list_gas_is_regular_not_state( @pytest.mark.valid_from("EIP8037") -def test_access_list_warm_savings_stay_regular( +def test_access_list_warm_savings_stay_execution( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, ) -> None: - """Verify access-list warm savings stay in regular gas.""" + """Verify access-list warm savings stay in execution gas.""" sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) contract = pre.deploy_contract( @@ -1671,7 +1672,7 @@ def test_subcall_revert_does_not_leak_grandchild_storage_clear_credit( # phantom credit surfaces as residual reservoir at tx end. legit_state_cost = 2 * num_slots * sstore_state_gas - # `bytecode.gas_cost(fork)` sums each opcode's regular and state + # `bytecode.gas_cost(fork)` sums each opcode's execution and state # contributions. Setup/phantom SSTOREs predict +sstore_state_gas # each; inner's clears predict 0 (the negative byte_delta is a # frame-level effect, not per-opcode). The frame-end byte_delta @@ -1825,11 +1826,11 @@ def test_subcall_set_clear_revert_pays_no_state_gas( ) -> None: """ A child frame doing SSTORE 0 to x to 0 then REVERT must bill the - sender only intrinsic + regular costs. + sender only intrinsic + execution costs. Both SSTOREs roll back with the REVERT, so the matching state-gas charge and refund cancel cleanly. The receipt's - `cumulative_gas_used` equals the regular baseline; a leftover + `cumulative_gas_used` equals the execution baseline; a leftover `sstore_state_gas` would surface a double-charge at the failure boundary. @@ -1862,8 +1863,8 @@ def test_subcall_set_clear_revert_pays_no_state_gas( expected_cumulative = ( intrinsic_cost - + top_code.regular_cost(fork) - + inner_code.regular_cost(fork) + + top_code.execution_cost(fork) + + inner_code.execution_cost(fork) ) tx = Transaction( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py index f02eaca1336..7ff6505041e 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py @@ -227,7 +227,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( The inner frame spills the NEW_ACCOUNT charge and self-destructs successfully, then the caller reverts: the beneficiary creation rolls back and the spilled state charge is refilled. The EIP-8038 - regular account-write charge for the attempted empty-account value + execution account-write charge for the attempted empty-account value transfer remains billed. """ beneficiary = 0xDEAD @@ -236,10 +236,10 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.REVERT(0, 0) caller = pre.deploy_contract(code=caller_code) - expected_regular = ( + expected_execution = ( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) - + inner_code.regular_cost(fork) + + inner_code.execution_cost(fork) ) tx = Transaction(to=caller, sender=pre.fund_eoa()) @@ -247,7 +247,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( pre=pre, post={beneficiary: Account.NONEXISTENT, inner: Account(balance=1)}, tx=tx, - blockchain_test_header_verify=Header(gas_used=expected_regular), + blockchain_test_header_verify=Header(gas_used=expected_execution), ) @@ -298,13 +298,13 @@ def test_create_selfdestruct_no_refund_account_and_storage( total_state_gas = factory_code.state_cost(fork) + init_code.state_cost( fork ) - regular_used = ( + execution_used = ( intrinsic_gas + factory_code.gas_cost(fork) + init_code.gas_cost(fork) - total_state_gas ) - expected_gas_used = max(regular_used, total_state_gas) + expected_gas_used = max(execution_used, total_state_gas) tx = Transaction( to=factory, @@ -439,8 +439,8 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( sender=pre.fund_eoa(), ) - baseline_block_regular = 0x94C8 - expected_gas_used = max(baseline_block_regular, total_state_gas) + baseline_block_execution = 0x94C8 + expected_gas_used = max(baseline_block_execution, total_state_gas) blockchain_test( pre=pre, @@ -493,14 +493,14 @@ def test_create_selfdestruct_sstore_restoration_refund( new_account_state_gas = factory_code.state_cost(fork) state_used = new_account_state_gas - regular_used = ( + execution_used = ( intrinsic_gas + factory_code.gas_cost(fork) + init_code.gas_cost(fork) - new_account_state_gas - sstore_state_gas ) - expected_gas_used = max(regular_used, state_used) + expected_gas_used = max(execution_used, state_used) tx = Transaction( to=factory, @@ -531,7 +531,7 @@ def test_selfdestruct_pre_existing_account_no_refund( state gas back into the reservoir. A contract deployed in `pre` is destroyed by the tx; `accounts_to_delete` contains it but `created_accounts` does not, so no refund is applied. The block - header `gas_used` reflects the full regular-gas tx cost (no + header `gas_used` reflects the full execution-gas tx cost (no state-gas refund offset). """ intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() @@ -545,8 +545,8 @@ def test_selfdestruct_pre_existing_account_no_refund( caller = pre.deploy_contract(code=caller_code) # No refund offset: both caller_code and victim_code are pure - # regular gas (SELFDESTRUCT to self, no value-to-new-account). - tx_regular = ( + # execution gas (SELFDESTRUCT to self, no value-to-new-account). + tx_execution = ( intrinsic_gas + caller_code.gas_cost(fork) + victim_code.gas_cost(fork) ) @@ -560,7 +560,7 @@ def test_selfdestruct_pre_existing_account_no_refund( # does not delete it — the account still exists after the tx. blockchain_test( pre=pre, - blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_execution))], post={victim: Account(code=victim_code)}, ) @@ -591,9 +591,9 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( # Bottom of the chain does the SELFDESTRUCT; intermediate helpers # just delegate further down. Track each frame's bytecode so we - # can sum its regular gas into `expected_gas_used` below. + # can sum its execution gas into `expected_gas_used` below. sd_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)(Op.ADDRESS) - chain_regular_gas = sd_code.gas_cost(fork) + chain_execution_gas = sd_code.gas_cost(fork) delegate_target = pre.deploy_contract(code=sd_code) for _ in range(num_hops - 1): hop_code = ( @@ -604,7 +604,7 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( ) + Op.STOP ) - chain_regular_gas += hop_code.gas_cost(fork) + chain_execution_gas += hop_code.gas_cost(fork) delegate_target = pre.deploy_contract(code=hop_code) # A's deployed runtime: one delegation into the top of the chain. @@ -666,15 +666,15 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( created_address = compute_create_address(address=factory, nonce=1) total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) - regular_used = ( + execution_used = ( intrinsic_gas + factory_code.gas_cost(fork) + initcode.gas_cost(fork) + deployed_code.gas_cost(fork) - + chain_regular_gas + + chain_execution_gas - total_state_gas ) - expected_gas_used = max(regular_used, total_state_gas) + expected_gas_used = max(execution_used, total_state_gas) tx = Transaction( to=factory, @@ -706,18 +706,18 @@ def test_selfdestruct_new_beneficiary_account_write_cost( ) -> None: """ Verify SELFDESTRUCT to a new beneficiary charges `ACCOUNT_WRITE` - regular gas plus the account-creation state gas, and not the - legacy combined regular account-creation cost. + execution gas plus the account-creation state gas, and not the + legacy combined execution account-creation cost. """ beneficiary = pre.fund_eoa(amount=0) victim_code = Op.SELFDESTRUCT(beneficiary, account_new=True) victim = pre.deploy_contract(code=victim_code, balance=1) - # Tight budget: slack is less than the legacy 25,000 regular - # account-creation cost minus `ACCOUNT_WRITE`, so any regular draw + # Tight budget: slack is less than the legacy 25,000 execution + # account-creation cost minus `ACCOUNT_WRITE`, so any execution draw # beyond `ACCOUNT_WRITE` would OOG. The opcode metadata folds the - # `ACCOUNT_WRITE` regular cost and the account-creation state gas + # `ACCOUNT_WRITE` execution cost and the account-creation state gas # into `gas_cost`. intrinsic = fork.transaction_intrinsic_cost_calculator()() tx = Transaction( @@ -777,20 +777,20 @@ def test_create_tx_selfdestruct_initcode_state_gas( init_code = Op.SELFDESTRUCT.with_metadata( account_new=creates_new_beneficiary )(beneficiary) - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) expected_state = fork.transaction_top_frame_state_gas( contract_creation=True ) + init_code.state_cost(fork) - expected_regular = intrinsic_regular + init_code.regular_cost(fork) - expected_gas_used = max(expected_regular, expected_state) + expected_execution = intrinsic_execution + init_code.execution_cost(fork) + expected_gas_used = max(expected_execution, expected_state) tx = Transaction( to=None, data=init_code, - gas_limit=intrinsic_regular + 100_000 + expected_state, + gas_limit=intrinsic_execution + 100_000 + expected_state, sender=sender, value=tx_value, ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py index c537b42088b..8a84ec8571b 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py @@ -8,7 +8,7 @@ are charged lazily at the top frame in ``set_delegation``, keyed on each authority's pre-transaction state: -* ``NEW_ACCOUNT`` (state) + ``ACCOUNT_WRITE`` (regular) when the +* ``NEW_ACCOUNT`` (state) + ``ACCOUNT_WRITE`` (execution) when the authority's account leaf does not exist pre-tx (it gets created); and * ``AUTH_BASE`` (state) when a net-new delegation indicator is written -- the authority holds no delegation both before the transaction and at @@ -18,12 +18,12 @@ For a value-free type-4 transaction whose recipient runs code ``code``: * the receipt ``cumulative_gas_used`` is the plain sum - ``intrinsic_regular + top_frame_regular + top_frame_state + - execution_regular + execution_state`` (no refund term); and -* the header ``gas_used`` is ``max(block_regular, block_state)`` where - ``block_regular = intrinsic_regular + top_frame_regular + - execution_regular`` and ``block_state = top_frame_state + - execution_state``. + ``intrinsic_execution + top_frame_execution + top_frame_state + + evm_execution + evm_state`` (no refund term); and +* the header ``gas_used`` is ``max(block_execution, block_state)`` where + ``block_execution = intrinsic_execution + top_frame_execution + + evm_execution`` and ``block_state = top_frame_state + + evm_state``. Tests for [EIP-8037: State Creation Gas Cost Increase] (https://eips.ethereum.org/EIPS/eip-8037); the ``valid_from("EIP8037")`` @@ -70,14 +70,14 @@ def _auth_gas( sends_value: bool = False, delegation_warm: bool = False, ) -> tuple[int, int, int]: - """Return (intrinsic_regular, top_frame_regular, top_frame_state).""" - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + """Return (intrinsic_execution, top_frame_execution, top_frame_state).""" + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=authorization_list, recipient_type=recipient_type, sends_value=sends_value, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( recipient_type=recipient_type, sends_value=sends_value, delegation_warm=delegation_warm, @@ -88,26 +88,26 @@ def _auth_gas( sends_value=sends_value, authorizations=authorization_list, ) - return intrinsic_regular, top_frame_regular, top_frame_state + return intrinsic_execution, top_frame_execution, top_frame_state def _receipt_and_header( - intrinsic_regular: int, - top_frame_regular: int, + intrinsic_execution: int, + top_frame_execution: int, top_frame_state: int, *, - execution_regular: int = 0, - execution_state: int = 0, + evm_execution: int = 0, + evm_state: int = 0, ) -> tuple[int, int]: """ Return the (receipt cumulative_gas_used, header gas_used) for a successful (non-reverting) transaction under the no-refund top-frame model. """ - block_regular = intrinsic_regular + top_frame_regular + execution_regular - block_state = top_frame_state + execution_state - cumulative_gas_used = block_regular + block_state - header_gas_used = max(block_regular, block_state) + block_execution = intrinsic_execution + top_frame_execution + evm_execution + block_state = top_frame_state + evm_state + cumulative_gas_used = block_execution + block_state + header_gas_used = max(block_execution, block_state) return cumulative_gas_used, header_gas_used @@ -131,8 +131,8 @@ def test_authorization_state_gas_scaling( Each authority is an existing funded EOA gaining a fresh delegation, so ``set_delegation`` charges only the top-frame ``AUTH_BASE`` per authorization (no ``NEW_ACCOUNT`` / ``ACCOUNT_WRITE`` and no refund). - The receipt gas is the regular intrinsic plus ``num_auths * - AUTH_BASE`` and the header ``gas_used`` is the max of the regular and + The receipt gas is the execution intrinsic plus ``num_auths * + AUTH_BASE`` and the header ``gas_used`` is the max of the execution and state blocks. """ contract = pre.deploy_contract(code=Op.STOP) @@ -149,11 +149,11 @@ def test_authorization_state_gas_scaling( for signer in signers ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -194,9 +194,9 @@ def test_set_code_tx_below_total_intrinsic( num_auths: int, ) -> None: """ - Reject a set_code tx one gas below the (now regular-only) intrinsic. + Reject a set_code tx one gas below the (now execution-only) intrinsic. - Under EIP-2780 the authorization intrinsic is entirely regular (the + Under EIP-2780 the authorization intrinsic is entirely execution (the state-dependent costs moved to the top frame), so the intrinsic gas the transaction must cover is exactly ``fork.transaction_intrinsic_cost_calculator()(auth_list)``. Sweeping @@ -245,7 +245,7 @@ def test_existing_account_no_refund( Its leaf exists, so ``set_delegation`` charges neither ``NEW_ACCOUNT`` nor ``ACCOUNT_WRITE`` (and, unlike the superseded EIP-8037 behaviour, refunds neither); it charges only the top-frame ``AUTH_BASE``. The - receipt gas is therefore exactly the regular intrinsic plus + receipt gas is therefore exactly the execution intrinsic plus ``AUTH_BASE``. """ contract = pre.deploy_contract(code=Op.STOP) @@ -261,11 +261,11 @@ def test_existing_account_no_refund( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -322,11 +322,11 @@ def test_mixed_new_and_existing_auths( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -367,15 +367,15 @@ def test_authorization_with_sstore( The authority (an existing EOA) gains a fresh delegation, charged the top-frame ``AUTH_BASE``; the called recipient then performs an SSTORE - whose regular and state costs are charged during execution. The header - ``gas_used`` is the max of the regular block and the (``AUTH_BASE`` + + whose execution and state costs are charged during execution. The header + ``gas_used`` is the max of the execution block and the (``AUTH_BASE`` + SSTORE) state block. """ storage = Storage() code = Op.SSTORE(storage.store_next(1), 1) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) - execution_state = code.state_cost(fork) + evm_execution = code.execution_cost(fork) + evm_state = code.state_cost(fork) signer = pre.fund_eoa() authorization_list = [ @@ -388,15 +388,15 @@ def test_authorization_with_sstore( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) _, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) tx = Transaction( @@ -429,15 +429,15 @@ def test_existing_account_no_refund_with_sstore( The existing authority pays only the top-frame ``AUTH_BASE`` (no ``NEW_ACCOUNT`` / ``ACCOUNT_WRITE`` and no refund), and the recipient's - SSTORE pays its own regular + state costs. The receipt gas is the + SSTORE pays its own execution + state costs. The receipt gas is the exact sum of the intrinsic, the ``AUTH_BASE`` and the SSTORE cost; there is no reservoir refund to draw on. """ storage = Storage() code = Op.SSTORE(storage.store_next(1), 1) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) - execution_state = code.state_cost(fork) + evm_execution = code.execution_cost(fork) + evm_state = code.state_cost(fork) signer = pre.fund_eoa() authorization_list = [ @@ -450,15 +450,15 @@ def test_existing_account_no_refund_with_sstore( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) tx = Transaction( @@ -569,11 +569,11 @@ def test_auth_block_gas_accounting( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) post_code = ( @@ -632,13 +632,13 @@ def test_invalid_nonce_auth_still_charges_intrinsic( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) - assert top_frame_regular == 0 + assert top_frame_execution == 0 assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -688,13 +688,13 @@ def test_invalid_chain_id_auth_still_charges_intrinsic( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) - assert top_frame_regular == 0 + assert top_frame_execution == 0 assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -752,11 +752,11 @@ def test_self_sponsored_authorization( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -821,11 +821,11 @@ def test_duplicate_signer_authorizations( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -860,17 +860,17 @@ def test_auth_with_calldata_and_access_list( """ Test authorization combined with calldata and an access list. - The regular intrinsic folds in the calldata and access-list costs; on + The execution intrinsic folds in the calldata and access-list costs; on top of it the existing authority pays the top-frame ``AUTH_BASE`` and - the recipient's SSTORE pays its execution regular + state costs. The + the recipient's SSTORE pays its execution + state costs. The receipt gas is the exact sum, with no refund term. Access lists do not warm the authority under EIP-2780, so the auth charge is unaffected. """ storage = Storage() code = Op.SSTORE(storage.store_next(0x42), Op.CALLDATALOAD(0)) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) - execution_state = code.state_cost(fork) + evm_execution = code.execution_cost(fork) + evm_state = code.state_cost(fork) signer = pre.fund_eoa() authorization_list = [ @@ -886,19 +886,21 @@ def test_auth_with_calldata_and_access_list( data = b"\x00" * 31 + b"\x42" access_list = [AccessList(address=contract, storage_keys=[])] - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=authorization_list, calldata=data, access_list=access_list, return_cost_deducted_prior_execution=True, ) - _, top_frame_regular, top_frame_state = _auth_gas(fork, authorization_list) + _, top_frame_execution, top_frame_state = _auth_gas( + fork, authorization_list + ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) tx = Transaction( @@ -948,7 +950,7 @@ def test_mixed_valid_and_invalid_auths( ``set_delegation`` and each writes a net-new delegation on an existing authority, paying the first-write ``ACCOUNT_WRITE`` and the top-frame ``AUTH_BASE``; the invalid (wrong nonce) tuples are skipped and pay - no top-frame charge. The receipt gas is ``intrinsic_regular + + no top-frame charge. The receipt gas is ``intrinsic_execution + num_valid * (ACCOUNT_WRITE + AUTH_BASE)``. """ contract = pre.deploy_contract(code=Op.STOP) @@ -977,11 +979,11 @@ def test_mixed_valid_and_invalid_auths( for signer in invalid_signers ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -1036,11 +1038,11 @@ def test_many_authorizations( for signer in signers ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -1075,7 +1077,7 @@ def test_auth_with_multiple_sstores( The existing authority pays the top-frame ``AUTH_BASE`` and the recipient performs five distinct zero-to-nonzero SSTOREs, each paying - its own regular + state cost during execution. Verifies combined + its own execution + state cost during execution. Verifies combined accounting across the top-frame and execution state charges, all drawn from ``gas_left`` with no refund. """ @@ -1085,8 +1087,8 @@ def test_auth_with_multiple_sstores( for _ in range(num_sstores): code += Op.SSTORE(storage.store_next(1), 1) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) - execution_state = code.state_cost(fork) + evm_execution = code.execution_cost(fork) + evm_state = code.state_cost(fork) signer = pre.fund_eoa() authorization_list = [ @@ -1099,15 +1101,15 @@ def test_auth_with_multiple_sstores( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) _, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) tx = Transaction( @@ -1149,7 +1151,7 @@ def test_authorization_exact_state_gas_boundary( """ Test the intrinsic-gas boundary and the top-frame OOG behaviour. - Under EIP-2780 the intrinsic is regular-only, so the boundary keys off + Under EIP-2780 the intrinsic is execution-only, so the boundary keys off ``fork.transaction_intrinsic_cost_calculator()(auth_list)``. With ``gas_delta=-1`` the transaction is one gas below the intrinsic and is rejected as intrinsic-gas-too-low. With ``gas_delta=0`` the gas limit @@ -1265,8 +1267,8 @@ def test_multi_tx_block_auth_and_sstore( 1. a SetCode tx delegating an existing authority (top-frame ``AUTH_BASE``, no refund); and - 2. a regular tx performing a zero-to-nonzero SSTORE (execution regular - + state). + 2. a normal tx performing a zero-to-nonzero SSTORE (execution + + state gas). The per-transaction receipt ``cumulative_gas_used`` accumulates across the block, so tx1's receipt is its own cost and tx2's is the running @@ -1285,11 +1287,11 @@ def test_multi_tx_block_auth_and_sstore( writes_delegation=True, ), ] - intrinsic_regular_1, top_frame_regular_1, top_frame_state_1 = _auth_gas( - fork, authorization_list + intrinsic_execution_1, top_frame_execution_1, top_frame_state_1 = ( + _auth_gas(fork, authorization_list) ) tx1_gas, _ = _receipt_and_header( - intrinsic_regular_1, top_frame_regular_1, top_frame_state_1 + intrinsic_execution_1, top_frame_execution_1, top_frame_state_1 ) tx_1 = Transaction( to=contract, @@ -1302,13 +1304,13 @@ def test_multi_tx_block_auth_and_sstore( storage = Storage() sstore_code = Op.SSTORE(storage.store_next(1), 1) sstore_contract = pre.deploy_contract(code=sstore_code) - intrinsic_regular_2 = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution_2 = fork.transaction_intrinsic_cost_calculator()( recipient_type=RecipientType.CONTRACT, return_cost_deducted_prior_execution=True, ) tx2_gas = ( - intrinsic_regular_2 - + sstore_code.regular_cost(fork) + intrinsic_execution_2 + + sstore_code.execution_cost(fork) + sstore_code.state_cost(fork) ) tx_2 = Transaction( @@ -1352,8 +1354,8 @@ def test_fresh_authority_and_sstores_full_state( for _ in range(num_sstores): code += Op.SSTORE(storage.store_next(1), 1) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) - execution_state = code.state_cost(fork) + evm_execution = code.execution_cost(fork) + evm_state = code.state_cost(fork) signer = pre.fund_eoa(amount=0) authorization_list = [ @@ -1366,15 +1368,15 @@ def test_fresh_authority_and_sstores_full_state( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) tx = Transaction( @@ -1422,7 +1424,7 @@ def test_existing_account_auth_header_gas_used( Every authority is an existing account gaining a fresh delegation, so each pays only the top-frame ``AUTH_BASE`` (no ``NEW_ACCOUNT`` / ``ACCOUNT_WRITE`` and no refund). With STOP execution the header - ``gas_used`` is ``max(intrinsic_regular, num_auths * AUTH_BASE)``. + ``gas_used`` is ``max(intrinsic_execution, num_auths * AUTH_BASE)``. """ contract = pre.deploy_contract(code=Op.STOP) @@ -1438,11 +1440,11 @@ def test_existing_account_auth_header_gas_used( for signer in signers ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) _, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -1484,8 +1486,8 @@ def test_mixed_auths_header_gas_used( Existing authorities pay only ``AUTH_BASE``; new (nonexistent) authorities additionally pay ``NEW_ACCOUNT`` (state) + ``ACCOUNT_WRITE`` - (regular) for the created leaf. The header ``gas_used`` is - ``max(block_regular, block_state)`` over the summed top-frame charges, + (execution) for the created leaf. The header ``gas_used`` is + ``max(block_execution, block_state)`` over the summed top-frame charges, with no refund term. """ contract = pre.deploy_contract(code=Op.STOP) @@ -1513,11 +1515,11 @@ def test_mixed_auths_header_gas_used( for signer in new_signers ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) _, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -1562,13 +1564,13 @@ def test_auth_state_gas_persists_on_top_level_revert( folded out of the frame's refillable pools. The recipient writes an SSTORE then REVERTs: the slot rolls back with the frame, so the SSTORE's ``STORAGE_SET`` state gas *is* refilled. The receipt is - therefore the intrinsic and top-frame charges (regular and state) - plus the regular execution gas, with only the authorization's state + therefore the intrinsic and top-frame charges (execution and state) + plus the execution gas, with only the authorization's state portion in the block's state component. """ code = Op.SSTORE(0, 1) + Op.REVERT(0, 0) contract = pre.deploy_contract(code=code) - execution_regular = code.regular_cost(fork) + evm_execution = code.execution_cost(fork) signer = pre.fund_eoa() authorization_list = [ @@ -1581,16 +1583,16 @@ def test_auth_state_gas_persists_on_top_level_revert( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) # The SSTORE's state gas is refilled by the REVERT (the slot rolls # back); the authorization's state gas persists with its delegation. cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, + evm_execution=evm_execution, ) tx = Transaction( @@ -1646,15 +1648,15 @@ def test_auth_state_gas_in_header_after_failure( and so does the state gas that paid for it (``NEW_ACCOUNT`` + ``AUTH_BASE`` for a fresh authority, ``AUTH_BASE`` for an existing one), which is folded out of the frame's refillable pools. The - header is ``max(block_regular, block_state)``: + header is ``max(block_execution, block_state)``: - * REVERT -- the unused execution budget returns, so the regular - component is ``intrinsic_regular + top_frame_regular + - execution_regular`` and the state component is the persisting + * REVERT -- the unused execution budget returns, so the execution + component is ``intrinsic_execution + top_frame_execution + + evm_execution`` and the state component is the persisting authorization state gas. * HALT / OOG -- the frame consumes its whole gas limit; the authorization state gas within it is accounted on the state - component, and the remainder on the regular component. + component, and the remainder on the execution component. """ gas_limit = 500_000 @@ -1687,22 +1689,22 @@ def test_auth_state_gas_in_header_after_failure( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) if failure_mode == "revert": # The authorization's state gas persists with its delegation. _, expected_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=revert_code.regular_cost(fork), + evm_execution=revert_code.execution_cost(fork), ) else: # HALT / OOG consume the whole gas limit, of which the # persisting authorization state gas is accounted on the state - # component and the remainder on the regular component. + # component and the remainder on the execution component. expected_gas_used = max(gas_limit - top_frame_state, top_frame_state) tx = Transaction( @@ -1743,8 +1745,8 @@ def test_auth_sender_billing_after_failure( top-level REVERT. The delegation persists through the REVERT, so the state gas that - paid for it stays billed alongside the regular gas: the sender pays - ``intrinsic_regular + top_frame_regular + revert_regular`` plus the + paid for it stays billed alongside the execution gas: the sender pays + ``intrinsic_execution + top_frame_execution + revert_execution`` plus the authorization's state charges. Both authorities pay the first-write ``ACCOUNT_WRITE`` and the ``AUTH_BASE``; a new authority additionally pays ``NEW_ACCOUNT`` for the created leaf, so its @@ -1772,16 +1774,16 @@ def test_auth_sender_billing_after_failure( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) # The authorization's state gas persists with its delegation across # the REVERT and stays billed to the sender. expected_cumulative, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=revert_code.regular_cost(fork), + evm_execution=revert_code.execution_cost(fork), ) tx = Transaction( @@ -1834,8 +1836,8 @@ def test_auth_and_execution_state_oog_boundary( storage = Storage() target_code = Op.SSTORE(storage.store_next(1), 1) target = pre.deploy_contract(code=target_code) - execution_regular = target_code.regular_cost(fork) - execution_state = target_code.state_cost(fork) + evm_execution = target_code.execution_cost(fork) + evm_state = target_code.state_cost(fork) authority = pre.fund_eoa() authorization_list = [ @@ -1848,15 +1850,15 @@ def test_auth_and_execution_state_oog_boundary( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) full_cost = ( - intrinsic_regular - + top_frame_regular + intrinsic_execution + + top_frame_execution + top_frame_state - + execution_regular - + execution_state + + evm_execution + + evm_state ) gas_limit = full_cost + gas_delta gas_limit_cap = fork.transaction_gas_limit_cap() @@ -1866,11 +1868,11 @@ def test_auth_and_execution_state_oog_boundary( fits = gas_delta >= 0 if fits: _, header_gas_used = _receipt_and_header( - intrinsic_regular, - top_frame_regular, + intrinsic_execution, + top_frame_execution, top_frame_state, - execution_regular=execution_regular, - execution_state=execution_state, + evm_execution=evm_execution, + evm_state=evm_state, ) else: # One gas short: execution OOGs at the top frame, consuming the @@ -1958,13 +1960,13 @@ def test_invalid_auth_no_top_frame_charge( else: raise ValueError(f"unknown invalidity: {invalidity!r}") - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, [auth] ) - assert top_frame_regular == 0 + assert top_frame_execution == 0 assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -2023,11 +2025,11 @@ def test_same_tx_create_then_clear( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -2089,12 +2091,12 @@ def test_same_tx_clear_then_reset_pre_delegated( ), ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( @@ -2154,11 +2156,11 @@ def test_same_authority_increasing_nonce_net_once( for i in range(num_auths) ] - intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( + intrinsic_execution, top_frame_execution, top_frame_state = _auth_gas( fork, authorization_list ) cumulative_gas_used, header_gas_used = _receipt_and_header( - intrinsic_regular, top_frame_regular, top_frame_state + intrinsic_execution, top_frame_execution, top_frame_state ) tx = Transaction( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py index 71dcef99b11..d04a553e39b 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py @@ -5,7 +5,7 @@ `STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte` of state gas. Nonzero-to-nonzero writes charge no state gas. 0 to x to 0 restoration in the same tx refunds state gas directly to `state_gas_reservoir` -(inline at x to 0) and the regular write-cost portion to +(inline at x to 0) and the execution write-cost portion to `refund_counter`. Tests for [EIP-8037: State Creation Gas Cost Increase] @@ -46,7 +46,7 @@ def test_sstore_zero_to_nonzero( Writing a nonzero value to a previously-zero slot charges STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte of state gas - in addition to regular gas. + in addition to execution gas. """ storage = Storage() contract = pre.deploy_contract( @@ -99,7 +99,7 @@ def test_sstore_nonzero_to_zero( Test SSTORE nonzero-to-zero charges no state gas. Clearing a storage slot (setting to zero) does not grow state and - earns a regular gas refund (GAS_STORAGE_CLEAR_REFUND). + earns an execution gas refund (GAS_STORAGE_CLEAR_REFUND). """ storage = Storage() contract = pre.deploy_contract( @@ -126,7 +126,7 @@ def test_sstore_zero_to_zero( Test SSTORE zero-to-zero charges no state gas. Writing zero to an already-zero slot creates no new state. Only - the warm access regular gas cost is charged. + the warm access execution gas cost is charged. """ storage = Storage() contract = pre.deploy_contract( @@ -182,7 +182,7 @@ def test_sstore_restoration_refund_credits_local_reservoir( # Sentinel written only if the CREATE returned (frame did not OOG). sentinel_slot = 2 # refund: clear (1→0, restoration refund). no refund: modify - # (1→2, no state growth, no refund) — same regular shape. + # (1→2, no state growth, no refund) — same execution shape. cleared_value = 0 if refund_sufficient else 2 clearing = pre.deploy_contract( code=( @@ -209,11 +209,13 @@ def test_sstore_restoration_refund_credits_local_reservoir( # The two parent `0→1` sets spill their state gas into `gas_left` # (tx is far below the per-tx cap, so no state-gas reservoir). - # Budget regular headroom for the call chain plus that spill, then + # Budget execution headroom for the call chain plus that spill, then # sit mid-window: short of also spill-funding `create_state_gas`, # so only a refund-credited reservoir can cover the CREATE. - regular_headroom = 200_000 - gas_limit = regular_headroom + 2 * sstore_state_gas + create_state_gas // 2 + execution_headroom = 200_000 + gas_limit = ( + execution_headroom + 2 * sstore_state_gas + create_state_gas // 2 + ) if refund_sufficient: post = {parent: Account(storage={0: 0, 1: 0, sentinel_slot: 1})} @@ -244,7 +246,7 @@ def test_sstore_restoration_refund( When a slot is written from zero to nonzero and then restored to zero in the same transaction, the state gas charge (STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte) is refunded - via refund_counter along with the regular gas write cost. + via refund_counter along with the execution gas write cost. """ contract = pre.deploy_contract( code=(Op.SSTORE(0, 1) + Op.SSTORE(0, 0)), @@ -271,7 +273,7 @@ def test_sstore_restoration_nonzero_no_state_refund( When a slot holds a nonzero original value, changing it and restoring it never involves state gas (no state growth occurred), - so only regular gas refunds apply. + so only execution gas refunds apply. """ contract = pre.deploy_contract( code=(Op.SSTORE(0, 2) + Op.SSTORE(0, 1)), @@ -434,7 +436,7 @@ def test_sstore_stipend_check_excludes_reservoir( excluded either way, which is what this test pins down. With below_stipend: SSTORE fails (gas_left too low, reservoir ignored). - With at_stipend: SSTORE has full regular gas and proceeds. + With at_stipend: SSTORE has full execution gas and proceeds. """ stipend = fork.call_value_stipend() + 1 sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -443,19 +445,19 @@ def test_sstore_stipend_check_excludes_reservoir( child_code = Op.SSTORE(0, 1) child = pre.deploy_contract(child_code) - # Full regular gas for the child (pushes + SSTORE regular cost). + # Full execution gas for the child (pushes + SSTORE execution cost). # State gas comes from the reservoir so it doesn't affect gas_left. - child_full_regular = child_code.regular_cost(fork) + child_full_execution = child_code.execution_cost(fork) # below_stipend: give 1 less than stipend after pushes, fails check. - # at_stipend: give full regular gas, passes check and completes. + # at_stipend: give full execution gas, passes check and completes. if gas_above_stipend < 0: - push_gas = 2 * Op.PUSH1(0).regular_cost(fork) + push_gas = 2 * Op.PUSH1(0).execution_cost(fork) child_gas = push_gas + stipend - 1 else: - child_gas = child_full_regular + child_gas = child_full_execution - # Caller forwards limited regular gas via CALL. State gas comes + # Caller forwards limited execution gas via CALL. State gas comes # from the reservoir (gas_limit above the cap). caller_storage = Storage() sstore_succeeds = gas_above_stipend >= 0 @@ -513,7 +515,7 @@ def test_sstore_restoration_block_state_gas_zero( current_value=1, new_value=0, )(i, 0) - tx_regular = ( + tx_execution = ( intrinsic_gas + code.gas_cost(fork) - num_cycles * sstore_state_gas ) @@ -526,7 +528,7 @@ def test_sstore_restoration_block_state_gas_zero( blockchain_test( pre=pre, - blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_execution))], post={contract: Account(storage=dict.fromkeys(range(num_cycles), 0))}, ) @@ -566,10 +568,10 @@ def test_sstore_restoration_mixed_with_genuine_sstore( code += Op.SSTORE(99, 1) num_0_to_1 = num_cycles + 1 - tx_regular = ( + tx_execution = ( intrinsic_gas + code.gas_cost(fork) - num_0_to_1 * sstore_state_gas ) - expected = max(tx_regular, sstore_state_gas) + expected = max(tx_execution, sstore_state_gas) contract = pre.deploy_contract(code=code) tx = Transaction( @@ -619,7 +621,7 @@ def test_sstore_restoration_intermediate_values( new_value=0, )(0, 0) ) - tx_regular = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas + tx_execution = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas contract = pre.deploy_contract(code=code) tx = Transaction( @@ -630,7 +632,7 @@ def test_sstore_restoration_intermediate_values( blockchain_test( pre=pre, - blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_execution))], post={contract: Account(storage={0: 0})}, ) @@ -666,8 +668,8 @@ def test_sstore_restoration_then_reset( new_value=1, )(0, 1) ) - tx_regular = intrinsic_gas + code.gas_cost(fork) - 2 * sstore_state_gas - expected = max(tx_regular, sstore_state_gas) + tx_execution = intrinsic_gas + code.gas_cost(fork) - 2 * sstore_state_gas + expected = max(tx_execution, sstore_state_gas) contract = pre.deploy_contract(code=code) tx = Transaction( @@ -709,8 +711,8 @@ def test_sstore_restoration_reservoir_replenished_inline( )(0, 0) + Op.SSTORE(1, 1) ) - tx_regular = intrinsic_gas + code.gas_cost(fork) - 2 * sstore_state_gas - expected = max(tx_regular, sstore_state_gas) + tx_execution = intrinsic_gas + code.gas_cost(fork) - 2 * sstore_state_gas + expected = max(tx_execution, sstore_state_gas) contract = pre.deploy_contract(code=code) tx = Transaction( @@ -758,14 +760,14 @@ def test_sstore_restoration_cross_frame( )(0, 0) + Op.STOP ) - # Callee's regular gas excludes the state gas (refunded at x to 0). - child_regular = child_code.gas_cost(fork) - sstore_state_gas + # Callee's execution gas excludes the state gas (refunded at x to 0). + child_execution = child_code.gas_cost(fork) - sstore_state_gas child = pre.deploy_contract(code=child_code) - parent_code = Op.POP(call_opcode(gas=child_regular, address=child)) + parent_code = Op.POP(call_opcode(gas=child_execution, address=child)) parent = pre.deploy_contract(code=parent_code) - tx_regular = intrinsic_gas + parent_code.gas_cost(fork) + child_regular + tx_execution = intrinsic_gas + parent_code.gas_cost(fork) + child_execution tx = Transaction( to=parent, @@ -777,7 +779,7 @@ def test_sstore_restoration_cross_frame( slot_owner = child if call_opcode == Op.CALL else parent blockchain_test( pre=pre, - blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_execution))], post={slot_owner: Account(storage={0: 0})}, ) @@ -956,7 +958,7 @@ def test_sstore_restoration_ancestor_revert( caller_storage = Storage() # The probe OOGs and returns 0, so the caller's outer SSTORE is a # cold no-op (0 to 0) on a fresh slot, charging only - # COLD_STORAGE_ACCESS rather than the cold set `regular_cost` + # COLD_STORAGE_ACCESS rather than the cold set `execution_cost` # assumes by default. caller_code = Op.POP( call_opcode(gas=Op.GAS, address=middle) @@ -974,14 +976,14 @@ def test_sstore_restoration_ancestor_revert( # No SSTORE-set persists (inner's set+clear cancel, middle reverts, # the probe OOGs and reverts, and the caller's outer SSTORE is a # no-op), so block state gas is zero and header gas_used (the max of - # regular and state) is just the regular total. The probe burns its + # execution and state) is just the execution total. The probe burns its # full forwarded budget on the OOG; its CALL's cold-access surcharge - # is already counted in the caller's regular cost. + # is already counted in the caller's execution cost. expected_gas_used = ( intrinsic_cost - + caller_code.regular_cost(fork) - + middle_code.regular_cost(fork) - + inner_code.regular_cost(fork) + + caller_code.execution_cost(fork) + + middle_code.execution_cost(fork) + + inner_code.execution_cost(fork) + probe_gas ) @@ -1064,16 +1066,16 @@ def test_sstore_restoration_charge_in_ancestor_intermediate_revert( # SSTORE-set + caller's outer SSTORE-set on slot 1. Middle's # own slot-1 set is washed by inner's deferred credit before # middle reverts, so it does not propagate. Header gas_used - # is max(regular, state). - expected_regular = ( + # is max(execution, state). + expected_execution = ( intrinsic_cost - + caller_code.regular_cost(fork) - + middle_code.regular_cost(fork) - + inner_code.regular_cost(fork) - + probe_code.regular_cost(fork) + + caller_code.execution_cost(fork) + + middle_code.execution_cost(fork) + + inner_code.execution_cost(fork) + + probe_code.execution_cost(fork) ) expected_state = 3 * sstore_state_gas - expected_gas_used = max(expected_regular, expected_state) + expected_gas_used = max(expected_execution, expected_state) # Reservoir = 2 * sstore_state_gas covers caller's and middle's # sets; the deferred credit refills middle by sstore_state_gas, @@ -1242,7 +1244,7 @@ def test_sstore_restoration_reservoir_spillover( current_value=1, new_value=0, )(0, 0) - tx_regular = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas + tx_execution = intrinsic_gas + code.gas_cost(fork) - sstore_state_gas contract = pre.deploy_contract(code=code) tx = Transaction( @@ -1253,6 +1255,6 @@ def test_sstore_restoration_reservoir_spillover( blockchain_test( pre=pre, - blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_execution))], post={contract: Account(storage={0: 0})}, ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py index d214e83bc21..f744952fe26 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py @@ -165,7 +165,7 @@ def test_access_list_warms_storage_slot( overwrite of a non-zero original to a new non-zero value pays ``WARM_SLOAD + STORAGE_WRITE``. """ - very_low = Op.PUSH1(0).regular_cost(fork) + very_low = Op.PUSH1(0).execution_cost(fork) slot = 0x42 if op == "SLOAD": @@ -178,7 +178,7 @@ def test_access_list_warms_storage_slot( else: measured_code = Op.SSTORE(slot, 2) # Overhead is the two PUSHes (key, value); the stored value is - # the bare warm SSTORE regular cost (overwrite of a non-zero + # the bare warm SSTORE execution cost (overwrite of a non-zero # original, no state gas). overhead_cost = 2 * very_low extra_stack_items = 0 @@ -188,7 +188,7 @@ def test_access_list_warms_storage_slot( original_value=1, current_value=1, new_value=2, - )(slot, 2).regular_cost(fork) + )(slot, 2).execution_cost(fork) - 2 * very_low ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py index c58fc42934d..946ea7f12db 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py @@ -1,8 +1,8 @@ """ Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) -``CALL``-family regular-gas dimension. +``CALL``-family execution-gas dimension. -Under EIP-8038 the call opcodes are repriced in their *regular* gas +Under EIP-8038 the call opcodes are repriced in their *execution* gas dimension: - account access costs ``COLD_ACCOUNT_ACCESS`` (3,000) cold or @@ -11,12 +11,12 @@ ``CALL_STIPEND`` = 10,300), charged only by ``CALL``/``CALLCODE``; - a value transfer to a *new* account additionally creates the account, whose ``GAS_NEW_ACCOUNT`` charge is the EIP-8037 *state* dimension and - is asserted via the block header ``max(regular, state)`` accounting, - never as regular gas; + is asserted via the block header ``max(execution, state)`` accounting, + never as execution gas; - an EIP-7702 delegated target is double-accessed (target leaf plus delegation leaf), each access cold or warm independently. -These tests assert the EIP-8038 *regular* dimension; the EIP-8037 +These tests assert the EIP-8038 *execution* dimension; the EIP-8037 *state* dimension for value-to-new-account is covered in ``eip8037_state_creation_gas_cost_increase/test_state_gas_call.py`` and is only re-derived here at the seam to feed header gas accounting. @@ -176,7 +176,7 @@ def test_call_value_alive_target_gas( pre, fork, measured_code, own_cold, balance=1 ) - # CALL gas is wholly regular under EIP-8038 (no state map). + # CALL gas is wholly execution under EIP-8038 (no state map). assert cost_metadata.state_cost(fork) == 0 # Consumed gas: the STOP callee returns the forwarded stipend, so the @@ -211,7 +211,7 @@ def test_callcode_value_to_nonexistent_no_new_account( ``CALLCODE`` runs the callee's code in the caller's own context, so the value never leaves the caller and no beneficiary account is - created. The block ``gas_used`` therefore equals the regular tx + created. The block ``gas_used`` therefore equals the execution tx cost with ``CALL_VALUE`` but with no 183,600 state-gas component. """ intrinsic = fork.transaction_intrinsic_cost_calculator()() @@ -241,7 +241,7 @@ def test_callcode_value_to_nonexistent_no_new_account( callcode_meta = Op.CALLCODE(address_warm=False, value_transfer=True) assert callcode_meta.state_cost(fork) == 0 - # Whole tx is regular gas; no NEW_ACCOUNT state component appears. + # Whole tx is execution gas; no NEW_ACCOUNT state component appears. # The CALLCODE forwards the value-call stipend to the callee, which # (running in the caller's own context with empty code) leaves it # unused and returns it, so consumed gas is the charge minus stipend. @@ -267,12 +267,12 @@ def test_call_value_to_new_account_seam( fork: Fork, ) -> None: """ - Verify the CALL value-to-new-account regular/state seam. + Verify the CALL value-to-new-account execution/state seam. - The EIP-8038 *regular* dimension is ``COLD_ACCOUNT_ACCESS`` + + The EIP-8038 *execution* dimension is ``COLD_ACCOUNT_ACCESS`` + ``CALL_VALUE`` = 13,300; the account creation charge ``GAS_NEW_ACCOUNT`` (183,600) lands in the EIP-8037 *state* - dimension. The block header reflects ``max(regular, state)``, which + dimension. The block header reflects ``max(execution, state)``, which is dominated by the state charge. """ intrinsic = fork.transaction_intrinsic_cost_calculator()() @@ -280,7 +280,7 @@ def test_call_value_to_new_account_seam( # Fresh, value-receiving target (state-empty, will be created). target = pre.fund_eoa(amount=0) - # Metadata-bearing CALL so its cost splits into the regular + # Metadata-bearing CALL so its cost splits into the execution # (access + value transfer) and state (NEW_ACCOUNT) dimensions. call = Op.CALL.with_metadata( address_warm=False, value_transfer=True, account_new=True @@ -298,12 +298,12 @@ def test_call_value_to_new_account_seam( new_account_state_gas = call.state_cost(fork) - # block_gas_used = max(block_regular, block_state). The CALL's - # NEW_ACCOUNT lands on the state axis; the regular axis is the + # block_gas_used = max(block_execution, block_state). The CALL's + # NEW_ACCOUNT lands on the state axis; the execution axis is the # access plus value-transfer cost. - tx_regular = intrinsic + caller_code.regular_cost(fork) + tx_execution = intrinsic + caller_code.execution_cost(fork) tx_state = caller_code.state_cost(fork) - expected_gas_used = max(tx_regular, tx_state) + expected_gas_used = max(tx_execution, tx_state) # State must dominate here, proving NEW_ACCOUNT hit the state axis. assert expected_gas_used == new_account_state_gas @@ -412,7 +412,7 @@ def test_call_exact_gas_oog( inner_code = call_opcode(gas=0, address=target) + Op.STOP inner = pre.deploy_contract(inner_code) - # Exact regular gas for the inner frame: bytecode cost (which folds + # Exact execution gas for the inner frame: bytecode cost (which folds # the cold call cost via the default metadata) under EIP-8038. inner_gas_exact = inner_code.gas_cost(fork) if not sufficient_gas: @@ -477,10 +477,10 @@ def test_call_forwarded_gas_63_64( gas. The spec charges the repriced ``COLD_ACCOUNT_ACCESS`` (3,000) up front and only then forwards ``floor(63/64 * gas_left)`` to the child. The wrapper is handed an exact budget so that, net of the - access charge, ``gas_left`` equals ``child_regular * 64 // 63``; - forwarding then yields exactly the child's regular need - (``child_regular``) and its cold ``SSTORE`` takes effect. With one - gas less the floor drops below ``child_regular`` and the child OOGs, + access charge, ``gas_left`` equals ``child_execution * 64 // 63``; + forwarding then yields exactly the child's execution need + (``child_execution``) and its cold ``SSTORE`` takes effect. With one + gas less the floor drops below ``child_execution`` and the child OOGs, so the slot stays zero. This pins that the floor is taken over ``gas_left`` already net of the post-8038 cold access cost (not before it, and not double-charging it). @@ -488,15 +488,15 @@ def test_call_forwarded_gas_63_64( sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: a single cold zero-to-nonzero SSTORE as proof of execution. - # Its regular need is the two operand pushes plus the cold storage + # Its execution need is the two operand pushes plus the cold storage # write (the state portion is funded separately via the reservoir, # which is passed to the child in full with no 63/64 rule). child_code = Op.SSTORE(0, 1) child = pre.deploy_contract(child_code) - child_regular = child_code.regular_cost(fork) + child_execution = child_code.execution_cost(fork) - # Smallest budget whose 63/64 floor still reaches `child_regular`. - forward_budget = child_regular * 64 // 63 + # Smallest budget whose 63/64 floor still reaches `child_execution`. + forward_budget = child_execution * 64 // 63 if not sufficient_gas: forward_budget -= 1 @@ -515,9 +515,9 @@ def test_call_forwarded_gas_63_64( wrapper = pre.deploy_contract(wrapper_call) # At the wrapper's CALL the cold access charge is deducted first - # (folded with the operand pushes into its regular cost), leaving + # (folded with the operand pushes into its execution cost), leaving # exactly `forward_budget` as `gas_left` for the 63/64 floor. - wrapper_gas = wrapper_call.regular_cost(fork) + forward_budget + wrapper_gas = wrapper_call.execution_cost(fork) + forward_budget # Outer caller hands the wrapper exactly `wrapper_gas`. caller = pre.deploy_contract( diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py index f91e6091f87..f981679c30b 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py @@ -1,9 +1,9 @@ """ Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) -``CREATE``/``CREATE2`` regular-gas dimension. +``CREATE``/``CREATE2`` execution-gas dimension. Under EIP-8038 the contract-creation opcodes are repriced in their -*regular* gas dimension to ``CREATE_ACCESS`` (``ACCOUNT_WRITE`` + +*execution* gas dimension to ``CREATE_ACCESS`` (``ACCOUNT_WRITE`` + ``COLD_STORAGE_ACCESS`` = 11,000), on top of which the EIP-3860 init code word cost (2 per word) and, for ``CREATE2`` only, an additional keccak word cost (6 per word) are charged. The new-account creation @@ -11,9 +11,9 @@ covered in ``eip8037_state_creation_gas_cost_increase/test_state_gas_create.py``. -These tests isolate and assert the EIP-8038 *regular* dimension. At the +These tests isolate and assert the EIP-8038 *execution* dimension. At the contract-creating-transaction boundary the state component is re-derived -only to feed the ``max(regular, state)`` block-header accounting. +only to feed the ``max(execution, state)`` block-header accounting. """ from typing import List @@ -58,7 +58,7 @@ pytest.param(96, id="three_words"), ], ) -def test_create_regular_gas( +def test_create_execution_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, @@ -66,36 +66,36 @@ def test_create_regular_gas( init_code_size: int, ) -> None: """ - Measure the regular gas of CREATE/CREATE2 and assert the schedule. + Measure the execution gas of CREATE/CREATE2 and assert the schedule. - The EIP-8038 *regular* dimension is ``CREATE_ACCESS`` (11,000) plus + The EIP-8038 *execution* dimension is ``CREATE_ACCESS`` (11,000) plus the EIP-3860 init code word cost (2 per word) plus, for ``CREATE2`` only, an additional keccak word cost (6 per word). The EIP-8037 account-creation state gas is excluded by subtracting ``create_state_gas(0)``. """ - # Isolate the regular dimension: opcode total minus its account + # Isolate the execution dimension: opcode total minus its account # creation state gas (the only state component carried by the CREATE # opcode itself; code deposit is charged on RETURN inside initcode). create_meta = create_opcode(init_code_size=init_code_size) - regular_gas = create_meta.gas_cost(fork) - fork.create_state_gas( + execution_gas = create_meta.gas_cost(fork) - fork.create_state_gas( code_size=0 ) - # Equivalent isolation via the regular_cost helper. - assert regular_gas == create_meta.regular_cost(fork) + # Equivalent isolation via the execution_cost helper. + assert execution_gas == create_meta.execution_cost(fork) # Runtime confirmation via CodeGasMeasure: a factory whose CREATE # deploys empty code, so no code-deposit state gas is charged and the # only state component is the account-creation gas funded from the # reservoir. The initcode is brought into memory BEFORE the measured # window, so the memory-expansion charge is excluded; the measured - # value is the CREATE opcode's regular cost exactly. The overhead + # value is the CREATE opcode's execution cost exactly. The overhead # subtracts the create-call argument pushes (the create leaves one # stack item, its result). # # The initcode is all-zero bytes (`STOP`), so the child frame halts # immediately consuming zero gas and deposits empty code. This keeps - # the measured value the CREATE opcode's own regular cost, with no + # the measured value the CREATE opcode's own execution cost, with no # child-execution gas folded in. `init_code_size` still drives the # opcode's per-init-word charge. padded_init = b"\x00" * init_code_size @@ -105,7 +105,7 @@ def test_create_regular_gas( if create_opcode == Op.CREATE2 else Op.CREATE(value=0, offset=0, size=init_code_size) ) - push_cost = Op.PUSH1(0).regular_cost(fork) + push_cost = Op.PUSH1(0).execution_cost(fork) arg_pushes = (4 if create_opcode == Op.CREATE2 else 3) * push_cost memory_setup = ( @@ -118,7 +118,7 @@ def test_create_regular_gas( code=create_call, overhead_cost=arg_pushes, extra_stack_items=1, - sstore_key=storage.store_next(regular_gas, "create_regular_gas"), + sstore_key=storage.store_next(execution_gas, "create_execution_gas"), ) factory = pre.deploy_contract(code=memory_setup + measure) @@ -155,33 +155,33 @@ def test_create2_keccak_word_delta( ``CREATE2`` hashes the init code to derive the salted address, adding ``OPCODE_KECCAK256_PER_WORD`` (6) per init-code word on top of the - regular cost shared with ``CREATE``. Both opcodes carry the identical + execution cost shared with ``CREATE``. Both opcodes carry the identical EIP-8038 ``CREATE_ACCESS`` base and EIP-3860 word cost. A factory measures a single ``CREATE2`` with ``CodeGasMeasure`` and - stores its absolute regular cost, confirming the opcode's own - ``regular_cost`` (which folds the keccak word surcharge) against the + stores its absolute execution cost, confirming the opcode's own + ``execution_cost`` (which folds the keccak word surcharge) against the runtime charge. """ - create2_regular = Op.CREATE2(init_code_size=init_code_size).regular_cost( - fork - ) + create2_execution = Op.CREATE2( + init_code_size=init_code_size + ).execution_cost(fork) # Init code is all-zero bytes (`STOP`), so the child frame halts # immediately (zero gas) depositing empty code; the CREATE2 charges no # code-deposit state gas and no child execution gas is folded into the - # measurement. The single CREATE2 regular cost is measured via + # measurement. The single CREATE2 execution cost is measured via # CodeGasMeasure with a reservoir sized for its account creation state # gas, keeping the GAS-measured `gas_left` free of state-gas spill. padded = b"\x00" * init_code_size - push4 = 4 * Op.PUSH1(0).regular_cost(fork) + push4 = 4 * Op.PUSH1(0).execution_cost(fork) storage = Storage() measure_create2 = CodeGasMeasure( code=Op.CREATE2(value=0, offset=0, size=init_code_size, salt=0), overhead_cost=push4, extra_stack_items=1, - sstore_key=storage.store_next(create2_regular, "create2_regular"), + sstore_key=storage.store_next(create2_execution, "create2_execution"), ) factory_code = ( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE, new_memory_size=init_code_size) @@ -245,7 +245,7 @@ def exact_intrinsic_gas( initcode: Initcode, tx_access_list: List[AccessList], ) -> int: - """Return the total (regular + state) intrinsic tx gas cost.""" + """Return the total (execution + state) intrinsic tx gas cost.""" calc = fork.transaction_intrinsic_cost_calculator() return calc( calldata=initcode, @@ -264,18 +264,18 @@ def exact_execution_gas( Under EIP-2780 the created account's ``NEW_ACCOUNT`` state gas moved out of the intrinsic and into the top frame, so it is added - explicitly here (the intrinsic is regular-only). + explicitly here (the intrinsic is execution-only). ``deployment_gas`` is fork-aware: under EIP-8037 it splits the - deposit into the keccak word cost (regular) and the per-byte cost + deposit into the keccak word cost (execution) and the per-byte cost (state), while on a fork without state-byte metering it is the - flat regular per-byte deposit cost. The single call is therefore + flat execution per-byte deposit cost. The single call is therefore correct in either regime. """ execution = exact_intrinsic_gas + fork.transaction_top_frame_state_gas( contract_creation=True ) - execution += initcode.execution_gas(fork) + execution += initcode.evm_gas(fork) execution += initcode.deployment_gas(fork) return execution @@ -339,7 +339,7 @@ def test_create_tx_gas_boundary( sender=sender, ) - # 2D block accounting: gas_used = max(regular, state). Under + # 2D block accounting: gas_used = max(execution, state). Under # EIP-2780 the state axis carries the fresh target's top-frame # NEW_ACCOUNT and (when the deposit succeeds) the per-byte # code-deposit gas. @@ -347,19 +347,19 @@ def test_create_tx_gas_boundary( header_verify = None elif succeeds: # Fresh target: top-frame NEW_ACCOUNT plus the per-byte code - # deposit are the state-gas axis; the rest is regular. + # deposit are the state-gas axis; the rest is execution. state_used = fork.transaction_top_frame_state_gas( contract_creation=True ) state_used += fork.code_deposit_state_gas( code_size=len(initcode.deploy_code) ) - regular_used = gas_limit - state_used - header_verify = Header(gas_used=max(regular_used, state_used)) + execution_used = gas_limit - state_used + header_verify = Header(gas_used=max(execution_used, state_used)) else: # exact_intrinsic / too_little_execution: the top-frame # NEW_ACCOUNT (and any deposit) cannot be covered, the whole - # preparation rolls back, and all gas is burned as regular. + # preparation rolls back, and all gas is burned as execution. header_verify = Header(gas_used=gas_limit) state_test( @@ -464,7 +464,7 @@ def test_create2_to_occupied_address( already-deployed contract, whose ``code_hash`` is non-empty), the creation aborts after the account-access charge: the opcode pushes ``0``, bumps the factory's nonce, charges the message gas to the - regular dimension, and refunds the ``NEW_ACCOUNT`` *state* gas so no + execution dimension, and refunds the ``NEW_ACCOUNT`` *state* gas so no net account-creation charge lands. No child frame runs, so the occupied contract's code and storage are left untouched. """ diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py index cc7f3d21802..08f290fba1e 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py @@ -6,12 +6,12 @@ at ``timestamp=14_999`` runs under the pre-fork (parent) schedule; a block at ``timestamp=15_000`` runs under the EIP-8038 schedule. Every before/after magnitude is derived from the opcode's own cost at each -fork (``bytecode.gas_cost`` / ``regular_cost`` / ``refund``) — nothing +fork (``bytecode.gas_cost`` / ``execution_cost`` / ``refund``) — nothing is hardcoded. Two proof styles are used: -* Account-access dimensions that are pure regular gas (``BALANCE`` cold +* Account-access dimensions that are pure execution gas (``BALANCE`` cold access and the ``EXT*`` code-read surcharge) are measured exactly with ``CodeGasMeasure`` in each regime and asserted against the derived cost. @@ -19,7 +19,7 @@ state-gas confounders (``CALL`` with value, ``CREATE``, ``SELFDESTRUCT`` to a fresh beneficiary, ``SSTORE`` first change) are exercised in both blocks to prove the operation still runs in each - regime, with the ``SSTORE`` regular/state split and clear refund + regime, with the ``SSTORE`` execution/state split and clear refund compared across forks via the bytecode's own cost methods. * The authorization intrinsic rise is proven behaviourally: a tx whose ``gas_limit`` equals the old auth intrinsic is valid before the fork @@ -261,7 +261,7 @@ def test_create_base_cost_at_transition( fork: Fork, ) -> None: """ - The ``CREATE`` regular base cost changes across the boundary + The ``CREATE`` execution base cost changes across the boundary (``OPCODE_CREATE_BASE``: 32000 -> 11000 on mainnet, redefined as ``ACCOUNT_WRITE + COLD_STORAGE_ACCESS``). The constant transition is asserted from the derived schedules and a ``CREATE`` is exercised in @@ -352,15 +352,15 @@ def test_sstore_write_cost_at_transition( boundary, and EIP-8038 changes the *model*, not a single number. Before the fork (parent schedule) a zero-to-nonzero ``SSTORE`` is a - flat regular charge (``COLD_STORAGE_ACCESS + STORAGE_SET``) with no - state-gas dimension. After the fork the charge splits: the regular + flat execution charge (``COLD_STORAGE_ACCESS + STORAGE_SET``) with no + state-gas dimension. After the fork the charge splits: the execution portion drops to ``COLD_STORAGE_ACCESS + STORAGE_WRITE`` while the bulk moves into the new state-gas dimension, and the clear refund rises. Every magnitude is derived from the two schedules; nothing is hardcoded. The transition is asserted at the derived-constant level (the - runtime opcode cost cannot isolate the regular portion without the + runtime opcode cost cannot isolate the execution portion without the state-gas confounder) and a zero-to-nonzero ``SSTORE`` is exercised in both blocks to prove it still sets the slot in each regime. """ @@ -370,16 +370,16 @@ def test_sstore_write_cost_at_transition( # First-change (zero -> nonzero, cold) SSTORE in each regime. sstore = Op.SSTORE(new_value=1) - regular_before = sstore.regular_cost(before) - regular_after = sstore.regular_cost(after) + execution_before = sstore.execution_cost(before) + execution_after = sstore.execution_cost(after) state_before = sstore.state_cost(before) state_after = sstore.state_cost(after) total_before = sstore.gas_cost(before) total_after = sstore.gas_cost(after) - # The repricing changes the regular charge, introduces the state + # The repricing changes the execution charge, introduces the state # dimension, and therefore moves the total. - assert regular_after != regular_before + assert execution_after != execution_before assert state_before == 0 assert state_after > 0 assert total_after != total_before @@ -424,7 +424,7 @@ def test_auth_intrinsic_at_transition( The ``7702`` authorization intrinsic *falls* across the boundary. EIP-2780 moves the state-dependent authorization costs (account creation and the delegation-write base) out of the intrinsic and into - the top frame, leaving only the regular ``REGULAR_PER_AUTH_BASE_COST`` + the top frame, leaving only the execution ``REGULAR_PER_AUTH_BASE_COST`` in the intrinsic. The post-fork single-authorization intrinsic is therefore strictly smaller than the pre-fork one, so a tx whose ``gas_limit`` equals the (lower) post-fork intrinsic is rejected with diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py index 99d0797731c..b78c595c4b5 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py @@ -1,8 +1,8 @@ """ Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) -``SELFDESTRUCT`` regular-gas dimension. +``SELFDESTRUCT`` execution-gas dimension. -Under EIP-8038 ``SELFDESTRUCT`` is charged, in its *regular* gas +Under EIP-8038 ``SELFDESTRUCT`` is charged, in its *execution* gas dimension: - ``OPCODE_SELFDESTRUCT_BASE`` (5,000); @@ -11,9 +11,9 @@ ``WARM_ACCESS`` surcharge); - a net-new ``ACCOUNT_WRITE`` (8,000) when a positive balance is sent to an empty (or non-existent) beneficiary, replacing the legacy combined - 25,000 regular account-creation cost. + 25,000 execution account-creation cost. -So ``regular = 5,000 + (3,000 if cold) + (8,000 if creating)``: 13,000 +So ``execution = 5,000 + (3,000 if cold) + (8,000 if creating)``: 13,000 warm / 16,000 cold when a new beneficiary is created, 5,000 warm / 8,000 cold otherwise. @@ -29,10 +29,10 @@ The framework opcode-gas model splits the two dimensions for ``SELFDESTRUCT`` exactly as the spec does: ``ACCOUNT_WRITE`` is charged -as regular gas and ``GAS_NEW_ACCOUNT`` as state gas, so -``Op.SELFDESTRUCT(account_new=True).regular_cost(fork)`` is the regular +as execution gas and ``GAS_NEW_ACCOUNT`` as state gas, so +``Op.SELFDESTRUCT(account_new=True).execution_cost(fork)`` is the execution charge (16,000 cold / 13,000 warm) and ``.state_cost(fork)`` is -``GAS_NEW_ACCOUNT``. These tests assert the regular dimension and verify +``GAS_NEW_ACCOUNT``. These tests assert the execution dimension and verify account-creation via balances; the state dimension is owned by ``eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py``. """ @@ -70,7 +70,7 @@ def _destructor_code( beneficiary: Address | Bytecode, *, warm: bool, account_new: bool ) -> Bytecode: """ - Build SELFDESTRUCT bytecode with metadata so ``regular_cost(fork)`` + Build SELFDESTRUCT bytecode with metadata so ``execution_cost(fork)`` folds the beneficiary PUSH and the correct access/account-write charge (account-creation state gas excluded — it is charged separately by the spec). @@ -82,7 +82,7 @@ def _destructor_code( @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) -def test_selfdestruct_new_beneficiary_regular_gas( +def test_selfdestruct_new_beneficiary_execution_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, @@ -94,7 +94,7 @@ def test_selfdestruct_new_beneficiary_regular_gas( The destructor has a non-zero balance and targets an empty, non-existent beneficiary, so the net-new ``ACCOUNT_WRITE`` applies: - ``regular = 5,000 + access + 8,000`` (13,000 warm, 16,000 cold). The + ``execution = 5,000 + access + 8,000`` (13,000 warm, 16,000 cold). The creation gas ``GAS_NEW_ACCOUNT`` is charged on the state axis (the EIP-8037 suite asserts it); here it is funded from the reservoir and the value transfer to the new beneficiary confirms the path. @@ -144,9 +144,9 @@ def test_selfdestruct_alive_beneficiary_no_account_write( """ SELFDESTRUCT to an already-alive beneficiary charges no ACCOUNT_WRITE. - The beneficiary already exists, so no account is created: regular = + The beneficiary already exists, so no account is created: execution = ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 cold) and no state gas is - charged. The block header reflects the pure regular consumption. + charged. The block header reflects the pure execution consumption. """ beneficiary = pre.fund_eoa(amount=1) # alive @@ -167,12 +167,12 @@ def test_selfdestruct_alive_beneficiary_no_account_write( access_list=access_list ) - # Pure regular: intrinsic + caller frame + destructor frame (whose - # regular_cost folds the SELFDESTRUCT charge and beneficiary PUSH). + # Pure execution: intrinsic + caller frame + destructor frame (whose + # execution_cost folds the SELFDESTRUCT charge and beneficiary PUSH). expected_gas_used = ( intrinsic + caller_code.gas_cost(fork) - + destructor_code.regular_cost(fork) + + destructor_code.execution_cost(fork) ) tx = Transaction( @@ -212,7 +212,7 @@ def test_selfdestruct_codebearing_zero_balance_beneficiary_no_account_write( The beneficiary is alive because it has code, not balance: it holds a zero balance but a non-empty code (``Op.STOP``), so EIP-161 emptiness does not apply and no account is created when a positive balance is - sent to it. Regular = ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 + sent to it. Execution = ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 cold) with no ACCOUNT_WRITE and no state gas — distinct from the alive-via-balance case, which exercises the same path through a different liveness source. @@ -237,12 +237,12 @@ def test_selfdestruct_codebearing_zero_balance_beneficiary_no_account_write( access_list=access_list ) - # Pure regular: intrinsic + caller frame + destructor frame (whose - # regular_cost folds the SELFDESTRUCT charge and beneficiary PUSH). + # Pure execution: intrinsic + caller frame + destructor frame (whose + # execution_cost folds the SELFDESTRUCT charge and beneficiary PUSH). expected_gas_used = ( intrinsic + caller_code.gas_cost(fork) - + destructor_code.regular_cost(fork) + + destructor_code.execution_cost(fork) ) tx = Transaction( @@ -280,7 +280,7 @@ def test_selfdestruct_zero_balance_no_account_write( SELFDESTRUCT with a zero-balance destructor charges no ACCOUNT_WRITE. No value is transferred, so even a non-existent beneficiary is not - created: regular = ``5,000 + access`` and no state gas is charged. + created: execution = ``5,000 + access`` and no state gas is charged. """ beneficiary = Address(0xDEAD) # non-existent, but no value sent @@ -302,7 +302,7 @@ def test_selfdestruct_zero_balance_no_account_write( expected_gas_used = ( intrinsic + caller_code.gas_cost(fork) - + destructor_code.regular_cost(fork) + + destructor_code.execution_cost(fork) ) tx = Transaction( @@ -345,7 +345,7 @@ def test_selfdestruct_self_or_precompile_beneficiary( The executing account is in the accessed set on entry (self), and precompiles are pre-warmed from the start, so neither pays a cold - surcharge: regular = ``5,000`` (warm base, no ``WARM_ACCESS``) with no + surcharge: execution = ``5,000`` (warm base, no ``WARM_ACCESS``) with no state gas. The destructor balance is chosen so no account creation occurs: self @@ -378,7 +378,7 @@ def test_selfdestruct_self_or_precompile_beneficiary( expected_gas_used = ( intrinsic + caller_code.gas_cost(fork) - + destructor_code.regular_cost(fork) + + destructor_code.execution_cost(fork) ) tx = Transaction( @@ -418,10 +418,10 @@ def test_selfdestruct_oog_boundary( gas and one short. The destructor sends value to an empty beneficiary, charging - ``5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000) in regular gas + ``5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000) in execution gas and ``GAS_NEW_ACCOUNT`` in state gas. The child CALL frame has no state reservoir of its own, so the state gas spills into the forwarded - regular gas and the frame needs its full ``gas_cost`` total. Forwarding + execution gas and the frame needs its full ``gas_cost`` total. Forwarding exactly that total lets the SELFDESTRUCT succeed (CALL returns 1); one gas short OOGs (CALL returns 0) before the value transfer, so the beneficiary is never created. @@ -434,7 +434,7 @@ def test_selfdestruct_oog_boundary( destructor = pre.deploy_contract(code=destructor_code, balance=1) # The child CALL frame gets no state reservoir, so the NEW_ACCOUNT - # state gas spills into the forwarded regular gas: forward the full + # state gas spills into the forwarded execution gas: forward the full # total. One gas short forces an out-of-gas before the value transfer. forwarded = destructor_code.gas_cost(fork) if not sufficient_gas: @@ -483,7 +483,7 @@ def test_same_tx_created_selfdestruct_self_burn( to ITSELF: the originator is created in this transaction so it is deleted, and because a same-tx-created contract holding balance is alive, ``account_new`` is false for the self-beneficiary — - ``regular = 5,000`` (warm self, no ``ACCOUNT_WRITE``) and no + ``execution = 5,000`` (warm self, no ``ACCOUNT_WRITE``) and no SELFDESTRUCT state gas. EIP-8246 removes the SELFDESTRUCT burn, so the self-send is a no-op: @@ -494,7 +494,7 @@ def test_same_tx_created_selfdestruct_self_burn( ``NEW_ACCOUNT`` is a top-frame charge levied only when the target is ``EMPTY`` pre-tx, but the pre-funded created target already has a balance, so it is never charged. The self-burn adds no state gas, so - the block ``gas_used`` is the pure regular consumption regardless of + the block ``gas_used`` is the pure execution consumption regardless of the burn behavior. """ intrinsic_calc = fork.transaction_intrinsic_cost_calculator() @@ -513,17 +513,17 @@ def test_same_tx_created_selfdestruct_self_burn( # Self-beneficiary on a balance-bearing same-tx-created contract is # alive: account_new is false, so only the warm base is charged. - # Creation intrinsic is regular-only under EIP-2780; the pre-existing + # Creation intrinsic is execution-only under EIP-2780; the pre-existing # target adds no top-frame NEW_ACCOUNT and the self-burn adds no state - # gas, so net state gas is zero. The regular consumption exceeds the + # gas, so net state gas is zero. The execution consumption exceeds the # decomposed calldata floor, so the floor never pins the billing. - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(init_code), contract_creation=True, return_cost_deducted_prior_execution=True, ) - expected_regular = intrinsic_regular + init_code.regular_cost(fork) - expected_gas_used = expected_regular + expected_execution = intrinsic_execution + init_code.execution_cost(fork) + expected_gas_used = expected_execution # EIP-8246 removes the SELFDESTRUCT burn: the self-send is a no-op, # the balance stays in the (otherwise emptied) originator, and no @@ -562,7 +562,7 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( A creation transaction whose initcode SELFDESTRUCTs the new contract to a fresh ``Address(0xDEAD)``: the fresh, non-existent beneficiary receives a positive balance, so ``account_new`` is true — - ``regular = 5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000 + ``execution = 5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000 cold) plus a beneficiary ``NEW_ACCOUNT`` on the state axis. The beneficiary creation charge keys on the beneficiary, while the originator (created in this transaction) is still deleted: a @@ -594,12 +594,12 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( # beneficiary's NEW_ACCOUNT (the SELFDESTRUCT state cost) persists. new_account_state_gas = init_code.state_cost(fork) - intrinsic_regular = intrinsic_calc( + intrinsic_execution = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) expected_state = new_account_state_gas - expected_regular = intrinsic_regular + init_code.regular_cost(fork) - expected_gas_used = max(expected_regular, expected_state) + expected_execution = intrinsic_execution + init_code.execution_cost(fork) + expected_gas_used = max(expected_execution, expected_state) tx = Transaction( to=None, @@ -607,7 +607,7 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( sender=sender, # Reservoir holds the beneficiary-creation state gas (above the # creation's intrinsic NEW_ACCOUNT) so it does not spill into - # regular gas. + # execution gas. state_gas_reservoir=new_account_state_gas, expected_receipt=TransactionReceipt( logs=[transfer_log(created, beneficiary, amount)] diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py index 1d4a9b0624c..72a0cbae289 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py @@ -1,17 +1,17 @@ """ -Tests for the EIP-7702 authorization *regular*-gas repricing under +Tests for the EIP-7702 authorization *execution*-gas repricing under [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). Under EIP-2780 each EIP-7702 authorization is charged in two parts: a -state-independent *regular* base cost paid in the intrinsic, and +state-independent *execution* base cost paid in the intrinsic, and state-dependent costs (``NEW_ACCOUNT`` / ``ACCOUNT_WRITE`` for a new authority leaf, ``AUTH_BASE`` for a net-new delegation indicator) paid lazily at the top frame in ``set_delegation``. This module pins the -**regular** per-authorization intrinsic magnitude and the repriced +**execution** per-authorization intrinsic magnitude and the repriced cold/warm account-access costs that an authorized delegation incurs when later accessed by a ``CALL``. -The regular per-authorization intrinsic magnitude is the fixed +The execution per-authorization intrinsic magnitude is the fixed per-authorization base cost charged by the intrinsic (on Amsterdam, ``101 * 16`` calldata tokens plus the ``3000`` ecrecover, ``3000`` cold and ``2 * 100`` warm accesses of the EIP-7702 base), isolated here as @@ -54,9 +54,9 @@ pytestmark = pytest.mark.valid_from("Amsterdam") -def _regular_per_auth(fork: Fork) -> int: +def _execution_per_auth(fork: Fork) -> int: """ - Return the *regular* intrinsic gas charged per EIP-7702 + Return the *execution* intrinsic gas charged per EIP-7702 authorization. Under EIP-2780 the intrinsic charges only the state-independent @@ -75,7 +75,7 @@ def _regular_per_auth(fork: Fork) -> int: ) -def _regular_intrinsic( +def _execution_intrinsic( fork: Fork, *, n: int, @@ -110,7 +110,7 @@ def _regular_intrinsic( pytest.param(True, id="access_list_contains_authority"), ], ) -def test_auth_regular_intrinsic_magnitude( +def test_auth_execution_intrinsic_magnitude( state_test: StateTestFiller, env: Environment, pre: Alloc, @@ -120,10 +120,10 @@ def test_auth_regular_intrinsic_magnitude( authority_in_access_list: bool, ) -> None: """ - Assert the EIP-8038 *regular* per-authorization intrinsic magnitude. + Assert the EIP-8038 *execution* per-authorization intrinsic magnitude. - The regular intrinsic above the ``n=0`` base must equal - ``n * regular_per_auth`` plus the access-list delta (derived from + The execution intrinsic above the ``n=0`` base must equal + ``n * execution_per_auth`` plus the access-list delta (derived from the calculator itself so the calldata-floor contribution of the access-list bytes is accounted for). """ @@ -144,17 +144,19 @@ def test_auth_regular_intrinsic_magnitude( AccessList(address=signer, storage_keys=[]) for signer in signers ] - base_regular = _regular_intrinsic(fork, n=0) - regular = _regular_intrinsic(fork, n=n, access_list=access_list) + base_execution = _execution_intrinsic(fork, n=0) + execution = _execution_intrinsic(fork, n=n, access_list=access_list) # Access-list delta is derived from the calculator (it folds in the # calldata-floor cost of the access-list bytes), never hardcoded. - access_list_delta = _regular_intrinsic( + access_list_delta = _execution_intrinsic( fork, n=0, access_list=access_list - ) - _regular_intrinsic(fork, n=0) + ) - _execution_intrinsic(fork, n=0) - expected_per_auth = _regular_per_auth(fork) - assert regular - base_regular == n * expected_per_auth + access_list_delta + expected_per_auth = _execution_per_auth(fork) + assert ( + execution - base_execution == n * expected_per_auth + access_list_delta + ) sender = pre.fund_eoa() tx = Transaction( @@ -185,8 +187,8 @@ def test_auth_intrinsic_oog_boundary( Reject a set-code transaction one gas below the full intrinsic. ``gas_limit`` is set to ``full_intrinsic - 1`` (full intrinsic = - regular + auth state gas). Catches an implementation that omits the - repriced regular per-authorization cost from the intrinsic check. + execution + auth state gas). Catches an implementation that omits the + repriced execution per-authorization cost from the intrinsic check. """ contract = pre.deploy_contract(code=Op.STOP) authorization_list = [ @@ -234,7 +236,7 @@ def test_invalid_auth_charged_intrinsic( Each invalidity kind (``INVALID_NONCE``, ``INVALID_CHAIN_ID``, ``REPEATED_NONCE``, ``AUTHORITY_IS_CONTRACT``) makes the authorization invalid during processing, so it is silently skipped, - but its regular + state intrinsic gas is still paid. The transaction + but its execution + state intrinsic gas is still paid. The transaction succeeds. """ contract = pre.deploy_contract(code=Op.STOP) @@ -288,7 +290,7 @@ def test_invalid_auth_charged_intrinsic( else: raise ValueError(f"unknown invalidity: {invalidity!r}") - # The full intrinsic (regular + state) is charged regardless of + # The full intrinsic (execution + state) is charged regardless of # validity. Provide a comfortable gas limit and let the receipt # accounting be verified by the framework; the key assertion is the # untouched-authority post state. @@ -438,18 +440,18 @@ def test_mixed_validity_multi_auth_receipt_gas( # for every tuple; the one valid authorization adds ``AUTH_BASE`` at # the top frame for its net-new delegation indicator. The skipped # tuple and the plain ``STOP`` recipient add nothing. - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=n, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( authorizations=authorization_list, ) top_frame_state = fork.transaction_top_frame_state_gas( authorizations=authorization_list, ) cumulative_gas_used = ( - intrinsic_regular + top_frame_regular + top_frame_state + intrinsic_execution + top_frame_execution + top_frame_state ) tx = Transaction( @@ -540,7 +542,7 @@ def test_auth_account_warming( # Measure the cost of a single CALL to the authority. The CALL # opcode leaves one stack item (success); the overhead is the PUSHes # for its arguments. - overhead_cost = Op.PUSH1(0).regular_cost(fork) * len(Op.CALL.kwargs) + overhead_cost = Op.PUSH1(0).execution_cost(fork) * len(Op.CALL.kwargs) storage = Storage() callee_code = CodeGasMeasure( code=Op.CALL(gas=0, address=authority), @@ -578,7 +580,7 @@ def test_many_auths_block_limit( limit cap and confirm it succeeds. The authorization count is sized from the per-authorization total - intrinsic (regular + state) and the transaction gas-limit cap, so it + intrinsic (execution + state) and the transaction gas-limit cap, so it automatically tracks the repriced cost. """ gas_limit_cap = fork.transaction_gas_limit_cap() @@ -586,7 +588,7 @@ def test_many_auths_block_limit( contract = pre.deploy_contract(code=Op.STOP) - # Per-authorization total for a fresh (empty) authority: the regular + # Per-authorization total for a fresh (empty) authority: the execution # intrinsic base plus the top-frame account-write, account-creation # and delegation-write charges, derived from the fork's calculators # so it tracks the repricing. The probe only feeds the gas @@ -601,7 +603,7 @@ def test_many_auths_block_limit( first_write=True, ) per_auth_total = ( - _regular_per_auth(fork) + _execution_per_auth(fork) + fork.transaction_top_frame_gas_calculator()( authorizations=[probe_auth] ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py index d202332d6da..d72e03aca97 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py @@ -4,7 +4,7 @@ EIP-8038 originally over-charged every authorization as if it created a new account and *refunded* the difference (``ACCOUNT_WRITE`` on the -regular channel, ``NEW_ACCOUNT`` -- and ``AUTH_BASE`` on a clear -- on +execution channel, ``NEW_ACCOUNT`` -- and ``AUTH_BASE`` on a clear -- on the state channel) when the authority leaf already existed. Under EIP-2780 that over-charge-then-refund is gone: the @@ -72,7 +72,7 @@ def test_existing_authority_no_new_account_charge( (and, unlike the superseded EIP-8038 behaviour, refunds none); it charges the first-write ``ACCOUNT_WRITE`` and the top-frame ``AUTH_BASE`` for the net-new delegation indicator. The receipt gas - is therefore exactly the regular intrinsic plus + is therefore exactly the execution intrinsic plus ``n * (ACCOUNT_WRITE + AUTH_BASE)``, with no refund term. """ recipient = pre.deploy_contract(code=Op.STOP) @@ -94,18 +94,18 @@ def test_existing_authority_no_new_account_charge( # Existing leaf + net-new delegation: the first-write ACCOUNT_WRITE # and AUTH_BASE at the top frame. NEW_ACCOUNT is neither charged # nor refunded, so the receipt gas is the exact charge. - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=n, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( authorizations=authorization_list, ) top_frame_state = fork.transaction_top_frame_state_gas( authorizations=authorization_list, ) cumulative_gas_used = ( - intrinsic_regular + top_frame_regular + top_frame_state + intrinsic_execution + top_frame_execution + top_frame_state ) tx = Transaction( @@ -145,7 +145,7 @@ def test_clearing_delegation_no_state_charge( still writes the authority's leaf (code emptied, nonce bumped), so the transaction's first-write ``ACCOUNT_WRITE`` applies. Nothing is refunded (the over-charge is gone), so the receipt gas is exactly - the regular intrinsic plus ``n * ACCOUNT_WRITE``. + the execution intrinsic plus ``n * ACCOUNT_WRITE``. """ recipient = pre.deploy_contract(code=Op.STOP) delegated_to = pre.deploy_contract(code=Op.STOP) @@ -169,18 +169,18 @@ def test_clearing_delegation_no_state_charge( # Clearing an existing delegation writes no net-new indicator, so # no top-frame state charge applies and no refund fires; only the # first-write ACCOUNT_WRITE is charged per authority. - intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + intrinsic_execution = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=n, return_cost_deducted_prior_execution=True, ) - top_frame_regular = fork.transaction_top_frame_gas_calculator()( + top_frame_execution = fork.transaction_top_frame_gas_calculator()( authorizations=authorization_list, ) top_frame_state = fork.transaction_top_frame_state_gas( authorizations=authorization_list, ) assert top_frame_state == 0 - cumulative_gas_used = intrinsic_regular + top_frame_regular + cumulative_gas_used = intrinsic_execution + top_frame_execution tx = Transaction( to=recipient, diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py index f3cdf3e1669..67be30b8213 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py @@ -1,12 +1,12 @@ """ Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). -Covers the EIP-8038 ``SSTORE`` *regular* (non-state) gas schedule. The +Covers the EIP-8038 ``SSTORE`` *execution* (non-state) gas schedule. The state-creation charge for a zero-to-nonzero write is owned by EIP-8037 and is asserted separately; here every expectation is taken from the -``regular_cost`` dimension only. +``execution_cost`` dimension only. -The regular ``SSTORE`` cost is the slot-access cost (``COLD_STORAGE_ACCESS`` +The execution ``SSTORE`` cost is the slot-access cost (``COLD_STORAGE_ACCESS`` when the key is cold, else ``WARM_SLOAD``) plus, on the first change of the slot in the transaction (``original == current != new``), the write cost ``STORAGE_WRITE`` (modeled as ``COLD_STORAGE_WRITE - COLD_STORAGE_ACCESS``). @@ -56,7 +56,7 @@ @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() @pytest.mark.parametrize("key_warm,original,current,new", SSTORE_ROWS) -def test_sstore_regular_gas( +def test_sstore_execution_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, @@ -66,19 +66,19 @@ def test_sstore_regular_gas( new: int, ) -> None: """ - Measure the regular ``SSTORE`` gas for each EIP-8038 row and assert it. + Measure the execution ``SSTORE`` gas for each EIP-8038 row and assert it. The final (measured) ``SSTORE`` is wrapped in ``CodeGasMeasure`` so the - executed regular cost is stored on-chain and asserted against - ``expected_regular`` (slot access plus write-on-first-change). The same + executed execution cost is stored on-chain and asserted against + ``expected_execution`` (slot access plus write-on-first-change). The same value is cross-checked against the framework opcode model's - ``regular_cost`` as a secondary guard. The state-gas dimension is owned + ``execution_cost`` as a secondary guard. The state-gas dimension is owned by EIP-8037 and funded from the reservoir, so it is excluded here. """ # Move the data off slot 0 so ``CodeGasMeasure`` can store the measured # cost in slot 0. The bare (operand-free) opcode carries the metadata so # the measure overhead resolves to just the two operand PUSHes, and - # ``regular_cost``/``gas_cost`` are exact. + # ``execution_cost``/``gas_cost`` are exact. data_slot = 0x42 result_slot = 0 measured_bare = Op.SSTORE.with_metadata( @@ -90,7 +90,7 @@ def test_sstore_regular_gas( measured = measured_bare(data_slot, new) # Cross-check the oracle agrees with the hand-derived formula. - expected_regular = measured_bare.regular_cost(fork) + expected_execution = measured_bare.execution_cost(fork) # Reach ``current`` from ``original`` with an unmeasured prep SSTORE when # they differ, then measure the write to ``new``. The slot is warmed for @@ -122,9 +122,9 @@ def test_sstore_regular_gas( ) # State gas (owned by EIP-8037) is funded from the reservoir so it never - # disturbs the regular gas this test isolates. ``gas_limit`` is left + # disturbs the execution gas this test isolates. ``gas_limit`` is left # unset so the reservoir lands above the EIP-7825 cap and ``Op.GAS`` - # measures regular gas only; an explicit gas_limit below the cap would + # measures execution gas only; an explicit gas_limit below the cap would # zero the reservoir and spill state gas into the measurement. single_set_state_gas = Op.SSTORE(new_value=1).state_cost(fork) tx = Transaction( @@ -134,9 +134,9 @@ def test_sstore_regular_gas( state_gas_reservoir=2 * single_set_state_gas, ) - # result_slot holds the measured regular cost; data_slot holds ``new`` + # result_slot holds the measured execution cost; data_slot holds ``new`` # (absent when new == 0, because the slot is cleared). - expected_storage = {result_slot: expected_regular} + expected_storage = {result_slot: expected_execution} if new != 0: expected_storage[data_slot] = new post = {contract: Account(storage=expected_storage)} @@ -183,8 +183,8 @@ def test_sstore_cold_then_warm_same_slot( ) second = second_bare(data_slot, 3) - expected_first = first_bare.regular_cost(fork) - expected_second = second_bare.regular_cost(fork) + expected_first = first_bare.execution_cost(fork) + expected_second = second_bare.execution_cost(fork) # Each measured write stores its own runtime cost; the overhead # subtraction strips the two operand PUSHes so the stored value is the diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py index 4fbe570673d..7755bdc9240 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py @@ -11,7 +11,7 @@ trip; this reversal is exercised by ``test_sstore_clear_then_reset_nets_zero``. -This module covers the EIP-8038 *regular* ``SSTORE`` refund schedule via +This module covers the EIP-8038 *execution* ``SSTORE`` refund schedule via the transaction receipt's ``cumulative_gas_used``: * Clearing a slot whose original value is non-zero grants @@ -26,7 +26,7 @@ * The applied refund is capped at ``gas_used // 5`` (EIP-3529 quotient). All refunds use a non-zero original so the state-creation refund owned by -EIP-8037 is never involved; only the EIP-8038 regular dimension is +EIP-8037 is never involved; only the EIP-8038 execution dimension is exercised. """ @@ -56,7 +56,7 @@ def _cumulative_gas_used(code: Bytecode, fork: Fork) -> int: Return the receipt ``cumulative_gas_used`` for a single transaction whose execution is exactly ``code``. - Mirrors the spec: gross gas is intrinsic plus the regular and state + Mirrors the spec: gross gas is intrinsic plus the execution and state gas of the code; the applied refund is ``min(gross // 5, refund)`` (EIP-3529 quotient cap); the receipt reports gross minus the applied refund. @@ -64,7 +64,7 @@ def _cumulative_gas_used(code: Bytecode, fork: Fork) -> int: intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) - gross = intrinsic + code.regular_cost(fork) + code.state_cost(fork) + gross = intrinsic + code.execution_cost(fork) + code.state_cost(fork) applied_refund = min(gross // 5, code.refund(fork)) return gross - applied_refund @@ -104,7 +104,7 @@ def test_sstore_clear_grants_refund( intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) - gross = intrinsic + code.regular_cost(fork) + gross = intrinsic + code.execution_cost(fork) assert gross // 5 > refund_clear assert expected_cumulative == gross - refund_clear @@ -200,7 +200,7 @@ def test_sstore_restore_nonzero_refunds_write( intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) - gross = intrinsic + code.regular_cost(fork) + gross = intrinsic + code.execution_cost(fork) assert gross // 5 > storage_write assert expected_cumulative == gross - storage_write @@ -253,7 +253,7 @@ def test_sstore_refund_quotient_cap( intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) - gross = intrinsic + code.regular_cost(fork) + gross = intrinsic + code.execution_cost(fork) # The cap binds for every parametrization (single-clear gross is far # below 5x a clear refund). cap = gross // 5 @@ -306,10 +306,10 @@ def test_sstore_refund_cap_exact_equality( # Target the exact boundary: gross == quotient * accrued, so that # gross // quotient == accrued with no slack. Solve for the JUMPDEST # count from the remaining gas after intrinsic and the clear's - # regular cost; each JUMPDEST costs exactly 1 gas. + # execution cost; each JUMPDEST costs exactly 1 gas. jumpdest_gas = Op.JUMPDEST.gas_cost(fork) target_gross = quotient * accrued - base_gross = intrinsic + clear.regular_cost(fork) + base_gross = intrinsic + clear.execution_cost(fork) burn_gas = target_gross - base_gross num_jumpdest, remainder = divmod(burn_gas, jumpdest_gas) # An exact integer JUMPDEST count must reach the boundary; otherwise @@ -320,7 +320,7 @@ def test_sstore_refund_cap_exact_equality( code = clear + Op.JUMPDEST * num_jumpdest contract = pre.deploy_contract(code=code, storage={0: 1}) - gross = intrinsic + code.regular_cost(fork) + code.state_cost(fork) + gross = intrinsic + code.execution_cost(fork) + code.state_cost(fork) # Exact equality: the cap is neither under nor over the accrued refund. assert gross == target_gross assert gross // quotient == accrued diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py index 5716c9dc277..afac3738334 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py @@ -53,7 +53,7 @@ def test_transient_storage_gas_unchanged( # Measure TSTORE then TLOAD of the same transient slot in one frame, # subtracting the PUSH wrapper so the stored value is the bare opcode # cost. - push_cost = Op.PUSH1(0).regular_cost(fork) + push_cost = Op.PUSH1(0).execution_cost(fork) tstore_code = CodeGasMeasure( code=Op.TSTORE(0, 1), overhead_cost=2 * push_cost, diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 0bc561435ee..02cbbbfcd83 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -407,7 +407,7 @@ def test_creates_collisions( ) proxy_contract = pre.deploy_contract(code=proxy_contract_code) - min_gas_required = proxy_contract_code.regular_cost( + min_gas_required = proxy_contract_code.execution_cost( fork ) + proxy_contract_code.state_cost(fork) setup = Op.PUSH20(proxy_contract) + Op.PUSH3(min_gas_required) @@ -425,7 +425,7 @@ def test_creates_collisions( ) pre.deploy_contract(address=addr, code=Op.INVALID) else: - creation_cost = proxy_contract_code.regular_cost(fork) + creation_cost = proxy_contract_code.execution_cost(fork) max_contract_count = ( 2 * gas_benchmark_value // creation_cost if fixed_opcode_count is None diff --git a/tests/benchmark/helper/contract_factory.py b/tests/benchmark/helper/contract_factory.py index 27176c325cf..6b06ef30c77 100644 --- a/tests/benchmark/helper/contract_factory.py +++ b/tests/benchmark/helper/contract_factory.py @@ -247,9 +247,9 @@ def transactions_by_total_contract_count( """ Create a list of transactions calling the factory to create the given number of contracts, each transaction capped by the fork's - regular-gas limit cap (EIP-7825). Under EIP-8037 the per-byte code + execution-gas limit cap (EIP-7825). Under EIP-8037 the per-byte code deposit is state gas drawn from a separate reservoir, so the split - bounds regular gas only and lets the combined gas exceed the cap. + bounds execution gas only and lets the combined gas exceed the cap. """ to = self.address() @@ -265,7 +265,7 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: start_iteration: int = contract_start_index tx_gas_limit: int | None = None - tx_regular_cost: int | None = None + tx_execution_cost: int | None = None tx_state_cost: int | None = None last_iteration_count: int = 0 @@ -277,7 +277,7 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: ): if ( tx_gas_limit is None - or tx_regular_cost is None + or tx_execution_cost is None or tx_state_cost is None or iteration_count != last_iteration_count ): @@ -288,11 +288,13 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: include_state_gas_reservoir=True, calldata=calldata_max, ) - tx_regular_cost = self.tx_regular_gas_cost_by_iteration_count( - fork=fork, - iteration_count=iteration_count, - start_iteration=start_iteration, - calldata=calldata_max, + tx_execution_cost = ( + self.tx_execution_gas_cost_by_iteration_count( + fork=fork, + iteration_count=iteration_count, + start_iteration=start_iteration, + calldata=calldata_max, + ) ) tx_state_cost = self.state_gas_cost_by_iteration_count( fork=fork, @@ -310,7 +312,7 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: to=to, gas_limit=tx_gas_limit, sender=sender, - regular_cost=tx_regular_cost, + execution_cost=tx_execution_cost, state_cost=tx_state_cost, data=calldata(iteration_count, start_iteration), deployed_contracts=deployed_contracts, diff --git a/tests/benchmark/stateful/bloatnet/test_sstore.py b/tests/benchmark/stateful/bloatnet/test_sstore.py index 5e612d5c118..ea958753444 100644 --- a/tests/benchmark/stateful/bloatnet/test_sstore.py +++ b/tests/benchmark/stateful/bloatnet/test_sstore.py @@ -463,7 +463,8 @@ def test_sstore_variants( [1, 0, 1, 0], id="oscillation_4x_from_zero", marks=pytest.mark.skip( - reason="net-zero state gas; degenerates to a regular-gas loop" + reason="net-zero state gas; degenerates to an " + "execution-gas loop" ), ), pytest.param( diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py index e7e0e5962af..a7e5900a463 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_create_gas.py @@ -11,7 +11,7 @@ state_tests/stEIP150singleCodeGasPrices/RawCreateFailGasValueTransfer2Filler.json @manually-enhanced: Do not overwrite. Six RawCreate*Gas fillers folded into one -CodeGasMeasure parametrize; failure path charges regular_cost (no state gas). +CodeGasMeasure parametrize; failure path charges execution_cost (no state gas). """ import pytest @@ -99,7 +99,7 @@ def test_raw_create_gas( if fails: # A balance-check failure runs no init code and creates no account, so # only the regular (execution) gas is charged, never state gas. - expected_gas = create_code.regular_cost(fork) + expected_gas = create_code.execution_cost(fork) created_account = Account.NONEXISTENT else: expected_gas = create_code.gas_cost(fork) From 3ae3d66cc2b3fd448a9578e58fb49bbae30752eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:48:44 +0800 Subject: [PATCH 30/55] feat(tests): implement eip-8070 sparse blob pool tests (#2948) Co-authored-by: spencer-tb --- .../execution/blob_transaction.py | 218 ++++++++++++- .../forks/forks/eips/amsterdam/eip_8070.py | 16 + .../src/execution_testing/rpc/__init__.py | 2 + .../testing/src/execution_testing/rpc/rpc.py | 28 +- .../src/execution_testing/rpc/rpc_types.py | 23 ++ .../src/execution_testing/specs/blobs.py | 8 + .../eip8070_sparse_blobpool/__init__.py | 3 + .../eip8070_sparse_blobpool/conftest.py | 148 +++++++++ .../amsterdam/eip8070_sparse_blobpool/spec.py | 42 +++ .../test_custody_columns.py | 108 +++++++ .../eip8070_sparse_blobpool/test_get_cells.py | 291 ++++++++++++++++++ 11 files changed, 878 insertions(+), 9 deletions(-) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8070.py create mode 100644 tests/amsterdam/eip8070_sparse_blobpool/__init__.py create mode 100644 tests/amsterdam/eip8070_sparse_blobpool/conftest.py create mode 100644 tests/amsterdam/eip8070_sparse_blobpool/spec.py create mode 100644 tests/amsterdam/eip8070_sparse_blobpool/test_custody_columns.py create mode 100644 tests/amsterdam/eip8070_sparse_blobpool/test_get_cells.py diff --git a/packages/testing/src/execution_testing/execution/blob_transaction.py b/packages/testing/src/execution_testing/execution/blob_transaction.py index 796f2c455ac..e120a91c79c 100644 --- a/packages/testing/src/execution_testing/execution/blob_transaction.py +++ b/packages/testing/src/execution_testing/execution/blob_transaction.py @@ -14,11 +14,19 @@ from execution_testing.rpc import ( BlobAndProofV1, BlobAndProofV2, + BlobCellsAndProofsV1, EngineRPC, EthRPC, ) -from execution_testing.rpc.rpc_types import GetBlobsResponse +from execution_testing.rpc.rpc_types import ( + ForkchoiceState, + GetBlobsResponse, + GetBlobsV4Response, + JSONRPCError, + PayloadStatusEnum, +) from execution_testing.test_types import ( + Blob, Environment, NetworkWrappedTransaction, Transaction, @@ -31,6 +39,20 @@ logger = get_logger(__name__) +CUSTODY_COLUMNS_BYTE_LENGTH = 16 +"""Byte length of a well-formed `custodyColumns` bitmap (EIP-8070).""" + + +def _interleave_hashes(a: List[Hash], b: List[Hash]) -> List[Hash]: + """Interleave two hash lists, starting with `a`, appending leftovers.""" + interleaved: List[Hash] = [] + for x, y in zip(a, b, strict=False): + interleaved.extend((x, y)) + shorter_length = min(len(a), len(b)) + interleaved.extend(a[shorter_length:]) + interleaved.extend(b[shorter_length:]) + return interleaved + def _validate_blob_and_proof( expected_blob: BlobAndProofV1 | BlobAndProofV2 | None, @@ -107,6 +129,81 @@ def _validate_blob_and_proof( ) +def _validate_cells_and_proofs( + expected_blob: Blob | None, + received: BlobCellsAndProofsV1 | None, + cell_mask: int, + index: int, +) -> None: + """ + Validate a received `engine_getBlobsV4` cell matrix against a local blob. + + The response is a compact matrix: for each existing blob the client + returns only the cells selected by `cell_mask`, ordered by ascending + cell index, so `blob_cells[k]` is the k-th requested cell. When + `expected_blob` is `None` (a non-existing hash), the whole entry must be + `null`. + + Per execution-apis `engine_getBlobsV4`, `cell_mask` is a little-endian + 16-byte bitmap where bit `i` selects cell `i` (see `EngineRPC.get_blobs`). + Network-wrapped txs deliver the full blob, so the client holds every + requested cell; a returned `null` means an unavailable cell and fails. + """ + if expected_blob is None: + if received is None: + logger.info( + f"Blob at index {index} correctly returned null " + "(non-existing blob hash)" + ) + return + raise ValueError( + f"Blob at index {index} should be null (non-existing hash), " + f"but client returned a cell matrix." + ) + if received is None: + raise ValueError(f"Received cell matrix at index {index} is empty.") + + assert expected_blob.cells is not None, ( + "Local blob has no cells; getBlobsV4 requires a fork with cell proofs." + ) + assert isinstance(expected_blob.proof, list), ( + "Local blob proof is not a cell-proof list." + ) + # Compact matrix: the client returns only the requested cells, in + # ascending cell-index order (bit `i` of the mask selects cell `i`). + requested_indices = [ + i for i in range(len(expected_blob.cells)) if (cell_mask >> i) & 1 + ] + if len(received.blob_cells) != len(requested_indices): + raise ValueError( + f"Cell matrix at index {index} has {len(received.blob_cells)} " + f"cells, expected {len(requested_indices)}." + ) + if len(received.proofs) != len(requested_indices): + raise ValueError( + f"Proof matrix at index {index} has {len(received.proofs)} " + f"proofs, expected {len(requested_indices)}." + ) + + for pos, cell_index in enumerate(requested_indices): + recv_cell = received.blob_cells[pos] + recv_proof = received.proofs[pos] + if recv_cell is None or recv_proof is None: + raise ValueError( + f"Requested cell {cell_index} at blob index {index} was " + "returned as null." + ) + if recv_cell != expected_blob.cells[cell_index]: + raise ValueError( + f"Cell mismatch at blob index {index}, cell {cell_index}." + ) + if recv_proof != expected_blob.proof[cell_index]: + raise ValueError( + f"Cell proof mismatch at blob index {index}, " + f"cell {cell_index}." + ) + + def versioned_hashes_with_blobs_and_proofs( tx: NetworkWrappedTransaction, ) -> Dict[Hash, BlobAndProofV1 | BlobAndProofV2]: @@ -148,7 +245,10 @@ class BlobTransaction(BaseExecute): txs: List[NetworkWrappedTransaction | Transaction] nonexisting_blob_hashes: List[Hash] | None = None + interleave_nonexisting_blob_hashes: bool = False get_blobs_version: int | None = None + cell_mask: int | None = None + custody_columns: bytes | None = None def prepare_transactions( self, @@ -199,6 +299,64 @@ def get_required_sender_balances( balances[sender] += tx.signer_minimum_balance(fork=fork) return balances + def _update_custody_columns( + self, + fork: Fork, + eth_rpc: EthRPC, + engine_rpc: EngineRPC, + ) -> None: + """ + Send a forkchoice update carrying the `custodyColumns` bitmap. + + A 16-byte bitmap must be accepted with a VALID payload status + (custody set update errors must not affect the forkchoice flow, + per `engine_forkchoiceUpdatedV4`); any other length must be + rejected with `-32602: Invalid params`. + """ + assert self.custody_columns is not None + fcu_version = fork.engine_forkchoice_updated_version() + assert fcu_version is not None and fcu_version >= 4, ( + "custodyColumns requires engine_forkchoiceUpdatedV4." + ) + latest_block = eth_rpc.get_block_by_number("latest") + assert latest_block is not None, "Failed to fetch the latest block." + forkchoice_state = ForkchoiceState( + head_block_hash=Hash(latest_block["hash"]), + ) + valid_length = len(self.custody_columns) == CUSTODY_COLUMNS_BYTE_LENGTH + try: + response = engine_rpc.forkchoice_updated( + forkchoice_state, + None, + version=fcu_version, + custody_columns=self.custody_columns, + ) + except JSONRPCError as e: + if valid_length: + raise + if e.code != -32602: + raise ValueError( + f"Expected error -32602 (Invalid params) for a " + f"{len(self.custody_columns)}-byte custodyColumns, " + f"got {e.code}: {e.message}" + ) from e + logger.info( + f"Client correctly rejected a " + f"{len(self.custody_columns)}-byte custodyColumns bitmap." + ) + return + if not valid_length: + raise ValueError( + f"Client accepted a {len(self.custody_columns)}-byte " + "custodyColumns bitmap; expected -32602 (Invalid params)." + ) + status = response.payload_status.status + if status != PayloadStatusEnum.VALID: + raise ValueError( + f"forkchoiceUpdatedV{fcu_version} with custodyColumns " + f"returned payload status {status}, expected VALID." + ) + def execute( self, fork: Fork, @@ -208,6 +366,7 @@ def execute( ) -> ExecuteResult: """Execute the format.""" versioned_hashes: Dict[Hash, BlobAndProofV1 | BlobAndProofV2] = {} + blobs_by_hash: Dict[Hash, Blob] = {} sent_txs: List[Transaction] = [] for tx_index, tx in enumerate(self.txs): tx = tx.with_signature_and_sender() @@ -218,6 +377,8 @@ def execute( versioned_hashes.update( versioned_hashes_with_blobs_and_proofs(tx) ) + for blob in tx.blob_objects: + blobs_by_hash[blob.versioned_hash] = blob else: sent_txs.append(tx) label = ( @@ -257,10 +418,27 @@ def execute( list_versioned_hashes = list(versioned_hashes.keys()) if self.nonexisting_blob_hashes is not None: - list_versioned_hashes.extend(self.nonexisting_blob_hashes) + if self.interleave_nonexisting_blob_hashes: + assert version >= 4, ( + "interleave_nonexisting_blob_hashes is only supported " + "with getBlobsV4." + ) + list_versioned_hashes = _interleave_hashes( + self.nonexisting_blob_hashes, list_versioned_hashes + ) + else: + list_versioned_hashes.extend(self.nonexisting_blob_hashes) - blob_response: GetBlobsResponse | None = engine_rpc.get_blobs( - list_versioned_hashes, version=version + if self.custody_columns is not None: + self._update_custody_columns(fork, eth_rpc, engine_rpc) + + indices_bitarray = self.cell_mask if version >= 4 else None + blob_response: GetBlobsResponse | GetBlobsV4Response | None = ( + engine_rpc.get_blobs( + list_versioned_hashes, + version=version, + indices_bitarray=indices_bitarray, + ) ) if version <= 2: @@ -284,6 +462,7 @@ def execute( f"getBlobsV{version} returned 'null' but all " "requested blobs should exist." ) + assert isinstance(blob_response, GetBlobsResponse) local_blobs_and_proofs = list(versioned_hashes.values()) assert len(blob_response) == len(local_blobs_and_proofs), ( f"Expected {len(local_blobs_and_proofs)} blobs and " @@ -305,6 +484,7 @@ def execute( "response, but V3 should always return an array " "(with null entries for missing blobs)." ) + assert isinstance(blob_response, GetBlobsResponse) expected_blobs_and_proofs: List[ BlobAndProofV1 | BlobAndProofV2 | None ] = list(versioned_hashes.values()) @@ -334,10 +514,38 @@ def execute( f"blobs and {nonexisting_count} null entries for " "missing blobs" ) + elif version == 4: + # V4 (EIP-8070): partial cell matrix, selected by cell_mask + assert self.cell_mask is not None, ( + f"getBlobsV{version} requires a cell_mask." + ) + if blob_response is None: + raise ValueError( + f"getBlobsV{version} returned 'null' for the entire " + "response, but V4 should always return an array " + "(with null entries for missing blobs)." + ) + assert isinstance(blob_response, GetBlobsV4Response) + # `blobs_by_hash` only holds existing blobs, so non-existing + # hashes map to `None` at their exact request positions. + expected_blobs: List[Blob | None] = [ + blobs_by_hash.get(vh) for vh in list_versioned_hashes + ] + if len(blob_response) != len(expected_blobs): + raise ValueError( + f"Expected {len(expected_blobs)} blob responses, " + f"got {len(blob_response)}." + ) + for i, (expected_cells, received_cells) in enumerate( + zip(expected_blobs, blob_response.root, strict=True) + ): + _validate_cells_and_proofs( + expected_cells, received_cells, self.cell_mask, i + ) else: raise NotImplementedError( f"getBlobsV{version} is not supported. " - "Supported versions: V1, V2, V3." + "Supported versions: V1, V2, V3, V4." ) eth_rpc.wait_for_transactions(sent_txs) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8070.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8070.py new file mode 100644 index 00000000000..2b43e4a68ec --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8070.py @@ -0,0 +1,16 @@ +""" +EIP-8070: eth/72 - Sparse Blobpool. + +Custody-aligned sampling of the blobpool, adding the `engine_getBlobsV4` +endpoint to retrieve a partial cell matrix of a blob. + +https://eips.ethereum.org/EIPS/eip-8070 +""" + +from ....base_fork import BaseFork + + +class EIP8070(BaseFork): + """EIP-8070 class.""" + + pass diff --git a/packages/testing/src/execution_testing/rpc/__init__.py b/packages/testing/src/execution_testing/rpc/__init__.py index d62b65bcd7a..1812a5fa98a 100644 --- a/packages/testing/src/execution_testing/rpc/__init__.py +++ b/packages/testing/src/execution_testing/rpc/__init__.py @@ -22,6 +22,7 @@ from .rpc_types import ( BlobAndProofV1, BlobAndProofV2, + BlobCellsAndProofsV1, EthConfigResponse, ForkConfig, ForkConfigBlobSchedule, @@ -35,6 +36,7 @@ "AdminRPC", "BlobAndProofV1", "BlobAndProofV2", + "BlobCellsAndProofsV1", "BlockNotAvailableError", "BlockNumberType", "DebugRPC", diff --git a/packages/testing/src/execution_testing/rpc/rpc.py b/packages/testing/src/execution_testing/rpc/rpc.py index cee13b98e30..739c2c26962 100644 --- a/packages/testing/src/execution_testing/rpc/rpc.py +++ b/packages/testing/src/execution_testing/rpc/rpc.py @@ -51,6 +51,7 @@ ForkchoiceState, ForkchoiceUpdateResponse, GetBlobsResponse, + GetBlobsV4Response, GetPayloadResponse, JSONRPCError, JSONRPCRequest, @@ -1444,6 +1445,7 @@ def forkchoice_updated( payload_attributes: PayloadAttributes | None = None, *, version: int, + custody_columns: bytes | None = None, ) -> ForkchoiceUpdateResponse: """ `engine_forkchoiceUpdatedVX`: Updates the forkchoice state of the @@ -1451,10 +1453,16 @@ def forkchoice_updated( """ method = f"forkchoiceUpdatedV{version}" + params: List[Any] if payload_attributes is None: params = [to_json(forkchoice_state), None] else: params = [to_json(forkchoice_state), to_json(payload_attributes)] + if custody_columns is not None: + # Third parameter of `engine_forkchoiceUpdatedV4` (EIP-8070): + # a bitmap of the blob columns custodied by the node. + assert version >= 4, "custodyColumns requires forkchoiceUpdatedV4." + params.append(f"0x{custody_columns.hex()}") return ForkchoiceUpdateResponse.model_validate( self.post_request( @@ -1487,21 +1495,33 @@ def get_blobs( versioned_hashes: List[Hash], *, version: int, - ) -> GetBlobsResponse | None: + indices_bitarray: int | None = None, + ) -> GetBlobsResponse | GetBlobsV4Response | None: """ `engine_getBlobsVX`: Retrieves blobs from an execution layers tx pool. """ method = f"getBlobsV{version}" - params = [f"{h}" for h in versioned_hashes] + params: List[Any] = [[f"{h}" for h in versioned_hashes]] + + if version >= 4: + assert indices_bitarray is not None, ( + f"getBlobsV{version} requires an indices_bitarray cell mask." + ) + # `indices_bitarray` is a little-endian 16-byte bitmap where bit + # `i` selects cell `i` (execution-apis `engine_getBlobsV4`). + params.append(f"0x{indices_bitarray.to_bytes(16, 'little').hex()}") response = self.post_request( - request=RPCCall(method=method, params=[params]), + request=RPCCall(method=method, params=params), ).result_or_raise() if response is None: # for tests that request non-existing blobs logger.debug("get_blobs response received but it has value: None") return None - return GetBlobsResponse.model_validate( + response_model = ( + GetBlobsV4Response if version >= 4 else GetBlobsResponse + ) + return response_model.model_validate( response, context=self.response_validation_context, ) diff --git a/packages/testing/src/execution_testing/rpc/rpc_types.py b/packages/testing/src/execution_testing/rpc/rpc_types.py index d4e7784a6c0..f58bd345378 100644 --- a/packages/testing/src/execution_testing/rpc/rpc_types.py +++ b/packages/testing/src/execution_testing/rpc/rpc_types.py @@ -313,6 +313,13 @@ class BlobAndProofV2(CamelModel): proofs: List[Bytes] +class BlobCellsAndProofsV1(CamelModel): + """Represents a partial cell and cell-proof structure (>= Amsterdam).""" + + blob_cells: List[Bytes | None] + proofs: List[Bytes | None] + + class GetPayloadResponse(CamelModel): """Represents the response of a get payload request.""" @@ -341,6 +348,22 @@ def __getitem__( return self.root[index] +class GetBlobsV4Response( + EthereumTestRootModel[List[BlobCellsAndProofsV1 | None]] +): + """Represents the response of an `engine_getBlobsV4` request.""" + + root: List[BlobCellsAndProofsV1 | None] + + def __len__(self) -> int: + """Return the number of blob entries in the response.""" + return len(self.root) + + def __getitem__(self, index: int) -> BlobCellsAndProofsV1 | None: + """Return the blob cell matrix at the given index.""" + return self.root[index] + + class ForkConfigBlobSchedule(CamelModel): """Representation of the blob schedule of a given fork.""" diff --git a/packages/testing/src/execution_testing/specs/blobs.py b/packages/testing/src/execution_testing/specs/blobs.py index 7eb8c8dc20e..630f6765dc6 100644 --- a/packages/testing/src/execution_testing/specs/blobs.py +++ b/packages/testing/src/execution_testing/specs/blobs.py @@ -23,7 +23,10 @@ class BlobsTest(BaseTest): pre: Alloc txs: List[NetworkWrappedTransaction | Transaction] nonexisting_blob_hashes: List[Hash] | None = None + interleave_nonexisting_blob_hashes: bool = False get_blobs_version: int | None = None + cell_mask: int | None = None + custody_columns: bytes | None = None supported_execute_formats: ClassVar[Sequence[LabeledExecuteFormat]] = [ LabeledExecuteFormat( @@ -53,7 +56,12 @@ def execute( return BlobTransaction( txs=self.txs, nonexisting_blob_hashes=self.nonexisting_blob_hashes, + interleave_nonexisting_blob_hashes=( + self.interleave_nonexisting_blob_hashes + ), get_blobs_version=self.get_blobs_version, + cell_mask=self.cell_mask, + custody_columns=self.custody_columns, ) raise Exception(f"Unsupported execute format: {execute_format}") diff --git a/tests/amsterdam/eip8070_sparse_blobpool/__init__.py b/tests/amsterdam/eip8070_sparse_blobpool/__init__.py new file mode 100644 index 00000000000..19ebd3f9418 --- /dev/null +++ b/tests/amsterdam/eip8070_sparse_blobpool/__init__.py @@ -0,0 +1,3 @@ +""" +Test suite for [EIP-8070: eth/72 - Sparse Blobpool](https://eips.ethereum.org/EIPS/eip-8070). +""" diff --git a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py new file mode 100644 index 00000000000..3952b6363cf --- /dev/null +++ b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py @@ -0,0 +1,148 @@ +"""Shared fixtures for building blob transactions in EIP-8070 tests.""" + +from typing import List, Optional + +import pytest +from execution_testing import ( + Address, + Alloc, + Blob, + Fork, + NetworkWrappedTransaction, + Transaction, + TransactionException, +) + + +@pytest.fixture +def destination_account(pre: Alloc) -> Address: + """Destination account for the blob transactions.""" + return pre.fund_eoa(amount=0) + + +@pytest.fixture +def tx_value() -> int: + """Value contained by the transactions sent during test.""" + return 1 + + +@pytest.fixture +def tx_gas(fork: Fork) -> int: + """Gas allocated to transactions sent during test.""" + return fork.transaction_intrinsic_cost_calculator()() + + +@pytest.fixture +def block_base_fee_per_gas() -> int: + """Return default max fee per gas for transactions sent during test.""" + return 7 + + +@pytest.fixture +def tx_calldata() -> bytes: + """Calldata in transactions sent during test.""" + return b"" + + +@pytest.fixture(autouse=True) +def parent_excess_blobs() -> int: + """Excess blobs of the parent block (defaults to a blob gas price of 1).""" + return 10 + + +@pytest.fixture(autouse=True) +def parent_blobs() -> int: + """Blobs of the parent block.""" + return 0 + + +@pytest.fixture +def excess_blob_gas( + fork: Fork, + parent_excess_blobs: int | None, + parent_blobs: int | None, + block_base_fee_per_gas: int, +) -> int | None: + """Calculate the excess blob gas of the block under test.""" + if parent_excess_blobs is None or parent_blobs is None: + return None + excess_blob_gas = fork.excess_blob_gas_calculator() + return excess_blob_gas( + parent_excess_blobs=parent_excess_blobs, + parent_blob_count=parent_blobs, + parent_base_fee_per_gas=block_base_fee_per_gas, + ) + + +@pytest.fixture +def blob_gas_price( + fork: Fork, + excess_blob_gas: int | None, +) -> int | None: + """Return blob gas price for the block of the test.""" + if excess_blob_gas is None: + return None + get_blob_gas_price = fork.blob_gas_price_calculator() + return get_blob_gas_price(excess_blob_gas=excess_blob_gas) + + +@pytest.fixture +def txs_versioned_hashes(txs_blobs: List[List[Blob]]) -> List[List[bytes]]: + """List of blob versioned hashes derived from the blobs.""" + return [[blob.versioned_hash for blob in blob_tx] for blob_tx in txs_blobs] + + +@pytest.fixture +def tx_max_fee_per_blob_gas(fork: Fork, blob_gas_price: Optional[int]) -> int: + """Max fee per blob gas for transactions sent during test.""" + if blob_gas_price is None: + return fork.min_base_fee_per_blob_gas() + return blob_gas_price + + +@pytest.fixture +def tx_error() -> Optional[TransactionException]: + """No transaction is expected to be rejected by the transition tool.""" + return None + + +@pytest.fixture(autouse=True) +def txs( + pre: Alloc, + destination_account: Optional[Address], + tx_gas: int, + tx_value: int, + tx_calldata: bytes, + tx_max_fee_per_blob_gas: int, + txs_versioned_hashes: List[List[bytes]], + tx_error: Optional[TransactionException], + txs_blobs: List[List[Blob]], + fork: Fork, +) -> List[NetworkWrappedTransaction | Transaction]: + """Prepare the list of transactions that are sent during the test.""" + if len(txs_blobs) != len(txs_versioned_hashes): + raise ValueError( + "txs_blobs and txs_versioned_hashes should have the same length" + ) + txs: List[NetworkWrappedTransaction | Transaction] = [] + for tx_blobs, tx_versioned_hashes in zip( + txs_blobs, txs_versioned_hashes, strict=False + ): + tx = Transaction( + sender=pre.fund_eoa(), + to=destination_account, + value=tx_value, + gas_limit=tx_gas, + data=tx_calldata, + max_fee_per_blob_gas=tx_max_fee_per_blob_gas, + access_list=[], + blob_versioned_hashes=tx_versioned_hashes, + error=tx_error, + ) + network_wrapped_tx = NetworkWrappedTransaction( + tx=tx, + blob_objects=tx_blobs, + wrapper_version=fork.full_blob_tx_wrapper_version(), + ) + txs.append(network_wrapped_tx) + return txs diff --git a/tests/amsterdam/eip8070_sparse_blobpool/spec.py b/tests/amsterdam/eip8070_sparse_blobpool/spec.py new file mode 100644 index 00000000000..d6ec4c58162 --- /dev/null +++ b/tests/amsterdam/eip8070_sparse_blobpool/spec.py @@ -0,0 +1,42 @@ +"""Defines EIP-8070 specification constants and functions.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_8070 = ReferenceSpec( + "EIPS/eip-8070.md", "64d1b463e1c75884c995f81d8ffab40401acbcaa" +) + + +@dataclass(frozen=True) +class Spec: + """ + Parameters from the EIP-8070 specification as defined at + https://eips.ethereum.org/EIPS/eip-8070. + """ + + CELLS_PER_EXT_BLOB = 128 + """Number of cells an extended blob is split into for `getBlobsV4`.""" + + RECONSTRUCTION_THRESHOLD = 64 + """Number of cells required for Reed-Solomon reconstruction of a blob.""" + + SAMPLES_PER_SLOT = 8 + """Minimum number of blob columns a node must custody.""" + + CUSTODY_BITMAP_BYTES = 16 + """Byte length of the `custodyColumns` and cell mask bitmaps.""" + + MIN_SUPPORTED_REQUEST_SIZE = 128 + """ + Minimum `getBlobsV4` request size (in versioned hashes) that clients + must support, per the execution-apis `engine_getBlobsV4` definition. + """ diff --git a/tests/amsterdam/eip8070_sparse_blobpool/test_custody_columns.py b/tests/amsterdam/eip8070_sparse_blobpool/test_custody_columns.py new file mode 100644 index 00000000000..52c6fcd78e8 --- /dev/null +++ b/tests/amsterdam/eip8070_sparse_blobpool/test_custody_columns.py @@ -0,0 +1,108 @@ +""" +Custody columns forkchoice tests. + +Tests for the `custodyColumns` parameter of `engine_forkchoiceUpdatedV4` +in [EIP-8070: eth/72 - Sparse Blobpool]( +https://eips.ethereum.org/EIPS/eip-8070). + +`custodyColumns` is an optional 16-byte bitmap informing the execution +client of the blob columns it must custody. A well-formed bitmap must be +accepted (custody set update errors must not affect the forkchoice flow); +a bitmap of any other length must be rejected with `-32602: Invalid +params`. Blob serving via `engine_getBlobsV4` must be unaffected either +way, since the client holds the full blobs. +""" + +from typing import List + +import pytest +from execution_testing import ( + Alloc, + Blob, + BlobsTestFiller, + Fork, + NetworkWrappedTransaction, + Transaction, +) + +from .spec import Spec, ref_spec_8070 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8070.git_path +REFERENCE_SPEC_VERSION = ref_spec_8070.version + +pytestmark = pytest.mark.valid_from("EIP8070") + +CELLS = Spec.CELLS_PER_EXT_BLOB +ALL_CELLS_MASK = (1 << CELLS) - 1 +BITMAP_BYTES = Spec.CUSTODY_BITMAP_BYTES + + +def generate_single_blob_layout(fork: Fork) -> List: + """Return a single-blob transaction layout.""" + return [ + pytest.param([[Blob.from_fork(fork)]], id="single_blob_transaction") + ] + + +@pytest.mark.parametrize( + "custody_columns", + [ + pytest.param(b"\xff" * BITMAP_BYTES, id="all_columns"), + pytest.param( + ((1 << Spec.SAMPLES_PER_SLOT) - 1).to_bytes( + BITMAP_BYTES, "little" + ), + id="custody_aligned_8", + ), + pytest.param(b"\x00" * BITMAP_BYTES, id="no_columns"), + ], +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_single_blob_layout) +@pytest.mark.exception_test +def test_fcu_custody_columns( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + custody_columns: bytes, +) -> None: + """ + Test that `engine_forkchoiceUpdatedV4` accepts a 16-byte + `custodyColumns` bitmap with a VALID payload status and that blob + serving via `getBlobsV4` is unaffected by the custody update. + """ + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=ALL_CELLS_MASK, + custody_columns=custody_columns, + ) + + +@pytest.mark.parametrize( + "custody_columns", + [ + pytest.param(b"\xff" * (BITMAP_BYTES - 1), id="fifteen_bytes"), + pytest.param(b"\xff" * (BITMAP_BYTES + 1), id="seventeen_bytes"), + pytest.param(b"", id="empty"), + ], +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_single_blob_layout) +@pytest.mark.exception_test +def test_fcu_custody_columns_invalid_length( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + custody_columns: bytes, +) -> None: + """ + Test that a malformed-length `custodyColumns` bitmap is rejected with + `-32602: Invalid params` and does not affect subsequent blob serving. + """ + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=ALL_CELLS_MASK, + custody_columns=custody_columns, + ) diff --git a/tests/amsterdam/eip8070_sparse_blobpool/test_get_cells.py b/tests/amsterdam/eip8070_sparse_blobpool/test_get_cells.py new file mode 100644 index 00000000000..486fa333f50 --- /dev/null +++ b/tests/amsterdam/eip8070_sparse_blobpool/test_get_cells.py @@ -0,0 +1,291 @@ +""" +Get cells engine endpoint tests. + +Tests for the `engine_getBlobsV4` endpoint in [EIP-8070: eth/72 - Sparse +Blobpool](https://eips.ethereum.org/EIPS/eip-8070). + +`engine_getBlobsV4` retrieves a custody-aligned subset of a blob's cells, +selected by a `uint128` `indices_bitarray` cell mask, and returns a partial +cell matrix with `null` entries for cells that were not requested or are not +held by the client. +""" + +from hashlib import sha256 +from typing import List + +import pytest +from execution_testing import ( + Alloc, + Blob, + BlobsTestFiller, + Fork, + Hash, + NetworkWrappedTransaction, + Transaction, +) + +from .spec import Spec, ref_spec_8070 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8070.git_path +REFERENCE_SPEC_VERSION = ref_spec_8070.version + +pytestmark = pytest.mark.valid_from("EIP8070") + +CELLS = Spec.CELLS_PER_EXT_BLOB +ALL_CELLS_MASK = (1 << CELLS) - 1 + + +def generate_blob_layouts(fork: Fork) -> List: + """Return blob transaction layouts to exercise `getBlobsV4`.""" + max_blobs_per_block = fork.max_blobs_per_block() + max_blobs_per_tx = fork.max_blobs_per_tx() + target_blobs_per_block = fork.target_blobs_per_block() + + # Ascending pattern (1, 2, 3... blobs per tx) capped at the target + ascending_txs = [] + total_blobs = 0 + blob_offset = 0 + for tx_size in range(1, max_blobs_per_tx + 1): + if total_blobs + tx_size > target_blobs_per_block: + break + ascending_txs.append( + [Blob.from_fork(fork, blob_offset + j) for j in range(tx_size)] + ) + total_blobs += tx_size + blob_offset += tx_size + + two_tx_blobs = min(target_blobs_per_block // 2, max_blobs_per_tx) + three_tx_blobs = min(target_blobs_per_block // 3, max_blobs_per_tx) + + return [ + pytest.param( + [[Blob.from_fork(fork)]], + id="single_blob_transaction", + ), + pytest.param( + [[Blob.from_fork(fork, s) for s in range(max_blobs_per_tx)]], + id="max_blobs_per_tx", + ), + pytest.param( + [[Blob.from_fork(fork, s)] for s in range(max_blobs_per_block)], + id="max_blobs_per_block", + ), + pytest.param( + [[Blob.from_fork(fork, s)] for s in range(target_blobs_per_block)], + id="target_blobs_per_block", + ), + pytest.param( + [ + [Blob.from_fork(fork, s) for s in range(two_tx_blobs)], + [ + Blob.from_fork(fork, s + two_tx_blobs) + for s in range(two_tx_blobs) + ], + ], + id="two_tx_equal_blobs", + ), + pytest.param( + [ + [ + Blob.from_fork(fork, s + i * three_tx_blobs) + for s in range(three_tx_blobs) + ] + for i in range(3) + ], + id="three_tx_equal_blobs", + ), + pytest.param( + [[Blob.from_fork(fork, s) for s in range(max_blobs_per_tx)]] + + [ + [Blob.from_fork(fork, max_blobs_per_tx + s)] + for s in range(max_blobs_per_block - max_blobs_per_tx) + ], + id="mixed_max_tx_plus_singles", + ), + pytest.param( + ascending_txs, + id="ascending_blob_pattern", + ), + ] + + +def generate_single_blob_layout(fork: Fork) -> List: + """Return a single-blob transaction layout.""" + return [ + pytest.param([[Blob.from_fork(fork)]], id="single_blob_transaction") + ] + + +def generate_single_blob_txs_layout(fork: Fork) -> List: + """Return a layout of three single-blob transactions.""" + return [ + pytest.param( + [[Blob.from_fork(fork, s)] for s in range(3)], + id="three_single_blob_txs", + ) + ] + + +def generate_cell_masks() -> List: + """Return cell masks to exercise `getBlobsV4`.""" + return [ + pytest.param(ALL_CELLS_MASK, id="all_cells"), + pytest.param((1 << Spec.RECONSTRUCTION_THRESHOLD) - 1, id="first_64"), + pytest.param( + ALL_CELLS_MASK ^ ((1 << Spec.RECONSTRUCTION_THRESHOLD) - 1), + id="top_64", + ), + pytest.param(0xFF, id="custody_aligned_8"), + pytest.param(1, id="single_cell"), + pytest.param(1 << (CELLS - 1), id="last_cell"), + pytest.param( + sum(1 << i for i in range(0, CELLS, 2)), id="alternating_cells" + ), + pytest.param(0, id="no_cells"), + ] + + +@pytest.mark.parametrize( + "cell_mask", + generate_cell_masks(), +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_blob_layouts) +@pytest.mark.exception_test +def test_get_cells( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + cell_mask: int, +) -> None: + """ + Test that `getBlobsV4` returns exactly the cells selected by the mask. + + Requested cells (and their proofs) must match the locally computed values; + non-requested cell indices must be `null` in the partial matrix. + """ + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=cell_mask, + ) + + +@pytest.mark.parametrize( + "cell_mask", + generate_cell_masks(), +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_blob_layouts) +@pytest.mark.exception_test +def test_get_cells_partial_and_missing( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + cell_mask: int, +) -> None: + """ + Test that `getBlobsV4` returns a partial response: existing blobs yield a + cell matrix while non-existing versioned hashes yield `null` entries. + """ + nonexisting_blob_hashes = [ + Hash(sha256(str(i).encode()).digest()) for i in range(5) + ] + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=cell_mask, + nonexisting_blob_hashes=nonexisting_blob_hashes, + ) + + +@pytest.mark.parametrize( + "cell_mask", + generate_cell_masks(), +) +@pytest.mark.parametrize("txs_blobs", [[]], ids=["no_blobs"]) +@pytest.mark.exception_test +def test_get_cells_only_nonexisting( + blobs_test: BlobsTestFiller, + pre: Alloc, + cell_mask: int, +) -> None: + """ + Test that `getBlobsV4` returns an array of `null` entries (one per + requested hash) when all requested blobs are non-existing. + """ + nonexisting_blob_hashes = [ + Hash(sha256(str(i).encode()).digest()) for i in range(5) + ] + blobs_test( + pre=pre, + txs=[], + get_blobs_version=4, + cell_mask=cell_mask, + nonexisting_blob_hashes=nonexisting_blob_hashes, + ) + + +@pytest.mark.parametrize( + "cell_mask", + [pytest.param(0xFF, id="custody_aligned_8")], +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_single_blob_layout) +@pytest.mark.exception_test +def test_get_cells_min_request_size( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + cell_mask: int, +) -> None: + """ + Test a request of 128 versioned hashes, the minimum request size a + client must support for `getBlobsV4`. + + The response must hold one entry per requested hash: a cell matrix for + the existing blob and `null` for each non-existing hash. + """ + nonexisting_blob_hashes = [ + Hash(sha256(str(i).encode()).digest()) + for i in range(Spec.MIN_SUPPORTED_REQUEST_SIZE - 1) + ] + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=cell_mask, + nonexisting_blob_hashes=nonexisting_blob_hashes, + ) + + +@pytest.mark.parametrize( + "cell_mask", + [ + pytest.param(ALL_CELLS_MASK, id="all_cells"), + pytest.param(0xFF, id="custody_aligned_8"), + ], +) +@pytest.mark.parametrize_by_fork("txs_blobs", generate_single_blob_txs_layout) +@pytest.mark.exception_test +def test_get_cells_interleaved_missing( + blobs_test: BlobsTestFiller, + pre: Alloc, + txs: List[NetworkWrappedTransaction | Transaction], + cell_mask: int, +) -> None: + """ + Test that `null` entries appear at the exact request positions when + non-existing hashes are interleaved with existing ones (leading, + middle, and trailing positions of the request). + """ + nonexisting_blob_hashes = [ + Hash(sha256(str(i).encode()).digest()) for i in range(5) + ] + blobs_test( + pre=pre, + txs=txs, + get_blobs_version=4, + cell_mask=cell_mask, + nonexisting_blob_hashes=nonexisting_blob_hashes, + interleave_nonexisting_blob_hashes=True, + ) From af137475d7b15842438cfcb30c01d8903bf57b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Thu, 30 Jul 2026 05:59:18 +0800 Subject: [PATCH 31/55] feat(test-benchmark): on-chain account verification (#3197) * feat: implement deployed account verification * refactor: add account verification to tests * refactor: update alloc to include the pre method * refactor: verify account flag name * refactor: clean up docstring * refactor: chunk-wise early raise in deployed accounts verification * refactor: move account verification to a helper, session-scope dedup state * refactor(test-execute): verify_full_accounts internal of pre --------- Co-authored-by: marioevz --- .../plugins/execute/pre_alloc.py | 168 ++++++++++++++++++ .../plugins/fill_stateful/fill_stateful.py | 11 ++ .../src/execution_testing/specs/blockchain.py | 4 + .../test_types/account_types.py | 25 +++ tests/benchmark/conftest.py | 7 + tests/benchmark/helper/account_creator.py | 91 +++++++++- .../helper/account_sender_receiver.py | 59 ++++++ .../benchmark/helper/account_verification.py | 96 ++++++++++ .../stateful/bloatnet/test_account_query.py | 12 ++ .../bloatnet/test_transaction_types.py | 73 +++++++- 10 files changed, 533 insertions(+), 13 deletions(-) create mode 100644 tests/benchmark/helper/account_verification.py diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py index fccb7ca45cd..4b80161a9b4 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py @@ -1,5 +1,6 @@ """Pre-allocation fixtures used for test filling.""" +from collections.abc import Sequence from dataclasses import dataclass from itertools import count from pathlib import Path @@ -224,6 +225,67 @@ class _DeferredFundAddress: minimum_balance: bool +@dataclass +class _DeferredAccountAssertion: + """ + Deferred assertion on a predeployed account. + + Verified at start_block before the benchmark runs. + Uses primitives only to stay independent of test expectations. + """ + + address: Address + is_existing_account: bool + is_contract: bool + min_balance: int | None + code_prefix: bytes | None + label: str | None + + +class DeployedAccountVerificationError(AssertionError): + """Raised when predeployed benchmark targets fail verification.""" + + +def _check_account_assertion( + d: _DeferredAccountAssertion, + account: Account | None, + code: Bytes | None, +) -> list[str]: + """Return human-readable failures for one account assertion (may be [].""" + who = f"{d.label or ''} at {d.address}" + if account is None: + return [f"{who}: no account data returned from the client"] + balance = int(account.balance) + nonce = int(account.nonce) + errors: list[str] = [] + if not d.is_existing_account: + if balance != 0 or nonce != 0: + errors.append( + f"{who}: expected NON-existent, got balance={balance} " + f"nonce={nonce}" + ) + return errors + if d.is_contract and nonce < 1: + errors.append( + f"{who}: expected a deployed contract (nonce>=1) but got " + f"nonce={nonce}, balance={balance} — likely NOT deployed on the " + "snapshot; the benchmark would silently hit an empty account" + ) + if d.min_balance is not None and balance < d.min_balance: + errors.append( + f"{who}: expected balance>={d.min_balance} but got {balance}" + ) + if d.code_prefix is not None: + actual = bytes(code) if code is not None else b"" + if not actual.startswith(d.code_prefix): + errors.append( + f"{who}: expected code to start with " + f"0x{d.code_prefix.hex()} (e.g. a delegated account) but " + f"got 0x{actual.hex()}" + ) + return errors + + def _compute_deploy_gas_limit( fork: Fork, *, @@ -320,8 +382,12 @@ class Alloc(SharedAlloc): _deferred_fund_addresses: List[_DeferredFundAddress] = PrivateAttr( default_factory=list ) + _deferred_account_assertions: List[_DeferredAccountAssertion] = ( + PrivateAttr(default_factory=list) + ) _block_number: int = PrivateAttr() _timestamp: int = PrivateAttr() + _verify_full: bool = PrivateAttr(default=False) def __init__( self, @@ -335,6 +401,7 @@ def __init__( block_number: int = 0, timestamp: int = 0, funding_gas_limit: int = 200_000, + verify_full: bool = False, **kwargs: Any, ) -> None: """Initialize the pre-alloc with the given parameters.""" @@ -348,6 +415,7 @@ def __init__( self._block_number = block_number self._timestamp = timestamp self._funding_gas_limit = funding_gas_limit + self._verify_full = verify_full def code_pre_processor(self, code: Bytecode) -> Bytecode: """Pre-processes the code before setting it.""" @@ -826,6 +894,103 @@ def _nonexistent_account(self) -> Address: logger.debug(f"Returning unused address {eoa} (nonexistent account)") return Address(eoa) + def expect_account_state( + self, + addresses: Address | Sequence[Address], + *, + is_existing_account: bool = True, + is_contract: bool = False, + min_balance: int | None = None, + code_prefix: bytes | None = None, + ) -> None: + """ + Register deferred assertion(s) on predeployed account(s). + + Verified at start_block (fill-stateful only). For a range, only the + first and last are checked unless ``--verify-full-accounts`` is set; + each assertion's label is taken from the address itself. + """ + if isinstance(addresses, Address): + targets: Sequence[Address] = (addresses,) + elif self._verify_full or len(addresses) <= 2: + targets = addresses + else: + targets = (addresses[0], addresses[-1]) + for address in targets: + self._deferred_account_assertions.append( + _DeferredAccountAssertion( + address=address, + is_existing_account=is_existing_account, + is_contract=is_contract, + min_balance=min_balance, + code_prefix=code_prefix, + label=address.label, + ) + ) + + def verify_deployed_accounts(self, block_number: int) -> None: + """ + Verify registered predeployed-account assertions at block_number. + + Batches eth_getBalance and eth_getTransactionCount queries. + Fetches code only for assertions with code_prefix (e.g., EIP-7702 + designation). Collects all failures before raising. + """ + deferred = self._deferred_account_assertions + self._deferred_account_assertions = [] + if not deferred: + return + + chunk, max_reported, verified = 2000, 20, 0 + for i in range(0, len(deferred), chunk): + batch = deferred[i : i + chunk] + + query = BaseAlloc(root={d.address: Account() for d in batch}) + accounts = self._eth_rpc.get_alloc( + query, block_number=block_number, skip_code=True + ).root + + code_targets = [ + d.address for d in batch if d.code_prefix is not None + ] + codes: dict[Address, Bytes] = dict( + zip( + code_targets, + self._eth_rpc.get_codes( + code_targets, block_number=block_number + ), + strict=True, + ) + ) + + errors: list[str] = [] + failed = 0 + for d in batch: + errs = _check_account_assertion( + d, accounts.get(d.address), codes.get(d.address) + ) + if errs: + failed += 1 + errors.extend(errs) + + if errors: + shown = errors[:max_reported] + omitted = len(errors) - len(shown) + suffix = f"\n ... and {omitted} more" if omitted else "" + raise DeployedAccountVerificationError( + f"{failed} predeployed benchmark target(s) failed " + f"verification at start_block (after checking " + f"{verified + len(batch)}):\n " + + "\n ".join(shown) + + suffix + ) + verified += len(batch) + + logger.info( + f"Verified {verified} predeployed benchmark target(s) at " + f"block {block_number}" + ) + def resolve_deferred_checks(self) -> None: """ Resolve all deferred on-chain checks using batched RPC calls. @@ -1156,6 +1321,9 @@ def pre( node_id=request.node.nodeid, address_stubs=address_stubs, funding_gas_limit=sender_fund_refund_gas_limit, + verify_full=getattr( + request.config.option, "verify_full_accounts", False + ), ) # Yield the pre-alloc for usage during the test diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index c98280b986f..e98f15a6573 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -147,6 +147,17 @@ def pytest_addoption(parser: pytest.Parser) -> None: "opt-in." ), ) + group.addoption( + "--verify-full-accounts", + action="store_true", + dest="verify_full_accounts", + default=False, + help=( + "Verify all predeployed targets instead of sampling. " + "By default, only first and last accounts per range are checked. " + "This flag checks every account at start_block.)" + ), + ) def _resolve_session_fork( diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index ec3bd0d88e2..a96b02df608 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -1468,6 +1468,10 @@ def make_stateful_fixture( max_fee_per_blob_gas=max_fee_per_blob_gas, ) + self.pre.verify_deployed_accounts( + int(HexNumber(start_block["number"])) + ) + # Materialise queued pre-alloc txs into a synthetic setup block. blocks_to_process: List[Block] = [] if callable(pending_getter): diff --git a/packages/testing/src/execution_testing/test_types/account_types.py b/packages/testing/src/execution_testing/test_types/account_types.py index e5e03717f28..e9f498b6fe3 100644 --- a/packages/testing/src/execution_testing/test_types/account_types.py +++ b/packages/testing/src/execution_testing/test_types/account_types.py @@ -1,6 +1,7 @@ """Account-related types for Ethereum tests.""" import json +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum, auto from typing import ( @@ -671,3 +672,27 @@ def nonexistent_account(self) -> Address: raise NotImplementedError( "nonexistent_account is not implemented in the base class" ) + + def expect_account_state( + self, + addresses: Address | Sequence[Address], + *, + is_existing_account: bool = True, + is_contract: bool = False, + min_balance: int | None = None, + code_prefix: bytes | None = None, + ) -> None: + """ + Register start-block expectation(s) for predeployed account(s). + + Accepts a single address or a range; labels ride on the addresses + themselves. Used only by fill-stateful; ignored by other + allocations. + """ + + def verify_deployed_accounts(self, block_number: int) -> None: + """ + Verify predeployed-account expectations at block_number. + + No-op unless fill-stateful allocation. + """ diff --git a/tests/benchmark/conftest.py b/tests/benchmark/conftest.py index 5a4643e2e38..616b4fdc692 100755 --- a/tests/benchmark/conftest.py +++ b/tests/benchmark/conftest.py @@ -1,5 +1,6 @@ """Pytest configuration for benchmark tests.""" +from collections.abc import Hashable from pathlib import Path from typing import Any @@ -82,3 +83,9 @@ def pytest_ignore_collect(collection_path: Path, config: Any) -> bool | None: def tx_gas_limit(fork: Fork, gas_benchmark_value: int) -> int: """Return the transaction gas limit cap.""" return fork.transaction_gas_limit_cap() or gas_benchmark_value + + +@pytest.fixture(scope="session") +def verified_accounts() -> dict[Hashable, int]: + """Session high-water-mark per target family, so each is verified once.""" + return {} diff --git a/tests/benchmark/helper/account_creator.py b/tests/benchmark/helper/account_creator.py index 2bb72d09cc6..464d04939f1 100644 --- a/tests/benchmark/helper/account_creator.py +++ b/tests/benchmark/helper/account_creator.py @@ -1,23 +1,40 @@ """Benchmark target accounts of various kinds for creation and location..""" from abc import ABC, abstractmethod +from collections.abc import Callable, Hashable from dataclasses import dataclass from enum import Enum, auto from typing import ClassVar, Self from execution_testing import ( DETERMINISTIC_FACTORY_ADDRESS, + Address, + Alloc, Bytecode, Create2PreimageLayout, Hash, Op, SequentialAddressLayout, + compute_create2_address, keccak256, ) from execution_testing.forks import Osaka +from tests.benchmark.helper.account_verification import ( + AccountExpectation, + register_target_range, +) + DEFAULT_CODE_SIZE = Osaka.max_code_size() +ADDRESS_MASK = (1 << 160) - 1 + +# Spamoor EOA creator starts created accounts at 0x1000 +# (https://github.com/CPerezz/spamoor/pull/12). +EXISTING_EOA_BASE = 0x1000 +# An address range that is never funded. +NON_EXISTING_BASE = keccak256(b"random") + class AccountMode(Enum): """Benchmark target account variant.""" @@ -345,12 +362,76 @@ def address_source(self, index_op: Bytecode) -> AddressSource: ) match self.mode: case AccountMode.EXISTING_EOA: - # Spamoor EOA creator starts created accounts at 0x1000. - # https://github.com/CPerezz/spamoor/pull/12 - base_addr = Hash(0x1000) + base_addr = Hash(EXISTING_EOA_BASE) case AccountMode.NON_EXISTING_ACCOUNT: - # An address range that is never funded. - base_addr = keccak256(b"random") + base_addr = NON_EXISTING_BASE case _: raise ValueError(f"{self.mode.name} has no address source") return SequentialAddressSource(base_addr=base_addr, index_op=index_op) + + def expected_account(self) -> AccountExpectation: + """Return the expected on-chain shape for this mode at start_block.""" + if self.derives_address_via_create2: + # CREATE2 address binds code; check presence only. + return AccountExpectation(is_contract=True) + match self.mode: + case AccountMode.EXISTING_EOA: + return AccountExpectation(min_balance=1) + case AccountMode.NON_EXISTING_ACCOUNT: + return AccountExpectation(is_existing_account=False) + case _: + raise ValueError(f"{self.mode.name} has no expected account") + + def target_address_of( + self, label: str | None = None + ) -> Callable[[int], Address]: + """ + Return an ``index -> target Address`` map mirroring address_source. + + CREATE2 initcode is assembled once (salt varies); ``label`` is + attached to every derived address. + """ + if self.derives_address_via_create2: + initcode = self.initcode + + def create2_address(index: int) -> Address: + return Address( + compute_create2_address( + address=DETERMINISTIC_FACTORY_ADDRESS, + salt=index, + initcode=initcode, + ), + label=label, + ) + + return create2_address + match self.mode: + case AccountMode.EXISTING_EOA: + base = EXISTING_EOA_BASE + case AccountMode.NON_EXISTING_ACCOUNT: + base = int.from_bytes(NON_EXISTING_BASE, "big") + case _: + raise ValueError(f"{self.mode.name} has no address source") + + def sequential_address(index: int) -> Address: + return Address((base + index) & ADDRESS_MASK, label=label) + + return sequential_address + + def register_targets( + self, + pre: Alloc, + count: int, + *, + verified_accounts: dict[Hashable, int], + label: str | None = None, + ) -> None: + """Register ``[0, count)`` of this mode's targets for verification.""" + register_target_range( + pre, + key=(self.mode, self.code_size), + count=count, + expectation=self.expected_account(), + address_of=self.target_address_of(label or self.mode.name), + verified_accounts=verified_accounts, + ) diff --git a/tests/benchmark/helper/account_sender_receiver.py b/tests/benchmark/helper/account_sender_receiver.py index 7a860607935..de75af8de49 100644 --- a/tests/benchmark/helper/account_sender_receiver.py +++ b/tests/benchmark/helper/account_sender_receiver.py @@ -1,17 +1,25 @@ """Deterministic benchmark sender and receiver accounts.""" import itertools +from collections.abc import Hashable from typing import Generator from execution_testing import ( DETERMINISTIC_FACTORY_ADDRESS, EOA, Address, + Alloc, compute_create2_address, compute_create_address, keccak256, ) +from tests.benchmark.helper.account_verification import ( + AccountExpectation, + register_target_range, +) +from tests.prague.eip7702_set_code_tx.spec import Spec + # Deterministic sender pool, pre-funded via system-contract withdrawals # (funding.txt) during payload generation. Kept out of the pre-allocation so # the accounts stay uncached. @@ -78,3 +86,54 @@ def yield_distinct_delegate_receiver() -> Generator[Address, None, None]: """Yield EOA delegating to a distinct EXISTING_CONTRACT_DIFF_MAX.""" for i in itertools.count(0): yield EOA(key=DELEGATE_BASE_KEY + i) + + +def expected_delegation() -> AccountExpectation: + """ + Expected shape of a 7702-delegated authority. + + Only asserts the account carries a delegation designator; the delegate + target it points to is not checked. + """ + return AccountExpectation(code_prefix=bytes(Spec.DELEGATION_DESIGNATION)) + + +def register_bittrex_targets( + pre: Alloc, + count: int, + *, + verified_accounts: dict[Hashable, int], +) -> None: + """Register the first *count* Bittrex CREATE contract receivers.""" + register_target_range( + pre, + key="bittrex_contract", + count=count, + expectation=AccountExpectation(is_contract=True), + address_of=lambda index: Address( + compute_create_address( + address=BITTREX_CONTROLLER_ADDRESS, nonce=2 + index + ), + label="diff_to_contract", + ), + verified_accounts=verified_accounts, + ) + + +def register_delegate_targets( + pre: Alloc, + count: int, + *, + verified_accounts: dict[Hashable, int], +) -> None: + """Register the first *count* delegated authorities (7702 designator).""" + register_target_range( + pre, + key="delegate_authority", + count=count, + expectation=expected_delegation(), + address_of=lambda index: Address( + EOA(key=DELEGATE_BASE_KEY + index), label="delegate_authority" + ), + verified_accounts=verified_accounts, + ) diff --git a/tests/benchmark/helper/account_verification.py b/tests/benchmark/helper/account_verification.py new file mode 100644 index 00000000000..0666b12b99a --- /dev/null +++ b/tests/benchmark/helper/account_verification.py @@ -0,0 +1,96 @@ +"""Verification of snapshot-predeployed benchmark target accounts.""" + +from collections.abc import Callable, Hashable, Sequence +from dataclasses import dataclass +from typing import overload + +from execution_testing import Address, Alloc + + +class AddressRange(Sequence[Address]): + """ + Lazily-indexed range of target addresses. + + Backed by an index -> Address map, so sampling the endpoints costs + O(1): checking only the first and last of a huge range never derives + the addresses in between. + """ + + def __init__( + self, start: int, stop: int, address_of: Callable[[int], Address] + ) -> None: + """Cover indices ``[start, stop)`` via ``address_of``.""" + self._start = start + self._stop = stop + self._address_of = address_of + + def __len__(self) -> int: + """Return the number of addresses in the range.""" + return self._stop - self._start + + @overload + def __getitem__(self, index: int) -> Address: ... + + @overload + def __getitem__(self, index: slice) -> Sequence[Address]: ... + + def __getitem__(self, index: int | slice) -> Address | Sequence[Address]: + """Derive the address at ``index`` (negatives and slices allowed).""" + if isinstance(index, slice): + return [self[i] for i in range(*index.indices(len(self)))] + if index < 0: + index += len(self) + if not 0 <= index < len(self): + raise IndexError(index) + return self._address_of(self._start + index) + + +@dataclass(frozen=True) +class AccountExpectation: + """ + Expected on-chain shape of a snapshot-predeployed target. + + Verified at `start_block`. Defaults skipped. + `is_contract`: nonce >= 1. CREATE2: address binds code. + `code_prefix`: on-chain code must start with given bytes. + """ + + is_existing_account: bool = True + is_contract: bool = False + min_balance: int | None = None + code_prefix: bytes | None = None + + def register( + self, pre: Alloc, addresses: Address | Sequence[Address] + ) -> None: + """Register this expectation for one address or a range.""" + pre.expect_account_state( + addresses, + is_existing_account=self.is_existing_account, + is_contract=self.is_contract, + min_balance=self.min_balance, + code_prefix=self.code_prefix, + ) + + +def register_target_range( + pre: Alloc, + *, + key: Hashable, + count: int, + expectation: AccountExpectation, + address_of: Callable[[int], Address], + verified_accounts: dict[Hashable, int], +) -> None: + """ + Register targets ``[0, count)`` for verification, deduped per family. + + Only the newly-seen tail ``[high-water, count)`` is handed to the + allocation; whether it samples the endpoints or checks every account + is decided there, from ``--verify-full-accounts``. + """ + start = verified_accounts.get(key, 0) + if count <= start: + return + expectation.register(pre, AddressRange(start, count, address_of)) + verified_accounts[key] = count diff --git a/tests/benchmark/stateful/bloatnet/test_account_query.py b/tests/benchmark/stateful/bloatnet/test_account_query.py index fd44cabce5d..bec412631d8 100644 --- a/tests/benchmark/stateful/bloatnet/test_account_query.py +++ b/tests/benchmark/stateful/bloatnet/test_account_query.py @@ -162,6 +162,7 @@ def test_account_access( account_mode: AccountMode, overhead_baseline: bool, cache_strategy: CacheStrategy, + verified_accounts: dict, ) -> None: """Benchmark account access with caching strategies.""" account_creator = AccountCreator(account_mode) @@ -313,6 +314,17 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: ) ) + if not overhead_baseline and attack_txs: + count = 1 + max( + int.from_bytes(bytes(tx.data)[32:64], "big") for tx in attack_txs + ) + account_creator.register_targets( + pre, + count, + verified_accounts=verified_accounts, + label=account_mode.name, + ) + if cache_strategy == CacheStrategy.CACHE_PREVIOUS_BLOCK: with TestPhaseManager.setup(): cache_sender = pre.fund_eoa() diff --git a/tests/benchmark/stateful/bloatnet/test_transaction_types.py b/tests/benchmark/stateful/bloatnet/test_transaction_types.py index 8094b0869a7..77f08358e04 100644 --- a/tests/benchmark/stateful/bloatnet/test_transaction_types.py +++ b/tests/benchmark/stateful/bloatnet/test_transaction_types.py @@ -1,6 +1,7 @@ """Benchmark ether transfers to receivers that exist on-chain.""" -from typing import Generator +from functools import partial +from typing import Callable, Generator import pytest from execution_testing import ( @@ -19,6 +20,8 @@ AccountMode, ) from tests.benchmark.helper.account_sender_receiver import ( + register_bittrex_targets, + register_delegate_targets, yield_distinct_contract_receiver, yield_distinct_create2_receiver, yield_distinct_delegate_receiver, @@ -51,12 +54,15 @@ def test_ether_transfers_onchain_receivers( transfer_amount: int, fork: Fork, gas_benchmark_value: int, + verified_accounts: dict, ) -> None: """Benchmark ether transfers across different receiver account types.""" senders = yield_distinct_sender() receiver_execution_gas = 0 recipient_type = RecipientType.CONTRACT receivers: Generator[Address, None, None] + + register_targets: Callable[[int], None] | None = None match case_id: case "diff_to_self": receivers = senders @@ -64,9 +70,23 @@ def test_ether_transfers_onchain_receivers( case "diff_to_nonexistent": receivers = yield_distinct_nonexistent_receiver() recipient_type = RecipientType.EMPTY_ACCOUNT + creator = AccountCreator(AccountMode.NON_EXISTING_ACCOUNT) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, + ) case "diff_to_existent": receivers = yield_distinct_existent_receiver() recipient_type = RecipientType.EOA + creator = AccountCreator(AccountMode.EXISTING_EOA) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, + ) case "diff_to_contract": receivers = yield_distinct_contract_receiver() # Runtime code is the same across all the receivers @@ -79,25 +99,58 @@ def test_ether_transfers_onchain_receivers( + Op.JUMPDEST ) receiver_execution_gas = executed_code.gas_cost(fork) + # Bittrex CREATE contracts: address does not bind code, so only + # presence (nonce>=1) is checked. + register_targets = partial( + register_bittrex_targets, + pre, + verified_accounts=verified_accounts, + ) case "diff_to_unique_code_jumpdest_contract": creator = AccountCreator(AccountMode.EXISTING_CONTRACT_JUMPDEST) receivers = yield_distinct_create2_receiver(creator.initcode) receiver_execution_gas = creator.execution_code.gas_cost(fork) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, + ) case "diff_to_contract_minimal": - receivers = yield_distinct_create2_receiver( - AccountCreator(AccountMode.EXISTING_CONTRACT_MINIMAL).initcode + creator = AccountCreator(AccountMode.EXISTING_CONTRACT_MINIMAL) + receivers = yield_distinct_create2_receiver(creator.initcode) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, ) case "diff_to_contract_same_max": - receivers = yield_distinct_create2_receiver( - AccountCreator(AccountMode.EXISTING_CONTRACT_SAME_MAX).initcode + creator = AccountCreator(AccountMode.EXISTING_CONTRACT_SAME_MAX) + receivers = yield_distinct_create2_receiver(creator.initcode) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, ) case "diff_to_contract_diff_max": - receivers = yield_distinct_create2_receiver( - AccountCreator(AccountMode.EXISTING_CONTRACT_DIFF_MAX).initcode + creator = AccountCreator(AccountMode.EXISTING_CONTRACT_DIFF_MAX) + receivers = yield_distinct_create2_receiver(creator.initcode) + register_targets = partial( + creator.register_targets, + pre, + verified_accounts=verified_accounts, + label=case_id, ) case "diff_to_delegated_contract_diff": receivers = yield_distinct_delegate_receiver() recipient_type = RecipientType.DELEGATION_7702 + register_targets = partial( + register_delegate_targets, + pre, + verified_accounts=verified_accounts, + ) case _: raise ValueError(f"Unknown case: {case_id}") @@ -122,15 +175,19 @@ def test_ether_transfers_onchain_receivers( txs = [] for _ in range(iteration_count): sender = next(senders) + to = sender if case_id == "diff_to_self" else next(receivers) txs.append( Transaction( - to=sender if case_id == "diff_to_self" else next(receivers), + to=to, value=transfer_amount, gas_limit=iteration_cost, sender=sender, ) ) + if register_targets is not None: + register_targets(iteration_count) + benchmark_test( pre=pre, post={}, From 6caece9a7553894cfb1b52b661a5ee2de24fc595 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 12:55:01 +0200 Subject: [PATCH 32/55] fix(tests): un-skip Amsterdam gas_cost SSTORE case via fork-derived cost --- tests/ported_static/amsterdam_skip_list.txt | 6 +--- .../test_gas_cost.py | 29 +++++++++++++++++-- .../test_gas_cost_berlin.py | 21 ++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 7aba6f38bb0..f6bcee6f9f5 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 153 +# Total entries: 151 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -126,10 +126,6 @@ stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0 stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] -# stEIP150singleCodeGasPrices (2) -stEIP150singleCodeGasPrices/test_gas_cost.py::test_gas_cost[fork_Amsterdam-d40] -stEIP150singleCodeGasPrices/test_gas_cost_berlin.py::test_gas_cost_berlin[fork_Amsterdam-d40] - # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py index d539c14f735..37564f6dc9b 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py @@ -1,11 +1,15 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of each opcode via a crafted one-opcode contract +(by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostFiller.yml @manually-enhanced: Do not overwrite. This crafts a one-opcode contract, CALLs it, and stores the opcode's measured gas via `Op.GAS`. +The SSTORE case (d40) derives its cost from the fork — EIP-8037 moves +the bulk into state gas — and the crafted CALL forwards effectively +all gas (0xFFFFFF, same PUSH3 width) so that case cannot OOG. EIP-8038 reprices state access, so four opcodes shift: `BALANCE` and `SELFDESTRUCT` (cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), `EXTCODESIZE` (cold account plus the extra `WARM_ACCESS` charged for @@ -733,6 +737,12 @@ def test_gas_cost( code_read_delta = cold_account_delta + ( gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 ) + # EIP-8037 moves the bulk of a zero->nonzero SSTORE into state gas; + # the explicit gas limit equals the cap (zero reservoir), so the + # crafted contract's GAS delta observes the full cost. + sstore_new_slot_cost = Op.SSTORE( + key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = EOA( key=0x40AC0FC28C27E961EE46EC43355A094DE205856EDBD4654CF2577C2608D4EC1E @@ -836,7 +846,12 @@ def test_gas_cost( + Op.MSTORE(offset=0x300, value=Op.GAS) + Op.POP( Op.CALL( - gas=0x10000, + # Effectively "all gas": EIP-8037 repriced the SSTORE case + # (d40) past the ported 0x10000 budget, OOGing the callee. + # 0xFFFFFF keeps the same PUSH3 width, so the hand-coded + # JUMP targets and every measurement stay unchanged + # (unused gas returns to the caller). + gas=0xFFFFFF, address=Op.MLOAD(offset=0x280), value=0x0, args_offset=0x0, @@ -1101,10 +1116,18 @@ def test_gas_cost( }, }, { + # SSTORE zero->nonzero to a fresh cold slot. Stored value = + # actual cost minus the 0x4E20 expected-cost operand in the + # data word minus the file-wide 0x258 baseline; 1500 before + # EIP-8037, dominated by state gas after. "indexes": {"data": [40], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - addr: Account(storage=_storage_with_any({0: 1500}, [1])) + addr: Account( + storage=_storage_with_any( + {0: sstore_new_slot_cost - 0x4E20 - 0x258}, [1] + ) + ) }, }, { diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py index 5a92e62ca28..b5f361384c2 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py @@ -1,5 +1,6 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of each opcode via a crafted one-opcode contract +(by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostBerlinFiller.yml @@ -7,6 +8,9 @@ @manually-enhanced: Do not overwrite. This crafts a one-opcode contract, CALLs it, and stores the opcode's measured gas minus the data's hardcoded Cancun-era expected cost (so the net is normally 0). +The SSTORE case (d40) derives its net from the fork — EIP-8037 moves +the bulk into state gas — and the crafted CALL forwards effectively +all gas (0xFFFFFF, same PUSH3 width) so that case cannot OOG. EIP-8038 reprices state access, so four opcodes now exceed their old expected cost by a fork-derived delta: `BALANCE` and `SELFDESTRUCT` (cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), `EXTCODESIZE` @@ -733,10 +737,18 @@ def test_gas_cost_berlin( # Each measured opcode subtracts its Cancun-era expected cost, so the # net is the (Amsterdam - Cancun) repricing of the one state access # it performs (cold address 0 / cold fresh slot), keyed by data index. + # EIP-8037 moves the bulk of a zero->nonzero SSTORE into state gas; + # the explicit gas limit equals the cap (zero reservoir), so the + # crafted contract's GAS delta observes the full cost. The data word + # encodes the 0x5654 (22100) pre-8037 cost, so the net is 0 there. + sstore_new_slot_cost = Op.SSTORE( + key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) measured_delta = { 23: cold_account_delta, # BALANCE 31: code_read_delta, # EXTCODESIZE 39: cold_storage_delta, # SLOAD + 40: sstore_new_slot_cost - 0x5654, # SSTORE zero->nonzero 45: cold_account_delta, # SELFDESTRUCT }.get(d, 0) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -839,7 +851,12 @@ def test_gas_cost_berlin( + Op.MSTORE(offset=0x300, value=Op.GAS) + Op.POP( Op.CALL( - gas=0x10000, + # Effectively "all gas": EIP-8037 repriced the SSTORE case + # (d40) past the ported 0x10000 budget, OOGing the callee. + # 0xFFFFFF keeps the same PUSH3 width, so the hand-coded + # JUMP targets and every measurement stay unchanged + # (unused gas returns to the caller). + gas=0xFFFFFF, address=Op.MLOAD(offset=0x280), value=0x0, args_offset=0x0, From 0727c4b5cca4d7cfa98b25f8b8c90596c0a40e22 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:04:39 +0200 Subject: [PATCH 33/55] fix(tests): un-skip Amsterdam stBadOpcode CREATE gas searches via fork-derived thresholds --- tests/ported_static/amsterdam_skip_list.txt | 8 +- .../stBadOpcode/test_measure_gas.py | 77 +++++++++++++------ .../stBadOpcode/test_operation_diff_gas.py | 62 +++++++++++++-- 3 files changed, 112 insertions(+), 35 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f6bcee6f9f5..cf5ff638048 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,17 +8,11 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 151 +# Total entries: 147 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] -# stBadOpcode (4) -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE] - # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] diff --git a/tests/ported_static/stBadOpcode/test_measure_gas.py b/tests/ported_static/stBadOpcode/test_measure_gas.py index f100b2b1ea8..4774049b48f 100644 --- a/tests/ported_static/stBadOpcode/test_measure_gas.py +++ b/tests/ported_static/stBadOpcode/test_measure_gas.py @@ -1,17 +1,21 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a binary +search (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/measureGasFiller.yml @manually-enhanced: Do not overwrite. A binary search measures the gas -an opcode needs to succeed. Only the EXTCODE case shifts: it runs a -warm `EXTCODESIZE` plus a warm `EXTCODECOPY` (the target is warmed by -earlier search iterations), and EIP-8038 adds a flat +100 to each warm -extcode access. The stored threshold therefore grows by the sum of the -two opcodes' warm `(Amsterdam - Cancun)` cost deltas, derived from the -fork's own gas model so it is exactly 0 before EIP-8038; do not -hardcode the Amsterdam number. +an opcode needs to succeed. The EXTCODE case runs a warm `EXTCODESIZE` +plus a warm `EXTCODECOPY` (the target is warmed by earlier search +iterations), and EIP-8038 adds a flat +100 to each warm extcode +access; its threshold grows by the two opcodes' warm cost deltas. The +CREATE/CREATE2 thresholds equal the probe bytecode's own +`gas_cost(fork)` (EIP-8037 adds new-account state gas), and the search +bound is supplied via calldata (same 3-byte width as the ported PUSH2 +60000, keeping JUMP targets and the CODESIZE trick intact) so those +cases cannot saturate. All values derive from the fork's own gas +model; do not hardcode them. """ import pytest @@ -239,7 +243,12 @@ def test_measure_gas( # sstore(0, max) # } contract_12 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH2[0xEA60] + # The search's upper bound comes from calldata (word at 0x24): + # EIP-8037's state gas pushes the CREATE/CREATE2 thresholds past + # the ported PUSH2 60000 bound, and CALLDATALOAD keeps the same + # 3-byte width so the hand-coded JUMP targets and the CODESIZE + # constant trick below are unaffected. + code=Op.CALLDATALOAD(offset=0x24) + Op.ADD(Op.CALLDATALOAD(offset=0x4), 0xC0DE00) + Op.PUSH1[0x0] + Op.JUMPDEST @@ -392,16 +401,36 @@ def test_measure_gas( - 103 ) + # The measured threshold for the CREATE/CREATE2 probes is exactly the + # probe bytecode's own cost (operand pushes + opcode); mirroring the + # deployed code in the metadata keeps the expectation fork-derived — + # EIP-8037 adds the new-account state gas and reprices the base. + create_probe_cost = Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + create2_probe_cost = Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=Op.ADD(0x5A17, Op.GAS), + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32089})}, + "result": {contract_12: Account(storage={0: create_probe_cost})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32193})}, + "result": {contract_12: Account(storage={0: create2_probe_cost})}, }, { "indexes": {"data": [2, 3], "gas": -1, "value": -1}, @@ -439,18 +468,22 @@ def test_measure_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Second calldata word: the binary search's upper bound. The bisection + # boundary is independent of the starting bound, so one generous value + # (covering EIP-8037's ~216k CREATE threshold) works on every fork. + search_max = Hash(0x100000) tx_data = [ - Bytes("693c6139") + Hash(0xF0), - Bytes("693c6139") + Hash(0xF5), - Bytes("693c6139") + Hash(0xF1), - Bytes("693c6139") + Hash(0xF2), - Bytes("693c6139") + Hash(0xF4), - Bytes("693c6139") + Hash(0xFA), - Bytes("693c6139") + Hash(0x51), - Bytes("693c6139") + Hash(0x52), - Bytes("693c6139") + Hash(0x53), - Bytes("693c6139") + Hash(0x20), - Bytes("693c6139") + Hash(0x3B), + Bytes("693c6139") + Hash(0xF0) + search_max, + Bytes("693c6139") + Hash(0xF5) + search_max, + Bytes("693c6139") + Hash(0xF1) + search_max, + Bytes("693c6139") + Hash(0xF2) + search_max, + Bytes("693c6139") + Hash(0xF4) + search_max, + Bytes("693c6139") + Hash(0xFA) + search_max, + Bytes("693c6139") + Hash(0x51) + search_max, + Bytes("693c6139") + Hash(0x52) + search_max, + Bytes("693c6139") + Hash(0x53) + search_max, + Bytes("693c6139") + Hash(0x20) + search_max, + Bytes("693c6139") + Hash(0x3B) + search_max, ] tx_gas = [16777216] diff --git a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py index be3bf8c0559..7ee9dc01598 100644 --- a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py +++ b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py @@ -1,11 +1,17 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a linear +search in 100-gas steps (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/operationDiffGasFiller.yml @manually-enhanced: Do not overwrite. A search measures the gas an -opcode needs to succeed. Two access classes shift under EIP-8038: the +opcode needs to succeed. The CREATE/CREATE2 thresholds equal the probe +bytecode's own `gas_cost(fork)` rounded up to the search step — +EIP-8037 adds new-account and storage-set state gas — and their search +start is supplied via calldata a few steps below the threshold so the +linear probe loop cannot exhaust the transaction's gas on Amsterdam. +Two access classes also shift under EIP-8038: the CALL-family probes (`CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL`) make one cold account access to the callee, repricing by `COLD_ACCOUNT_ACCESS - 2600`; the EXTCODE probe runs a cold @@ -377,6 +383,44 @@ def test_operation_diff_gas( # memory) so only the account-access component varies across forks. gas_costs = fork.gas_costs() cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + # The CREATE/CREATE2 probes wrap the create in a cold zero->nonzero + # SSTORE of the returned address (cost depends only on the + # transition, so new_value=1 stands in for the address). The stored + # threshold is the first search step (multiples of GAS_DIFF) at or + # above the probe bytecode's own fork-derived cost; EIP-8037 adds + # the new-account and storage-set state gas. The search starts a few + # steps below the expected threshold (via calldata) so the linear + # probe loop cannot exhaust the transaction's gas on Amsterdam. + gas_diff = 0x64 + create_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create2_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=0x5A17, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create_threshold = -(-create_probe_cost // gas_diff) * gas_diff + create2_threshold = -(-create2_probe_cost // gas_diff) * gas_diff extcode_probe_delta = ( Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 ) + ( @@ -393,12 +437,12 @@ def test_operation_diff_gas( { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54200})}, + "result": {contract_12: Account(storage={0: create_threshold})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54300})}, + "result": {contract_12: Account(storage={0: create2_threshold})}, }, { "indexes": {"data": [2, 3, 4, 5], "gas": -1, "value": -1}, @@ -429,8 +473,14 @@ def test_operation_diff_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) tx_data = [ - Bytes("048071d3") + Hash(0xF0) + Hash(0x0) + Hash(0x64), - Bytes("048071d3") + Hash(0xF5) + Hash(0x0) + Hash(0x64), + Bytes("048071d3") + + Hash(0xF0) + + Hash(create_threshold - 5 * gas_diff) + + Hash(gas_diff), + Bytes("048071d3") + + Hash(0xF5) + + Hash(create2_threshold - 5 * gas_diff) + + Hash(gas_diff), Bytes("048071d3") + Hash(0xF1) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF2) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF4) + Hash(0x0) + Hash(0x64), From 2ff70e16b3afb48d5fff9ce349f42809b447c263 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:09:39 +0200 Subject: [PATCH 34/55] fix(tests): consolidate & un-skip CREATE empty-contract-with-storage ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- ...test_create_empty_contract_with_storage.py | 220 +++++++++++++----- ..._contract_with_storage_and_call_it_0wei.py | 112 --------- ..._contract_with_storage_and_call_it_1wei.py | 115 --------- 4 files changed, 163 insertions(+), 291 deletions(-) delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index cf5ff638048..e4b71ac326c 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 147 +# Total entries: 144 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -67,7 +67,7 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (36) +# stCreateTest (33) stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] @@ -84,9 +84,6 @@ stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_af stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage.py::test_create_empty_contract_with_storage[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py::test_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py::test_create_empty_contract_with_storage_and_call_it_1wei[fork_Amsterdam] stCreateTest/test_create_oo_gafter_init_code_returndata_size.py::test_create_oo_gafter_init_code_returndata_size[fork_Amsterdam] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py index 9ca299282ef..1f406840b63 100644 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py +++ b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py @@ -1,18 +1,29 @@ """ -Test_create_empty_contract_with_storage. +Measure CREATE of a codeless-but-storage-writing contract, and optionally +a following CALL to it, via CodeGasMeasure. + +The init code writes the created account's own storage and calls a +storage-writer contract, then deposits no code: the result is an "empty" +(codeless) account with storage and nonce 1. Ported from: state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the init code is composed (not hex blobs) so the measured +CREATE/CALL expectations derive from the same bytecode; the init code's +inner CALL forwards all gas (the ported 0xEA60 budget OOGs under +EIP-8037); the CALL success flag stays inside the measured window. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,78 +33,169 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +CALL_FLAG_SLOT = 0x3 +CALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json"], + [ + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json", + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "call_created, call_value", + [ + pytest.param(False, 0, id="with_storage"), + pytest.param(True, 0, id="and_call_it_0wei"), + pytest.param(True, 1, id="and_call_it_1wei"), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_create_empty_contract_with_storage( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + call_created: bool, + call_value: int, ) -> None: - """Test_create_empty_contract_with_storage.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure CREATE (and optionally CALL) gas for a storage-only account.""" + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes the created account's own slot 0 and calls the + # writer, then runs off its end (STOP) so no code is deposited. The + # inner CALL forwards all remaining gas (default Op.GAS operand). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two MSTORE words" - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" + ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The created address is stored inside the measured window (as in the + # ported filler) so the optional CALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the CALL success flag is stored inside the measured window — a + # wrongly failed call would otherwise be unobservable for the 0wei arm. + call_code = Op.CALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + value=call_value, + address_warm=True, + value_transfer=call_value > 0, + account_new=False, + ) + call_store = Op.SSTORE( + key=CALL_FLAG_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=1, + ) + + code = setup + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, + ) + if call_created: + code += CodeGasMeasure( + code=call_store, + extra_stack_items=0, + sstore_key=CALL_GAS_SLOT, + ) + contract = pre.deploy_contract(code=code, balance=call_value) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + # The measured CREATE includes the child's work: the init code's own + # consumption plus the writer's store it calls. + measured_create = ( + create_store.gas_cost(fork) + + initcode.gas_cost(fork) + + writer_store.gas_cost(fork) ) + # A value-bearing CALL whose codeless callee consumes nothing measures + # gas_cost minus the stipend (forwarded then returned unused). + stipend = fork.gas_costs().CALL_STIPEND if call_value else 0 + measured_call = call_store.gas_cost(fork) - stipend + + created = compute_create_address(address=contract, nonce=1) + contract_storage: dict = { + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + } + if call_created: + contract_storage[CALL_FLAG_SLOT] = 1 + contract_storage[CALL_GAS_SLOT] = measured_call post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 100: 0x6F4F0, - }, + contract: Account(storage=contract_storage, balance=0), + # Codeless, but with storage and (for the 1wei arm) the value the + # measured CALL transferred — proving both the init code and the + # CALL executed. + created: Account( + nonce=1, + balance=call_value if call_created else 0, + storage={0: STORED_VALUE}, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), + writer: Account(storage={1: STORED_VALUE}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index d7940716427..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64763, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py deleted file mode 100644 index cbd15afeba3..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_1wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_1wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_1wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 1 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x62D37, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 12}, balance=1 - ), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) From 2e7516d8ecfa13a7a2a0a290cad5c11998fe0aa7 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:15:04 +0200 Subject: [PATCH 35/55] fix(tests): un-skip Amsterdam stStaticCall create/ask ported tests --- .claude/commands/enhance-ported-test.md | 13 + tests/ported_static/amsterdam_skip_list.txt | 7 +- ..._create_empty_contract_and_call_it_0wei.py | 224 ++++++++++++------ ..._contract_with_storage_and_call_it_0wei.py | 124 ---------- ..._that_ask_fore_gas_then_trabsaction_has.py | 204 +++++++--------- 5 files changed, 259 insertions(+), 313 deletions(-) delete mode 100644 tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index f6d3f6852e5..2e6a14f9635 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -402,6 +402,19 @@ transition, not the magnitude — which also breaks the `forward_gas`/`new_value circularity.) Set `state_gas_reservoir=0` so the state gas is captured. Validated on `test_raw_call_gas`. +**An expensive store after a callee that eats all forwarded gas — pre-write +the slot.** When a frame must SSTORE a result *after* a subcall that +deliberately consumes its whole 63/64 grant (an OOG-probe callee), the frame +retains only 1/64 — under EIP-8037 that cannot afford a cold zero→nonzero +store (~111k), and pre-8037 it often couldn't afford the cold 2.2k either +(making the ported `{slot: 0}` expectation vacuous: caller-OOG and +callee-failure were indistinguishable). Fix: write a sentinel to the slot +*before* the call (paying cold + state with the full budget), then store +`BASE + result` after it — now a dirty-warm write (100 gas) the retention +always covers, and the three outcomes (success `BASE+1`, failure `BASE`, +caller OOG `sentinel`) are all distinct. Validated on +`test_static_execute_call_that_ask_fore_gas_then_trabsaction_has`. + **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more than is available" by pinning an absolute forwarded amount — fork-fragile, diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index e4b71ac326c..a19a104c584 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 144 +# Total entries: 141 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -176,11 +176,6 @@ stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contrac stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] -# stStaticCall (3) -stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_create_empty_contract_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py::test_static_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py::test_static_execute_call_that_ask_fore_gas_then_trabsaction_has[fork_Amsterdam-d0] - # stSystemOperationsTest (5) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py index d426d271f6e..95c600b91ce 100644 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py +++ b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py @@ -1,106 +1,198 @@ """ -Test_static_create_empty_contract_and_call_it_0wei. +Measure CREATE of a codeless contract (optionally writing storage in its +init code) followed by a STATICCALL to it, via CodeGasMeasure. Ported from: state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json +state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json + +@manually-enhanced: Do not overwrite. Two fillers folded into one +parametrize; the storage-writing init code is composed (not hex blobs) so +the measured CREATE/STATICCALL expectations derive from the same bytecode; +the init code's inner CALL forwards all gas (the ported 0xEA60 budget OOGs +under EIP-8037); the STATICCALL success flag stays inside the measured +window. Replaces the prior EIP-8037 expect-any band-aid with fork-derived +gas assertions. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + CodeGasMeasure, + Fork, StateTestFiller, - Storage, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +STATICCALL_FLAG_SLOT = 0x3 +STATICCALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( [ - "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json" # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "with_storage", + [ + pytest.param(False, id="empty_contract"), + pytest.param(True, id="with_storage"), ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable def test_static_create_empty_contract_and_call_it_0wei( state_test: StateTestFiller, pre: Alloc, fork: Fork, + with_storage: bool, ) -> None: - """Test_static_create_empty_contract_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """Measure CREATE and STATICCALL gas for a created codeless account.""" + if with_storage: + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) + + # The init code writes the created account's own slot 0 and calls + # the writer, then runs off its end (STOP) so no code is deposited. + # The inner CALL forwards all remaining gas (default Op.GAS). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, + ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two words" - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The measured CREATE includes the child's work: the init code's + # own consumption plus the writer's store it calls. + child_cost = initcode.gas_cost(fork) + writer_store.gas_cost(fork) + else: + # CREATE over never-written memory runs 32 zero bytes as init code + # (STOP on the first byte), depositing no code and consuming + # nothing; the memory expansion happens inside the window. + setup = Bytecode() + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + child_cost = 0 + + # The created address is stored inside the measured window (as in the + # ported filler) so the STATICCALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the success flag is stored inside the measured window — a + # wrongly failed STATICCALL would otherwise be unobservable. + staticcall_code = Op.STATICCALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + address_warm=True, + ) + staticcall_store = Op.SSTORE( + key=STATICCALL_FLAG_SLOT, + value=staticcall_code, + key_warm=False, + original_value=0, + new_value=1, ) - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6FE6E, - } + contract = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, ) + + CodeGasMeasure( + code=staticcall_store, + extra_stack_items=0, + sstore_key=STATICCALL_GAS_SLOT, + ), + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + measured_create = create_store.gas_cost(fork) + child_cost + measured_staticcall = staticcall_store.gas_cost(fork) + + created = compute_create_address(address=contract, nonce=1) post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), + contract: Account( + storage={ + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + STATICCALL_FLAG_SLOT: 1, + STATICCALL_GAS_SLOT: measured_staticcall, + }, + ), + created: Account( + nonce=1, + storage={0: STORED_VALUE} if with_storage else {}, + ), } + if with_storage: + post[writer] = Account(storage={1: STORED_VALUE}) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index 91623e4efe9..00000000000 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -Test_static_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Storage, - Transaction, - compute_create_address, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable -def test_static_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, -) -> None: - """Test_static_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64766, - } - ) - post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py index c48d9cfe3d4..3ea3f5959b4 100644 --- a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py +++ b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py @@ -1,161 +1,131 @@ """ -Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has. +Verify a STATICCALL that asks for more gas than is available is clamped to +63/64 of the remaining gas (EIP-150), across callees that succeed, out-of-gas, +and violate the static context. Ported from: state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json + +@manually-enhanced: Do not overwrite. An outer call caps the caller frame so +the callee budgets are fork-independent; the flag slot is pre-written so the +post-call store is a cheap dirty-warm write affordable from the 1/64 +retention even under EIP-8037; distinct flag values discriminate success, +callee failure, and caller OOG (the ported {1: 0} expectation could not). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x1 +# Pre-written sentinel: if the caller frame dies after the call, the slot +# keeps this value instead of reverting to an ambiguous zero. +FLAG_PREWRITE = 0xFF +# Stored flag = 0x10 + STATICCALL result: 0x11 success, 0x10 failure. +FLAG_BASE = 0x10 + +# Far larger than any gas the caller frame can hold, so the EIP-150 clamp +# (not the operand) decides what the callee receives. +OVERSIZED_GAS_ASK = 2**61 +# The outer call pins the caller frame's budget: large enough to cover the +# caller's own cold flag store (~111k under EIP-8037) and the successful +# callee, small enough that the looping callee (~6.5M) still runs out. +CALLER_GAS = 1_000_000 + @pytest.mark.ported_from( [ "state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow +@pytest.mark.valid_from("Byzantium") @pytest.mark.parametrize( - "d, g, v", + "callee_kind, callee_succeeds", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param("mstore", True, id="d0"), + pytest.param("extcodesize_loop", False, id="d1"), + pytest.param("sstore_static_violation", False, id="d2"), ], ) -@pytest.mark.pre_alloc_mutable def test_static_execute_call_that_ask_fore_gas_then_trabsaction_has( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + callee_kind: str, + callee_succeeds: bool, ) -> None: - """Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """A STATICCALL asking for more gas than available gets 63/64 of it.""" + if callee_kind == "mstore": + # Trivial: succeeds well within the forwarded gas. + callee = pre.deploy_contract( + code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP + ) + elif callee_kind == "extcodesize_loop": + # 50000 EXTCODESIZE iterations (~6.5M gas): must exhaust the + # clamped forwarded gas, proving the callee did not receive the + # oversized ask. + callee = pre.deploy_contract( + code=Op.JUMPDEST + + Op.JUMPI( + pc=0x1C, + condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)), + ) + + Op.POP(Op.EXTCODESIZE(address=0x1)) + + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) + + Op.JUMP(pc=0x0) + + Op.JUMPDEST + + Op.STOP, + ) + else: + # SSTORE inside a static context: exceptional halt regardless of + # gas. + callee = pre.deploy_contract( + code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP + ) - # Source: lll - # { [[1]] (STATICCALL 600000 (CALLDATALOAD 0) 0 0 0 0) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x927C0, - address=Op.CALLDATALOAD(offset=0x0), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # The flag slot is written twice: the pre-write pays the cold/state + # cost with the full budget, so the post-call store is a dirty-warm + # write the 1/64 retention can always afford. + caller = pre.deploy_contract( + code=Op.SSTORE(key=FLAG_SLOT, value=FLAG_PREWRITE) + + Op.SSTORE( + key=FLAG_SLOT, + value=Op.ADD( + FLAG_BASE, + Op.STATICCALL(gas=OVERSIZED_GAS_ASK, address=callee), ), ) + Op.STOP, - nonce=0, - address=Address(0xA256EBCC5536CDA56E04C39FE9584ECC7594A438), # noqa: E501 - ) - # Source: lll - # { (MSTORE 1 1) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x3DC16A13CF554533F380CC938A2C1AB04DAC534F), # noqa: E501 ) - # Source: lll - # { (def 'i 0x80) (for {} (< @i 50000) [i](+ @i 1) (EXTCODESIZE 1)) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.JUMPDEST - + Op.JUMPI( - pc=0x1C, condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)) - ) - + Op.POP(Op.EXTCODESIZE(address=0x1)) - + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) - + Op.JUMP(pc=0x0) - + Op.JUMPDEST + + # The outer call pins the caller frame's gas so the callee budgets do + # not depend on the tx gas limit; the clamp must always bite. + assert CALLER_GAS < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=caller)) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x73EF1878A0F2C9629DEDC1B1E9BE8D77DCF93688), # noqa: E501 - ) - # Source: lll - # { (SSTORE 1 1) } - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xCE4CCBFFAF450AE2126EB96DCD7C891F37764F20), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [1, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 0})}, - }, - { - "indexes": {"data": [0], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 1})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - ] - tx_gas = [100000] - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + entry: Account(storage={0: 1}), + caller: Account( + storage={FLAG_SLOT: FLAG_BASE + (1 if callee_succeeds else 0)}, + ), + } + + state_test(pre=pre, post=post, tx=tx) From 5fa82174fc8f18194ded82757c3b8b18c5948043 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:20:34 +0200 Subject: [PATCH 36/55] fix(tests): un-skip Amsterdam stSStoreTest via metadata-derived costs and warm stipend boundary --- tests/ported_static/amsterdam_skip_list.txt | 8 +- .../stSStoreTest/test_sstore_gas.py | 65 ++- .../stSStoreTest/test_sstore_gas_left.py | 463 +++--------------- 3 files changed, 125 insertions(+), 411 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index a19a104c584..b91b0416e98 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 141 +# Total entries: 137 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -165,12 +165,6 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSStoreTest (4) -stSStoreTest/test_sstore_gas.py::test_sstore_gas[fork_Amsterdam] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d2] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d5] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d8] - # stSolidityTest (3) stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contracts[fork_Amsterdam] stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] diff --git a/tests/ported_static/stSStoreTest/test_sstore_gas.py b/tests/ported_static/stSStoreTest/test_sstore_gas.py index 975d3765c56..06e840689ad 100644 --- a/tests/ported_static/stSStoreTest/test_sstore_gas.py +++ b/tests/ported_static/stSStoreTest/test_sstore_gas.py @@ -1,8 +1,16 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of every SSTORE transition class (cold/warm x +original/current/new value combinations) via inline GAS deltas (by Ori +Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stSStoreTest/sstoreGasFiller.yml + +@manually-enhanced: Do not overwrite. The nine measured transition costs +are derived from SSTORE opcode metadata instead of pinned numbers, so +EIP-8037's state-gas repricing (and any future one) is tracked +automatically; the explicit gas limit equals the EIP-7825 cap, so the +state gas spills into the measured deltas. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,8 +38,9 @@ def test_sstore_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Ori Pomerantz qbzzt1@gmail.""" + """Measure each SSTORE transition's gas against opcode metadata.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xBA1A9CE0BA1A9CE, nonce=1) @@ -171,18 +181,51 @@ def test_sstore_gas( nonce=1, ) + # The measured transitions, in bytecode order (slots 0 and 1 start at + # 24743; slots 2 and 3 start empty). Each stored delta is the pure + # SSTORE cost (the contract subtracts its own 8-gas overhead). + transitions = [ + # slot 0: cold, original nonzero -> different nonzero + dict(key_warm=False, original_value=24743, new_value=0xBEEF), + # slot 0: warm dirty, nonzero -> nonzero + dict( + key_warm=True, + original_value=24743, + current_value=0xBEEF, + new_value=0xDEADBEEF, + ), + # slot 0: warm dirty, nonzero -> zero + dict( + key_warm=True, + original_value=24743, + current_value=0xDEADBEEF, + new_value=0, + ), + # slot 0: warm dirty, zero -> zero + dict( + key_warm=True, original_value=24743, current_value=0, new_value=0 + ), + # slot 0: warm dirty, zero -> nonzero + dict( + key_warm=True, + original_value=24743, + current_value=0, + new_value=0x1234, + ), + # slot 1: cold, original nonzero -> zero + dict(key_warm=False, original_value=24743, new_value=0), + # slot 2: cold fresh, zero -> nonzero + dict(key_warm=False, original_value=0, new_value=0x60A7), + # slot 3: cold fresh, zero -> zero + dict(key_warm=False, original_value=0, new_value=0), + # slot 3: warm fresh, zero -> nonzero + dict(key_warm=True, original_value=0, new_value=0x60A7), + ] post = { target: Account( storage={ - 4096: 5000, - 4097: 100, - 4098: 100, - 4099: 100, - 4100: 100, - 4101: 5000, - 4102: 22100, - 4103: 2200, - 4104: 20000, + 0x1000 + i: Op.SSTORE.with_metadata(**md).gas_cost(fork) + for i, md in enumerate(transitions) }, ), } diff --git a/tests/ported_static/stSStoreTest/test_sstore_gas_left.py b/tests/ported_static/stSStoreTest/test_sstore_gas_left.py index 4c4bf0c54c0..34fefa79a0c 100644 --- a/tests/ported_static/stSStoreTest/test_sstore_gas_left.py +++ b/tests/ported_static/stSStoreTest/test_sstore_gas_left.py @@ -1,439 +1,116 @@ """ -Checks EIP-1706/EIP-2200 out of gas requirement for non-mutating SSTOREs. +Verify the EIP-2200 (EIP-1706) minimum-gas rule for SSTORE: a non-mutating +store fails unless the gas left exceeds the call stipend, across CALL, +CALLCODE and DELEGATECALL entry into the storing frame. Ported from: state_tests/stSStoreTest/sstore_gasLeftFiller.json -@manually-enhanced: Do not overwrite. Gas budget refactored to be -fork-aware (`tx_gas = [intrinsic + tx_data[d].gas_cost(fork)]`), and -each `Op.CALL` annotated with `inner_call_cost=` metadata so -`Bytecode.gas_cost(fork)` covers the forwarded inner-frame gas. -Required for the test to fill correctly under EIP-8037's two- -dimensional gas model. Hex `gas=` literals also converted to -human-readable decimals. +@manually-enhanced: Do not overwrite. The stored-to slot is warmed before +the boundary call so the stipend check (not the cold-access charge, which +EIP-8037/8038 reprice) is the binding constraint on every fork; the +boundary gas is derived as stipend + push cost +/- 1; the success +indicator forwards gas via `flag * INDICATOR_GAS` instead of the ported +hardcoded-pc JUMPI; the tx gas is maxed so the indicator's storage write +is not budget-bound. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas forwarded to the success indicator; only needs to cover its regular +# costs (state gas rides on the transaction's implicit reservoir). +INDICATOR_GAS = 30_000 + @pytest.mark.ported_from( ["state_tests/stSStoreTest/sstore_gasLeftFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Istanbul") +@pytest.mark.parametrize( + "opcode", + [ + pytest.param(Op.CALL, id="call"), + pytest.param(Op.CALLCODE, id="callcode"), + pytest.param(Op.DELEGATECALL, id="delegatecall"), + ], +) @pytest.mark.parametrize( - "d, g, v", + "gas_offset, store_succeeds", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), - pytest.param( - 4, - 0, - 0, - id="d4", - ), - pytest.param( - 5, - 0, - 0, - id="d5", - ), - pytest.param( - 6, - 0, - 0, - id="d6", - ), - pytest.param( - 7, - 0, - 0, - id="d7", - ), - pytest.param( - 8, - 0, - 0, - id="d8", - ), + pytest.param(-1, False, id="below_boundary"), + pytest.param(0, False, id="at_boundary"), + pytest.param(1, True, id="above_boundary"), ], ) -@pytest.mark.pre_alloc_mutable def test_sstore_gas_left( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + opcode: Op, + gas_offset: int, + store_succeeds: bool, ) -> None: - """Checks EIP-1706/EIP-2200 out of gas requirement for non-mutating...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """A non-mutating SSTORE needs gas left above the call stipend.""" + gas_costs = fork.gas_costs() - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[1]] 1 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - storage={1: 1}, - nonce=0, - address=Address(0xB0409D84AB61455CB8BEC14B94F635146AB55613), # noqa: E501 - ) - # Source: lll - # { [[1]] 1 } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x4092B3905CFEA2485EA53222F41EB26E67587802), # noqa: E501 - ) + # The storing contract: a no-op SSTORE (slot 1 already holds 1 for the + # CALL arm; the CALLCODE/DELEGATECALL arms pre-set the caller's own + # slot 1). At the SSTORE, gas left = forwarded - two pushes; EIP-2200 + # requires it to exceed the stipend. + store_code = Op.SSTORE(key=0x1, value=0x1) + storer = pre.deploy_contract(code=store_code + Op.STOP, storage={1: 1}) + push_cost = 2 * gas_costs.VERY_LOW + boundary_gas = gas_costs.CALL_STIPEND + push_cost + gas_offset - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1, 3, 4, 6, 7], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {addr_2: Account(storage={1: 0})}, - }, - { - "indexes": {"data": [8, 2, 5], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {addr_2: Account(storage={1: 1})}, - }, - ] + # Written by the success indicator call. + indicator = pre.deploy_contract(code=Op.SSTORE(key=0x1, value=0x1)) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + if opcode == Op.CALL: + # Warm the storer's slot (and pre-write it back to 1) with an + # unbounded call, so the boundary call's SSTORE is a warm no-op + # and only the stipend check can fail it. + prelude = Op.POP(Op.CALL(address=storer)) + boundary_call = opcode(gas=boundary_gas, address=storer) + else: + # CALLCODE/DELEGATECALL store into the caller's own slot 1: the + # pre-write makes the boundary store a warm no-op. + prelude = Op.SSTORE(key=0x1, value=0x1) + if opcode == Op.CALLCODE: + boundary_call = opcode(gas=boundary_gas, address=storer, value=0) + else: + boundary_call = opcode(gas=boundary_gas, address=storer) - tx_data = [ - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2305, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2305, - ) - ), - ) + # The indicator receives gas only if the boundary call succeeded + # (flag * INDICATOR_GAS), so no jump destinations are needed. + caller = pre.deploy_contract( + code=prelude + Op.POP( Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, + gas=Op.MUL(INDICATOR_GAS, boundary_call), + address=indicator, ) ) - + Op.JUMPDEST + Op.STOP, - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2306, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2306, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2307, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2307, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2305, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2305, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2306, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2306, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2307, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2307, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2305, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2306, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2307, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - ] - # Fork-aware gas budget: contract-creation intrinsic from the - # fork's calculator, plus the bytecode's own gas cost (which - # already includes the gas forwarded to inner CALLs via opcode - # metadata). Any future fork-cost change is automatically - # respected. - intrinsic = fork.transaction_intrinsic_cost_calculator()( - calldata=tx_data[d], - contract_creation=True, ) - tx_gas = [intrinsic + tx_data[d].gas_cost(fork)] - tx_value = [1] tx = Transaction( - sender=sender, - to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + sender=pre.fund_eoa(), + to=caller, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + indicator: Account(storage={1: 1 if store_succeeds else 0}), + } + + state_test(pre=pre, post=post, tx=tx) From 63e085442bd7852095742216049d1c6a6264c6b2 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:13:41 +0200 Subject: [PATCH 37/55] fix(tests): un-skip Amsterdam 63/64 ask-family ported tests via observed-gas returns --- .claude/commands/enhance-ported-test.md | 17 ++ tests/ported_static/amsterdam_skip_list.txt | 15 +- ..._ask_more_gas_then_transaction_provided.py | 192 ++++++++---------- ...more_gas_on_depth2_then_transaction_has.py | 134 ++++++------ .../test_transaction64_rule.py | 109 ++++++++++ .../test_transaction64_rule_d64e0.py | 84 -------- .../test_transaction64_rule_d64m1.py | 84 -------- .../test_transaction64_rule_d64p1.py | 84 -------- ...ransaction_has_with_mem_expanding_calls.py | 156 ++++++++------ 9 files changed, 381 insertions(+), 494 deletions(-) create mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 2e6a14f9635..37b4980601b 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -414,6 +414,23 @@ callee-failure were indistinguishable). Fix: write a sentinel to the slot always covers, and the three outcomes (success `BASE+1`, failure `BASE`, caller OOG `sentinel`) are all distinct. Validated on `test_static_execute_call_that_ask_fore_gas_then_trabsaction_has`. +**Caveat — EIP-2200's stipend rule caps this trick.** Any SSTORE (even a +100-gas dirty-warm one) exceptionally halts unless `gas_left > 2300` +(Istanbul+), so the 1/64 retention must exceed ~2400, i.e. the pre-call +budget must exceed ~154k. When the scenario *requires* a smaller budget +(e.g. a starved arm whose forwarded gas must undercut the callee's cost), +no post-call SSTORE is possible at all: write the sentinel *before* the +call and put nothing but a `POP` after it — frame completion (the account +persists with the sentinel) plus the callee-side observable already +separate the outcomes. Validated on +`test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. + +**A creation transaction's top frame pays new-account state gas +(EIP-8037).** When deriving a create-tx budget, the intrinsic calculator +does not include the created account's state gas — add +`fork.transaction_top_frame_state_gas(contract_creation=True)` (183,600 on +Amsterdam, 0 before) or the whole creation silently OOGs only on the +future fork. **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index b91b0416e98..a3fe6dc3556 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 137 +# Total entries: 130 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -18,15 +18,13 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] -# stCallCreateCallCodeTest (11) +# stCallCreateCallCodeTest (9) stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] @@ -108,14 +106,10 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (7) -stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py::test_call_ask_more_gas_on_depth2_then_transaction_has[fork_Amsterdam] +# stEIP150Specific (3) stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] @@ -132,8 +126,7 @@ stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_p stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] -# stMemExpandingEIP150Calls (4) -stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py::test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls[fork_Amsterdam] +# stMemExpandingEIP150Calls (3) stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py index 8571a26ae45..5e3075039ea 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py @@ -1,151 +1,133 @@ """ -Test_contract_creation_make_call_that_ask_more_gas_then_transaction_prov... +Verify a CALL made inside a contract-creation transaction's init code that +asks for more gas than the transaction provided: the EIP-150 clamp decides +what the callee receives, and the transaction budget decides whether that +grant covers the callee's work. Ported from: state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json + +@manually-enhanced: Do not overwrite. The ask is explicitly oversized (the +ported 50000 was schedule-sized); both transaction budgets are derived from +the fork so the clamped grant lands above/below the callee's cost on every +fork; the init code writes a canary before the call (nothing after it needs +more than a POP — the 1/64 retention cannot afford an SSTORE, whose +EIP-2200 stipend rule would kill the creation), so a failed call and a +failed creation stay distinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF + +# Far larger than any gas the init frame can hold: the clamp always +# applies, which is the scenario the ported filler names. +OVERSIZED_GAS_ASK = 2**61 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "call_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(True, id="enough_gas"), + pytest.param(False, id="not_enough_gas"), ], ) -@pytest.mark.pre_alloc_mutable def test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided( # noqa: E501 state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_covered: bool, ) -> None: - """Test_contract_creation_make_call_that_ask_more_gas_then_transaction...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """An init-code CALL asking above the tx budget gets the 63/64 clamp.""" + # Success indicator: writes one cold fresh slot when called. + writer_store = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes a completion canary before the call (a failed + # creation persists nothing, so the canary distinguishes it from a + # failed call), makes the oversized ask, and deposits no code. Only a + # POP runs after the call: the 1/64 retention on the starved arm is + # far below the EIP-2200 stipend an SSTORE would require. + canary_store = Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, ) - - pre[sender] = Account(balance=0x10C8E0) - # Source: lll - # {(SSTORE 1 1)} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + ask_call = Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, ) - # Source: lll - # {(CALL 50000 0x1000000000000000000000000000000000000001 0 0 64 0 64)} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, + initcode = canary_store + Op.POP(ask_call) + Op.STOP + + # Derive the two budgets around the callee's fork-priced cost: the + # clamped grant (63/64 of the base left after the charges made before + # the forward point) lands above it on one arm and below it on the + # other. The post-call flag write runs on the 1/64 retention. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, ) - + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # EIP-8037 charges the created account's state gas to the + # creation transaction's top frame (zero before Amsterdam). + + fork.transaction_top_frame_state_gas(contract_creation=True) + + canary_store.gas_cost(fork) + + ask_call.gas_cost(fork) ) + callee_needed = writer_store.gas_cost(fork) + if call_covered: + base = -(-callee_needed * 64 // 63) + 2_000 + else: + base = callee_needed // 2 + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = overhead + base - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 1}), - }, - }, - { - "indexes": {"data": -1, "gas": [1], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 0}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ), - ] - tx_gas = [96000, 60000] - + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + post = { + created: Account( + nonce=1, + code=b"", + storage={CANARY_SLOT: CANARY}, + ), + writer: Account(storage={1: 1 if call_covered else 0}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py index 61fd1052dec..20371a9ce34 100644 --- a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py +++ b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py @@ -1,17 +1,23 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has. +Verify the EIP-150 63/64 clamp at call depth 2: a first-level call receives +its exact (affordable) ask, and its own oversized ask is clamped to 63/64 +of what remains in that frame. Ported from: state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps), and both expectations are +derived from the fork: the depth-1 frame sees exactly its asked budget, +the depth-2 frame sees `base - base // 64` of the depth-1 remainder. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +26,86 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 + @pytest.mark.ported_from( [ "state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 call asking above the frame budget gets 63/64 of it.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: lll - # { (SSTORE 8 (GAS))} - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + Op.STOP, - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds; both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x40, + old_memory_size=0x40, ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 600000 0 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 200000 0 0 0 0 0)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x30D40, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + # Top frame: forwards the exact (affordable) depth-1 budget and stores + # the success flag plus both returned observations. + entry = pre.deploy_contract( + code=Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL(gas=CALLER_GAS, address=caller, ret_size=0x40), ) - + Op.STOP, - nonce=0, + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)), ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # Depth 1 received exactly CALLER_GAS; its snapshot reads it minus the + # GAS opcode itself. The depth-2 base is what remains after the + # snapshot and the call's own costs, clamped by EIP-150. + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) post = { - addr: Account(storage={8: 0x30D3E, 9: 1}), - addr_2: Account(storage={8: 0x2A1F6}), + entry: Account( + storage={ + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py new file mode 100644 index 00000000000..5930e8a26c2 --- /dev/null +++ b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py @@ -0,0 +1,109 @@ +""" +Verify the EIP-150 "all but one 64th" rounding at the transaction level: the +gas available when a subcall asks for more than the transaction provided is +floored as `base - base // 64`, probed with the base exactly divisible by +64 and one gas below/above it. + +Ported from: +state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the callee reports its observed GAS so the exact forwarded +amount is asserted (`base - base // 64` differs from `base * 63 // 64` by +one whenever the base is not a multiple of 64 — the ported posts could not +see that difference); the tx gas limit is derived from the fork so the +divisibility residue holds on every fork. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + StateTestFiller, + Transaction, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +GAS_SLOT = 0x1 +# Far larger than any gas the frame can hold: the clamp always applies. +OVERSIZED_GAS_ASK = 2**61 + + +@pytest.mark.ported_from( + [ + "state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json", + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "residue", + [ + pytest.param(0, id="d64e0"), + pytest.param(-1, id="d64m1"), + pytest.param(1, id="d64p1"), + ], +) +def test_transaction64_rule( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + residue: int, +) -> None: + """A subcall asking above the tx budget receives `base - base // 64`.""" + # Callee returns the gas it observed on entry back to the caller. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), + ) + + call_code = Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x20, + ) + # The observed-gas store is the only op after the call; the callee's + # returned surplus always covers it. + store_code = Op.SSTORE( + key=GAS_SLOT, + value=Op.MLOAD(0), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract(code=call_code + store_code + Op.STOP) + + # Choose the 63/64 rounding base: large enough that the frame can + # afford the trailing store from what the callee hands back, shaped to + # the parametrized residue mod 64. The +1024 margin absorbs the ops + # around the store. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + min_base = store_code.gas_cost(fork) + 1024 + base = -(-min_base // 64) * 64 + residue + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = intrinsic + call_code.gas_cost(fork) + base + + tx = Transaction( + sender=pre.fund_eoa(), + to=caller, + gas_limit=gas_limit, + ) + + # The EVM floors the forwarded gas as `base - base // 64`; the callee + # observes it minus its own GAS opcode. An implementation using + # `base * 63 // 64` is exactly one gas short on the m1/p1 residues. + forwarded = base - base // 64 + expected_gas = forwarded - Op.GAS.gas_cost(fork) + + post = {caller: Account(storage={GAS_SLOT: expected_gas})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py deleted file mode 100644 index 256cf7ea0bb..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64e0. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64e0( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64e0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160062, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py deleted file mode 100644 index dd89bd167ec..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64m1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64m1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64m1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160061, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py deleted file mode 100644 index 2dead5d9e1a..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64p1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64p1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64p1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160063, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py index e181c5c7f03..f080fc4a5db 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py @@ -1,17 +1,24 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding... +Verify the EIP-150 63/64 clamp at call depth 2 when the calls also expand +memory: a first-level call receives its exact (affordable) ask, and its own +oversized ask is clamped to 63/64 of what remains after the memory +expansion. Ported from: state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps); every expectation is derived +from the fork, including the top frame's entry snapshot, which pins the +transaction intrinsic cost. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +27,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 +ENTRY_GAS_SLOT = 0x3 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls( # noqa: E501 state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expa...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 memory-expanding call is clamped to 63/64 of its frame.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: hex - # 0x5a600855 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS), - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds, expanding memory through the args window; + # both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + old_memory_size=0x40, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), - ), - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff60007362030d40f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Top frame: snapshots its entry gas (pinning the tx intrinsic), then + # forwards the exact depth-1 budget and stores the success flag plus + # both returned observations. + entry_code = ( + Op.SSTORE(key=ENTRY_GAS_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x30D40, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=CALLER_GAS, + address=caller, + ret_size=0x40, + address_warm=False, + account_new=False, + new_memory_size=0x40, ), - ), - nonce=0, + ) + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)) ) + entry = pre.deploy_contract(code=entry_code + Op.STOP) + + # Conservative fork-derived budget: the entry's own costs (incl. the + # trailing state-priced stores) plus the full depth-1 grant. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + entry_code.gas_cost(fork) + CALLER_GAS tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + gas_limit=gas_limit, ) + # The entry snapshot observes everything after the intrinsic; depth 1 + # received exactly CALLER_GAS; the depth-2 base is what remains after + # the snapshot and the call's own costs (incl. memory expansion), + # clamped by EIP-150. + entry_observed = gas_limit - intrinsic - Op.GAS.gas_cost(fork) + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) + ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) + post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x8D5B6, 9: 1}), - addr: Account(storage={8: 0x2A1C7}), - addr_2: Account(storage={8: 0x30D3E, 9: 1}), + entry: Account( + storage={ + ENTRY_GAS_SLOT: entry_observed, + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 49a06865b930c3de33b03ed16a64cd945e5198b4 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:35:21 +0200 Subject: [PATCH 38/55] fix(tests): un-skip Amsterdam stInitCodeTest OOG creation tests via derived budgets --- .claude/commands/enhance-ported-test.md | 14 +- tests/ported_static/amsterdam_skip_list.txt | 11 +- .../test_out_of_gas_contract_creation.py | 201 ++++++++-------- ..._out_of_gas_prefunded_contract_creation.py | 214 ++++++++++-------- 4 files changed, 227 insertions(+), 213 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 37b4980601b..46041c1da2a 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -426,11 +426,19 @@ separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. **A creation transaction's top frame pays new-account state gas -(EIP-8037).** When deriving a create-tx budget, the intrinsic calculator -does not include the created account's state gas — add +(EIP-8037) — but only for a fresh target.** When deriving a create-tx +budget, the intrinsic calculator does not include the created account's +state gas — add `fork.transaction_top_frame_state_gas(contract_creation=True)` (183,600 on Amsterdam, 0 before) or the whole creation silently OOGs only on the -future fork. +future fork. Exception: `prepare_dispatch` charges it only when the +target's *pre-state* account is `EMPTY_ACCOUNT` — a prefunded create +address pays nothing (validated on +`test_out_of_gas_prefunded_contract_creation`, whose budgets omit the +term). A nested CREATE's new-account state is charged to the parent +before the 63/64 withhold and refunded if the child fails, so a derived +budget must cover its *peak* (use the composite `gas_cost(fork)`), even +on paths where the net is zero. **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index a3fe6dc3556..d172f98ee4c 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 130 +# Total entries: 123 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -117,15 +117,6 @@ stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stInitCodeTest (7) -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g1] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g0] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] - # stMemExpandingEIP150Calls (3) stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py index 9525495cff9..a35d2a4453a 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py @@ -1,149 +1,132 @@ """ -Test_out_of_gas_contract_creation. +Verify a contract-creation transaction whose init code runs out of gas (or +halts on invalid code) leaves no account behind, while a sufficient budget +creates it. Ported from: state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json + +@manually-enhanced: Do not overwrite. Both transaction budgets are derived +from the fork (intrinsic + the created account's top-frame state gas + the +init code's metadata-priced cost), so the insufficient arm keeps running +out mid-init-code and the sufficient arm keeps succeeding on every fork; +the success post pins the final storage value, not just the nonce. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +def storage_writes_initcode() -> Bytecode: + """Six stores to one slot: one cold set, then five dirty warm writes.""" + code = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + for value in range(2, 7): + code += Op.SSTORE( + key=0x1, + value=value, + key_warm=True, + original_value=0, + current_value=value - 1, + new_value=value, + ) + return code + + +def stack_underflow_initcode() -> Bytecode: + """The ported junk init code: CALLCODE underflows the stack.""" + return ( + Op.PUSH1[0xA] + + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + + Op.PUSH1[0x0] + + Op.CALLCODE + + Op.STOP + + Op.PUSH1[0x1] + + Op.PUSH1[0x0] + + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) + + Op.DUP2 + + Op.STOP + ) + + @pytest.mark.ported_from( ["state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "invalid_initcode", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), + pytest.param(True, id="d0"), + pytest.param(False, id="d1"), + ], +) +@pytest.mark.parametrize( + "enough_gas", + [ + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_out_of_gas_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + invalid_initcode: bool, + enough_gas: bool, ) -> None: - """Test_out_of_gas_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + """An under-budgeted or invalid init code creates no account.""" + if invalid_initcode: + initcode = stack_underflow_initcode() + else: + initcode = storage_writes_initcode() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000000000, + # The insufficient budget runs out midway through the init code; the + # sufficient one covers it with margin. EIP-8037 charges the created + # account's state gas to the creation transaction's top frame. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + # The sufficient margin must exceed the EIP-2200 stipend (2300), or + # the final SSTOREs of the init code fail their minimum-gas check. + initcode_cost = storage_writes_initcode().gas_cost(fork) + gas_limit = overhead + ( + initcode_cost + 5_000 if enough_gas else initcode_cost // 2 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - nonce=1 - ), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.PUSH1[0xA] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.CALLCODE - + Op.STOP - + Op.PUSH1[0x1] - + Op.PUSH1[0x0] - + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) - + Op.DUP2 - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.SSTORE(key=0x1, value=0x2) - + Op.SSTORE(key=0x1, value=0x3) - + Op.SSTORE(key=0x1, value=0x4) - + Op.SSTORE(key=0x1, value=0x5) - + Op.SSTORE(key=0x1, value=0x6), - ] - tx_gas = [56000, 150000] - tx_value = [1] - + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=1, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas and not invalid_initcode: + created_account: Account | type = Account( + nonce=1, code=b"", storage={1: 6}, balance=1 + ) + else: + # OOG / invalid init code: the creation is rolled back entirely. + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py index 51dd0b6725e..d3cc55522fa 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py @@ -1,136 +1,168 @@ """ -Test_out_of_gas_prefunded_contract_creation. +Verify a contract-creation transaction targeting a prefunded address, whose +init code CREATEs a value-bearing child: the budget decides whether the +outer creation fails (prefund untouched), the child fails (value stays), +or the child succeeds (one wei moves into it). Ported from: state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json + +@manually-enhanced: Do not overwrite. All three budgets are derived from +the fork (intrinsic + top-frame state gas + the composed init/child code +costs), and the child account is asserted, disambiguating the ported +"balance 1" outcomes (outer-failure vs child-success) that were previously +indistinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 1 +TX_VALUE = 1 +CHILD_VALUE = 1 +CHILD_STORED = 0x112233 + @pytest.mark.ported_from( [ "state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "outcome", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), + pytest.param("child_succeeds", id="g0"), + pytest.param("outer_oog", id="g1"), + pytest.param("child_oog", id="g2"), ], ) -@pytest.mark.pre_alloc_mutable def test_out_of_gas_prefunded_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + outcome: str, ) -> None: - """Test_out_of_gas_prefunded_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Budget decides how deep a prefunded creation's child CREATE gets.""" + # Child init code: one cold store, deposits nothing. + child_code = ( + Op.SSTORE( + key=0x0, + value=CHILD_STORED, + key_warm=False, + original_value=0, + new_value=CHILD_STORED, + ) + + Op.STOP * 2 ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000, + # Outer init code: copy the child init code from its own tail, then + # CREATE a value-bearing child from it; deposits nothing. The copy + # window and memory usage stay within one word. + inner_create = Op.CREATE( + value=CHILD_VALUE, + offset=0x0, + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), ) - - pre[sender] = Account(balance=0xF424000) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=1, - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=0x1A, # placeholder; recomputed below + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, ) + body = prefix + Op.POP(inner_create) + Op.STOP + # The child code sits immediately after the executable body. + initcode_prefix_len = len(bytes(body)) + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=initcode_prefix_len, + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, + ) + body = prefix + Op.POP(inner_create) + Op.STOP + assert len(bytes(body)) == initcode_prefix_len, "stable code layout" + initcode = body + child_code - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0, 1], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=1), - }, - }, - { - "indexes": {"data": -1, "gas": [2], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=2), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Fork-derived budgets. The prefunded target is not EMPTY_ACCOUNT in + # the pre-state, so EIP-8037 charges no top-frame new-account state + # gas for this creation — an Amsterdam behavior this test pins. The + # inner CREATE's composite cost covers its peak charge (its + # new-account state gas is refunded if the child fails, but must be + # affordable when charged). + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + + prefix.gas_cost(fork) + + inner_create.gas_cost(fork) + ) + child_needed = child_code.gas_cost(fork) + if outcome == "outer_oog": + # Dies charging the inner CREATE. + gas_limit = overhead - inner_create.gas_cost(fork) // 2 + elif outcome == "child_oog": + # Outer completes; the child's 63/64 grant undercuts its cost. + gas_limit = overhead + child_needed // 2 + else: + # Child completes too and keeps the transferred wei. + gas_limit = overhead + -(-child_needed * 64 // 63) + 2_000 - tx_data = [ - Op.PUSH1[0x9] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.POP(Op.CREATE) - + Op.STOP * 2 - + Op.INVALID - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP * 2, - ] - tx_gas = [154000, 65000, 95000] - tx_value = [1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + child = compute_create_address(address=created, nonce=1) + if outcome == "outer_oog": + # Creation rolled back: only the prefund remains, nonce untouched. + created_account = Account(nonce=0, balance=PREFUND) + child_account: Account | type = Account.NONEXISTENT + elif outcome == "child_oog": + # The inner CREATE increments the creator's nonce even when the + # child fails. + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE + ) + child_account = Account.NONEXISTENT + else: + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE - CHILD_VALUE + ) + child_account = Account( + nonce=1, + balance=CHILD_VALUE, + storage={0: CHILD_STORED}, + ) + + post = { + sender: Account(nonce=1), + created: created_account, + child: child_account, + } + + state_test(pre=pre, post=post, tx=tx) From bd03c66f85591e1e00fc470d17c4c2daf70b8264 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:54:03 +0200 Subject: [PATCH 39/55] fix(tests): un-skip Amsterdam EIP-150 create/delegate/oog-cascade ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- .../test_create_and_gas_inside_create.py | 123 ++++++++---- .../test_delegate_call_on_eip.py | 113 ++++++----- ...n_second_level_with_mem_expanding_calls.py | 180 ++++++++++++------ ..._inside_create_with_mem_expanding_calls.py | 150 +++++++++++---- 5 files changed, 390 insertions(+), 183 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index d172f98ee4c..6cc835d20c9 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 123 +# Total entries: 119 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -106,7 +106,7 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (3) +# stEIP150Specific (1) stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] @@ -117,8 +117,7 @@ stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stMemExpandingEIP150Calls (3) -stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] +# stMemExpandingEIP150Calls (1) stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py index 0bcf52c7d85..47a20e3a725 100644 --- a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py +++ b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py @@ -1,17 +1,23 @@ """ -Test_create_and_gas_inside_create. +Verify the gas a CREATE's init code observes: the child receives all but +one 64th of what remains in the creating frame, and the parent's CREATE +cost is measured alongside it. Ported from: state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the creating +frame's budget so the child's stored GAS observation is fork-derived +(`63/64` of the derived base); the parent measures the CREATE with +CodeGasMeasure instead of raw snapshots. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,58 +27,105 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0xB +GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + +# The creating frame's pinned budget (the ported transaction's). +CALLER_GAS = 600_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes into its own storage + # and deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The child bytes sit right-aligned in the first memory word. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), ) - # Source: lll - # { [100] (GAS) (MSTORE 0 0x5a60fd55) (SSTORE 11 (CREATE 0 28 4)) (SSTORE 9 (SUB @100 (GAS))) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x64, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.SUB(Op.MLOAD(offset=0x64), Op.GAS)) + # The outer call pins the creating frame's budget so the child's + # observation does not depend on the tx gas limit. + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=creator)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # The child receives all but one 64th of what remains after the + # setup, the measuring GAS read, and the CREATE's own charges (its + # new-account state gas is taken before the withhold). + base = ( + CALLER_GAS + - setup.gas_cost(fork) + - Op.GAS.gas_cost(fork) + - create_code.gas_cost(fork) ) + assert base > 0, "CALLER_GAS must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + measured_create = create_store.gas_cost(fork) + child_code.gas_cost(fork) + created = compute_create_address(address=creator, nonce=1) post = { - contract_0: Account( + entry: Account(storage={0: 1}), + creator: Account( storage={ - 9: 0x129DB, - 11: compute_create_address(address=contract_0, nonce=0), + ADDRESS_SLOT: created, + GAS_SLOT: measured_create, }, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x83729} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py index 5156516411b..89d33ae0248 100644 --- a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py +++ b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py @@ -1,17 +1,23 @@ """ -Test_delegate_call_on_eip. +Measure a DELEGATECALL that asks for more gas than its frame holds: the +EIP-150 clamp decides the grant, the delegate writes into the caller's +storage, and the measured cost is the call plus the delegate's work. Ported from: state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the frame budget +so the oversized ask always clamps; the DELEGATECALL is measured with +CodeGasMeasure (success flag inside the window) and the expectation is the +composite plus the delegate's fork-priced store. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) @@ -20,62 +26,79 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +DELEGATE_VALUE = 0x12 +FLAG_SLOT = 0x9 +GAS_SLOT = 0x8 + +# The ported ask (600000): above the pinned frame budget, so the EIP-150 +# clamp decides the grant on every fork. +ASK_GAS = 0x927C0 +CALLER_GAS = 400_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_delegate_call_on_eip( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_delegate_call_on_eip.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Measure a clamped DELEGATECALL running a store in the caller.""" + # Runs in the caller's storage context: one cold fresh store. + delegate_store = Op.SSTORE( + key=0x0, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + delegate = pre.deploy_contract(code=delegate_store + Op.STOP) - # Source: lll - # { (SSTORE 0 0x12) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x12) + Op.STOP, - nonce=0, + delegatecall_code = Op.DELEGATECALL( + gas=ASK_GAS, + address=delegate, + address_warm=False, ) - # Source: lll - # { [8] (GAS) (SSTORE 9 (DELEGATECALL 600000 0 0 0 0)) [[8]] (SUB @8 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.DELEGATECALL( - gas=0x927C0, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x8, value=Op.SUB(Op.MLOAD(offset=0x8), Op.GAS)) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=delegatecall_code, + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=CodeGasMeasure( + code=flag_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), + ) + + assert CALLER_GAS < ASK_GAS, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=target)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - post = {target: Account(storage={0: 18, 8: 46841, 9: 1})} + measured = flag_store.gas_cost(fork) + delegate_store.gas_cost(fork) + + post = { + entry: Account(storage={0: 1}), + target: Account( + storage={ + 0: DELEGATE_VALUE, + GAS_SLOT: measured, + FLAG_SLOT: 1, + }, + ), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py index 5f31ce42a8a..5163dea2f5e 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py @@ -1,17 +1,23 @@ """ -Test_call_goes_oog_on_second_level_with_mem_expanding_calls. +Verify a two-level call chain (with memory-expanding call windows) where +the second-level frame runs out of gas: its own frame and everything below +it revert, while the top frame survives and records the failure. Ported from: state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The first-level budget is pinned and +derived from the fork so the second level keeps starving on every fork +(its 1/64 retention cannot afford the post-call store); the second-level +ask stays oversized; the top frame's entry snapshot is derived and pins +the transaction intrinsic. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,88 +26,142 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +SNAPSHOT_SLOT = 0x8 +FLAG_SLOT = 0x9 +# The ported second-level ask: far above the pinned budget. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_goes_oog_on_second_level_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_goes_oog_on_second_level_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + """A starved second-level frame reverts itself and everything below.""" + # Deepest contract: snapshots and creates twice; its cost anchors the + # starvation budget. + deep_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) - - # Source: hex - # 0x5a600855600060006000f050600060006000f0505a6009555a600a55 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + deep = pre.deploy_contract( + code=deep_snapshot + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) * 2 - + Op.SSTORE(key=0x9, value=Op.GAS) + + Op.SSTORE(key=FLAG_SLOT, value=Op.GAS) + Op.SSTORE(key=0xA, value=Op.GAS), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Second level: snapshots, then asks far more than it holds; the ported + # memory-expanding argument window is kept. + mid = pre.deploy_contract( + code=Op.SSTORE(key=SNAPSHOT_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, ), ), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), + + # Pin the second level's budget so it starves on every fork: enough + # to pay its own snapshot and call, but its grant to the deep frame + # undercuts the deep frame's first store, and its 1/64 retention + # cannot afford its own post-call flag store. + mid_snapshot_cost = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + mid_call_cost = Op.CALL( + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + ).gas_cost(fork) + deep_needed = deep_snapshot.gas_cost(fork) + caller_gas = mid_snapshot_cost + mid_call_cost + deep_needed // 2 + assert caller_gas < ASK_GAS, "the second-level ask must exceed its frame" + + # Top frame: derived entry snapshot (pins the intrinsic), the pinned + # call, and the failure flag. + entry_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL( + gas=caller_gas, + address=mid, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, ), - nonce=0, + key_warm=False, + original_value=0, + new_value=0, + ) + target = pre.deploy_contract( + code=entry_snapshot + flag_store + Op.STOP, + ) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + flag_store.gas_cost(fork) + + caller_gas + + 5_000 ) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=220000, + gas_limit=gas_limit, ) post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x30956}), - addr_2: Account(storage={}), - addr: Account(storage={}), + # The failed call's flag slot stays zero; the entry snapshot pins + # the intrinsic. + target: Account( + storage={ + SNAPSHOT_SLOT: gas_limit - intrinsic - Op.GAS.gas_cost(fork), + }, + ), + # Both lower frames reverted entirely. + mid: Account(storage={}), + deep: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py index 705e9e760f9..74f0be4227b 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py @@ -1,17 +1,22 @@ """ -Test_create_and_gas_inside_create_with_mem_expanding_calls. +Verify the gas a CREATE's init code observes when the creating frame also +expands memory: the child receives all but one 64th of what remains, and +the creating frame's entry and post-CREATE gas readings are asserted. Ported from: state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode is kept, but the +transaction budget and every stored gas reading (entry snapshot, child +observation, post-CREATE reading) are derived from the fork instead of +pinned — the entry snapshot doubles as a transaction-intrinsic pin. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,62 +26,129 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ENTRY_GAS_SLOT = 0xA +ADDRESS_SLOT = 0xB +AFTER_GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes, deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) + child_bytes = bytes(child_code) - # Source: hex - # 0x5a600a55635a60fd556000526004601c6000f0600b555a600955 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.GAS), - nonce=0, + entry_snapshot = Op.SSTORE( + key=ENTRY_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + after_snapshot = Op.SSTORE( + key=AFTER_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=entry_snapshot + setup + create_store + after_snapshot + Op.STOP, ) + # Fork-derived budget: the ported 600000 no longer covers the three + # state-priced stores plus the CREATE under EIP-8037. The margin + # keeps the final store above the EIP-2200 stipend. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + tx_gas = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + setup.gas_cost(fork) + + create_store.gas_cost(fork) + + child_code.gas_cost(fork) + + after_snapshot.gas_cost(fork) + + 5_000 + ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=creator, + gas_limit=tx_gas, ) + # Entry reading: everything after the intrinsic, minus the GAS opcode + # itself (it executes first in the store's operand order). + entry_observed = tx_gas - intrinsic - Op.GAS.gas_cost(fork) + # The child receives all but one 64th of what remains after the entry + # store, the setup, and the CREATE's own charges. + base = ( + tx_gas + - intrinsic + - entry_snapshot.gas_cost(fork) + - setup.gas_cost(fork) + - create_code.gas_cost(fork) + ) + assert base > 0, "the budget must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + # After the CREATE: the child's consumption and the address store are + # gone; the address store's own cost is the composite minus the + # CREATE it wraps. + after_observed = ( + base + - child_code.gas_cost(fork) + - (create_store.gas_cost(fork) - create_code.gas_cost(fork)) + - Op.GAS.gas_cost(fork) + ) + + created = compute_create_address(address=creator, nonce=1) post = { - sender: Account(nonce=1), - contract_0: Account( + creator: Account( storage={ - 9: 0x75596, - 10: 0x8D5B6, - 11: compute_create_address(address=contract_0, nonce=0), + ENTRY_GAS_SLOT: entry_observed, + ADDRESS_SLOT: created, + AFTER_GAS_SLOT: after_observed, }, - nonce=1, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x7E23D} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From e319431b0deb134ff7326739123cff688ce20415 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:59:33 +0200 Subject: [PATCH 40/55] fix(tests): un-skip Amsterdam name-registrator creation ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- ...name_registrator_per_txs_not_enough_gas.py | 149 +++++++++--------- ...e_registrator_pre_store1_not_enough_gas.py | 134 +++++++++++----- 3 files changed, 171 insertions(+), 119 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 6cc835d20c9..f2043f1dfbf 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 119 +# Total entries: 116 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -18,16 +18,13 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] -# stCallCreateCallCodeTest (9) +# stCallCreateCallCodeTest (6) stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] # stCallDelegateCodesCallCodeHomestead (1) stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py index a6159ad7409..7e3bd9b3473 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py @@ -1,103 +1,71 @@ """ -Legacy Test from Christoph. J. +Verify a name-registrator contract creation succeeds or fails with the +transaction budget: the init code writes a storage slot and deposits the +registrar's runtime code. Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from the +fork (intrinsic + top-frame state gas + init code execution + code deposit +regular and state costs); the success arm also pins the deposited code and +transferred balance, which the ported post never checked. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 100_000 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "enough_gas", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_create_name_registrator_per_txs_not_enough_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + enough_gas: bool, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000000, + """An under-budgeted registrar creation leaves no account behind.""" + # The ported init code: write slot 1, then deposit 16 bytes of + # registrar runtime copied from the init code's own bytes. + store = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - storage={1: 1} - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1) - + Op.PUSH1[0x10] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + initcode = ( + store + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + Op.PUSH1[0x0] + Op.RETURN + Op.STOP @@ -108,19 +76,50 @@ def test_create_name_registrator_per_txs_not_enough_gas( + Op.STOP + Op.JUMPDEST + Op.SSTORE( - key=Op.CALLDATALOAD(offset=0x0), value=Op.CALLDATALOAD(offset=0x20) - ), - ] - tx_gas = [56157, 86157] - tx_value = [100000] + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) + ) + deposited = bytes(initcode)[COPY_OFFSET : COPY_OFFSET + DEPOSITED_SIZE] + # Fork-derived budgets: the sufficient one covers the init code, the + # code deposit (regular and EIP-8037 state), and the created account's + # top-frame state gas; the insufficient one dies mid-init-code. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + execution_cost = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + gas_limit = overhead + ( + execution_cost + 5_000 if enough_gas else execution_cost // 2 + ) + + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas: + created_account: Account | type = Account( + nonce=1, + code=deposited, + balance=TX_VALUE, + storage={1: 1}, + ) + else: + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py index cc76b3587f5..212b244d922 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py @@ -1,17 +1,22 @@ """ -Legacy Test from Christoph. J. +Verify a nested CREATE of the name registrar whose child grant cannot cover +the init code: the child account never materializes, while the creating +frame completes (its nonce still advances). Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. The registrar init code is composed +(not a hex blob) and the transaction budget is derived from the fork so +the child's 63/64 grant undercuts its cost on every fork; the creator's +balance is asserted (the endowment returns on failure). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,60 +26,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 0x186A0 +CREATE_VALUE = 0x17 +INITIAL_BALANCE = 10**15 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_name_registrator_pre_store1_not_enough_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """A starved nested registrar creation leaves no account behind.""" + # The registrar init code (same as the per-txs sibling): write slot 1, + # deposit 16 bytes of runtime copied from its own bytes. + initcode = ( + Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + + Op.PUSH1[0x0] + + Op.RETURN + + Op.STOP + + Op.JUMPI( + pc=0x9, + condition=Op.ISZERO(Op.SLOAD(key=Op.CALLDATALOAD(offset=0x0))), + ) + + Op.STOP + + Op.JUMPDEST + + Op.SSTORE( + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) == 0x22, "ported init code is 34 bytes" - # Source: lll - # {(MSTORE 0 0x6001600155601080600c6000396000f3006000355415600957005b6020356000 ) (MSTORE8 32 0x35) (MSTORE8 33 0x55) (CREATE 23 0 34) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( + # Memory setup derived from the composed bytes (one word plus two + # trailing byte stores, as in the ported filler). + setup = ( + Op.MSTORE( offset=0x0, - value=0x6001600155601080600C6000396000F3006000355415600957005B6020356000, # noqa: E501 + value=int.from_bytes(initcode_bytes[:0x20], "big"), + new_memory_size=0x20, ) - + Op.MSTORE8(offset=0x20, value=0x35) - + Op.MSTORE8(offset=0x21, value=0x55) - + Op.CREATE(value=0x17, offset=0x0, size=0x22) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, + + Op.MSTORE8( + offset=0x20, value=initcode_bytes[0x20], new_memory_size=0x40 + ) + + Op.MSTORE8( + offset=0x21, value=initcode_bytes[0x21], new_memory_size=0x40 + ) + ) + create_code = Op.CREATE( + value=CREATE_VALUE, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=setup + Op.POP(create_code) + Op.STOP, + balance=INITIAL_BALANCE, + ) + + # Budget: covers the frame's own work and the CREATE's peak charge, + # but the child's 63/64 grant undercuts the init code plus deposit. + child_needed = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + setup.gas_cost(fork) + + create_code.gas_cost(fork) + + child_needed // 2 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=73071, - value=0x186A0, + sender=pre.fund_eoa(), + to=creator, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(nonce=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The CREATE advanced the nonce even though its child failed, and + # the endowment returned. + creator: Account(nonce=2, balance=INITIAL_BALANCE + TX_VALUE), + compute_create_address(address=creator, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 7c772017bf0dc33b7d8909f1a21ce035f6ff3606 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 16:09:00 +0200 Subject: [PATCH 41/55] fix(tests): un-skip Amsterdam gas-price-for-codes and memory OOG ported tests --- .claude/commands/enhance-ported-test.md | 14 + tests/ported_static/amsterdam_skip_list.txt | 15 +- .../test_new_gas_price_for_codes.py | 242 ++++++++++------ ...rice_for_codes_with_mem_expanding_calls.py | 261 ++++++++++++------ tests/ported_static/stMemoryTest/test_oog.py | 27 +- 5 files changed, 371 insertions(+), 188 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 46041c1da2a..5aafcc152b5 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -386,6 +386,20 @@ previous_bytes=)`; EIP-3860 init-code words → `fork.gas_costs().CODE_INIT_PER_ * ceil(size/32)`. You can also call `.gas_cost` / `.regular_cost` / `.state_cost` on exactly the measured bytecode. +**Reservoir-less sub-calls pay state gas from their regular grant.** With +the tx reservoir at 0, a sub-frame's state charges spill from its own +`gas_left` — a delegate that does one first-set SSTORE needs its *whole* +~111k inside the forwarded grant on Amsterdam, not just the ~13k regular +part. Size derived sub-call budgets from the callee composite's full +`gas_cost(fork)`. Corollaries: (a) a *failed* sub-frame contributes its +entire forfeited grant to the parent's measured window, not its "cost"; +(b) `SSTORE(flag, )` silently degrades to a ~3k no-op store when +the call fails — the flag reads 0 and no state gas is charged, which can +mask a broken callee behind a plausible-looking measurement. Validated on +`test_new_gas_price_for_codes` (delegate budget derived; failed value +calls return their stipends: subtract one `CALL_STIPEND` per failed +value-bearing call from window measurements). + **Nested / callee-side measurements.** When the measured op is a `CALL` whose callee does real work, the measured cost = `call_code.gas_cost(fork) + callee_code.gas_cost(fork)` (the CALL's own cost plus what the callee consumed). diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f2043f1dfbf..2ff669c7937 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 116 +# Total entries: 112 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -103,25 +103,12 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (1) -stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] -stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] -stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] - # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stMemExpandingEIP150Calls (1) -stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] -stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] - -# stMemoryTest (2) -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success14] -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success15] - # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py b/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py index 0be45107816..6ed52393b2d 100644 --- a/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py +++ b/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py @@ -1,18 +1,25 @@ """ -Test_new_gas_price_for_codes. +Verify the EIP-150 repriced code/account operations in one frame: +EXTCODESIZE, EXTCODECOPY, SLOAD, failing value CALL/CALLCODE (insufficient +balance), DELEGATECALL that writes the caller's storage, a call to a +nonexistent account, BALANCE, and the whole window's measured gas. Ported from: state_tests/stEIP150Specific/NewGasPriceForCodesFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode shape is kept, +but the window delta, the mid-execution sender balance, and the copied +code word are derived (opcode metadata, fee formula, the deployed bytes); +the delegate's budget is derived so its store — state-priced under +EIP-8037 — fits inside the grant (a reservoir-less sub-call pays state +gas from its regular grant); each failed value call returns its stipend. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,133 +28,196 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +EXTCODE_BYTES = bytes.fromhex( + "1122334455667788991011121314151617181920212223242526272829303132" +) +COPY_SIZE = 0x14 +DELEGATE_VALUE = 0x11 +# Budget for the calls whose outcome does not depend on it (the value +# calls fail on insufficient balance; the absent target runs nothing). +FORWARDED_GAS = 0x7530 +GAS_PRICE = 10 +INITIAL_BALANCE = 10**15 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/NewGasPriceForCodesFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_new_gas_price_for_codes( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_new_gas_price_for_codes.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 + """Measure a frame exercising every repriced code/account operation.""" + sender = pre.fund_eoa(amount=INITIAL_BALANCE) + code_target = pre.deploy_contract(code=EXTCODE_BYTES, balance=111) + delegate_store = Op.SSTORE( + key=0x64, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + storage_writer = pre.deploy_contract(code=delegate_store + Op.STOP) + absent = pre.nonexistent_account() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The delegate must succeed: with a zero reservoir its state-priced + # store is paid from the regular grant, so the budget is derived. + delegate_budget = delegate_store.gas_cost(fork) + 2_000 - pre[sender] = Account(balance=0xE8D4A51000) - # Source: raw - # 0x1122334455667788991011121314151617181920212223242526272829303132 - addr = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex( - "1122334455667788991011121314151617181920212223242526272829303132" - ), - balance=111, - nonce=0, - address=Address(0xC572A70AFAAB9D01D0A2AFB855BFBAFB47C8211B), # noqa: E501 - ) - # Source: lll - # { (SSTORE 100 0x11) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x64, value=0x11) + Op.STOP, - nonce=0, - address=Address(0xAD9D325B811CB0701839C07C6F139F3799476798), # noqa: E501 - ) - # Source: lll - # { [999] (GAS) (SSTORE 1 (EXTCODESIZE )) (EXTCODECOPY 0 0 20) (SSTORE 2 (MLOAD 0)) (SSTORE 4 (SLOAD 0)) (SSTORE 5 (CALL 30000 1 0 0 0 0)) (SSTORE 6 (CALLCODE 30000 1 0 0 0 0)) (SSTORE 7 (DELEGATECALL 30000 0 0 0 0)) (SSTORE 8 (CALL 30000 0x1000000000000000000000000000000000000013 0 0 0 0 0)) (SSTORE 3 (BALANCE )) (SSTORE 10 (SUB (MLOAD 999) (GAS))) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x3E7, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.EXTCODESIZE(address=addr)) - + Op.EXTCODECOPY(address=addr, dest_offset=0x0, offset=0x0, size=0x14) - + Op.SSTORE(key=0x2, value=Op.MLOAD(offset=0x0)) - + Op.SSTORE(key=0x4, value=Op.SLOAD(key=0x0)) + # The measured window: entry GAS snapshot through the closing GAS. + # The value-bearing CALL and CALLCODE fail on insufficient balance + # (this contract holds nothing), costing their access and transfer + # charges minus the returned stipend; the DELEGATECALL runs the + # writer against this contract's storage. + window = ( + Op.MSTORE(offset=0x3E7, value=Op.GAS, new_memory_size=0x407) + + Op.SSTORE( + key=0x1, + value=Op.EXTCODESIZE(address=code_target, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.EXTCODECOPY( + address=code_target, + dest_offset=0x0, + offset=0x0, + size=COPY_SIZE, + address_warm=True, + data_size=COPY_SIZE, + new_memory_size=0x407, + old_memory_size=0x407, + ) + + Op.SSTORE( + key=0x2, + value=Op.MLOAD(offset=0x0), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x4, + value=Op.SLOAD(key=0x0, key_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( key=0x5, value=Op.CALL( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=False, + value_transfer=True, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x6, value=Op.CALLCODE( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=True, + value_transfer=True, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x7, value=Op.DELEGATECALL( - gas=0x7530, - address=addr_2, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + gas=delegate_budget, + address=storage_writer, + address_warm=True, ), + key_warm=False, + original_value=0, + new_value=1, ) + Op.SSTORE( key=0x8, value=Op.CALL( - gas=0x7530, - address=0x1000000000000000000000000000000000000013, + gas=FORWARDED_GAS, + address=absent, value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=False, + value_transfer=False, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=1, ) - + Op.SSTORE(key=0x3, value=Op.BALANCE(address=sender)) - + Op.SSTORE(key=0xA, value=Op.SUB(Op.MLOAD(offset=0x3E7), Op.GAS)) - + Op.STOP, + + Op.SSTORE( + key=0x3, + value=Op.BALANCE(address=sender, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + ) + delta_store = Op.SSTORE( + key=0xA, + value=Op.SUB(Op.MLOAD(offset=0x3E7), Op.GAS), + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=window + delta_store + Op.STOP, storage={0: 18}, - nonce=0, - address=Address(0xFD9AFC8315A88141164E2A753157EA3E0F72C707), # noqa: E501 ) + # Window delta: everything from the entry GAS read to the closing + # one; the lead GAS and the closing GAS cancel out of the composite, + # the delegate's work is added on top, and each failed value call + # hands back its stipend along with the unused grant. + measured = ( + window.gas_cost(fork) + + delegate_store.gas_cost(fork) + - 2 * fork.gas_costs().CALL_STIPEND + ) + + # Fork-derived budget with an EIP-2200 stipend margin for the + # trailing delta store. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + measured + delta_store.gas_cost(fork) + 5_000 + tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + copied_word = int.from_bytes( + EXTCODE_BYTES[:COPY_SIZE].ljust(0x20, b"\x00"), "big" + ) post = { target: Account( storage={ - 0: 18, - 1: 32, - 2: 0x1122334455667788991011121314151617181920000000000000000000000000, # noqa: E501 - 3: 0xE8D4498280, - 4: 18, - 7: 1, - 8: 1, - 10: 0x2CB0A, - 100: 17, + 0x0: 18, + 0x1: len(EXTCODE_BYTES), + 0x2: copied_word, + # Mid-execution balance: the full fee is charged upfront. + 0x3: INITIAL_BALANCE - gas_limit * GAS_PRICE, + 0x4: 18, + # Slots 5 and 6 stay zero: the value calls failed. + 0x7: 1, + 0x8: 1, + 0xA: measured, + 0x64: DELEGATE_VALUE, }, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py index 47aed815167..3a6aabd3bc2 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py @@ -1,18 +1,26 @@ """ -Test_new_gas_price_for_codes_with_mem_expanding_calls. +Verify the EIP-150 repriced code/account operations in one frame whose +calls also expand memory: EXTCODESIZE, EXTCODECOPY, failing value +CALL/CALLCODE (insufficient balance), DELEGATECALL that writes the +caller's storage, a call to a nonexistent account, BALANCE, and a final +raw gas reading that pins the whole execution. Ported from: state_tests/stMemExpandingEIP150Calls/NewGasPriceForCodesWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode shape is kept, +but the final gas reading, the mid-execution sender balance, and the +copied code word are derived (opcode metadata, fee formula, the deployed +bytes); the delegate's budget is derived so its store — state-priced +under EIP-8037 — fits inside the grant; each failed value call returns +its stipend. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,135 +29,216 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +EXTCODE_BYTES = bytes.fromhex( + "1122334455667788991011121314151617181920212223242526272829303132" +) +COPY_SIZE = 0x14 +DELEGATE_VALUE = 0x11 +# Budget for the calls whose outcome does not depend on it. +FORWARDED_GAS = 0x7530 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF +GAS_PRICE = 10 +INITIAL_BALANCE = 10**15 + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/NewGasPriceForCodesWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_new_gas_price_for_codes_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_new_gas_price_for_codes_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x3956FC06BD55836ACDB92DA0E38A15F2E568C088022CF2278180477F3F7702A + """Measure repriced operations with memory-expanding call windows.""" + sender = pre.fund_eoa(amount=INITIAL_BALANCE) + code_target = pre.deploy_contract(code=EXTCODE_BYTES, balance=111) + delegate_store = Op.SSTORE( + key=0x64, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + storage_writer = pre.deploy_contract(code=delegate_store) + absent = pre.nonexistent_account() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The delegate must succeed: with a zero reservoir its state-priced + # store is paid from the regular grant, so the budget is derived. + delegate_budget = delegate_store.gas_cost(fork) + 2_000 - pre[sender] = Account(balance=0xE8D4A5100000) - # Source: hex - # 0x1122334455667788991011121314151617181920212223242526272829303132 - addr = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex( - "1122334455667788991011121314151617181920212223242526272829303132" - ), - balance=111, - nonce=0, - address=Address(0x6B6AF3C6E1714081C8C3085ACBAC8C2B21FADF0B), # noqa: E501 - ) - # Source: hex - # 0x6011606455 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x64, value=0x11), - nonce=0, - address=Address(0x7B8C83E74CC8DFADB03138C2743C70588ACE4222), # noqa: E501 - ) - # Source: hex - # 0x733b600155601460006000733c60005160025560005460045560ff60ff60ff60ff600173617530f160055560ff60ff60ff60ff600173617530f260065560ff60ff60ff60ff73617530f460075560ff60ff60ff60ff6000731000000000000000000000000000000000000013617530f160085573316003555a600a55 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=Op.EXTCODESIZE(address=addr)) - + Op.EXTCODECOPY(address=addr, dest_offset=0x0, offset=0x0, size=0x14) - + Op.SSTORE(key=0x2, value=Op.MLOAD(offset=0x0)) - + Op.SSTORE(key=0x4, value=Op.SLOAD(key=0x0)) + call_window = MEM_OFFSET + MEM_SIZE + body = ( + Op.SSTORE( + key=0x1, + value=Op.EXTCODESIZE(address=code_target, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.EXTCODECOPY( + address=code_target, + dest_offset=0x0, + offset=0x0, + size=COPY_SIZE, + address_warm=True, + data_size=COPY_SIZE, + new_memory_size=0x20, + ) + + Op.SSTORE( + key=0x2, + value=Op.MLOAD(offset=0x0), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x4, + value=Op.SLOAD(key=0x0, key_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( key=0x5, value=Op.CALL( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + value_transfer=True, + account_new=False, + new_memory_size=call_window, + old_memory_size=0x20, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x6, value=Op.CALLCODE( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=True, + value_transfer=True, + account_new=False, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x7, value=Op.DELEGATECALL( - gas=0x7530, - address=addr_2, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=delegate_budget, + address=storage_writer, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=True, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=1, ) + Op.SSTORE( key=0x8, value=Op.CALL( - gas=0x7530, - address=0x1000000000000000000000000000000000000013, + gas=FORWARDED_GAS, + address=absent, value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + value_transfer=False, + account_new=False, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=1, ) - + Op.SSTORE(key=0x3, value=Op.BALANCE(address=sender)) - + Op.SSTORE(key=0xA, value=Op.GAS), + + Op.SSTORE( + key=0x3, + value=Op.BALANCE(address=sender, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + ) + final_store = Op.SSTORE( + key=0xA, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=body + final_store + Op.STOP, storage={0: 18}, - nonce=0, - address=Address(0x23A2EC54F5F8589778DA7C2199CAF3B179A24CB9), # noqa: E501 ) + # Consumption before the final GAS read: the body's composite plus + # the delegate's work, minus the two returned stipends. + consumed = ( + body.gas_cost(fork) + + delegate_store.gas_cost(fork) + - 2 * fork.gas_costs().CALL_STIPEND + ) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + consumed + final_store.gas_cost(fork) + 5_000 + tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + copied_word = int.from_bytes( + EXTCODE_BYTES[:COPY_SIZE].ljust(0x20, b"\x00"), "big" + ) post = { - addr: Account(balance=111), target: Account( storage={ - 0: 18, - 1: 32, - 2: 0x1122334455667788991011121314151617181920000000000000000000000000, # noqa: E501 - 3: 0xE8D4A4B47280, - 4: 18, - 7: 1, - 8: 1, - 10: 0x60AE9, - 100: 17, + 0x0: 18, + 0x1: len(EXTCODE_BYTES), + 0x2: copied_word, + # Mid-execution balance: the full fee is charged upfront. + 0x3: INITIAL_BALANCE - gas_limit * GAS_PRICE, + 0x4: 18, + # Slots 5 and 6 stay zero: the value calls failed. + 0x7: 1, + 0x8: 1, + # Raw reading: everything left after the body's work and + # the GAS opcode itself. + 0xA: gas_limit - intrinsic - consumed - Op.GAS.gas_cost(fork), + 0x64: DELEGATE_VALUE, }, ), - sender: Account(nonce=1), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemoryTest/test_oog.py b/tests/ported_static/stMemoryTest/test_oog.py index cd47deb6687..5fb60c5d28c 100644 --- a/tests/ported_static/stMemoryTest/test_oog.py +++ b/tests/ported_static/stMemoryTest/test_oog.py @@ -311,6 +311,29 @@ def test_oog( # nested CALL to a cold contract plus the copy; the reprice eats the # slack, so add it back to that one budget. cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + # The CREATE/CREATE2 success budgets are derived: EIP-8037 adds the + # new-account state gas (~183k), far past the ported 0xFFFF budget. + create_budget = ( + Op.CREATE( + value=0x0, + offset=0x10000, + size=0x20, + new_memory_size=0x10020, + init_code_size=0x20, + ).gas_cost(fork) + + 1_000 + ) + create2_budget = ( + Op.CREATE2( + value=0x0, + offset=0x10000, + size=0x20, + salt=0x5A17, + new_memory_size=0x10020, + init_code_size=0x20, + ).gas_cost(fork) + + 1_000 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x0000000000000000000000000000000000010020) contract_1 = Address(0x0000000000000000000000000000000000010037) @@ -786,9 +809,9 @@ def test_oog( Bytes("1a8451e6") + Hash(0xA3) + Hash(0x39D0), Bytes("1a8451e6") + Hash(0xA4) + Hash(0xFFFF), Bytes("1a8451e6") + Hash(0xA4) + Hash(0x39D0), - Bytes("1a8451e6") + Hash(0xF0) + Hash(0xFFFF), + Bytes("1a8451e6") + Hash(0xF0) + Hash(create_budget), Bytes("1a8451e6") + Hash(0xF0) + Hash(0x7D00), - Bytes("1a8451e6") + Hash(0xF5) + Hash(0xFFFF), + Bytes("1a8451e6") + Hash(0xF5) + Hash(create2_budget), Bytes("1a8451e6") + Hash(0xF5) + Hash(0x7D00), Bytes("1a8451e6") + Hash(0xF3) + Hash(0xFFFF), Bytes("1a8451e6") + Hash(0xF3) + Hash(0x36B0), From 8def63104047b72b3d7b069e9940a3beb21e3492 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 18:56:36 +0200 Subject: [PATCH 42/55] fix(tests): un-skip Amsterdam exp-empty, homestead-oog, and create-warmth ported tests --- tests/ported_static/amsterdam_skip_list.txt | 35 +-- .../test_create_address_warm_after_fail.py | 221 +++++++++++------- .../stEIP158Specific/test_exp_empty.py | 150 ++++++------ ...nt_leave_empty_contract_via_transaction.py | 118 +++++----- 4 files changed, 271 insertions(+), 253 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 2ff669c7937..7353a6bc6aa 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 112 +# Total entries: 97 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -62,20 +62,13 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (33) -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-post-constr-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-post-constr-v1] +# stCreateTest (20) +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] @@ -84,12 +77,6 @@ stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_ref stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG2] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG3] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v0] stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v1] stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v0] @@ -103,11 +90,9 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP158Specific (1) -stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] +# stEIP158Specific (0) -# stHomesteadSpecific (1) -stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] +# stHomesteadSpecific (0) # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] diff --git a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py index 358473d4fb0..f9ab7c36933 100644 --- a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py +++ b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py @@ -10,13 +10,13 @@ Ported from: state_tests/stCreateTest/CreateAddressWarmAfterFailFiller.yml -@manually-enhanced: Do not overwrite. The post-state records the -measured cost of accessing the create address after a failed CREATE, -which is a cold account access. EIP-8038 reprices a cold account -access from 2 600 to 3 000, so each such measurement gains 400 at -Amsterdam. Derive that delta from the fork's gas model so it is -exactly 0 pre-EIP-8037 and tracks parameter changes; do not hardcode -the Amsterdam value. +@manually-enhanced: Do not overwrite. The post-state records measured +probe-CALL costs; derive them from the fork's gas model (CALL regular +cost with warm/cold, value-transfer, and new-account metadata, minus +the returned stipend) plus the dispatcher's fixed framing gas, so +EIP-2929/8037/8038 repricings track automatically. The transaction +gas limit carries reservoir headroom for the state gas the dispatcher +incurs on EIP-8037 forks, keeping state gas out of the measurements. """ import pytest @@ -389,10 +389,43 @@ def test_create_address_warm_after_fail( address=Address(0x00000000000000000000000000000000000C0DEC), # noqa: E501 ) - # The create address access after a failed CREATE is cold here; - # EIP-8038 reprices a cold account access from 2 600 to 3 000. - # Derive the delta from the fork so it is 0 pre-EIP-8037. - cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + # The dispatcher measures each probe CALL with a GAS-delta window. + # The window's framing (stack shuffling, the two dirty-warm SSTOREs + # bracketing the call, and the closing GAS read) is baked into the + # ported bytecode blob and fork-stable; the CALL's own cost is + # derived from the fork so warm/cold, value-transfer, and + # new-account repricings (EIP-2929, EIP-8037, EIP-8038) track + # automatically. On EIP-8037 forks the state-gas component is paid + # from the transaction's reservoir (see the gas limit below), so + # the windows observe only the regular cost. + first_call_frame = 228 + repeat_call_frame = 216 + + def measured_call(frame: int, *, warm: bool, new: bool) -> int: + """Compute the gas one dispatcher probe-CALL window measures.""" + call = Op.CALL( + address_warm=warm, + value_transfer=bool(v), + account_new=new and bool(v), + ) + measured = frame + call.regular_cost(fork) + if v: + # The callee is empty (or STOP-only), so the stipend + # forwarded with the value returns unused. + measured -= fork.gas_costs().CALL_STIPEND + return measured + + # Slot 12: first call to the CREATE target. Warm if a failed CREATE + # accessed it (the subject, EIP-2929), cold if the creating frame + # itself failed (or CREATE aborted on a nonce overflow), and an + # existing account when the CREATE succeeded. + warm_new_call = measured_call(first_call_frame, warm=True, new=True) + cold_new_call = measured_call(first_call_frame, warm=False, new=True) + warm_existing_call = measured_call(first_call_frame, warm=True, new=False) + # Slots 13/15: repeated calls, always warm to an existing account. + repeat_call = measured_call(repeat_call_frame, warm=True, new=False) + # Slot 14: first call to the never-created empty address. + empty_call = cold_new_call expect_entries_: list[dict] = [ { @@ -407,10 +440,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -431,10 +464,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -458,10 +491,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -482,10 +515,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -509,10 +542,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -533,10 +566,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -560,10 +593,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -584,10 +617,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -611,10 +644,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -635,10 +668,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -662,10 +695,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828 + cold_account_delta, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -689,10 +722,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 34528, - 13: 7016, - 14: 34528, - 15: 7016, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -716,10 +749,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828 + cold_account_delta, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -740,10 +773,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 34528, - 13: 7016, - 14: 34528, - 15: 7016, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -764,10 +797,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -788,10 +821,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 7028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -815,10 +848,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -839,10 +872,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 7028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -875,12 +908,22 @@ def test_create_address_warm_after_fail( Bytes("52c3fd24") + Hash(0x7), Bytes("52c3fd24") + Hash(0x11), ] - # The dispatcher writes to ~14 fresh storage slots; under EIP-8037 - # each slot's 32-byte cost is settled at frame end out of the - # reservoir/`gas_left` (~37_500 gas/slot on Amsterdam). Add that - # headroom — `sstore_state_gas` is 0 pre-EIP-8037, so the budget - # is unchanged on older forks. - tx_gas = [16777216 + 14 * Op.SSTORE(new_value=1).state_cost(fork)] + # Under EIP-8037 the gas above the execution cap becomes the + # state-gas reservoir. Size it to cover every state charge the + # dispatcher can incur — nine 0→non-zero SSTOREs, the CREATE's + # peak new-account charge plus up to two accounts created by the + # value-bearing probe calls (one spare), and the code deposit — + # so no state gas spills into the measured windows. The headroom + # is 0 pre-EIP-8037, leaving the budget unchanged on older forks. + state_gas_headroom = ( + 9 * Op.SSTORE(new_value=1).state_cost(fork) + + 4 + * Op.CALL( + address_warm=True, value_transfer=True, account_new=True + ).state_cost(fork) + + fork.code_deposit_state_gas(code_size=1) + ) + tx_gas = [16777216 + state_gas_headroom] tx_value = [0, 1] tx = Transaction( diff --git a/tests/ported_static/stEIP158Specific/test_exp_empty.py b/tests/ported_static/stEIP158Specific/test_exp_empty.py index 9b7b323744c..558f1b6d071 100644 --- a/tests/ported_static/stEIP158Specific/test_exp_empty.py +++ b/tests/ported_static/stEIP158Specific/test_exp_empty.py @@ -1,17 +1,23 @@ """ -Test_exp_empty. +Measure the gas cost of EXP with a zero base or a zero exponent across +exponent widths (the per-byte exponent charge applies only to the +exponent operand). Ported from: state_tests/stEIP158Specific/EXP_EmptyFiller.json + +@manually-enhanced: Do not overwrite. The eight measurement windows are +generated from one case list and every stored delta is derived from +opcode metadata (`exponent=` drives the per-byte charge); the transaction +budget is fork-derived. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) @@ -20,100 +26,78 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# (base, exponent) pairs: a zero on either side, exponent widths 1-32. +EXP_CASES = [ + (0x0, 0xC), + (0xC, 0x0), + (0x0, 2**64 - 1), + (0x0, 2**128 - 1), + (0x0, 2**256 - 1), + (2**64 - 1, 0x0), + (2**128 - 1, 0x0), + (2**256 - 1, 0x0), +] + @pytest.mark.ported_from( ["state_tests/stEIP158Specific/EXP_EmptyFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_exp_empty( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_exp_empty.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """Measure EXP's cost for zero-base and zero-exponent operands.""" + code = Bytecode() + storage: dict = {} + budget = 0 + for i, (base, exponent) in enumerate(EXP_CASES): + result = 1 if exponent == 0 else 0 + result_slot = 1 + 2 * i + # The last window stores its delta at slot 100, as ported. + delta_slot = 0x64 if i == len(EXP_CASES) - 1 else result_slot + 1 - # Source: lll - # { [0](GAS) [[1]](EXP 0 12) [[2]](SUB @0 (GAS)) [0](GAS) [[3]](EXP 12 0) [[4]](SUB @0 (GAS)) [0](GAS) [[5]](EXP 0 0xffffffffffffffff) [[6]](SUB @0 (GAS)) [0](GAS) [[7]](EXP 0 0xffffffffffffffffffffffffffffffff) [[8]](SUB @0 (GAS)) [0](GAS) [[9]](EXP 0 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) [[10]](SUB @0 (GAS)) [0](GAS) [[11]](EXP 0xffffffffffffffff 0) [[12]](SUB @0 (GAS)) [0](GAS) [[13]](EXP 0xffffffffffffffffffffffffffffffff 0) [[14]](SUB @0 (GAS)) [0] (GAS) [[15]](EXP 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.EXP(0x0, 0xC)) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x3, value=Op.EXP(0xC, 0x0)) - + Op.SSTORE(key=0x4, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x5, value=Op.EXP(0x0, 0xFFFFFFFFFFFFFFFF)) - + Op.SSTORE(key=0x6, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x7, value=Op.EXP(0x0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + lead = Op.MSTORE( + offset=0x0, + value=Op.GAS, + new_memory_size=0x20, + old_memory_size=0x0 if i == 0 else 0x20, ) - + Op.SSTORE(key=0x8, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.EXP( - 0x0, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - ), + exp_store = Op.SSTORE( + key=result_slot, + value=Op.EXP(base, exponent, exponent=exponent), + key_warm=False, + original_value=0, + new_value=result, ) - + Op.SSTORE(key=0xA, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0xB, value=Op.EXP(0xFFFFFFFFFFFFFFFF, 0x0)) - + Op.SSTORE(key=0xC, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0xD, value=Op.EXP(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0x0) + # The window's measured delta: both GAS reads cancel out of the + # two composites' sum. + measured = lead.gas_cost(fork) + exp_store.gas_cost(fork) + delta_store = Op.SSTORE( + key=delta_slot, + value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS), + key_warm=False, + original_value=0, + new_value=measured, ) - + Op.SSTORE(key=0xE, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0xF, - value=Op.EXP( - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - 0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) + code += lead + exp_store + delta_store + storage[result_slot] = result + storage[delta_slot] = measured + budget += measured + delta_store.gas_cost(fork) + 9 + + target = pre.deploy_contract(code=code + Op.STOP) + + # Fork-derived budget with an EIP-2200 stipend margin for the final + # store. + gas_limit = fork.transaction_intrinsic_cost_calculator()() + budget + 5_000 tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) - post = { - target: Account( - storage={ - 2: 2280, - 3: 1, - 4: 22127, - 6: 2627, - 8: 3027, - 10: 3827, - 11: 1, - 12: 22127, - 13: 1, - 14: 22127, - 15: 1, - 100: 22127, - }, - ), - } + post = {target: Account(storage=storage)} - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py index 82d92551fc3..57bec829c78 100644 --- a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py +++ b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py @@ -1,17 +1,23 @@ """ -Test_contract_creation_oo_gdont_leave_empty_contract_via_transaction. +Verify an out-of-gas contract creation leaves no account behind (the +Homestead-era bug left empty shells), while a sufficient budget creates a +codeless account whose init code called out to a storage writer. Ported from: state_tests/stHomesteadSpecific/contractCreationOOGdontLeaveEmptyContractViaTransactionFiller.json + +@manually-enhanced: Do not overwrite. The ported single case had silently +become success-only (its OOG arm was gone); both arms are restored with +fork-derived budgets, the init code's call budget is derived (the writer's +store is state-priced under EIP-8037), and the writer's slot plus the +created account's fields are asserted. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -27,72 +33,72 @@ "state_tests/stHomesteadSpecific/contractCreationOOGdontLeaveEmptyContractViaTransactionFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "enough_gas", + [ + pytest.param(True, id="created"), + pytest.param(False, id="oog_no_account"), + ], +) def test_contract_creation_oo_gdont_leave_empty_contract_via_transaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + enough_gas: bool, ) -> None: - """Test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """An OOG creation must not leave an account behind.""" + writer_store = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The init code calls the writer and deposits nothing. The forwarded + # budget is derived: with a zero reservoir the writer's state-priced + # store must fit inside its grant. + writer_needed = writer_store.gas_cost(fork) + call_code = Op.CALL( + gas=writer_needed + 1_000, + address=writer, + args_size=0x40, + ret_size=0x40, + address_warm=False, + value_transfer=False, + account_new=False, + new_memory_size=0x40, ) + initcode = call_code + Op.STOP - pre[sender] = Account(balance=0x10C8E0) - # Source: lll - # {(SSTORE 1 1)} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(CALL 50000 0x1000000000000000000000000000000000000001 0 0 64 0 64)} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ) - + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + execution = call_code.gas_cost(fork) + writer_needed + # The OOG arm dies charging the init code's own CALL (a failed inner + # call alone would not fail the creation): a bare 100-gas allowance + # over the fixed charges cannot cover the call's access cost even + # with the intrinsic estimate's slack. + gas_limit = overhead + (execution + 2_000 if enough_gas else 100) + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ), - gas_limit=96000, + data=initcode, + gas_limit=gas_limit, ) + created = compute_create_address(address=sender, nonce=0) + if enough_gas: + created_account: Account | type = Account(nonce=1, code=b"", balance=0) + writer_storage = {1: 1} + else: + created_account = Account.NONEXISTENT + writer_storage = {1: 0} post = { - compute_create_address(address=sender, nonce=0): Account(balance=0) + sender: Account(nonce=1), + created: created_account, + writer: Account(storage=writer_storage), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 065e20d0a538c6556692fdb8961bbddd7321e18a Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:02:01 +0200 Subject: [PATCH 43/55] fix(tests): un-skip Amsterdam attack-replay and callcode-suicide ported tests --- tests/ported_static/amsterdam_skip_list.txt | 12 +---- .../stAttackTest/test_crashing_transaction.py | 31 +++++++++--- .../test_callcallcallcode_001_suicide_end.py | 48 +++++++++++-------- 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 7353a6bc6aa..fc45312a611 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,10 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 97 - -# stAttackTest (1) -stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] +# Total entries: 95 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -26,9 +23,6 @@ stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam- stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -# stCallDelegateCodesCallCodeHomestead (1) -stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] - # stCreate2 (31) stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] @@ -90,10 +84,6 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP158Specific (0) - -# stHomesteadSpecific (0) - # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index 8f8320dd1b8..6e62bb785ee 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -1,8 +1,17 @@ """ -Https://ropsten.etherscan.io/tx/0x8ec445380649f6c75a042a438ea9256c2fab2a... +Verify the Ropsten "crashing transaction" attack replay: a creation +transaction whose init code loops CREATEing children while more than +50000 gas remains, then deposits its runtime code. Ported from: state_tests/stAttackTest/CrashingTransactionFiller.json + +@manually-enhanced: Do not overwrite. On pre-EIP-8037 forks the loop +drains to the ported child count (created nonce 124); under EIP-8037 an +iteration costs more than the loop's 50000-gas guard (new-account plus +code-deposit state gas spill from the frame), so the loop enters an +iteration it cannot afford and the whole creation deterministically +reverts — the split post pins both behaviors. """ import pytest @@ -11,6 +20,7 @@ Address, Alloc, Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -25,12 +35,14 @@ ["state_tests/stAttackTest/CrashingTransactionFiller.json"], ) @pytest.mark.valid_from("Cancun") +# Required: the sender is funded at the attack's historical nonce 3270. @pytest.mark.pre_alloc_mutable def test_crashing_transaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Https://ropsten.""" + """Replay the attack loop; EIP-8037 makes the creation revert.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xDE0B6B3A7640000, nonce=3270) @@ -95,13 +107,20 @@ def test_crashing_transaction( gas_price=11, ) - post = { - sender: Account(nonce=3271), - compute_create_address(address=sender, nonce=3270): Account( + created = compute_create_address(address=sender, nonce=3270) + if fork.is_eip_enabled(8037): + # An iteration's state gas exceeds the loop's 50000-gas guard, + # so the init frame dies mid-CREATE and no account survives. + created_account: Account | type = Account.NONEXISTENT + else: + created_account = Account( code=bytes.fromhex("60606040526008565b00"), balance=1, nonce=124, - ), + ) + post = { + sender: Account(nonce=3271), + created: created_account, } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py b/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py index 9d540998144..ae588b130d8 100644 --- a/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py +++ b/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py @@ -1,12 +1,15 @@ """ -Test_callcallcallcode_001_suicide_end. +Verify a CALLCODE -> CALLCODE -> (DELEGATECALL + SELFDESTRUCT) chain: +every store lands in the outermost target's storage and the SELFDESTRUCT +(running in the target's context) destroys the target. Ported from: state_tests/stCallDelegateCodesCallCodeHomestead/callcallcallcode_001_SuicideEndFiller.json -@manually-enhanced: Do not overwrite. The hardcoded inner-CALL gas -values (50k / 100k / 150k) were tuned to the pre-EIP-8037 gas budget. - +@manually-enhanced: Do not overwrite. The three call budgets are derived +bottom-up from the fork (each frame pays its stores' state gas from its +own grant under EIP-8037), and the target's post code is coupled to the +composed bytecode (the gas operand varies by fork). """ import pytest @@ -38,17 +41,16 @@ def test_callcallcallcode_001_suicide_end( pre: Alloc, fork: Fork, ) -> None: - """Test_callcallcallcode_001_suicide_end.""" - # EIP-8037 inner-CALL gas bumps: original values restored for - # pre-EIP-8037 forks; bumped values cover the per-storage state- - # gas spill into regular gas on Amsterdam. - outer_call_gas = 150000 - middle_call_gas = 100000 - inner_call_gas = 50000 - if fork.is_eip_enabled(8037): - outer_call_gas = 1000000 - middle_call_gas = 800000 - inner_call_gas = 100000 + """Chained callcode stores land in the target; it self-destructs.""" + # Derived bottom-up call budgets: with a zero state-gas reservoir + # each frame pays its stores' state gas from its own grant, so every + # level's budget covers its callee plus its own work with margin. + store_cost = Op.SSTORE( + key=0x3, value=0x1, key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) + inner_call_gas = store_cost + 5_000 + middle_call_gas = inner_call_gas + 2 * store_cost + 30_000 + outer_call_gas = middle_call_gas + store_cost + 30_000 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) @@ -72,8 +74,8 @@ def test_callcallcallcode_001_suicide_end( ) # Source: lll # { [[ 0 ]] (CALLCODE 150000 0 0 64 0 64 ) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( + target_code = ( + Op.SSTORE( key=0x0, value=Op.CALLCODE( gas=outer_call_gas, @@ -85,7 +87,10 @@ def test_callcallcallcode_001_suicide_end( ret_size=0x40, ), ) - + Op.STOP, + + Op.STOP + ) + target = pre.deploy_contract( # noqa: F841 + code=target_code, balance=0xDE0B6B3A7640000, nonce=0, address=Address(0xA74CA10B765DCDA3B60687F73F2881E2A56EDA64), # noqa: E501 @@ -141,9 +146,10 @@ def test_callcallcallcode_001_suicide_end( post = { target: Account( storage={0: 1, 1: 1, 2: 1, 3: 1}, - code=bytes.fromhex( - "6040600060406000600073eaf8c2ae0d01a880cea4e1aa88def5edd153d57b620249f0f260005500" # noqa: E501 - ), + # Coupled to the deployed bytecode (the gas operand varies + # by fork), proving SELFDESTRUCT in a CALLCODE context kills + # nothing here. + code=target_code, balance=0, nonce=0, ), From debb521ba6c99bb98422f2ea1fcfe36108111182 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:05:40 +0200 Subject: [PATCH 44/55] fix(tests): un-skip Amsterdam stSolidityTest via fork-derived state-gas surcharges --- tests/ported_static/amsterdam_skip_list.txt | 7 +------ .../test_recursive_create_contracts.py | 19 ++++++++++++++++--- .../test_test_contract_interaction.py | 18 +++++++++++++++--- .../test_test_contract_suicide.py | 18 +++++++++++++++--- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index fc45312a611..014211929e7 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 95 +# Total entries: 92 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -107,11 +107,6 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSolidityTest (3) -stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contracts[fork_Amsterdam] -stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] -stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] - # stSystemOperationsTest (5) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] diff --git a/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py b/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py index 836f16024d1..8f23e6ec41e 100644 --- a/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py +++ b/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py @@ -1,8 +1,14 @@ """ -Test_recursive_create_contracts. +Verify recursively self-creating Solidity contracts stop when the +transaction budget runs dry, leaving exactly one child. Ported from: state_tests/stSolidityTest/RecursiveCreateContractsFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -17,6 +23,7 @@ Transaction, compute_create_address, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +38,9 @@ def test_recursive_create_contracts( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_recursive_create_contracts.""" + """Recursive contract creation runs dry at the expected depth.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) sender = pre.fund_eoa(amount=0x1DCD6500) @@ -250,7 +258,12 @@ def test_recursive_create_contracts( sender=sender, to=contract_0, data=Bytes("a444f5e9") + Hash(0x304), - gas_limit=300000, + # EIP-8037 surcharge (0 before): the first child creation's + # new-account and code-deposit state gas spill into this + # budget; the recursion still runs dry at the ported depth. + gas_limit=300000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0xC8), value=1, ) diff --git a/tests/ported_static/stSolidityTest/test_test_contract_interaction.py b/tests/ported_static/stSolidityTest/test_test_contract_interaction.py index 0bcc63132a1..594a260899a 100644 --- a/tests/ported_static/stSolidityTest/test_test_contract_interaction.py +++ b/tests/ported_static/stSolidityTest/test_test_contract_interaction.py @@ -1,8 +1,14 @@ """ -Test_test_contract_interaction. +Verify a Solidity contract creating a child and interacting with it +through its dispatcher within the same transaction. Ported from: state_tests/stSolidityTest/TestContractInteractionFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -15,6 +21,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +36,9 @@ def test_test_contract_interaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_test_contract_interaction.""" + """Create a child contract and interact with it in one transaction.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0x5F5E100) @@ -165,7 +173,11 @@ def test_test_contract_interaction( sender=sender, to=target, data=Bytes("c0406226"), - gas_limit=350000, + # EIP-8037 surcharge (0 before): the created child's new-account + # and code-deposit state gas spill into this budget. + gas_limit=350000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0x81), value=1, ) diff --git a/tests/ported_static/stSolidityTest/test_test_contract_suicide.py b/tests/ported_static/stSolidityTest/test_test_contract_suicide.py index 1dd26ffea76..fc12dd3e319 100644 --- a/tests/ported_static/stSolidityTest/test_test_contract_suicide.py +++ b/tests/ported_static/stSolidityTest/test_test_contract_suicide.py @@ -1,8 +1,14 @@ """ -Test_test_contract_suicide. +Verify a Solidity contract that creates a child, tells it to +self-destruct, and re-calls it within the same transaction. Ported from: state_tests/stSolidityTest/TestContractSuicideFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -15,6 +21,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +36,9 @@ def test_test_contract_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_test_contract_suicide.""" + """Create a child, destroy it, and call it again in one transaction.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0x5F5E100) @@ -187,7 +195,11 @@ def test_test_contract_suicide( sender=sender, to=target, data=Bytes("c0406226"), - gas_limit=350000, + # EIP-8037 surcharge (0 before): the created child's new-account + # and code-deposit state gas spill into this budget. + gas_limit=350000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0x81), value=1, ) From 28d47824b2a188549681a86b18e3bb5477ea0ef1 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:10:00 +0200 Subject: [PATCH 45/55] fix(tests): un-skip Amsterdam double-selfdestruct-touch ported test --- tests/ported_static/amsterdam_skip_list.txt | 28 ++----------------- .../test_double_selfdestruct_touch_paris.py | 24 ++++++++++++---- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 014211929e7..f7e9c2ad716 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 92 +# Total entries: 70 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -56,28 +56,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (20) -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] -stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] -stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] -stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_oo_gafter_init_code_returndata_size.py::test_create_oo_gafter_init_code_returndata_size[fork_Amsterdam] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG2] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG3] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v1] - # stDelegatecallTestHomestead (4) stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] @@ -107,12 +85,10 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSystemOperationsTest (5) +# stSystemOperationsTest (3) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v1] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v2] # stTransactionTest (4) stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-d120] diff --git a/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py b/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py index 26364936fba..a50ef7de17f 100644 --- a/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py +++ b/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py @@ -1,11 +1,13 @@ """ -A single contract can execute SELFDESTRUCT multiple times using by... - -multiple times. The second and later SELFDESTRUCTs have little effect but can -touch some new beneficiary addresses. +Verify a contract executing SELFDESTRUCT twice in one transaction: the +second has little effect but touches a new beneficiary address. Ported from: state_tests/stSystemOperationsTest/doubleSelfdestructTouch_ParisFiller.yml + +@manually-enhanced: Do not overwrite. Both forwarded call budgets derive +from the fork (the callee's cold first-set store is state-priced under +EIP-8037 and must fit the grant). """ import pytest @@ -84,6 +86,16 @@ def test_double_selfdestruct_touch_paris( gas_limit=30000000, ) + # Derived budget for each selfdestruct call: the callee's cold + # first-set store is state-priced under EIP-8037 and must fit the + # grant; the margin covers its SLOAD, SELFDESTRUCT, and accesses. + sd_call_gas = ( + Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) + + 20_000 + ) + pre[sender] = Account(balance=0x5F5E102) pre[empty_account_1] = Account(balance=10) pre[empty_account_2] = Account(balance=10) @@ -119,7 +131,7 @@ def test_double_selfdestruct_touch_paris( + Op.SWAP1 + Op.POP( Op.CALL( - gas=0x11170, + gas=sd_call_gas, address=0x29E4504A3D2A0E0AE0EBBBEFEDD4570639B3EBEE, value=Op.DUP6, args_offset=Op.DUP1, @@ -130,7 +142,7 @@ def test_double_selfdestruct_touch_paris( ) + Op.SUB + Op.PUSH20[0x29E4504A3D2A0E0AE0EBBBEFEDD4570639B3EBEE] - + Op.PUSH3[0x11170] + + Op.PUSH3[sd_call_gas] + Op.CALL + Op.STOP, nonce=0, From b108707d30eb2da63d5ad5b24355b3d25fc0d11a Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:17:12 +0200 Subject: [PATCH 46/55] fix(tests): un-skip Amsterdam stTransactionTest ported tests via derived budgets --- .claude/commands/enhance-ported-test.md | 9 ++ tests/ported_static/amsterdam_skip_list.txt | 7 +- .../test_opcodes_transaction_init.py | 20 +++- .../test_store_gas_on_create.py | 96 ++++++++++++------- ...ides_and_internal_call_suicides_success.py | 30 +++++- 5 files changed, 115 insertions(+), 47 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 5aafcc152b5..ce96fcea0d4 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,15 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**Match the intrinsic calculator's kwargs to the transaction's shape.** +`fork.transaction_intrinsic_cost_calculator()()` defaults to +`sends_value=False`; under EIP-2780 a value-bearing transaction's intrinsic +includes the folded value-transfer cost (~5.9k), so a derived budget or +GAS-observation formula silently skews by that amount on Amsterdam only. +Pass `sends_value=True` when the tx carries value — or drop an incidental +tx `value` entirely (step 5) so the default holds. Validated on +`test_store_gas_on_create`. + **A creation transaction's top frame pays new-account state gas (EIP-8037) — but only for a fresh target.** When deriving a create-tx budget, the intrinsic calculator does not include the created account's diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f7e9c2ad716..411ecaea29f 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 70 +# Total entries: 66 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -90,8 +90,3 @@ stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] -# stTransactionTest (4) -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-d120] -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-side_effects] -stTransactionTest/test_store_gas_on_create.py::test_store_gas_on_create[fork_Amsterdam] -stTransactionTest/test_suicides_and_internal_call_suicides_success.py::test_suicides_and_internal_call_suicides_success[fork_Amsterdam-d1] diff --git a/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py b/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py index f93284cfd53..13e8cae466a 100644 --- a/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py +++ b/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py @@ -1,5 +1,6 @@ """ -Test_opcodes_transaction_init. +Verify each opcode family executes inside a creation transaction's init +code, including invalid-code and side-effect cases. Ported from: state_tests/stTransactionTest/Opcodes_TransactionInitFiller.json @@ -837,7 +838,7 @@ def test_opcodes_transaction_init( g: int, v: int, ) -> None: - """Test_opcodes_transaction_init.""" + """Run each opcode inside a creation transaction's init code.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) contract_1 = Address(0x0F572E5295C57F15886F9B263E2F6D2D6C7B5EC6) @@ -1467,7 +1468,16 @@ def test_opcodes_transaction_init( Op.SELFDESTRUCT(address=Op.ORIGIN), Bytes("ef"), Op.CALL( - gas=0xC350, + # Derived: the callee's cold first-set store is state-priced + # under EIP-8037 and must fit the grant. + gas=Op.SSTORE( + key=0x0, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + + 5_000, address=contract_0, value=Op.DUP1, args_offset=Op.DUP1, @@ -1502,7 +1512,9 @@ def test_opcodes_transaction_init( + Op.MSTORE8(offset=0x0, value=0xEF) + Op.RETURN(offset=0x0, size=0x1), ] - tx_gas = [400000] + # The d120 arm's nested CREATE adds a new-account state charge under + # EIP-8037 (0 before); every other arm keeps the ported budget. + tx_gas = [400000 + (fork.create_state_gas() if d == 120 else 0)] tx_value = [100000] tx = Transaction( diff --git a/tests/ported_static/stTransactionTest/test_store_gas_on_create.py b/tests/ported_static/stTransactionTest/test_store_gas_on_create.py index 2d97f61d885..0cad55501a9 100644 --- a/tests/ported_static/stTransactionTest/test_store_gas_on_create.py +++ b/tests/ported_static/stTransactionTest/test_store_gas_on_create.py @@ -1,17 +1,21 @@ """ -Test_store_gas_on_create. +Verify the gas a CREATE's init code observes when the creating contract is +entered directly by the transaction: the child receives all but one 64th +of what remains in the creating frame. Ported from: state_tests/stTransactionTest/StoreGasOnCreateFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode is kept, but the +transaction budget and the child's stored GAS observation are derived from +the fork (the ported absolute pin moved with every schedule change). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,51 +25,79 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CHILD_GAS_SLOT = 0xFD + @pytest.mark.ported_from( ["state_tests/stTransactionTest/StoreGasOnCreateFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_store_gas_on_create( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_store_gas_on_create.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0x17D78400) + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes, deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + creator = pre.deploy_contract( + code=setup + Op.POP(create_code) + Op.STOP, ) - # Source: lll - # { (MSTORE 0 0x5a60fd55) (CREATE 0 28 4)} - coinbase = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.CREATE(value=0x0, offset=0x1C, size=0x4) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Fork-derived budget with margin left after the child's work. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + setup.gas_cost(fork) + + create_code.gas_cost(fork) + + child_code.gas_cost(fork) + + 15_000 ) tx = Transaction( - sender=sender, - to=coinbase, - data=Bytes(""), - gas_limit=131882, - value=100, + sender=pre.fund_eoa(), + to=creator, + gas_limit=gas_limit, + ) + + # The child receives all but one 64th of what remains after the + # setup and the CREATE's own charges; its GAS read costs 2. + base = ( + gas_limit + - intrinsic + - setup.gas_cost(fork) + - create_code.gas_cost(fork) ) + assert base > 0, "the budget must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) post = { - compute_create_address(address=coinbase, nonce=0): Account( - storage={253: 0x12F39} + compute_create_address(address=creator, nonce=1): Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py b/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py index 661b972ec23..f2e923dbb01 100644 --- a/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py +++ b/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py @@ -1,8 +1,14 @@ """ -Test_suicides_and_internal_call_suicides_success. +Verify SELFDESTRUCT inside an internal call: the callee self-destructs to +a previously nonexistent beneficiary, which materializes only when the +forwarded gas covers the new-account charge. Ported from: state_tests/stTransactionTest/SuicidesAndInternalCallSuicidesSuccessFiller.json + +@manually-enhanced: Do not overwrite. The two forwarded-gas calldata words +derive from the fork's SELFDESTRUCT new-account cost (state-priced under +EIP-8037), keeping one arm starved and one funded on every fork. """ import pytest @@ -58,7 +64,7 @@ def test_suicides_and_internal_call_suicides_success( g: int, v: int, ) -> None: - """Test_suicides_and_internal_call_suicides_success.""" + """A funded SELFDESTRUCT materializes its beneficiary.""" coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) contract_0 = Address(0x0000000000000000000000000000000000000000) contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) @@ -126,11 +132,25 @@ def test_suicides_and_internal_call_suicides_success( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The calldata word is the gas forwarded to the self-destructing + # callee. Its SELFDESTRUCT pays a new-account charge for the funded + # beneficiary (state-priced under EIP-8037, spilling from the + # callee's grant), so both budgets derive from that cost: one starves + # it, one funds it with margin. + sd_cost = Op.SELFDESTRUCT.with_metadata( + address_warm=True, account_new=True + ).gas_cost(fork) tx_data = [ - Hash(0x55F0), - Hash(0xAAF0), + Hash(sd_cost // 2), + Hash(sd_cost + 5_000), + ] + tx_gas = [ + fork.transaction_intrinsic_cost_calculator()( + calldata=Hash(0), sends_value=True + ) + + sd_cost + + 40_000 ] - tx_gas = [150000] tx_value = [10] tx = Transaction( From d7b9c974c72ac7da2fba6fac2a310fa1a82a1494 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:21:20 +0200 Subject: [PATCH 47/55] fix(tests): un-skip Amsterdam refund600 via derived refund-cap formula --- tests/ported_static/amsterdam_skip_list.txt | 5 - .../stRefundTest/test_refund600.py | 116 +++++++++++------- 2 files changed, 70 insertions(+), 51 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 411ecaea29f..30412768fab 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -16,10 +16,6 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] # stCallCreateCallCodeTest (6) -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] @@ -65,7 +61,6 @@ stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.p # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] -stRefundTest/test_refund600.py::test_refund600[fork_Amsterdam] stRefundTest/test_refund_call_a.py::test_refund_call_a[fork_Amsterdam] stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d0] stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d1] diff --git a/tests/ported_static/stRefundTest/test_refund600.py b/tests/ported_static/stRefundTest/test_refund600.py index cce448a08c4..3f5def95619 100644 --- a/tests/ported_static/stRefundTest/test_refund600.py +++ b/tests/ported_static/stRefundTest/test_refund600.py @@ -1,18 +1,21 @@ """ -Test_refund600. +Verify the EIP-3529 refund cap over six storage clears: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund600Filler.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,64 +24,85 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund600Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund600( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund600.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xDC4EFA209AECDD4C2D5201A419EA27506151B4EC687F14A613229E310932491B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Six storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.POP(Op.SLOAD(key=0x1, key_warm=False)) + + Op.POP(Op.SLOAD(key=0x2, key_warm=False)) + # EXP(2, 0xFFFF) wraps to 0 mod 2^256, so this store is a no-op. + + Op.SSTORE( + key=0xA, + value=Op.EXP(0x2, 0xFFFF, exponent=0xFFFF), + key_warm=False, + original_value=0, + new_value=0, + ) + + Op.SSTORE( + key=0xB, + value=Op.BALANCE(address=Op.ADDRESS, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x6, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x989680) - # Source: lll - # { @@1 @@2 [[ 10 ]] (EXP 2 0xffff) [[ 11 ]] (BALANCE (ADDRESS)) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP(Op.SLOAD(key=0x1)) - + Op.POP(Op.SLOAD(key=0x2)) - + Op.SSTORE(key=0xA, value=Op.EXP(0x2, 0xFFFF)) - + Op.SSTORE(key=0xB, value=Op.BALANCE(address=Op.ADDRESS)) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xC09923E2275E4EE7822A1FEB5EEE1C18143575C7), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - target: Account(storage={11: 0xDE0B6B3A7640000}), - coinbase: Account(balance=0), - sender: Account(balance=0x8F5CF0), + target: Account( + storage={0xA: 0, 0xB: CONTRACT_BALANCE}, + ), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 89c727f0cfb41c67cc84af682fb776872fd2a529 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:30:40 +0200 Subject: [PATCH 48/55] fix(tests): un-skip Amsterdam 1024-depth recursion and initcode-call ported tests --- tests/ported_static/amsterdam_skip_list.txt | 9 - ...llcode_in_initcode_to_existing_contract.py | 250 +++++-------- ...o_existing_contract_with_value_transfer.py | 120 ++++--- .../test_call1024_oog.py | 333 +++++++++++------- .../test_callcode1024_oog.py | 312 +++++++++++----- .../test_call1024_oog.py | 312 +++++++++++----- .../test_delegatecall1024_oog.py | 271 +++++++++++--- ...tecall_in_initcode_to_existing_contract.py | 143 ++++---- 8 files changed, 1108 insertions(+), 642 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 30412768fab..79e930c6a20 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -11,13 +11,8 @@ # Total entries: 66 # stCallCodes (3) -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] -stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] # stCallCreateCallCodeTest (6) -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] # stCreate2 (31) stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] @@ -53,10 +48,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] # stDelegatecallTestHomestead (4) -stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] -stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] -stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py index 68451f0f3ad..5e2547c7a1f 100644 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py @@ -1,198 +1,132 @@ """ -Callcode inside create/create2 contract init to existing contract. +Verify a CALLCODE made from inside init code to an existing contract. + +The created account's init code CALLCODEs an already-deployed contract, +so that contract's code runs in the freshly created account's context: +its storage write lands in the created account (never in the existing +contract), and the transferred value stays with the created account. Ported from: state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The calldata-dispatch entry +contract is collapsed into a direct transaction to the create-runner, +the init code is composed and shared with the CREATE2 address +computation, sub-calls forward all gas (EIP-8037-proof), and the post +also pins the created account's code/nonce/balance and that the +existing contract's own storage stays untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, - Hash, + Bytecode, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) -from execution_testing.vm import Op +from execution_testing.vm import Op, Opcode REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Endowment the runner sends into the creation; the init code's CALLCODE +# then names the same amount, a self-to-self transfer the created +# account's balance must cover and keep. +CREATE_ENDOWMENT = 1 +CALLCODE_VALUE = CREATE_ENDOWMENT +RUNNER_BALANCE = 10_000 +CREATE2_SALT = 0 + +# Written by the init code with the CALLCODE's success flag. +SUCCESS_FLAG_SLOT = 1 +# Written by the existing contract's code, in the caller's context. +DELEGATE_SLOT = 2 + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Constantinople") +@pytest.mark.parametrize("opcode", [Op.CREATE, Op.CREATE2]) def test_callcode_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: Opcode, ) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1100000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000000) - contract_2 = Address(0x2000000000000000000000000000000000000000) - contract_3 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CALLCODE in init code runs in the created account's context.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_SLOT, value=1) + Op.STOP, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (CALL 300000 (CALLDATALOAD 0) 0 0 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0x493E0, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + initcode = ( + Op.SSTORE( + key=SUCCESS_FLAG_SLOT, + value=Op.CALLCODE(address=existing, value=CALLCODE_VALUE), ) - + Op.STOP, - nonce=0, - address=Address(0x1100000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(seq (CREATE2 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) 0) )} # noqa: E501 - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x0] - + Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE2 + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x2000000000000000000000000000000000000000), # noqa: E501 ) - # Source: lll - # {(seq (CREATE 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) ) )} # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0xF, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE - + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + initcode_bytes = bytes(initcode) + + if opcode == Op.CREATE2: + create_call = Op.CREATE2( + value=CREATE_ENDOWMENT, + offset=0, + size=len(initcode_bytes), + salt=CREATE2_SALT, + ) + else: + create_call = Op.CREATE( + value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes) ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + create_call + Op.STOP, + balance=RUNNER_BALANCE, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x11B62573BE8F72B4085BAFE5B675B3E7F08ED522): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(contract_1, left_padding=True), - Hash(contract_2, left_padding=True), - ] - tx_gas = [1000000] + if opcode == Op.CREATE2: + created = compute_create_address( + address=runner, + salt=CREATE2_SALT, + initcode=initcode, + opcode=Op.CREATE2, + ) + else: + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=runner, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={SUCCESS_FLAG_SLOT: 1, DELEGATE_SLOT: 1}, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, + ), + # The existing contract's own storage must stay untouched. + existing: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py index 4822486a1ea..f58442f3cf0 100644 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py +++ b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py @@ -1,18 +1,28 @@ """ -Callcode inside create/create2 contract init to existing contract. +Verify a value-bearing CALLCODE made from inside init code to an +existing contract. + +The runner endows the creation with value; the init code CALLCODEs an +already-deployed contract naming that same value, so the existing +contract's code runs in the created account's context: its storage +write lands in the created account, and the value transfer is +self-to-self, leaving the endowment with the created account. Ported from: state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json + +@manually-enhanced: Do not overwrite. The raw-word init code is +composed, sub-calls forward all gas (EIP-8037-proof), the transaction +budget is maxed, and the post also pins the created account's +code/nonce/balance and that the existing contract's own storage stays +untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, StateTestFiller, Transaction, compute_create_address, @@ -22,72 +32,82 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Endowment the runner sends into the creation; the init code's CALLCODE +# then names the same amount, a self-to-self transfer the created +# account's balance must cover and keep. +CREATE_ENDOWMENT = 5 +CALLCODE_VALUE = CREATE_ENDOWMENT +RUNNER_BALANCE = 10_000 + +# Written by the init code with the CALLCODE's success flag. +SUCCESS_FLAG_SLOT = 0 +# Written by the existing contract's code, in the caller's context. +DELEGATE_SLOT = 2 + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("SpuriousDragon") def test_callcode_in_initcode_to_existing_contract_with_value_transfer( state_test: StateTestFiller, pre: Alloc, ) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A value-bearing CALLCODE in init code keeps the endowment.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_SLOT, value=1) + Op.STOP, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + initcode = ( + Op.SSTORE( + key=SUCCESS_FLAG_SLOT, + value=Op.CALLCODE(address=existing, value=CALLCODE_VALUE), + ) + + Op.STOP ) + initcode_bytes = bytes(initcode) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x6040600060406000600573945304eb96065b2a98b57a48a06ae28d285a71b562) (MSTORE 32 0x0186a0f260005500000000000000000000000000000000000000000000000000) (CREATE 5 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x6040600060406000600573945304EB96065B2A98B57A48A06AE28D285A71B562, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x186A0F260005500000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.CREATE(value=0x5, offset=0x0, size=0x40) + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + + Op.CREATE(value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes)) + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 + balance=RUNNER_BALANCE, ) + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, + sender=pre.fund_eoa(), + to=runner, ) post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 2: 1}, balance=5 + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={SUCCESS_FLAG_SLOT: 1, DELEGATE_SLOT: 1}, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, ), + # The existing contract's own storage must stay untouched. + existing: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py index 127b02327fd..c73e13d0235 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py @@ -1,152 +1,249 @@ """ -Calldepth with oog. +Verify a self-recursive CALL chain that terminates by out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a call to itself (keeping a 10,000 reserve for its post-call stores), +then records the call's success flag and a depth marker. Levels too deep +to afford their stores halt and roll back, so the surviving storage pins +the exact depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.CALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), - pytest.param( - 0, - 3, - 0, - id="-g3", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [13_120_826, 9_320_826, 15_720_826, 11_220_826], ) -@pytest.mark.pre_alloc_mutable def test_call1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Calldepth with oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x878BC1C3D660907B056E31C854A309F7EF1B4C4, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x0878BC1C3D660907B056E31C854A309F7EF1B4C4), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 113, 1: 1, 2: 0x1B969})}, - }, - { - "indexes": {"data": -1, "gas": 2, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 3, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 124, 1: 1, 2: 0x1E461})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 9320826, 15720826, 11220826] - tx_value = [10] + """Pin the depth an OOG-terminated CALL self-recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py index 2b2f42bd104..c475d6e6a48 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py @@ -1,130 +1,250 @@ """ -Calldepth and oog. +Verify a self-recursive CALLCODE chain that terminates by out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a CALLCODE to its own address (same code, same storage context, keeping +a 10,000 reserve for its post-call stores), then records the call's +success flag and a depth marker. Levels too deep to afford their stores +halt and roll back, so the surviving storage pins the exact depth the +budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.CALLCODE + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [15_720_826, 13_120_826], ) -@pytest.mark.pre_alloc_mutable def test_callcode1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Calldepth and oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALLCODE (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x1B803058288DC00000F98311B059597434253374, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x1B803058288DC00000F98311B059597434253374), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [15720826, 13120826] - tx_value = [10] + """Pin the depth an OOG-terminated CALLCODE self-recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py index 2eb7e09753e..96a5cf26fab 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py @@ -1,129 +1,251 @@ """ -Test_call1024_oog. +Verify a self-recursive DELEGATECALL chain that terminates by +out-of-gas (the Homestead delegatecall suite's variant of Call1024OOG). + +Each level bumps a shared depth counter, forwards almost all its gas to +a DELEGATECALL to its own address (same code, same storage context, +keeping a 10,000 reserve for its post-call stores), then records the +call's success flag and a depth marker. Levels too deep to afford their +stores halt and roll back, so the surviving storage pins the exact +depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.DELEGATECALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [13_120_826, 15_720_826], ) -@pytest.mark.pre_alloc_mutable def test_call1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Test_call1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 15720826] - tx_value = [10] + """Pin the depth an OOG-terminated DELEGATECALL recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py index 42959c58111..2bc71a23c44 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py @@ -1,17 +1,29 @@ """ -Test_delegatecall1024_oog. +Verify a self-recursive DELEGATECALL chain that terminates by +out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a DELEGATECALL to its own address (same code, same storage context, +keeping a 10,000 reserve for its post-call stores), then records the +call's success flag and a depth marker. Levels too deep to afford their +stores halt and roll back, so the surviving storage pins the exact +depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,65 +32,220 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.DELEGATECALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [15_720_826], +) def test_delegatecall1024_oog( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + tx_gas_limit: int, ) -> None: - """Test_delegatecall1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) + """Pin the depth an OOG-terminated DELEGATECALL recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=15720826, - value=10, + gas_limit=tx_gas_limit, ) - post = {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})} + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py index e20c162ae59..2ceeefd5137 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py @@ -1,18 +1,28 @@ """ -Test_delegatecall_in_initcode_to_existing_contract. +Verify a DELEGATECALL made from inside init code to an existing +contract. + +The created account's init code DELEGATECALLs an already-deployed +contract, so that contract's code runs in the freshly created account's +context with the init frame's caller preserved: both the delegate and +the init code itself observe the creating contract as CALLER, and every +storage write lands in the created account, never in the delegate. Ported from: state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The port's unused second creator +contract is deleted, the raw-word init code is composed, the delegate +call forwards all gas (EIP-8037-proof), the transaction budget is +maxed, and the post also pins the created account's code/nonce/balance +and that the delegate's own storage stays untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, StateTestFiller, Transaction, compute_create_address, @@ -22,86 +32,91 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_ENDOWMENT = 1 +RUNNER_BALANCE = 10_000 + +# Written by the init code with the DELEGATECALL's success flag. +DELEGATE_RESULT_SLOT = 0 +# Written by the init code with the CALLER it observes (the runner). +INITCODE_CALLER_SLOT = 1 +# Written by the delegate's code, in the created account's context. +DELEGATE_WRITE_SLOT = 2 +# Written by the delegate with the CALLER it observes (still the +# runner: DELEGATECALL preserves the init frame's caller). +DELEGATE_CALLER_SLOT = 0xB + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("SpuriousDragon") def test_delegatecall_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, ) -> None: - """Test_delegatecall_in_initcode_to_existing_contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000001) - contract_2 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A DELEGATECALL in init code runs in the created account.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_WRITE_SLOT, value=1) + + Op.SSTORE(key=DELEGATE_CALLER_SLOT, value=Op.CALLER) + + Op.STOP, ) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x604060006040600073945304eb96065b2a98b57a48a06ae28d285a71b5620186) (MSTORE 32 0xa0f4600055336001550000000000000000000000000000000000000000000000) (CREATE 1 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x604060006040600073945304EB96065B2A98B57A48A06AE28D285A71B5620186, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0xA0F4600055336001550000000000000000000000000000000000000000000000, # noqa: E501 + initcode = ( + Op.SSTORE( + key=DELEGATE_RESULT_SLOT, + value=Op.DELEGATECALL(address=existing), ) - + Op.CREATE(value=0x1, offset=0x0, size=0x40) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (MSTORE 0 0x6001600055) (CREATE 1 27 5) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6001600055) - + Op.CREATE(value=0x1, offset=0x1B, size=0x5) - + Op.STOP, - balance=1000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + + Op.SSTORE(key=INITCODE_CALLER_SLOT, value=Op.CALLER) + + Op.STOP ) - # Source: lll - # { (SSTORE 2 1) [[ 11 ]] (CALLER) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) - + Op.SSTORE(key=0xB, value=Op.CALLER) + initcode_bytes = bytes(initcode) + + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + + Op.CREATE(value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes)) + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 + balance=RUNNER_BALANCE, ) + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, + sender=pre.fund_eoa(), + to=runner, ) post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 1: contract_0, 2: 1, 11: contract_0}, - balance=1, + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={ + DELEGATE_RESULT_SLOT: 1, + INITCODE_CALLER_SLOT: runner, + DELEGATE_WRITE_SLOT: 1, + DELEGATE_CALLER_SLOT: runner, + }, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, ), + # The delegate's own storage must stay untouched. + existing: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 2d191a779b4cd66dc6210a875aab4db1fe1702aa Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:43:31 +0200 Subject: [PATCH 49/55] fix(tests): un-skip Amsterdam stCreateTest remainder via derived budgets and boundaries --- .claude/commands/enhance-ported-test.md | 10 + tests/ported_static/amsterdam_skip_list.txt | 12 - ...tract_create_ne_contract_in_init_oog_tr.py | 208 ++++--- ..._contract_then_call_to_non_existent_acc.py | 133 ++-- ...ate_oo_gafter_init_code_returndata_size.py | 120 ++-- .../test_create_oog_from_call_refunds.py | 80 ++- .../stCreateTest/test_create_results.py | 589 +++++++----------- .../test_transaction_collision_to_empty2.py | 157 ++--- ...transaction_collision_to_empty_but_code.py | 155 ++--- ...ransaction_collision_to_empty_but_nonce.py | 110 ++-- 10 files changed, 710 insertions(+), 864 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index ce96fcea0d4..b15231cb32b 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,16 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**EIP-8037 repriced the code deposit's regular part — boundaries beware.** +On 8037 forks the deposit charges only the keccak word cost +(`OPCODE_KECCAK256_PER_WORD * ceil32(len)/32`, ~6 gas) as regular gas plus +`len * 1530` state; `fork.gas_costs().CODE_DEPOSIT_PER_BYTE` (200) is the +*pre-8037* constant. Using 200/byte in a *sufficiency* budget merely +overshoots (safe); using it in a one-gas-short *boundary* silently funds +the deposit on Amsterdam. Branch on `fork.is_eip_enabled(8037)` for exact +deposit boundaries. Validated on +`test_create_oo_gafter_init_code_returndata_size`. + **Match the intrinsic calculator's kwargs to the transaction's shape.** `fork.transaction_intrinsic_cost_calculator()()` defaults to `sends_value=False`; under EIP-2780 a value-bearing transaction's intrinsic diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 79e930c6a20..cb02f797b6b 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -15,7 +15,6 @@ # stCallCreateCallCodeTest (6) # stCreate2 (31) -stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] @@ -23,7 +22,6 @@ stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refu stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d0] stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d1] stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d2] -stCreate2/test_create2no_cash.py::test_create2no_cash[fork_Amsterdam-d1] stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g0] stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g1] stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v0] @@ -50,12 +48,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept # stDelegatecallTestHomestead (4) # stRefundTest (7) -stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] -stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] -stRefundTest/test_refund_call_a.py::test_refund_call_a[fork_Amsterdam] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d0] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d1] -stRefundTest/test_refund_tx_to_suicide.py::test_refund_tx_to_suicide[fork_Amsterdam] # stRevertTest (12) stRevertTest/test_loop_calls_depth_then_revert.py::test_loop_calls_depth_then_revert[fork_Amsterdam] @@ -66,10 +58,6 @@ stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_ stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v1] stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v0] stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v1] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d0-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d1-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] # stSystemOperationsTest (3) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py index c5b2fbd670f..bac280a1984 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py @@ -1,140 +1,154 @@ """ -Test_create_e_contract_create_ne_contract_in_init_oog_tr. +Verify a contract-creation transaction whose init code first calls an +existing contract and then CREATEs a child: with a full budget both the +call and the nested creation land (the child at the creator's nonce-1 +address, not nonce 0); with a starved budget the callee and the whole +creation fail together. Ported from: state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + EIP-8037 top-frame and nested-create state gas + composed +code costs), the callee call forwards all gas instead of a ported fixed +budget, and the nested child is now asserted at its real nonce-1 address +(the port only checked the vacuous nonce-0 address). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALLEE_STORED = 0xC + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) def test_create_e_contract_create_ne_contract_in_init_oog_tr( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, ) -> None: - """Test_create_e_contract_create_ne_contract_in_init_oog_tr.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """Budget decides how far a creation's call-then-CREATE init gets.""" + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Callee: one cold zero->non-zero store observed in the post. + callee_code = ( + Op.SSTORE( + key=0x1, + value=CALLEE_STORED, + key_warm=False, + original_value=0, + new_value=CALLEE_STORED, + ) + + Op.STOP ) + callee = pre.deploy_contract(code=callee_code) - # Source: lll - # {[[1]]12} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, + # Child init code: return a small runtime code from memory. + child_runtime = Op.SSTORE(key=0x0, value=CALLEE_STORED) + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - len(bytes(child_runtime)), + size=len(bytes(child_runtime)), ) + child_initcode_bytes = bytes(child_initcode) - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 12}), - compute_create_address(address=sender, nonce=0): Account( - nonce=2 - ), - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 0}), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Transaction init code: call the callee (forwarding all gas), then + # CREATE the child from memory; deploys nothing itself. + call_code = Op.POP(Op.CALL(address=callee)) + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_initcode_bytes, "big"), + new_memory_size=0x20, + old_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(child_initcode_bytes), + size=len(child_initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_initcode_bytes), + ) + initcode = call_code + stage_code + create_code - tx_data = [ - Op.POP( - Op.CALL( - gas=0xEA60, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + if oog: + # Enough to start executing, but the forwarded 63/64 undercuts + # the callee's store and the CREATE is unaffordable after it. + gas_limit = intrinsic + callee_code.gas_cost(fork) // 2 + else: + # Everything must land: the fresh create target's top-frame + # state gas (EIP-8037), the callee, and the nested creation's + # peak charge plus the child's execution and code deposit. + runtime_size = len(bytes(child_runtime)) + child_total = ( + child_initcode.gas_cost(fork) + + runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=runtime_size) ) - + Op.MSTORE(offset=0x0, value=0x64600C6000556000526005601BF3) - + Op.CREATE(value=0x0, offset=0x12, size=0xE), - ] - tx_gas = [160000, 60000] + needed = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + + callee_code.gas_cost(fork) + + child_total + ) + # Headroom for the 63/64 withhold at the call and the CREATE. + gas_limit = needed + needed // 63 tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + # The nested CREATE runs while the creator's nonce is 1 (EIP-161), + # so the child lands at the nonce-1 address and the nonce-0 address + # must stay empty. + child = compute_create_address(address=created, nonce=1) + child_at_nonce0 = compute_create_address(address=created, nonce=0) + + if oog: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: 0}), + created: Account.NONEXISTENT, + child: Account.NONEXISTENT, + child_at_nonce0: Account.NONEXISTENT, + } + else: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: CALLEE_STORED}), + created: Account(nonce=2, code=b""), + child: Account(nonce=1, code=bytes(child_runtime), storage={}), + child_at_nonce0: Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py index f752c8b1f6b..b85370ef6d2 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py @@ -1,18 +1,24 @@ """ -Test_create_e_contract_then_call_to_non_existent_acc. +Verify a CREATE of an empty contract followed by a CALL to a non-existent +account: both operations are gas-measured, the created address and the +call's success flag are stored, and the absent callee stays non-existent. Ported from: state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json + +@manually-enhanced: Do not overwrite. The ported absolute GAS snapshots +(slots 0/2/100) are re-expressed as two CodeGasMeasure windows asserted +via the fork's gas model, the created address and call flag stay in the +measured windows' SSTOREs, and the callee is a dynamic non-existent +account called with all gas forwarded. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,80 +28,95 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_GAS_SLOT = 0x0 +ADDRESS_SLOT = 0x1 +CALL_GAS_SLOT = 0x2 +FLAG_SLOT = 0x3 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_e_contract_then_call_to_non_existent_acc( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_e_contract_then_call_to_non_existent_acc.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure a CREATE of an empty contract and a call to no account.""" + absent = pre.nonexistent_account() + + # CREATE over never-written memory: the all-STOP init code deposits + # nothing, leaving an empty account with nonce 1. + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + # Storing the created address keeps it observable and folds the + # store into the measured window (the address is non-zero, so the + # placeholder new_value only sizes the zero->non-zero transition). + store_create = Op.SSTORE( + ADDRESS_SLOT, + create_code, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # A value-less call to an absent account creates nothing on any + # fork; the callee consumes no gas, so the window measures only the + # cold CALL itself. Storing the success flag keeps it observable. + call_code = Op.CALL( + address=absent, + address_warm=False, + value_transfer=False, + account_new=False, + ) + store_flag = Op.SSTORE( + FLAG_SLOT, + call_code, + key_warm=False, + original_value=0, + new_value=1, ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (CALL 60000 0xe1ecf98489fa9ed60a664fc4998db699cfa39d40 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=store_create, + sstore_key=CREATE_GAS_SLOT, + ) + + CodeGasMeasure( + code=store_flag, + sstore_key=CALL_GAS_SLOT, ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, ) post = { - contract_0: Account( + contract: Account( storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6F50B, + CREATE_GAS_SLOT: store_create.gas_cost(fork), + ADDRESS_SLOT: compute_create_address( + address=contract, nonce=1 + ), + CALL_GAS_SLOT: store_flag.gas_cost(fork), + FLAG_SLOT: 1, }, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - Address( - 0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40 - ): Account.NONEXISTENT, + compute_create_address(address=contract, nonce=1): Account( + nonce=1, code=b"", balance=0 + ), + absent: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py index 6f458740e9d..aaa2962d0ec 100644 --- a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py +++ b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py @@ -1,17 +1,23 @@ """ -Calls a contract that runs CREATE which deploy a code. then OOG happens... +Verify a CREATE whose child completes its init code but cannot afford the +code deposit: the creation fails (no account is deployed), yet the parent +frame survives on its 63/64 retention and the transaction succeeds. Ported from: state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json + +@manually-enhanced: Do not overwrite. The gas limit is derived from the +fork so the child's 63/64 grant covers init execution but not the code +deposit (including EIP-8037 deposit state gas), and the budget carries +the CREATE's peak new-account state charge, refunded when the child +fails. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,57 +27,99 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 1 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_oo_gafter_init_code_returndata_size( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """CREATE fails at the code deposit; the parent frame completes.""" + # The child would deploy two stores; it never runs, only its deposit + # price matters. + child_runtime = Op.SSTORE(key=0x1, value=0x1) + Op.SSTORE( + key=0x2, value=0x1 + ) + runtime_size = len(bytes(child_runtime)) + + # Child init code: return the runtime code from memory. + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - runtime_size, + size=runtime_size, + ) + initcode_bytes = bytes(child_initcode) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Parent: stage the init code in memory, CREATE from it, then read + # RETURNDATASIZE (zero after the deposit failure) before stopping. + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(initcode_bytes), + size=len(initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + tail_code = Op.POP + Op.EXP(0x2, Op.RETURNDATASIZE) + Op.STOP + contract = pre.deploy_contract(code=stage_code + create_code + tail_code) - # Source: lll - # { (MSTORE 0 0x6960016001556001600255600052600a6016f3) (CREATE 0 13 19) (EXP 2 (RETURNDATASIZE)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, value=0x6960016001556001600255600052600A6016F3 + # Grant the child enough for its init execution but one gas short of + # the code deposit, so the deposit is what fails. Under EIP-8037 the + # regular deposit cost is only the keccak word cost (the per-byte + # price moved into deposit state gas); before it, 200 per byte. + child_exec = child_initcode.gas_cost(fork) + if fork.is_eip_enabled(8037): + deposit_regular = fork.gas_costs().OPCODE_KECCAK256_PER_WORD * ( + (runtime_size + 31) // 32 ) - + Op.POP(Op.CREATE(value=0x0, offset=0xD, size=0x13)) - + Op.EXP(0x2, Op.RETURNDATASIZE) - + Op.STOP, - nonce=0, + else: + deposit_regular = runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit = deposit_regular + fork.code_deposit_state_gas( + code_size=runtime_size + ) + available = (child_exec + deposit - 1) * 64 // 63 + forwarded = available - available // 64 + assert child_exec <= forwarded < child_exec + deposit, ( + "63/64 grant must cover init execution but not the deposit" + ) + # The parent's 1/64 retention must still afford the tail. + assert available // 64 > tail_code.gas_cost(fork), ( + "retention must cover the post-CREATE tail" + ) + + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()(sends_value=True) + + stage_code.gas_cost(fork) + + create_code.gas_cost(fork) + + available ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=55054, - value=1, + sender=pre.fund_eoa(), + to=contract, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(balance=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The transferred value proves the parent frame completed. + contract: Account(balance=TX_VALUE, storage={}), + compute_create_address(address=contract, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py index 665dccfb452..c5ca79b876a 100644 --- a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py +++ b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create_oog_from_call_refunds. +Verify that gas refunds earned during (or via calls made from) a CREATE's +init code cannot rescue the creation from an out-of-gas failure: each OoG +variant burns the whole budget through the dispatcher's INVALID, while +the NoOoG variants deploy and keep their refunds. Ported from: state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The gas limit and the sender's +exact prefund are derived from the fork so the child's 63/64 grant +covers the deepest nested-create chain (EIP-8037 state gas included) +yet stays below the 5000-byte code-deposit price that drives the OoG +arms. """ import pytest @@ -12,7 +21,6 @@ Address, Alloc, Bytes, - Environment, Hash, StateTestFiller, Transaction, @@ -27,6 +35,11 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Returning this much code makes the deposit unaffordable in the OoG +# variants; the gas limit below is derived against it. +OOG_DEPOSIT_SIZE = 0x1388 +TX_GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml"], @@ -190,8 +203,7 @@ def test_create_oog_from_call_refunds( g: int, v: int, ) -> None: - """Test_create_oog_from_call_refunds.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) + """Refunds earned inside a creation cannot avert its OOG.""" contract_0 = Address(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) contract_1 = Address(0x000000000000000000000000000000000000001A) contract_2 = Address(0x000000000000000000000000000000000000001B) @@ -225,15 +237,38 @@ def test_create_oog_from_call_refunds( key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # Budget: covers the deepest NoOoG chain (a nested CREATE with + # EIP-8037 peak state gas at each level) while any init frame's + # 63/64 grant stays below the OoG arms' code-deposit price. The + # intrinsic bound uses all-non-zero calldata (selector + address). + gas_costs = fork.gas_costs() + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"\xff" * 36 + ) + create_op = Op.CREATE( + value=0x0, + offset=0x0, + size=0x40, + new_memory_size=0x40, + init_code_size=0x40, + ) + gas_limit = ( + intrinsic + + create_op.gas_cost(fork) + + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + ) + deposit_price = ( + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + # No init frame can receive enough to pay the OoG arms' deposit. + grant_bound = gas_limit - intrinsic - create_op.regular_cost(fork) + assert grant_bound * 63 // 64 < deposit_price, ( + "63/64 grant must stay below the OoG deposit price" ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # The exact prefund makes "all gas burned" observable as balance 0. + pre[sender] = Account(balance=gas_limit * TX_GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -293,7 +328,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000001B), # noqa: E501 ) @@ -463,7 +498,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000002B), # noqa: E501 ) @@ -559,7 +594,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000004B), # noqa: E501 ) @@ -582,7 +617,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000003B), # noqa: E501 ) @@ -654,7 +689,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000005B), # noqa: E501 ) @@ -744,7 +779,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000006B), # noqa: E501 ) @@ -788,7 +823,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -854,7 +889,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -1122,15 +1157,14 @@ def test_create_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] - tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], - gas_limit=tx_gas[g], + gas_limit=gas_limit, + gas_price=TX_GAS_PRICE, nonce=1, error=_exc, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_results.py b/tests/ported_static/stCreateTest/test_create_results.py index 3d9a852d13e..1a92cd79ad2 100644 --- a/tests/ported_static/stCreateTest/test_create_results.py +++ b/tests/ported_static/stCreateTest/test_create_results.py @@ -1,223 +1,139 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Verify the value CREATE/CREATE2 leaves on the stack, the returndata, and +the deployed code for each constructor outcome — success, OOG, empty +revert, revert with data, empty deploy, and in-init SELFDESTRUCT — plus +each CALL-kind's result when calling the successfully created contract, +and the frame-aborting RETURNDATACOPY past an empty return buffer. + +Written by Ori Pomerantz (qbzzt1@gmail.com). Ported from: state_tests/stCreateTest/CreateResultsFiller.yml + +@manually-enhanced: Do not overwrite. The ported PUSH2 0xFFFF sub-call +budgets are replaced by length-preserving forward-all-gas sequences (the +fixed budget starves EIP-8037 state gas), the created accounts are now +asserted per case (code, nonce, or non-existence), and the per-case +posts are an explicit switch over the decoded calldata triple. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, Bytes, - Environment, + Fork, Hash, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create2_address, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The dispatcher's LLL-derived bytecode hardcodes every jump target and +# code-copy offset, so all edits below preserve instruction lengths. +CONTRACT_1_ADDRESS = 0x60A7 +CREATE2_SALT = 0x5A17 +# PC values the dispatcher snapshots right after the create (slot 0x20) +# and after the call section (slot 0x21); fixed by the code layout. +CREATE_PC = 295 +CALL_PC = 551 +# Below the SHA3-OOG constructor's memory-expansion cost (which must +# fail) and above Amsterdam's state-gas needs (which must not). +TX_GAS = 9_437_184 + +# Calldata triples (creation kind, call kind, constructor kind) in the +# ported data order. creation: 1=CREATE, 2=CREATE2. call: 0=none, +# 1=CALL, 2=CALLCODE, 3=DELEGATECALL, 4=STATICCALL. constructor: +# 0/4=success, 1=OOG, 2=revert, 3=revert-with-data, 5=empty deploy, +# 6=SELFDESTRUCT in init (4 also RETURNDATACOPYs past the empty +# return buffer, aborting the whole dispatcher frame). +CASES: list[tuple[int, int, int]] = [ + (1, 1, 0), + (1, 2, 0), + (1, 3, 0), + (1, 4, 0), + (2, 1, 0), + (2, 2, 0), + (2, 3, 0), + (2, 4, 0), + (1, 0, 1), + (2, 0, 1), + (1, 0, 2), + (2, 0, 2), + (1, 0, 5), + (2, 0, 5), + (1, 0, 6), + (2, 0, 6), + (1, 0, 3), + (2, 0, 3), + (1, 1, 4), + (1, 2, 4), + (1, 3, 4), + (1, 4, 4), + (2, 1, 4), + (2, 2, 4), + (2, 3, 4), + (2, 4, 4), +] + +# Constructor fragment (offset, size) within the dispatcher's code: +# the dispatcher CODECOPYs these windows as the init code it creates +# from, keyed by the constructor kind. +FRAGMENTS: dict[int, tuple[int, int]] = { + 0: (0x250, 0x21), + 1: (0x271, 0x29), + 2: (0x29A, 0x26), + 3: (0x2C0, 0x2C), + 4: (0x250, 0x21), + 5: (0x2EC, 0x28), + 6: (0x314, 0x2A), +} + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateResultsFiller.yml"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), - pytest.param( - 4, - 0, - 0, - id="d4", - ), - pytest.param( - 5, - 0, - 0, - id="d5", - ), - pytest.param( - 6, - 0, - 0, - id="d6", - ), - pytest.param( - 7, - 0, - 0, - id="d7", - ), - pytest.param( - 8, - 0, - 0, - id="d8", - ), - pytest.param( - 9, - 0, - 0, - id="d9", - ), - pytest.param( - 10, - 0, - 0, - id="d10", - ), - pytest.param( - 11, - 0, - 0, - id="d11", - ), - pytest.param( - 12, - 0, - 0, - id="d12", - ), - pytest.param( - 13, - 0, - 0, - id="d13", - ), - pytest.param( - 14, - 0, - 0, - id="d14", - ), - pytest.param( - 15, - 0, - 0, - id="d15", - ), - pytest.param( - 16, - 0, - 0, - id="d16", - ), - pytest.param( - 17, - 0, - 0, - id="d17", - ), - pytest.param( - 18, - 0, - 0, - id="d18", - ), - pytest.param( - 19, - 0, - 0, - id="d19", - ), - pytest.param( - 20, - 0, - 0, - id="d20", - ), - pytest.param( - 21, - 0, - 0, - id="d21", - ), - pytest.param( - 22, - 0, - 0, - id="d22", - ), - pytest.param( - 23, - 0, - 0, - id="d23", - ), - pytest.param( - 24, - 0, - 0, - id="d24", - ), - pytest.param( - 25, - 0, - 0, - id="d25", - ), - ], -) +@pytest.mark.parametrize("d", range(len(CASES)), ids=lambda d: f"d{d}") @pytest.mark.pre_alloc_mutable def test_create_results( state_test: StateTestFiller, pre: Alloc, fork: Fork, d: int, - g: int, - v: int, ) -> None: - """Ori Pomerantz qbzzt1@gmail.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC) - contract_1 = Address(0x00000000000000000000000000000000000060A7) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """Verify create results and follow-up calls per constructor kind.""" + creation, call_kind, constructor = CASES[d] + contract_1 = Address(CONTRACT_1_ADDRESS) + sender = pre.fund_eoa() + + # Length-preserving stand-in for the ported PUSH2 0xFFFF gas + # operand: two JUMPDESTs pad the 3-byte slot so every hardcoded + # jump target and code-copy offset stays valid, while GAS forwards + # everything (a fixed budget starves EIP-8037 state gas). + forward_all_gas = Op.JUMPDEST + Op.JUMPDEST + Op.GAS - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The 18-byte contract each successful constructor deploys: call + # contract_1 (as a 2-byte push, part of the fixed layout) and stop. + contract_code = ( + Op.CALL( + gas=forward_all_gas, + address=CONTRACT_1_ADDRESS, + value=0x0, + args_offset=0x0, + args_size=0x0, + ret_offset=0x0, + ret_size=0x0, + ) + + Op.STOP ) - pre[sender] = Account(balance=0xBA1A9CE0BA1A9CE) # Source: lll # { # ; Variables are 0x20 bytes (= 256 bits) apart, except for @@ -250,8 +166,8 @@ def test_create_results( # ) # ; I did not want to rely on knowing the address at which the contract # ... (138 more lines) - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + dispatcher_code = ( + Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + Op.MSTORE(offset=0x120, value=Op.CALLDATALOAD(offset=0x24)) + Op.MSTORE(offset=0x140, value=Op.CALLDATALOAD(offset=0x44)) + Op.JUMPI( @@ -385,7 +301,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -402,7 +318,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALLCODE( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -419,7 +335,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.DELEGATECALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -435,7 +351,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.STATICCALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -462,16 +378,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.POP(Op.SHA3(offset=0x0, size=0x2FFFFF)) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x17, size=Op.DUP1) @@ -479,16 +386,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.REVERT(offset=0x0, size=0x0) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x14, size=Op.DUP1) @@ -496,16 +394,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.REVERT(offset=0x0, size=0x20) + Op.PUSH1[0x12] @@ -514,16 +403,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.STOP + Op.PUSH1[0x12] @@ -532,16 +412,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.SELFDESTRUCT(address=0x0) + Op.PUSH1[0x12] @@ -550,26 +421,26 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, + + contract_code + + contract_code + ) + + # Guard the hardcoded layout the bytecode's jump targets and the + # FRAGMENTS table rely on. + dispatcher_bytes = bytes(dispatcher_code) + assert len(dispatcher_bytes) == 0x350, "dispatcher layout drifted" + assert dispatcher_bytes[0x33E:0x350] == bytes(contract_code), ( + "reference contract code slot drifted" + ) + # The SHA3-OOG constructor must stay unaffordable. + assert TX_GAS < fork.memory_expansion_gas_calculator()( + new_bytes=0x2FFFFF + ), "budget must not afford the SHA3-OOG constructor" + + # Slots 16-33 hold non-zero sentinels so every overwrite (even with + # zero) is observable. + contract_0 = pre.deploy_contract( + code=dispatcher_code, storage={ 16: contract_1, 18: contract_1, @@ -579,135 +450,99 @@ def test_create_results( 32: contract_1, 33: contract_1, }, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) # Source: lll # { # [[0]] 0x60A7 # } ; end of LLL code - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x60A7) + Op.STOP, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0x00000000000000000000000000000000000060A7), # noqa: E501 + pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=CONTRACT_1_ADDRESS) + Op.STOP, + address=contract_1, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1, 2, 4, 5, 6], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={32: 295, 33: 551}), - contract_1: Account(storage={0: contract_1}), - }, - }, - { - "indexes": {"data": [3, 7], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": {contract_0: Account(storage={32: 295, 33: 551})}, - }, - { - "indexes": { - "data": [8, 9, 10, 11, 12, 13, 14, 15], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": {"data": [16, 17], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: 32, - 17: contract_1, - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": { - "data": [18, 19, 20, 21, 22, 23, 24, 25], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: contract_1, - 17: 0, - 18: contract_1, - 19: contract_1, - 20: contract_1, - 21: contract_1, - 32: contract_1, - 33: contract_1, - }, - ), - }, - }, - ] + # Decode the case into the created account's address and the + # expected post-state. + if creation == 1: + created = compute_create_address(address=contract_0, nonce=1) + else: + frag_offset, frag_size = FRAGMENTS[constructor] + created = compute_create2_address( + contract_0, + CREATE2_SALT, + dispatcher_bytes[frag_offset : frag_offset + frag_size], + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The word the dispatcher stores when comparing its reference copy + # of the contract code against a non-existent account's ext code. + contract_code_word = int.from_bytes( + bytes(contract_code).ljust(32, b"\x00"), "big" + ) - tx_data = [ - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x4), - ] - tx_gas = [9437184] + post: dict = {} + if constructor == 4: + # The create succeeds with an empty return buffer, so the + # forced RETURNDATACOPY of 32 bytes aborts the whole dispatcher + # frame: every sentinel survives and nothing was created. + post[contract_0] = Account( + storage={ + 16: contract_1, + 18: contract_1, + 19: contract_1, + 20: contract_1, + 21: contract_1, + 32: contract_1, + 33: contract_1, + }, + ) + post[contract_1] = Account(storage={}) + post[created] = Account.NONEXISTENT + elif constructor == 0: + # Successful creation and a follow-up call to the new contract, + # which calls contract_1. Every sentinel is overwritten (the + # zero results are observable), and only the non-static call + # kinds let contract_1 store its own address. + post[contract_0] = Account( + storage={32: CREATE_PC, 33: CALL_PC}, + ) + post[contract_1] = Account( + storage={} if call_kind == 4 else {0: contract_1}, + ) + post[created] = Account(code=bytes(contract_code), nonce=1, storage={}) + else: + # No follow-up call: slots 20/21 keep their sentinels, and the + # dispatcher records the code/length differences against the + # created (or never-created) account's empty ext code. + storage = { + 18: len(bytes(contract_code)), + 19: contract_code_word, + 20: contract_1, + 21: contract_1, + 32: CREATE_PC, + 33: CALL_PC, + } + if constructor == 3: + # The constructor reverted 32 bytes holding contract_1's + # address; the dispatcher copied them out. + storage[16] = 32 + storage[17] = contract_1 + post[contract_0] = Account(storage=storage) + post[contract_1] = Account(storage={}) + if constructor == 5: + # Empty deploy: the account exists with no code. + post[created] = Account(code=b"", nonce=1, storage={}) + else: + # OOG (1), reverts (2, 3), and an in-init SELFDESTRUCT (6, + # destroyed in its creation transaction per EIP-6780). + post[created] = Account.NONEXISTENT tx = Transaction( sender=sender, to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=Bytes("048071d3") + + Hash(creation) + + Hash(call_kind) + + Hash(constructor), + gas_limit=TX_GAS, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py index 2e85f6b9b3c..c697e7352be 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py @@ -1,133 +1,96 @@ """ -Test_transaction_collision_to_empty2. +Verify a contract-creation transaction targeting an address that holds +only a balance: the prefund is not a collision, so creation proceeds and +the budget alone decides whether the init code completes. Ported from: state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + init code cost, success arm exact), pinning that a prefunded +create target incurs no EIP-8037 top-frame new-account state gas. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) def test_transaction_collision_to_empty2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Prefunded create target is no collision; budget decides the rest.""" + # Init code: one cold zero->non-zero store, deploys nothing. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=10) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=10, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": 1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=11, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={}, balance=10, nonce=0), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The prefunded target is not EMPTY_ACCOUNT in the pre-state, so + # EIP-8037 charges no top-frame new-account state gas: the exact + # success budget below would OOG if it were charged. + success_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, + ) + initcode.gas_cost(fork) + # The OOG arm misses half the store's cost rather than one gas: the + # intrinsic calculator over-estimates by the initcode word cost on + # pre-Shanghai forks, so a one-gas boundary is not portable. + gas_limit = success_gas + if oog: + gas_limit -= initcode.gas_cost(fork) // 2 - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + if oog: + # Creation rolled back: prefund kept, no value, nonce untouched. + created_account = Account( + storage={}, code=b"", nonce=0, balance=PREFUND + ) + else: + created_account = Account( + storage={1: 1}, code=b"", nonce=1, balance=PREFUND + tx_value + ) + + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py index ab1aedcd0b5..ff51bba2eee 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py @@ -1,148 +1,99 @@ """ -Test_transaction_collision_to_empty_but_code. +Verify a contract-creation transaction whose target address already holds +code: the collision aborts the creation, consumes the whole gas limit, +transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Any non-empty code at the target address triggers the collision. +COLLIDING_CODE = bytes.fromhex("1122334455") + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_code( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Creation collision with code burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: raw - # 0x1122334455 - contract_0 = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex("1122334455"), - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={1: 0}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(code=COLLIDING_CODE) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) + post = { + sender: Account(nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=COLLIDING_CODE, nonce=0, balance=0), + } + state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py index 14a3c066470..a4b4c40e06a 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py @@ -1,22 +1,26 @@ """ -Test_transaction_collision_to_empty_but_nonce. +Verify a contract-creation transaction whose target address already has a +non-zero nonce: the collision aborts the creation, consumes the whole gas +limit, transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's empty code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -28,89 +32,67 @@ "state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_nonce( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_nonce.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Creation collision with a nonce burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=0, nonce=1) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(nonce=1) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) post = { sender: Account(nonce=1), - contract_0: Account(storage={1: 0}, nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=b"", nonce=1, balance=0), } state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) From 8657e32ab52e2e9a36beb2dc58a65597800d2824 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:50:48 +0200 Subject: [PATCH 50/55] fix(tests): un-skip Amsterdam mutual-recursion and call-bomb ported tests --- .../stSystemOperationsTest/test_ab_acalls0.py | 259 +++++++++++++----- .../stSystemOperationsTest/test_ab_acalls3.py | 234 ++++++++++++---- .../test_call_recursive_bomb3.py | 223 ++++++++++++--- 3 files changed, 549 insertions(+), 167 deletions(-) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py index 863e937e7bb..c87ef000bf1 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py @@ -1,8 +1,24 @@ """ -Test_ab_acalls0. +Verify mutual A<->B recursion with value transfers and fixed gas asks. + +Contract A calls B forwarding a fixed 100,000-gas ask with 24 wei; B +calls its caller back with a 50,000 ask and 23 wei, storing one plus +the result. Both store into a PC-derived slot only after their call +returns, so every level's store competes with what the descent left +behind: levels too deep to afford it halt and forfeit, rolling back +their stores and transfers, and the surviving storage and balances pin +exactly how far the budget reaches. Ported from: state_tests/stSystemOperationsTest/ABAcalls0Filler.json + +@manually-enhanced: Do not overwrite. The post state (stores and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting and return, warm/cold and SSTORE +pricing via opcode metadata, EIP-8037 state-gas spill), validated +against the ported Cancun stores. B reaches A as its CALLER instead of +a hardcoded address, which shifts B's PC-derived slot; both slots are +computed from the assembled code. """ import pytest @@ -10,8 +26,7 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,84 +35,198 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +A_CALL_GAS = 100_000 +A_CALL_VALUE = 0x18 +B_CALL_GAS = 50_000 +B_CALL_VALUE = 0x17 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Exactly one return payment before any income (the ported balance). +B_INITIAL_BALANCE = B_CALL_VALUE +# Ported budget; pins how deep the mutual recursion reaches. +TX_GAS_LIMIT = 1_000_000 + + +def predict_final_state( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int, int, int]: + """ + Replay the mutual recursion's gas flow. + + Return A's stored value, B's stored value, and the committed + balance deltas of A and B. Descend the alternating call chain + computing each level's EIP-150 grant (both asks are pushed + constants; a value-bearing call gifts the callee the stipend and + gets any unused part back), then unwind: a level that cannot afford + its post-call store (EIP-2200's stipend rule included) halts and + forfeits its grant, reverting its own store and the transfer that + funded it. Every cost is derived from the fork via opcode metadata, + including EIP-8037 state gas: with a sub-cap gas limit the state + reservoir is zero, so state charges spill from the charging frame's + own gas. + """ + stipend = fork.gas_costs().CALL_STIPEND + pc_cost = Op.PC.gas_cost(fork) + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + # A's charges before forwarding: argument pushes plus the call's + # upfront costs (B is cold only in the top level). The ask is a + # pushed constant, so the whole call expression charges up front. + def a_charges(b_warm: bool) -> int: + return Op.CALL( + gas=A_CALL_GAS, + address=b_address, + value=A_CALL_VALUE, + address_warm=b_warm, + value_transfer=True, + ).gas_cost(fork) + + b_value_expr = Op.ADD( + 1, + Op.CALL( + gas=B_CALL_GAS, + address=Op.CALLER, + value=B_CALL_VALUE, + # A is the transaction target: always warm. + address_warm=True, + value_transfer=True, + ), + ) + # B's ADD and its constant push run only after the call returns. + b_post_call = Op.PUSH1[0].gas_cost(fork) + Op.ADD.gas_cost(fork) + b_charges = b_value_expr.gas_cost(fork) - b_post_call + + # Descend: alternate A and B levels until one dies mid-charges. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + balance = {"A": A_INITIAL_BALANCE, "B": B_INITIAL_BALANCE} + while True: + level += 1 + is_a = level % 2 == 1 + if is_a: + gas -= a_charges(b_warm=level > 1) + ask, value = A_CALL_GAS, A_CALL_VALUE + else: + gas -= b_charges + ask, value = B_CALL_GAS, B_CALL_VALUE + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + payer = "A" if is_a else "B" + assert balance[payer] >= value, "value transfer must be funded" + balance[payer] -= value + balance["B" if is_a else "A"] += value + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + stipend + + # Unwind: a failed level forfeits its grant and reverts the whole + # committed state below it (stores, warmth, and transfers). + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + is_a = lvl % 2 == 1 + gas = available - forwarded + (leftover if child_ok else 0) + result = 1 if child_ok else 0 + if is_a: + gas -= pc_cost + store_value, current, warm = result, a_val, a_warm + else: + gas -= b_post_call + pc_cost + store_value, current, warm = 1 + result, b_val, b_warm + ok = gas >= 0 and gas > stipend + if ok: + gas -= raw_store_cost(warm, current, store_value) + ok = gas >= 0 + if ok: + # Commit this level: its store and the transfer into it. + if is_a: + a_val, a_warm = store_value, True + if lvl > 1: + a_delta += B_CALL_VALUE + b_delta -= B_CALL_VALUE + else: + b_val, b_warm = store_value, True + a_delta -= A_CALL_VALUE + b_delta += A_CALL_VALUE + leftover = gas + child_ok = True + else: + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + assert child_ok, "the top level must complete" + return a_val, b_val, a_delta, b_delta + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls0Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls0( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Pin how deep a value-bearing A<->B recursion reaches.""" + # B calls whoever called it, so it needs no embedded address. + b_value_expr = Op.ADD( + 1, + Op.CALL(gas=B_CALL_GAS, address=Op.CALLER, value=B_CALL_VALUE), + ) + contract_b = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=b_value_expr) + Op.STOP, + balance=B_INITIAL_BALANCE, ) - # Source: lll - # { [[ (PC) ]] (CALL 100000 24 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.CALL( - gas=0x186A0, - address=0x44EB1162303B6A60F2F8882D43D661787B3011E6, - value=0x18, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40), # noqa: E501 + a_value_expr = Op.CALL( + gas=A_CALL_GAS, address=contract_b, value=A_CALL_VALUE ) - # Source: lll - # { [[ (PC) ]] (ADD 1 (CALL 50000 23 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.ADD( - 0x1, - Op.CALL( - gas=0xC350, - address=0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40, - value=0x17, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ), - ) - + Op.STOP, - balance=23, - nonce=0, - address=Address(0x44EB1162303B6A60F2F8882D43D661787B3011E6), # noqa: E501 + contract_a = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=a_value_expr) + Op.STOP, + balance=A_INITIAL_BALANCE, ) + # PC keys: each store's key is the code offset of its PC opcode, + # which sits right after the assembled value expression. + a_key = len(bytes(a_value_expr)) + b_key = len(bytes(b_value_expr)) + tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_val, b_val, a_delta, b_delta = predict_final_state( + fork, TX_GAS_LIMIT, contract_b + ) post = { - target: Account(storage={36: 1}), - addr: Account(storage={38: 1}), + contract_a: Account( + storage={a_key: a_val}, + balance=A_INITIAL_BALANCE + a_delta, + ), + contract_b: Account( + storage={b_key: b_val}, + balance=B_INITIAL_BALANCE + b_delta, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py index 8f2b77d0966..4ad46a6f155 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py @@ -1,8 +1,22 @@ """ -Test_ab_acalls3. +Verify mutual A<->B recursion where each side reserves 100,000 gas. + +Both contracts bump their own depth counter during descent, then call +the other side forwarding everything but a 100,000-gas reserve (A sends +one wei each level; B sends nothing back). Nothing runs after the call, +so only the single deepest level dies of gas and every completed +level's counter bump and transfer persist: the counters and balances +pin exactly how many rounds the budget sustains. Ported from: state_tests/stSystemOperationsTest/ABAcalls3Filler.json + +@manually-enhanced: Do not overwrite. The post state (counters and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting, warm/cold and SSTORE pricing via +opcode metadata, EIP-8037 state-gas spill), validated against the +ported Cancun counters. B reaches A as its CALLER instead of a +hardcoded address. """ import pytest @@ -10,8 +24,8 @@ Account, Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) @@ -20,76 +34,182 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +# Gas each level keeps back for itself before forwarding the rest. +GAS_RESERVE = 100_000 +A_CALL_VALUE = 1 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Ported budget; pins how many rounds the recursion sustains. +TX_GAS_LIMIT = 10_000_000 + + +def predict_depths( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int]: + """ + Replay the mutual recursion's gas flow. + + Return how many A and B levels complete. Descend the alternating + call chain: each level bumps its own counter (one cold set per + contract, then dirty rewrites), pays its call charges, and forwards + everything but the reserve under the EIP-150 63/64 rule; once the + reserve underflows, the wrapped ask forwards the 63/64 maximum. + Nothing runs after a call, so only the single deepest level dies + and its bump and incoming transfer revert. Every cost is derived + from the fork via opcode metadata, including EIP-8037 state gas: + with a sub-cap gas limit the state reservoir is zero, so state + charges spill from the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + def call_split(address: object, warm: bool, value: int) -> tuple[int, int]: + """Pre-GAS-read and upfront charges of one side's call.""" + upfront = Op.CALL( + address_warm=warm, value_transfer=value > 0 + ).gas_cost(fork) + composite = Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=address, + value=value, + address_warm=warm, + value_transfer=value > 0, + ).gas_cost(fork) + return composite - upfront - post_gas_read, upfront + + a_pre, a_upfront_cold = call_split(b_address, False, A_CALL_VALUE) + _, a_upfront_warm = call_split(b_address, True, A_CALL_VALUE) + # A is the transaction target: always warm for B's call back. + b_pre, b_upfront = call_split(Op.CALLER, True, 0) + + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + level = 0 + a_balance = A_INITIAL_BALANCE + while True: + level += 1 + is_a = level % 2 == 1 + # Each contract's first level pays the cold counter set. + first = level <= 2 + gas -= bump_statics(key_warm=not first) + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= a_pre if is_a else b_pre + if gas < 0: + break + gas_read = gas + if is_a: + gas -= post_gas_read + ( + a_upfront_cold if level == 1 else a_upfront_warm + ) + else: + gas -= post_gas_read + b_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + if is_a: + assert a_balance >= A_CALL_VALUE, "transfer must be funded" + a_balance -= A_CALL_VALUE + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + gas = forwarded + (stipend if is_a else 0) + + completed = level - 1 + assert completed >= 2, "both sides must run at least once" + a_count = (completed + 1) // 2 + b_count = completed // 2 + return a_count, b_count + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) + """Pin how many rounds a reserve-throttled A<->B recursion runs.""" - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 1 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + def bounce_code(call: Bytecode) -> Bytecode: + """Bump the own-depth counter, then call the other side.""" + return ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + call + + Op.STOP ) - + Op.STOP, - balance=0xFA3E8, - nonce=0, - address=Address(0x4776B53DEB22F16581088F679DBA75E205B65D34), # noqa: E501 + + # B calls whoever called it, so it needs no embedded address. + contract_b = pre.deploy_contract( + code=bounce_code( + Op.CALL(gas=Op.SUB(Op.GAS, GAS_RESERVE), address=Op.CALLER) + ), ) - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0x4776B53DEB22F16581088F679DBA75E205B65D34, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9), # noqa: E501 + contract_a = pre.deploy_contract( + code=bounce_code( + Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=contract_b, + value=A_CALL_VALUE, + ) + ), + balance=A_INITIAL_BALANCE, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_count, b_count = predict_depths(fork, TX_GAS_LIMIT, contract_b) + # Each completed B level keeps the wei its calling A level sent. post = { - target: Account(storage={0: 52}), - addr: Account(storage={0: 52}), + contract_a: Account( + storage={COUNTER_SLOT: a_count}, + balance=A_INITIAL_BALANCE - b_count * A_CALL_VALUE, + ), + contract_b: Account( + storage={COUNTER_SLOT: b_count}, + balance=b_count * A_CALL_VALUE, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py index ee666d8bf61..eaac392a28b 100644 --- a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py +++ b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py @@ -1,17 +1,29 @@ """ -Test_call_recursive_bomb3. +Verify a self-recursive CALL bomb that keeps only a 224-gas reserve. + +Each level bumps a shared depth counter and forwards everything but a +tiny reserve to a call to itself, so descent is throttled only by the +EIP-150 63/64 withhold. On the way back up a level must afford its +success-flag store from its 1/64 retention plus whatever its child +returned; levels that cannot (EIP-2200's stipend rule included) halt +and forfeit, so the surviving storage pins the exact depth the budget +sustains. Ported from: state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +returned-leftover propagation, warm/cold and SSTORE pricing via opcode +metadata, EIP-8037 state-gas spill), validated against the ported +Cancun depth. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,58 +32,179 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +# Gas each level keeps back; far below a cold store, so completing the +# post-call flag store depends on the 1/64 retention and the child's +# returned leftover. +GAS_RESERVE = 224 +# Ported budget; pins the OOG-terminated depth. +TX_GAS_LIMIT = 1_000_000 + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=Op.ADDRESS, + ), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its flag store halts and + forfeits its entire grant to its parent, so the deepest level that + completes fixes the surviving depth counter (deeper levels' writes + and warmth all revert). The level above the deepest survivor funds + its more expensive zero-to-one flag set partly from the survivor's + returned leftover. Every cost is derived from the fork via opcode + metadata, including EIP-8037 state gas: with a sub-cap gas limit + the state reservoir is zero, so state charges spill from the + charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.SUB(Op.GAS, GAS_RESERVE) + call_upfront = Op.CALL(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the reserve push plus the GAS opcode itself. + pre_gas_read = ( + Op.CALL(gas=ask_expr, address=Op.ADDRESS, address_warm=True).gas_cost( + fork + ) + - call_upfront + - post_gas_read + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Flag store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own store finds a cold slot and a zero current value. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return {COUNTER_SLOT: survivor, RESULT_SLOT: result_below} + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_recursive_bomb3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_recursive_bomb3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[ 0 ]] (+ (SLOAD 0) 1) [[ 1 ]] (CALL (- (GAS) 224) (ADDRESS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.SUB(Op.GAS, 0xE0), - address=Op.ADDRESS, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0x1312D00, - nonce=0, - ) + """Pin the depth a thin-reserve CALL self-recursion sustains.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + gas_limit=TX_GAS_LIMIT, ) - post = {target: Account(storage={0: 18, 1: 1})} + post = { + target: Account(storage=predict_recursion_storage(fork, TX_GAS_LIMIT)), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 341b16cd7510f001de326a94c8f5e1f69da5cb85 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:01:45 +0200 Subject: [PATCH 51/55] fix(tests): un-skip remaining Amsterdam ported static tests, emptying the skip list --- tests/ported_static/amsterdam_skip_list.txt | 57 +- ...est_create2_oo_gafter_init_code_revert2.py | 174 ++++-- .../test_create2_oog_from_call_refunds.py | 50 +- ...est_create2collision_selfdestructed_oog.py | 245 ++++----- .../stCreate2/test_create2no_cash.py | 187 +++---- ...st_create_message_reverted_oog_in_init2.py | 187 ++++--- .../test_revert_depth_create2_oog.py | 346 ++++++------ .../test_revert_depth_create2_oog_berlin.py | 199 ------- ...t_revert_depth_create_address_collision.py | 393 ++++++++------ ...t_depth_create_address_collision_berlin.py | 223 -------- .../stRefundTest/test_refund50_2.py | 90 +-- .../stRefundTest/test_refund50percent_cap.py | 116 ++-- .../stRefundTest/test_refund_call_a.py | 101 ++-- .../test_refund_suicide50procent_cap.py | 252 +++++---- .../stRefundTest/test_refund_tx_to_suicide.py | 85 +-- .../test_loop_calls_depth_then_revert.py | 102 ++-- ...t_loop_delegate_calls_depth_then_revert.py | 100 ++-- ...t_revert_depth_create_address_collision.py | 318 +++++------ .../test_revert_depth_create_oog.py | 288 +++++----- ...pcode_in_calls_on_non_empty_return_data.py | 512 +++++------------- 20 files changed, 1722 insertions(+), 2303 deletions(-) delete mode 100644 tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py delete mode 100644 tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index cb02f797b6b..1baa46c3978 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,59 +8,4 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 66 - -# stCallCodes (3) - -# stCallCreateCallCodeTest (6) - -# stCreate2 (31) -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_DelegateCall_Refund_NoOoG] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d0] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d1] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d2] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g0] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] - -# stDelegatecallTestHomestead (4) - -# stRefundTest (7) - -# stRevertTest (12) -stRevertTest/test_loop_calls_depth_then_revert.py::test_loop_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_loop_delegate_calls_depth_then_revert.py::test_loop_delegate_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v1] - -# stSystemOperationsTest (3) -stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] -stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] -stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] - +# Total entries: 0 diff --git a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py index 2b48b4cb0bd..8ca16e80b5b 100644 --- a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py +++ b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py @@ -1,98 +1,158 @@ """ -Calls a contract that runs CREATE2 which deploy a code. then after... +Verify a CREATE2 whose child completes its init code but runs out of gas +at the code-deposit charge, inside a frame that then REVERTs: the revert +payload carries the CREATE2 result (zero) back to the caller and every +side effect of the creating frame is rolled back. Ported from: state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json + +@manually-enhanced: Do not overwrite. The forwarded grant is derived from +fork composites so the child fails exactly at the deposit charge on every +fork; the revert payload now also carries the CREATE2 result, and the +caller stores the call result plus both payload words. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, - compute_create_address, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PAYLOAD_SLOT = 0x1 +CREATE2_RESULT_SLOT = 0x2 +CALL_RESULT_SLOT = 0x3 + +# The init code returns this many memory bytes as the code to deposit; +# the grant is sized so this charge is exactly what the child cannot pay. +DEPOSIT_SIZE = 0x40 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Constantinople") def test_create2_oo_gafter_init_code_revert2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE2 which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 child dies at the deposit charge; its creator reverts.""" + # The child's init code: one word of scratch data, then a deposit + # request the sized grant cannot cover. The would-be deposited code + # (an SSTORE) never runs. + child_mstore = Op.MSTORE( + offset=0x0, + value=0x6001600155, + new_memory_size=0x20, ) + child_return = Op.RETURN( + offset=0x0, + size=DEPOSIT_SIZE, + new_memory_size=DEPOSIT_SIZE, + old_memory_size=0x20, + ) + initcode = child_mstore + child_return + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x20, "init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The creator writes the init code into memory (right-aligned in the + # first word), runs CREATE2, appends the CREATE2 result to memory and + # reverts both words back to the caller. + creator_setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(initcode_bytes), + size=len(initcode_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=creator_setup + + Op.MSTORE( + offset=0x20, + value=create2_code, + new_memory_size=0x40, + old_memory_size=0x20, + ) + + Op.REVERT(offset=0x0, size=0x40) ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { (MSTORE 0 0x6460016001556000526005601bf3) (CREATE2 0 18 14 0) (REVERT 0 32) } # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.REVERT(offset=0x0, size=0x20) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Size the grant so the child's grant covers its init-code execution + # but not the deposit charge, and the creator's 1/64 retention still + # covers its tail (the result MSTORE and the REVERT). + child_exec = child_mstore.gas_cost(fork) + child_return.gas_cost(fork) + deposit_cost = DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit_cost += fork.code_deposit_state_gas(code_size=DEPOSIT_SIZE) + child_grant = child_exec + deposit_cost // 2 + rem_after_create = -(-child_grant * 64 // 63) + forwarded = ( + creator_setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + rem_after_create + ) + granted = rem_after_create - rem_after_create // 64 + assert child_exec + 10 <= granted <= child_exec + deposit_cost - 10, ( + "the child grant must die exactly at the deposit charge" ) - # Source: lll - # { (CALL 33000 0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0 32) [[ 1 ]] (MLOAD 0) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x80E8, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x20, - ) + creator_tail = Op.MSTORE( + offset=0x20, + value=0x0, + new_memory_size=0x40, + old_memory_size=0x20, + ).gas_cost(fork) + Op.REVERT(offset=0x0, size=0x40).gas_cost(fork) + assert rem_after_create // 64 >= creator_tail + 5, ( + "the creator's retention must cover its tail" + ) + + # The caller forwards the sized grant, then stores the call result + # and both words of the revert payload. + call_code = Op.CALL(gas=forwarded, address=creator, ret_size=0x40) + caller = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.SSTORE(key=PAYLOAD_SLOT, value=Op.MLOAD(offset=0x0)) + + Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, Op.MLOAD(offset=0x20)), ) - + Op.SSTORE(key=0x1, value=Op.MLOAD(offset=0x0)) + Op.STOP, - storage={1: 1}, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + storage={PAYLOAD_SLOT: 0x1}, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=75000, + sender=pre.fund_eoa(), + to=caller, + state_gas_reservoir=0, ) post = { - contract_0: Account(storage={1: 0x6460016001556000526005601BF3}), - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, + caller: Account( + storage={ + # The creator reverted: the call result is 0. + CALL_RESULT_SLOT: 0x1, + # First payload word: the init code the creator staged. + PAYLOAD_SLOT: int.from_bytes(initcode_bytes, "big"), + # Second payload word: the failed CREATE2 returned 0. + CREATE2_RESULT_SLOT: 0x1, + } + ), + # Everything inside the creator was rolled back. + creator: Account(nonce=1, storage={}), + compute_create2_address(creator, 0, initcode): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py index 2c5c0b12a72..17737bfb37d 100644 --- a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py +++ b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create2_oog_from_call_refunds. +Verify gas refunds earned inside a CREATE2's init code (storage clears, +via direct stores and CALL/CALLCODE/DELEGATECALL helpers, selfdestructs, +and nested creations) against out-of-gas boundaries: each scenario runs +once completing normally and twice dying — on an oversized code deposit +and on an INVALID that pins the refund bookkeeping. Ported from: state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The transaction budget and the +sender's funding derive from the fork: the ported 400k regular budget +plus the deepest arm's peak outstanding EIP-8037 state gas, guarded to +stay below the 5000-byte deposit charge that starves the OoG arms. """ import pytest @@ -26,6 +35,13 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_PRICE = 10 +# The ported budget, proven to cover every NoOoG arm's regular gas. +PORTED_GAS_LIMIT = 400_000 +# The OoG arms return this much memory as code; the deposit charge is +# what must exceed the transaction budget so they starve. +OOG_DEPOSIT_SIZE = 0x1388 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml"], @@ -232,7 +248,34 @@ def test_create2_oog_from_call_refunds( base_fee_per_gas=10, ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # EIP-8037 charges state gas on top of the ported regular budget. + # The headroom is the deepest arm's (create-inside-create2) peak + # outstanding state gas: a NEW_ACCOUNT charge and one live fresh + # slot set at each creation depth, plus the one-byte code deposits; + # all terms are zero before Amsterdam. + fresh_set_state = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ).state_cost(fork) + new_account_state = Op.CREATE2( + value=0x0, offset=0x0, size=0x0, salt=0x0 + ).state_cost(fork) + tx_gas_limit = ( + PORTED_GAS_LIMIT + + 2 * new_account_state + + 2 * fresh_set_state + + 3 * fork.code_deposit_state_gas(code_size=1) + + 20_000 + ) + # The budget must stay below the oversized deposit charge so the + # OoG arms keep starving on it on every fork. + oog_deposit = ( + OOG_DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + assert tx_gas_limit < oog_deposit, "the OoG arms must stay starved" + + # The exact funding makes the OoG arms' post-state balance zero. + pre[sender] = Account(balance=tx_gas_limit * GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -1188,13 +1231,14 @@ def test_create2_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] + tx_gas = [tx_gas_limit] tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], gas_limit=tx_gas[g], + gas_price=GAS_PRICE, nonce=1, error=_exc, ) diff --git a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py index 4def59bad89..70ae08055a7 100644 --- a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py +++ b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py @@ -1,52 +1,57 @@ """ -Collision with address that has been selfdestructed in the same... +Verify a CREATE2 whose target address holds a pre-existing account that +SELFDESTRUCTed earlier in the same transaction: the collision stands +(the account is only emptied, not freed), consuming the child's grant, +and the sized budget then runs the creating init code out of gas so the +whole creation transaction rolls back — including the selfdestruct's +balance transfer. Ported from: state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json + +@manually-enhanced: Do not overwrite. Collider and beneficiary addresses +are computed instead of hardcoded, the budget is derived from fork +composites, and the post-collision work is sized above the collision's +1/64 retention on every fork (the alive collider means the CREATE2 +charges — and refunds — no new-account state gas). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, + compute_create2_address, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COLLIDER_BALANCE = 1 +# Gas left for the collision to consume: the CREATE2's child grant. Its +# 1/64 retention (plus any state refund) must stay below the two-store +# victim cost, which the guard below asserts. +CHILD_GRANT_SLACK = 30_000 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "inner_initcode", [ + pytest.param(Bytecode(), id="empty_initcode"), + pytest.param(Op.SSTORE(key=0x1, value=0x1), id="storing_initcode"), pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", + Op.MSTORE(offset=0x0, value=0x6001600155) + + Op.RETURN(offset=0x1B, size=0x5), + id="depositing_initcode", ), ], ) @@ -55,120 +60,118 @@ def test_create2collision_selfdestructed_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + inner_initcode: Bytecode, ) -> None: - """Collision with address that has been selfdestructed in the same...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - contract_1 = Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01) - contract_2 = Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """A selfdestructed account still collides, and its grant is lost.""" + sender = pre.fund_eoa() + outer_created = compute_create_address(address=sender, nonce=0) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The collider occupies the CREATE2 target (which depends on the + # init code below through its hash) and selfdestructs when called; + # its address is derived from the pre-funded sender, so the pre + # allocation must stay mutable. + beneficiary = pre.nonexistent_account() + collider_work = Op.SELFDESTRUCT( + address=beneficiary, + address_warm=False, + account_new=True, + ) + collider = compute_create2_address(outer_created, 0, inner_initcode) + pre.deploy_contract( + code=collider_work, + balance=COLLIDER_BALANCE, + address=collider, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + # The outer init code selfdestructs the collider, stages the child's + # init code (never executed: the collision aborts before dispatch) + # and runs the CREATE2 into the collision; the two stores after it + # are the victims the burned grant leaves unaffordable. + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" + call_code = Op.CALL( + address=collider, + address_warm=False, + value_transfer=False, + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01), # noqa: E501 + setup = ( + Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, + ) + if inner_bytes + else Bytecode() + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes) if inner_bytes else 0x0, + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20 if inner_bytes else 0x0, + old_memory_size=0x20 if inner_bytes else 0x0, + init_code_size=len(inner_bytes), + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50), # noqa: E501 + victim_stores = Op.SSTORE( + key=0x0, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + Op.SSTORE( + key=0x1, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + outer_initcode = ( + Op.POP(call_code) + setup + Op.POP(create2_code) + victim_stores ) - tx_data = [ - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + # The budget covers everything up to and including the CREATE2's own + # charges plus the slack the collision consumes; the guard proves + # the victims exceed what the collision leaves behind, so the outer + # frame must die and the whole creation rolls back. The collider is + # alive at the CREATE2 (only emptied by its selfdestruct), so no + # new-account state gas is charged — or refunded — there. + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, ) - + Op.MSTORE(offset=0x0, value=0x6001600155) - + Op.POP(Op.CREATE2(value=0x0, offset=0x1B, size=0x5, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - ] - tx_gas = [200000] - tx_value = [1] + + fork.transaction_top_frame_state_gas(contract_creation=True) + + call_code.gas_cost(fork) + + collider_work.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + CHILD_GRANT_SLACK + ) + leftover = CHILD_GRANT_SLACK // 64 + assert leftover + 2_500 < victim_stores.gas_cost(fork), ( + "the collision's leavings must not afford the victim stores" + ) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=outer_initcode, + gas_limit=gas_limit, ) post = { - contract_0: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_1: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_2: Account(code=bytes.fromhex("6010ff00"), balance=1), - Address( - 0x0000000000000000000000000000000000000010 - ): Account.NONEXISTENT, - compute_create_address(address=sender, nonce=0): Account.NONEXISTENT, sender: Account(nonce=1), + # Rolled back wholesale: the collider keeps its code and its + # balance, the beneficiary was never credited, nothing created. + collider: Account( + code=collider_work, + balance=COLLIDER_BALANCE, + nonce=1, + ), + beneficiary: Account.NONEXISTENT, + outer_created: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2no_cash.py b/tests/ported_static/stCreate2/test_create2no_cash.py index 0d5894a3320..5dc49d75d32 100644 --- a/tests/ported_static/stCreate2/test_create2no_cash.py +++ b/tests/ported_static/stCreate2/test_create2no_cash.py @@ -1,160 +1,105 @@ """ -Create2 fails with not enough cash (endowment of a new account) +... +Verify CREATE2's endowment balance preflight: a creator one wei short of +the endowment fails without creating (and without a nonce bump), a +one-wei top-up sent with the call makes the same CREATE2 succeed, and in +a static context the CREATE2 faults the whole frame instead. Ported from: state_tests/stCreate2/create2noCashFiller.json + +@manually-enhanced: Do not overwrite. The creation-transaction wrapper +and tuned gas budgets are replaced by a deployed entry contract that +records the call result; the created account and the creator's nonce +(no bump on the balance preflight) are asserted explicitly. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALL_RESULT_SLOT = 0x0 +CREATE2_ENDOWMENT = 0x65 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2noCashFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "opcode, top_up", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param(Op.CALL, 0, id="call_insufficient_balance"), + pytest.param(Op.CALL, 1, id="call_topped_up_balance"), + pytest.param(Op.STATICCALL, 0, id="staticcall_write_protection"), ], ) -@pytest.mark.pre_alloc_mutable def test_create2no_cash( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: Op, + top_up: int, ) -> None: - """Create2 fails with not enough cash (endowment of a new account) +...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 endowment beyond the creator's balance cannot create.""" + # The creator holds one wei less than the endowment it attempts to + # transfer; only the topped-up arm can afford it. + creator = pre.deploy_contract( + code=Op.POP( + Op.CREATE2(value=CREATE2_ENDOWMENT, offset=0x0, size=0x0, salt=0x0) + ) + + Op.STOP, + balance=CREATE2_ENDOWMENT - 1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The entry contract forwards the transaction's value (the optional + # top-up) and records the call result, shifted so that a failed call + # (1), a successful call (2) and no call at all (0) all differ. + if opcode == Op.CALL: + call_code = Op.CALL(address=creator, value=top_up) + else: + call_code = Op.STATICCALL(address=creator) + entry = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.STOP, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (CREATE2 101 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CREATE2(value=0x65, offset=0x0, size=0x0, salt=0x0) + Op.STOP, - balance=100, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + tx = Transaction( + sender=pre.fund_eoa(), + to=entry, + value=top_up, + state_gas_reservoir=0, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=100), - Address( - 0x12AAEFBC0350A026228076E5369E6CE148CE67BE - ): Account.NONEXISTENT, - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=0), - Address(0x12AAEFBC0350A026228076E5369E6CE148CE67BE): Account( - balance=101 - ), - sender: Account(nonce=1), - }, - }, - ] + created = compute_create2_address(creator, 0, b"") + create_succeeds = opcode == Op.CALL and top_up > 0 + if create_succeeds: + # The whole (topped-up) balance moved into the created account, + # and the creator's nonce was consumed by the creation. + creator_account = Account(nonce=2, balance=0) + created_account = Account(nonce=1, code=b"", balance=CREATE2_ENDOWMENT) + else: + # The balance preflight (or the static fault) aborts before any + # account is touched: no creation and no nonce bump. + creator_account = Account(nonce=1, balance=CREATE2_ENDOWMENT - 1) + created_account = Account.NONEXISTENT - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # A static frame faults on CREATE2, so only that arm's call fails. + call_result = 0 if opcode == Op.STATICCALL else 1 - tx_data = [ - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.STATICCALL( - gas=0x249F0, - address=contract_0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - ] - tx_gas = [400000] - tx_value = [1] - - tx = Transaction( - sender=sender, - to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + entry: Account( + storage={CALL_RESULT_SLOT: 0x1 + call_result}, balance=0 + ), + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py index 927b3fa082e..e75adcb8de8 100644 --- a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py +++ b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py @@ -1,126 +1,145 @@ """ -Create2 oog during the init code, + when create2 is from transaction... +Verify a CREATE2 issued from a contract-creation transaction's init +code: the transaction budget decides whether the CREATE2's child +completes its storage-writing init code or dies out of gas, while the +outer creation completes either way. Ported from: state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from fork +composites (intrinsic + top-frame state gas + the composed init code); +the outer created account is asserted on both arms with a pre-CREATE2 +canary, and the child account's storage on the success arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create2_address, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF +TX_VALUE = 100 +CHILD_STORED = {0x0: 0xC, 0x1: 0xD} + @pytest.mark.ported_from( ["state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "child_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="child_oog"), + pytest.param(True, id="child_succeeds"), ], ) -@pytest.mark.pre_alloc_mutable def test_create_message_reverted_oog_in_init2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + child_covered: bool, ) -> None: - """Create2 oog during the init code, + when create2 is from...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """The budget decides how far an init-code CREATE2's child gets.""" + # The child's init code writes two fresh slots and deposits nothing. + inner_initcode = Op.SSTORE( + key=0x0, + value=CHILD_STORED[0x0], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x0], + ) + Op.SSTORE( + key=0x1, + value=CHILD_STORED[0x1], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x1], ) + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000000, + # The outer init code writes a completion canary before the CREATE2 + # (only a POP runs after it: on the starved arm the 1/64 retention + # cannot afford an SSTORE), stages the child's init code in memory + # and runs the CREATE2; it deposits no code. + canary_store = Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, ) - - pre[sender] = Account(balance=0x2DC6C0) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=10, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes), + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(inner_bytes), + ) + outer_initcode = canary_store + setup + Op.POP(create2_code) + Op.STOP - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address( - 0xF3059E18A327C662766F6BA11808C400635847EF - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address(0xF3059E18A327C662766F6BA11808C400635847EF): Account( - storage={0: 12, 1: 13}, balance=0, nonce=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.MSTORE(offset=0x0, value=0x600C600055600D600155) - + Op.CREATE2(value=0x0, offset=0x16, size=0xA, salt=0x0) - + Op.STOP, - ] - tx_gas = [110000, 150000] - tx_value = [100] + # Both budgets derive from the same overhead: everything the outer + # frame pays before (and including) the CREATE2's own charges. The + # child's grant is what remains after the 1/64 withhold. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, + sends_value=True, + ) + + fork.transaction_top_frame_state_gas(contract_creation=True) + + canary_store.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + ) + child_needed = inner_initcode.gas_cost(fork) + if child_covered: + gas_limit = overhead + -(-child_needed * 64 // 63) + 3_000 + else: + gas_limit = overhead + child_needed // 2 + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=outer_initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + outer_created = compute_create_address(address=sender, nonce=0) + child = compute_create2_address(outer_created, 0, inner_initcode) + post = { + sender: Account(nonce=1), + # The outer creation completes on both arms: the CREATE2 always + # bumps its nonce, and a failed child costs it only the grant. + outer_created: Account( + nonce=2, + code=b"", + balance=TX_VALUE, + storage={CANARY_SLOT: CANARY}, + ), + child: Account(nonce=1, code=b"", balance=0, storage=CHILD_STORED) + if child_covered + else Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py index 860237360e1..a5dd5bc6260 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py @@ -1,199 +1,235 @@ """ -Test_revert_depth_create2_oog. +Verify a CREATE2 two frames deep under out-of-gas pressure: the calldata +sets the grant a caller forwards to a creating contract, and the two +budgets decide whether the creation completes, the creator dies mid-way, +or the whole outer frame runs dry — each with a distinct post-state. Ported from: state_tests/stCreate2/RevertDepthCreate2OOGFiller.json +state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json + +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in; every budget derives from fork composites; the creator now +stores the CREATE2 result so a wrongly failed (or wrongly succeeding) +creation is visible beyond the created account itself. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2/0x3 as ported, plus the CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATOR_DONE_SLOT = 0x3 +CREATE2_RESULT_SLOT = 0x5 + +# Gas available in the caller frame at the CALL on the starved-outer +# arms: far below the creator's needs, and retaining under 1/64th of the +# EIP-2200 stipend so no post-call store can run — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreate2OOGFiller.json", + "state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json", + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), + ], +) +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), ], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize("tx_value", [1, 0]) def test_revert_depth_create2_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create2_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Two stacked budgets decide how deep a nested CREATE2 gets.""" + # The creator: entry marker, an empty-init-code CREATE2 whose result + # is stored (success leaves the created address plus one, a failure + # leaves exactly one), and a completion marker. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD( + 0x1, Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0) + ), + key_warm=False, + original_value=0, + new_value=0x1, + ) + sstore_3 = Op.SSTORE( + key=CREATOR_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + creator = pre.deploy_contract( + code=sstore_2 + result_store + sstore_3 + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The empty-init-code child consumes nothing and returns its whole + # grant, so the creator's needs are just its composite costs. + creator_needed = ( + sstore_2.gas_cost(fork) + + result_store.gas_cost(fork) + + sstore_3.gas_cost(fork) ) + if creator_covered: + forwarded = creator_needed + 1_000 + else: + forwarded = creator_needed // 2 - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The caller: entry marker, the CALL with its grant taken from + # calldata (as in the ported filler), result store, completion + # marker. + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, + ) + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available + ) + else: + # The clamped grant starves the creator, and the 1/64 retention + # cannot run any store afterwards: the caller must die too. + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < creator_needed, "the creator must be starved" + assert STARVED_AVAILABLE // 64 <= fork.gas_costs().CALL_STIPEND, ( + "the retention must not afford the post-call store" ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), + created = compute_create2_address(creator, 0, b"") + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + created_account: Account | type = Account.NONEXISTENT + elif not creator_covered: + # The creator died mid-creation and was rolled back. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + created_account = Account.NONEXISTENT + else: + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: int.from_bytes(bytes(created), "big") + 1, + CREATOR_DONE_SLOT: 0xC, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] + nonce=2, + ) + created_account = Account(nonce=1, code=b"", balance=0) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py deleted file mode 100644 index d39042da203..00000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Test_revert_depth_create2_oog_berlin. - -Ported from: -state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create2_oog_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Test_revert_depth_create2_oog_berlin.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py index 9ecb01ac432..ac9cbaf99d6 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py @@ -1,221 +1,264 @@ """ -Copy of this test for CREATE2. +Verify a CREATE2 that collides with a live account — the very caller +that funded the attempt: the collision burns the child's grant and bumps +the creator's nonce without creating anything, and the two stacked +budgets decide whether the creator survives its aftermath, dies on it, +or the whole outer frame runs dry. Ported from: state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json +state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in, and the legacy fillers' vacancy is repaired: they kept the +collider at contract_1's CREATE address while the code runs CREATE2, so +nothing ever collided — the caller now occupies the CREATE2 target. The +creator pre-writes its result slot so the post-collision store is a +dirty-warm write its 1/64 retention can afford, and all budgets derive +from fork composites. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2 as ported, plus the pre-written CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATE2_RESULT_SLOT = 0x5 +# Pre-written sentinel, overwritten by the CREATE2 result plus one: a +# surviving creator must show 1 (collision), never 0xFF or an address. +RESULT_PREWRITE = 0xFF + +# Post-collision slack for the starved-creator arm: retains under 1/64th +# of the EIP-2200 stipend, so the result store cannot run. +STARVED_SLACK = 10_000 +# The SSTORE composite prices a dirty re-store at the Berlin-era 100 on +# every fork, but the un-metered pre-Berlin schedule charges up to 5000 +# (EIP-1283 was reverted in ConstantinopleFix); the covered arm's +# retention carries this headroom so those forks stay covered too. +DIRTY_STORE_HEADROOM = 5_000 +# Gas available in the caller frame at the CALL on the starved-outer +# arms: too little for the creator, and retaining too little for any +# post-call store — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json", + "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json", # noqa: E501 + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), ], ) +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), + ], +) +@pytest.mark.parametrize("tx_value", [1, 0]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 collision burns the grant; budgets decide what's left.""" + # The creator: entry marker, result-slot pre-write, then the CREATE2 + # aimed at the caller's (occupied) address, whose result overwrites + # the sentinel as a dirty-warm store the 1/64 retention can pay. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + sentinel_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=RESULT_PREWRITE, + key_warm=False, + original_value=0, + new_value=RESULT_PREWRITE, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x0, + size=0x0, + salt=0x0, + account_new=False, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, create2_code), + key_warm=True, + original_value=0, + current_value=RESULT_PREWRITE, + new_value=0x1, + ) + creator = pre.deploy_contract( + code=sstore_2 + sentinel_store + result_store + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The caller occupies the creator's CREATE2 target (this is the + # repaired collision, and why the pre allocation must stay mutable). + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, + ) + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, ) + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = compute_create2_address(creator, 0, b"") + pre.deploy_contract(code=caller_code, address=caller) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The collision consumes everything left after the CREATE2's charges + # but one 64th (the live target means no new-account state gas moves + # in either direction), so the creator's fate is set by the slack + # riding on top of its pre-collision costs. + create2_charge = create2_code.gas_cost(fork) + result_tail = result_store.gas_cost(fork) - create2_charge + stipend = fork.gas_costs().CALL_STIPEND + if creator_covered: + slack = 64 * (stipend + result_tail + DIRTY_STORE_HEADROOM + 100) + else: + slack = STARVED_SLACK + assert slack // 64 <= stipend, ( + "the retention must not afford the result store" + ) + forwarded = ( + sstore_2.gas_cost(fork) + + sentinel_store.gas_cost(fork) + + create2_charge + + slack ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available + ) + else: + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < sstore_2.gas_cost(fork) + sentinel_store.gas_cost( + fork + ), "the creator must die before its CREATE2" + assert STARVED_AVAILABLE // 64 <= stipend, ( + "the retention must not afford the post-call store" ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + elif not creator_covered: + # The creator reached the collision but died on its aftermath + # and was rolled back — including the collision's nonce bump. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + else: + # The collision's signature: a nonce bump with nothing created, + # a zero CREATE2 result, and the caller's account untouched. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: 0x1, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] + nonce=2, + ) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py deleted file mode 100644 index 7e8a5f6ff4b..00000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -Copy of this test for CREATE2. - -Ported from: -state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json - -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create_address_collision_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50_2.py b/tests/ported_static/stRefundTest/test_refund50_2.py index 4d471f24074..94ed5341671 100644 --- a/tests/ported_static/stRefundTest/test_refund50_2.py +++ b/tests/ported_static/stRefundTest/test_refund50_2.py @@ -1,17 +1,21 @@ """ -Test_refund50_2. +Verify the EIP-3529 refund cap over five storage clears: the sender's +final balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50_2Filler.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,57 +24,67 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50_2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50_2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50_2.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Five storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.SSTORE( + key=0xA, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0xB, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 10 ]] 1 [[ 11 ]] 1 [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE(key=0xB, value=0x1) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - target: Account(storage={10: 1, 11: 1}), - coinbase: Account(balance=0), - sender: Account(balance=0x8D926C, nonce=1), + target: Account(storage={0xA: 1, 0xB: 1}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50percent_cap.py b/tests/ported_static/stRefundTest/test_refund50percent_cap.py index d92a991123c..ece04d36914 100644 --- a/tests/ported_static/stRefundTest/test_refund50percent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund50percent_cap.py @@ -1,18 +1,21 @@ """ -Test_refund50percent_cap. +Verify the EIP-3529 refund cap over six storage clears: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50percentCapFiller.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,69 +24,84 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50percentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50percent_cap( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50percent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xDC4EFA209AECDD4C2D5201A419EA27506151B4EC687F14A613229E310932491B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Six storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.POP(Op.SLOAD(key=0x1, key_warm=False)) + + Op.POP(Op.SLOAD(key=0x2, key_warm=False)) + + Op.SSTORE( + key=0xA, + value=Op.EXP(0x2, 0xFF, exponent=0xFF), + key_warm=False, + original_value=0, + new_value=2**255, + ) + + Op.SSTORE( + key=0xB, + value=Op.BALANCE(address=Op.ADDRESS, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x6, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x989680) - # Source: lll - # { @@1 @@2 [[ 10 ]] (EXP 2 0xff) [[ 11 ]] (BALANCE (ADDRESS)) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP(Op.SLOAD(key=0x1)) - + Op.POP(Op.SLOAD(key=0x2)) - + Op.SSTORE(key=0xA, value=Op.EXP(0x2, 0xFF)) - + Op.SSTORE(key=0xB, value=Op.BALANCE(address=Op.ADDRESS)) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xEF67F354C8505E1056889970C3D9B5E0FE65D1E2), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { target: Account( - storage={ - 10: 0x8000000000000000000000000000000000000000000000000000000000000000, # noqa: E501 - 11: 0xDE0B6B3A7640000, - }, + storage={0xA: 2**255, 0xB: CONTRACT_BALANCE}, ), - coinbase: Account(balance=0), - sender: Account(balance=0x8CF0A0), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_call_a.py b/tests/ported_static/stRefundTest/test_refund_call_a.py index bc745f449be..c42de668a97 100644 --- a/tests/ported_static/stRefundTest/test_refund_call_a.py +++ b/tests/ported_static/stRefundTest/test_refund_call_a.py @@ -1,17 +1,21 @@ """ -Test_refund_call_a. +Verify a storage-clear refund earned inside a sub-call: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund_CallAFiller.json + +@manually-enhanced: Do not overwrite. The sub-call forwards all gas +instead of a schedule-sized constant, and the sender's balance, refund cap +and budget derive from the fork (`code.gas_cost` / `code.refund` +composites), so EIP-8037's repriced stores are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,72 +24,65 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_CallAFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_call_a( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_call_a.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x1312D00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A callee's storage clear refunds gas up to the EIP-3529 cap.""" + callee_code = Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 1 ]] 0 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x0) + Op.STOP, + callee = pre.deploy_contract( + code=callee_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) - # Source: lll - # { [[ 0 ]] (CALL 5500 0 0 0 0 0 )} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x157C, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, + + # The caller's own slot 1 stays set: the callee clears its own storage. + caller_code = Op.SSTORE( + key=0x0, + value=Op.CALL(address=callee, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract( + code=caller_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = ( + intrinsic + caller_code.gas_cost(fork) + callee_code.gas_cost(fork) + ) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=200000, - value=10, + to=caller, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + ) + + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min( + caller_code.refund(fork) + callee_code.refund(fork), executed // 5 ) + gas_used = executed - refund post = { - target: Account(storage={0: 1, 1: 1}, balance=0xDE0B6B3A764000A), - coinbase: Account(balance=0), - sender: Account(balance=0x12A2AD2, nonce=1), - addr: Account(storage={}), + caller: Account(storage={0: 1, 1: 1}), + callee: Account(storage={}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py index 2fc32dac534..812a0cb1ed4 100644 --- a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py @@ -1,157 +1,181 @@ """ -Test_refund_suicide50procent_cap. +Verify the EIP-3529 refund cap when eight storage clears surround a +gas-limited call to a self-destructing contract: the stored gas delta and +the sender's final balance track the executed gas minus the capped refund, +for both a starved and a fully funded sub-call. Ported from: state_tests/stRefundTest/refundSuicide50procentCapFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grant, the stored gas +delta, the refund cap and the budget all derive from fork composites; the +destructor self-destructs to CALLER so every address is dynamic; the post +branches on EIP-6780 for the destructor's survival. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TARGET_BALANCE = 0xDE0B6B3A7640000 +DESTRUCTOR_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +FLAG_SLOT = 0xA +RESULT_SLOT = 0xB +GAS_SLOT = 0x17 +SNAPSHOT_OFFSET = 0x16 +MEMORY_SIZE = SNAPSHOT_OFFSET + 32 +GRANT_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refundSuicide50procentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("London") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - ], + "call_succeeds", + [False, True], + ids=["starved_grant", "full_grant"], ) -@pytest.mark.pre_alloc_mutable def test_refund_suicide50procent_cap( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_succeeds: bool, ) -> None: - """Test_refund_suicide50procent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x3B9ACA00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """Storage clears around a self-destruct call refund up to the cap.""" + destructor_code = Op.SELFDESTRUCT( + address=Op.CALLER, address_warm=True, account_new=False + ) + destructor = pre.deploy_contract( + code=destructor_code, + balance=DESTRUCTOR_BALANCE, ) - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [22] (GAS) [[ 10 ]] 1 [[ 11 ]] (CALL (CALLDATALOAD 0) 0 0 0 0 0 ) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 [[ 7 ]] 0 [[ 8 ]] 0 [[ 23 ]] (SUB @22 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x16, value=Op.GAS) - + Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE( - key=0xB, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0x4FF65047CE9C85F968689E4369C10003026A41A9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.SSTORE(key=0x7, value=0x0) - + Op.SSTORE(key=0x8, value=0x0) - + Op.SSTORE(key=0x17, value=Op.SUB(Op.MLOAD(offset=0x16), Op.GAS)) - + Op.STOP, - storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45), # noqa: E501 + # The grant either covers the destructor completely or falls one gas + # short, so the sub-call forfeits its whole grant. + destructor_cost = destructor_code.gas_cost(fork) + if call_succeeds: + grant = destructor_cost + GRANT_MARGIN + inner_consumed = destructor_cost + else: + grant = destructor_cost - 1 + inner_consumed = grant + call_result = 1 if call_succeeds else 0 + + # First GAS read: the delta window opens after the GAS opcode itself. + head = Op.MSTORE( + offset=SNAPSHOT_OFFSET, value=Op.GAS, new_memory_size=MEMORY_SIZE ) - # Source: lll - # { (SELFDESTRUCT ) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT( - address=0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45 + body = Op.SSTORE( + key=FLAG_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=destructor, + address_warm=False, + ), + key_warm=False, + original_value=0, + new_value=call_result, + ) + for slot in range(1, 9): + body += Op.SSTORE( + key=slot, + value=0x0, + key_warm=False, + original_value=1, + new_value=0, ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x4FF65047CE9C85F968689E4369C10003026A41A9), # noqa: E501 + # Second GAS read closes the window; the head's own GAS cost stands in + # for it in the derived delta (both GAS reads cost the same). + # new_value is a placeholder: an SSTORE's cost depends only on the + # zero/non-zero transition, not the stored magnitude. + tail = Op.SSTORE( + key=GAS_SLOT, + value=Op.SUB( + Op.MLOAD( + offset=SNAPSHOT_OFFSET, + new_memory_size=MEMORY_SIZE, + old_memory_size=MEMORY_SIZE, + ), + Op.GAS, + ), + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=head + body + tail + Op.STOP, + storage=dict.fromkeys(range(1, 9), 1), + balance=TARGET_BALANCE, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 0, 23: 0x107A7}, - balance=0xDE0B6B3A7640000, - ), - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 1, 23: 0x166FA}, - balance=0x1BC16D674EC80000, - ), - sender: Account(nonce=1), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + gas_delta = head.gas_cost(fork) + body.gas_cost(fork) + inner_consumed - tx_data = [ - Hash(0x1F4), - Hash(0x10000), - ] - tx_gas = [10000000] + data = Hash(grant) + # The refund cap is a fifth of the gas actually deducted before + # execution, which excludes the EIP-7623 calldata floor. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + executed = intrinsic + gas_delta + tail.gas_cost(fork) + gas_limit = executed + 5_000 + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=data, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + # EIP-3529 caps the refund at a fifth of the executed gas. + total_refund = body.refund(fork) + ( + destructor_code.refund(fork) if call_succeeds else 0 + ) + refund = min(total_refund, executed // 5) + gas_used = executed - refund + + post = { + target: Account( + storage={ + FLAG_SLOT: 1, + RESULT_SLOT: call_result, + GAS_SLOT: gas_delta, + }, + balance=TARGET_BALANCE + + (DESTRUCTOR_BALANCE if call_succeeds else 0), + ), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + destructor: ( + ( + Account(balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ) + if call_succeeds + else Account(balance=DESTRUCTOR_BALANCE, storage={}) + ), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py index ab754657c75..0d9d5be2f6b 100644 --- a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py +++ b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py @@ -1,18 +1,21 @@ """ -Test_refund_tx_to_suicide. +Verify a transaction into a self-destructing contract: the balance +(including the transaction value) moves to the beneficiary and, post +EIP-3529, no self-destruct refund is granted. Ported from: state_tests/stRefundTest/refund_TxToSuicideFiller.json + +@manually-enhanced: Do not overwrite. Beneficiary and budget are derived +(nonexistent account, `code.gas_cost` composite) and the post branches on +EIP-6780 (pre-Cancun the contract is deleted, after it persists). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,59 +24,61 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +TX_VALUE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_TxToSuicideFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_tx_to_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_tx_to_suicide.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xA2333EEF5630066B928DEA5FD85A239F511B5B067D1441EE7AC290D0122B917B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Self-destruct moves the balance and grants no refund.""" + beneficiary = pre.nonexistent_account() + code = Op.SELFDESTRUCT( + address=beneficiary, address_warm=False, account_new=True ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x5F5E100) - # Source: lll - # { (SELFDESTRUCT 0x095e7baea6a6c7c4c2dfeb977efac326af552d87) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x95E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - + Op.STOP, + target = pre.deploy_contract( + code=code, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x2BC33A472F0FBA1E30BF2317D07910367908C7F6), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()(sends_value=True) + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=61003, - value=10, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + value=TX_VALUE, ) + # EIP-3529 removed the self-destruct refund entirely. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87): Account( - storage={}, balance=0xDE0B6B3A764000A + beneficiary: Account(balance=CONTRACT_BALANCE + TX_VALUE), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + target: ( + Account(storage={1: 1}, balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ), + sender: Account( + balance=INITIAL_BALANCE - TX_VALUE - gas_used * GAS_PRICE ), - coinbase: Account(balance=0), - sender: Account(balance=0x5EDB318, nonce=1), - target: Account(storage={1: 1}, balance=0, nonce=0), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py index fc4b0089a75..f94a727fd09 100644 --- a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py @@ -1,8 +1,16 @@ """ -Test_loop_calls_depth_then_revert. +Verify a mutual CALL recursion that terminates by gas exhaustion: two +contracts increment their own counters and call each other until the +EIP-150 63/64 attenuation starves the deepest frame, whose failed store +reverts alone while every ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame counts are +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +18,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame counts. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which calls it) can be built. +PONG_ADDRESS = Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the own counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.CALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json"], @@ -29,65 +52,30 @@ def test_loop_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF59FD1C021541704A4A52C067454304566717666), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0xF59FD1C021541704A4A52C067454304566717666, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2), # noqa: E501 - ) + """Only the gas-starved deepest frame of a call loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # EIP-8037's state gas for the two first stores trims one frame off + # the depth the 63/64 attenuation allows. + if fork.is_eip_enabled(8037): + ping_frames, pong_frames = 192, 192 + else: + ping_frames, pong_frames = 193, 192 + post = { - target: Account(storage={0: 193}), - addr: Account(storage={0: 192}), + ping: Account(storage={0: ping_frames}), + pong: Account(storage={0: pong_frames}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py index 1be3fb70d90..9267e1e2355 100644 --- a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py @@ -1,8 +1,17 @@ """ -Test_loop_delegate_calls_depth_then_revert. +Verify a mutual DELEGATECALL recursion that terminates by gas +exhaustion: both contracts' code increments the entry contract's counter +(the storage context never changes) until the EIP-150 63/64 attenuation +starves the deepest frame, whose failed store reverts alone while every +ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame count is +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +19,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame count. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which delegate-calls it) can be built. +PONG_ADDRESS = Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the context counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.DELEGATECALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json"], @@ -29,63 +53,29 @@ def test_loop_delegate_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_delegate_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228), # noqa: E501 - ) + """Only the gas-starved deepest frame of a delegate loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # every frame increments the entry contract's counter because + # DELEGATECALL keeps the storage context; the partner's own storage + # is never touched. EIP-8037's state gas for the first store shifts + # the depth the 63/64 attenuation allows. + frames = 385 if fork.is_eip_enabled(8037) else 386 + post = { - target: Account(storage={0: 386}), - addr: Account(storage={}), + ping: Account(storage={0: frames}), + pong: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py index fe255c65d17..b123500878c 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py @@ -1,221 +1,183 @@ """ -Test_revert_depth_create_address_collision. +Verify revert propagation around a nested CREATE whose target address +collides with an existing contract - its own caller: the creating frame +always fails and the collided account survives untouched, while the +caller completes only when its budget covers the forfeited grant. Ported from: state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json + +@manually-enhanced: Do not overwrite. Restores the collision the machine +port lost: the caller is deployed at the creator's CREATE address (as in +the original filler). Grants and budgets derive from fork composites and +the collided account's code, nonce and storage are pinned in every arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, -) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, + compute_create_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame when its CREATE executes: the collision +# consumes it, so the following store can never be paid. +BURN_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "oversized_ask", + [False, True], + ids=["modest_ask", "oversized_ask"], ) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], +) +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oversized_ask: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_address_collision.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 + """A CREATE address collision leaves the collided account intact.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, + ) ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB1B49241A4ECF7860872E686090781C906B1B437), # noqa: E501 + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0 0 0 0 0) [[4]] 12 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB1B49241A4ECF7860872E686090781C906B1B437, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x97E33A176B7C8D61B356D1C170AC2119D28867DF), # noqa: E501 + creator = pre.deploy_contract( + code=creator_store + create_code + creator_tail + Op.STOP ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={0: 1, 4: 12}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + # The creator's frame always fails, so the call result is always 0. + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller_code = head_store + call_store + tail_store + Op.STOP + # The caller sits exactly at the creator's CREATE address: the + # nested creation collides with the very contract that called it. + caller = pre.deploy_contract( + code=caller_code, + address=compute_create_address(address=creator, nonce=1), + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 160000] - tx_value = [1, 0] + intrinsic_calculator = fork.transaction_intrinsic_cost_calculator() + modest_grant = (creator_store + create_code).gas_cost(fork) + BURN_MARGIN + gas_limit = ( + intrinsic_calculator( + calldata=Hash(modest_grant), + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + modest_grant + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # An oversized ask is clamped to the EIP-150 63/64 cap, leaving the + # caller only 1/64 of its remaining gas: it can never complete. + grant = gas_limit if oversized_ask else modest_grant + data = Hash(grant) + intrinsic = intrinsic_calculator( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + if oversized_ask: + store_costs = ( + call_store.gas_cost(fork) + - call_code.gas_cost(fork) + + tail_store.gas_cost(fork) + ) + assert available // 64 < store_costs, "caller must fail" + else: + assert grant <= available - available // 64, "grant is granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + caller_completes = ample_budget and not oversized_ask + post = { + # The collided account survives with its code and nonce intact. + caller: Account( + code=caller_code, + nonce=1, + storage={0: 1, 4: 0xC} if caller_completes else {}, + balance=tx_value if caller_completes else 0, + ), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py index 8f3fa8594eb..9eca41eee77 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py @@ -1,194 +1,180 @@ """ -Test_revert_depth_create_oog. +Verify revert propagation around a nested CREATE that runs out of gas: +a sub-call either funds its CREATE-then-store sequence completely, or +runs out of gas after the CREATE, reverting the created account but not +the caller; a starved outer budget reverts everything. Ported from: state_tests/stRevertTest/RevertDepthCreateOOGFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grants and both +transaction budgets derive from fork composites (EIP-8037 state gas is +tracked instead of pinned), all addresses are dynamic, and every account +including the created one is pinned in each arm. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame after its CREATE: enough to reach the +# following store, never enough to pay for it. +PARTIAL_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateOOGFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "full_grant", + [False, True], + ids=["partial_grant", "full_grant"], +) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) def test_revert_depth_create_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_grant: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, + """An out-of-gas CREATE frame reverts alone; a starved caller fully.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, ) + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + creator_code = creator_store + create_code + creator_tail + Op.STOP + creator = pre.deploy_contract(code=creator_code) + created = compute_create_address(address=creator, nonce=1) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] + # The grant covers the creator completely, or only up to and + # including its CREATE, leaving too little for the following store. + if full_grant: + grant = creator_code.gas_cost(fork) + BUDGET_MARGIN + inner_consumed = creator_code.gas_cost(fork) + else: + grant = (creator_store + create_code).gas_cost(fork) + PARTIAL_MARGIN + inner_consumed = grant + inner_succeeds = ample_budget and full_grant + inner_fails_reaching_create = ample_budget and not full_grant - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=1 if inner_succeeds else 0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller = pre.deploy_contract( + code=head_store + call_store + tail_store + Op.STOP + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 180000] - tx_value = [1, 0] + data = Hash(grant) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + inner_consumed + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # The requested grant must fit under the EIP-150 63/64 cap. + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + assert grant <= available - available // 64, "grant must be granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post: dict + if inner_succeeds: + post = { + created: Account(nonce=1), + caller: Account(storage={0: 1, 1: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={2: 8, 3: 0xC}), + } + elif inner_fails_reaching_create: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={0: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={}), + } + else: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={}, balance=0), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py index 6079b5cf452..2f6ab5355af 100644 --- a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py +++ b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py @@ -1,34 +1,46 @@ """ -Test: this test checks that the returndata buffer is changed when a... +Verify that a reverting callee's return data replaces the empty return +data left by a previously failed call: each prober records the failed +call's result and RETURNDATASIZE, for CALL, CALLCODE, DELEGATECALL and a +nested CALL chain, with an ample and a starved transaction budget. Ported from: state_tests/stRevertTest/RevertOpcodeInCallsOnNonEmptyReturnDataFiller.json -@manually-enhanced: Do not overwrite. Inner-CALL/DELEGATECALL gas -bumped on Amsterdam to cover EIP-8037 state-gas spill into regular gas; -pre-EIP-8037 unchanged. +@manually-enhanced: Do not overwrite. Sub-calls forward all gas and the +ample arm omits the gas limit (maxing the EIP-8037 reservoir), replacing +per-fork gas bumps; the starved budget derives from fork composites; all +addresses are dynamic and every contract is pinned in the post. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +RESULT_SLOT = 0x0 +RETURN_DATA_SIZE_SLOT = 0x2 +NESTED_RESULT_SLOT = 0x4 +NESTED_RETURN_DATA_SIZE_SLOT = 0x5 +ENTRY_SLOT = 0xA +ENTRY_SLOT_INITIAL = 255 +# A zero-gas grant: the prelude call must fail without touching the +# return data buffer. +FAILING_CALL_GAS = 0 +# Gas left at the entry frame's call site in the starved arm: enough to +# start the call, too little for any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( [ @@ -37,396 +49,146 @@ ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), - pytest.param( - 2, - 0, - 0, - id="d2-g0", - ), - pytest.param( - 2, - 1, - 0, - id="d2-g1", - ), - pytest.param( - 3, - 0, - 0, - id="d3-g0", - ), - pytest.param( - 3, - 1, - 0, - id="d3-g1", - ), - ], + "call_op", + [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, None], + ids=["call", "callcode", "delegatecall", "nested_call"], +) +@pytest.mark.parametrize( + "ample_gas", + [True, False], + ids=["ample", "starved"], ) -@pytest.mark.pre_alloc_mutable def test_revert_opcode_in_calls_on_non_empty_return_data( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_op: Op | None, + ample_gas: bool, ) -> None: - """Test: tis test checks that the returndata buffer is changed when a...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # EIP-8037 inner-CALL/DELEGATECALL gas bumps: original values - # restored for pre-EIP-8037 forks; bumped for state-gas spill on - # Amsterdam. - inner_call_gas = 50000 - deeper_call_gas = 100000 - deepest_call_gas = 260000 - if fork.is_eip_enabled(8037): - inner_call_gas = 100000 - deeper_call_gas = 1000000 - deepest_call_gas = 1000000 - # Source: lll - # { [[1]] 12 (REVERT 0 1) [[3]] 13 } - addr_6 = pre.deploy_contract( # noqa: F841 + """A revert's return data is observed after a failed call.""" + # Reverts one byte of return data; the store before the REVERT is + # undone and the code after it must never run. + reverter = pre.deploy_contract( code=Op.SSTORE(key=0x1, value=0xC) + Op.REVERT(offset=0x0, size=0x1) + Op.SSTORE(key=0x3, value=0xD) + Op.STOP, - balance=1, - nonce=0, - address=Address(0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B), # noqa: E501 ) - # Source: lll - # { [1] 12 (RETURN 0 64) } - addr_7 = pre.deploy_contract( # noqa: F841 + # Would return 64 bytes, but is only ever called with zero gas. + returner = pre.deploy_contract( code=Op.MSTORE(offset=0x1, value=0xC) - + Op.RETURN(offset=0x0, size=0x40) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x127EAF7E31D691A8393B7A2F84A6E94372190C01), # noqa: E501 + + Op.RETURN(offset=0x0, size=0x40), ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (DELEGATECALL 50000 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.DELEGATECALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xF20CCAF271BEAA36E7CF4C9CED2867FAC9558F14), # noqa: E501 + + prelude = Op.POP( + Op.CALL(gas=FAILING_CALL_GAS, address=returner, address_warm=False) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALLCODE 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.CALLCODE( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + def prober_code( + op: Op, callee: Address, result_slot: int, rds_slot: int + ) -> Bytecode: + """Call the callee and record its result and RETURNDATASIZE.""" + return ( + prelude + + Op.SSTORE(key=result_slot, value=op(address=callee)) + + Op.SSTORE(key=rds_slot, value=Op.RETURNDATASIZE) + + Op.STOP ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xC9DA6CD8413F64323F12CD44C99671F280F15E1C), # noqa: E501 + + prober_call = pre.deploy_contract( + code=prober_code(Op.CALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[4]] (CALL 50000 0 0 0 0 0) [[5]] (RETURNDATASIZE) } # noqa: E501 - addr_5 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x4, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + prober_callcode = pre.deploy_contract( + code=prober_code( + Op.CALLCODE, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE(key=0x5, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + prober_delegatecall = pre.deploy_contract( + code=prober_code( + Op.DELEGATECALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE73611B5B479B30C93AC377AEB3BFB199764F3C3), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[10]] (CALL 260000 (CALLDATALOAD 0) 0 0 0 0 0)} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0xA, - value=Op.CALL( - gas=deepest_call_gas, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + inner_prober = pre.deploy_contract( + code=prober_code( + Op.CALL, + reverter, + NESTED_RESULT_SLOT, + NESTED_RETURN_DATA_SIZE_SLOT, ) - + Op.STOP, - storage={10: 255}, - balance=1, - nonce=0, - address=Address(0x172A8F572404293AA810685DFDC6F740C300CC4B), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 100000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_4 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=deeper_call_gas, - address=0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + prober_nested = pre.deploy_contract( + code=prober_code( + Op.CALL, inner_prober, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x6BACDFA8216DBB2A09819F8739E57AE3574C9FFF), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_2: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_2: Account(storage={}), - }, - }, - { - "indexes": {"data": 2, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_3: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 2, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_3: Account(storage={}), - }, - }, - { - "indexes": {"data": 3, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_4: Account(storage={0: 1}, nonce=0), - addr_5: Account(storage={5: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 3, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 255}), - addr_4: Account(storage={0: 0}, nonce=0), - addr_5: Account(storage={5: 0}, nonce=0), - }, - }, - ] + big_call = Op.CALL(address=Op.CALLDATALOAD(offset=0x0), address_warm=False) + entry = pre.deploy_contract( + code=prelude + Op.SSTORE(key=ENTRY_SLOT, value=big_call) + Op.STOP, + storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}, + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + probers = { + Op.CALL: prober_call, + Op.CALLCODE: prober_callcode, + Op.DELEGATECALL: prober_delegatecall, + None: prober_nested, + } + prober = probers[call_op] + data = Hash(prober, left_padding=True) - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - Hash(addr_4, left_padding=True), - ] - tx_gas = [860000, 28000] + sender = pre.fund_eoa() + if ample_gas: + tx = Transaction(sender=sender, to=entry, data=data) + else: + # The entry frame reaches its call with only STARVE_MARGIN left: + # the prober cannot even pay its prelude, and the retained 1/64 + # cannot pass the EIP-2200 stipend check, so everything reverts. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + starved = ( + intrinsic + + prelude.gas_cost(fork) + + big_call.gas_cost(fork) + + STARVE_MARGIN + ) + forwarded = STARVE_MARGIN - STARVE_MARGIN // 64 + assert forwarded < prelude.gas_cost(fork), "prober must starve" + assert STARVE_MARGIN // 64 <= 2300, "entry store must halt" + tx = Transaction(sender=sender, to=entry, data=data, gas_limit=starved) - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, - ) + untouched = { + reverter: Account(storage={}), + prober_call: Account(storage={}), + prober_callcode: Account(storage={}), + prober_delegatecall: Account(storage={}), + inner_prober: Account(storage={}), + prober_nested: Account(storage={}), + } + if not ample_gas: + # The transaction runs out of gas at the entry frame: everything + # reverts. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}), + } + elif call_op is None: + # The nested prober's callee completes (its own probe fails), so + # the outer call succeeds and returns no data. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober_nested: Account(storage={RESULT_SLOT: 1}), + inner_prober: Account(storage={NESTED_RETURN_DATA_SIZE_SLOT: 1}), + } + else: + # The probed call reverts with one byte of return data: result 0, + # RETURNDATASIZE 1. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober: Account(storage={RETURN_DATA_SIZE_SLOT: 1}), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From ca79dcbe44808edba3a8fd1b22d4b3e084ccc7c6 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:04:04 +0200 Subject: [PATCH 52/55] fix(tests): correct Account.NONEXISTENT annotations for mypy --- tests/ported_static/stAttackTest/test_crashing_transaction.py | 2 +- .../test_create_name_registrator_per_txs_not_enough_gas.py | 2 +- tests/ported_static/stCreate2/test_create2no_cash.py | 4 +++- .../ported_static/stCreate2/test_revert_depth_create2_oog.py | 2 +- ..._creation_oo_gdont_leave_empty_contract_via_transaction.py | 2 +- .../stInitCodeTest/test_out_of_gas_contract_creation.py | 2 +- .../test_out_of_gas_prefunded_contract_creation.py | 2 +- tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py | 4 +++- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index 6e62bb785ee..d45d2a1b9fe 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -111,7 +111,7 @@ def test_crashing_transaction( if fork.is_eip_enabled(8037): # An iteration's state gas exceeds the loop's 50000-gas guard, # so the init frame dies mid-CREATE and no account survives. - created_account: Account | type = Account.NONEXISTENT + created_account: Account | None = Account.NONEXISTENT else: created_account = Account( code=bytes.fromhex("60606040526008565b00"), diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py index 7e3bd9b3473..48a1e1efced 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py @@ -109,7 +109,7 @@ def test_create_name_registrator_per_txs_not_enough_gas( created = compute_create_address(address=sender, nonce=0) if enough_gas: - created_account: Account | type = Account( + created_account: Account | None = Account( nonce=1, code=deposited, balance=TX_VALUE, diff --git a/tests/ported_static/stCreate2/test_create2no_cash.py b/tests/ported_static/stCreate2/test_create2no_cash.py index 5dc49d75d32..c61a5f6fed3 100644 --- a/tests/ported_static/stCreate2/test_create2no_cash.py +++ b/tests/ported_static/stCreate2/test_create2no_cash.py @@ -84,7 +84,9 @@ def test_create2no_cash( # The whole (topped-up) balance moved into the created account, # and the creator's nonce was consumed by the creation. creator_account = Account(nonce=2, balance=0) - created_account = Account(nonce=1, code=b"", balance=CREATE2_ENDOWMENT) + created_account: Account | None = Account( + nonce=1, code=b"", balance=CREATE2_ENDOWMENT + ) else: # The balance preflight (or the static fault) aborts before any # account is touched: no creation and no nonce bump. diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py index a5dd5bc6260..17782417272 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py @@ -193,7 +193,7 @@ def test_revert_depth_create2_oog( # The whole transaction ran dry: only the code survives. caller_account = Account(storage={}, code=caller_code, balance=0) creator_account = Account(storage={}, nonce=1) - created_account: Account | type = Account.NONEXISTENT + created_account: Account | None = Account.NONEXISTENT elif not creator_covered: # The creator died mid-creation and was rolled back. caller_account = Account( diff --git a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py index 57bec829c78..b59d7c67858 100644 --- a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py +++ b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py @@ -90,7 +90,7 @@ def test_contract_creation_oo_gdont_leave_empty_contract_via_transaction( created = compute_create_address(address=sender, nonce=0) if enough_gas: - created_account: Account | type = Account(nonce=1, code=b"", balance=0) + created_account: Account | None = Account(nonce=1, code=b"", balance=0) writer_storage = {1: 1} else: created_account = Account.NONEXISTENT diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py index a35d2a4453a..078fb6506fe 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py @@ -118,7 +118,7 @@ def test_out_of_gas_contract_creation( created = compute_create_address(address=sender, nonce=0) if enough_gas and not invalid_initcode: - created_account: Account | type = Account( + created_account: Account | None = Account( nonce=1, code=b"", storage={1: 6}, balance=1 ) else: diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py index d3cc55522fa..c72984e1633 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py @@ -141,7 +141,7 @@ def test_out_of_gas_prefunded_contract_creation( if outcome == "outer_oog": # Creation rolled back: only the prefund remains, nonce untouched. created_account = Account(nonce=0, balance=PREFUND) - child_account: Account | type = Account.NONEXISTENT + child_account: Account | None = Account.NONEXISTENT elif outcome == "child_oog": # The inner CREATE increments the creator's nonce even when the # child fails. diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py index 4ad46a6f155..5647ebd2c95 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py @@ -88,7 +88,9 @@ def bump_statics(key_warm: bool) -> int: + push_cost ) - def call_split(address: object, warm: bool, value: int) -> tuple[int, int]: + def call_split( + address: Address | Op, warm: bool, value: int + ) -> tuple[int, int]: """Pre-GAS-read and upfront charges of one side's call.""" upfront = Op.CALL( address_warm=warm, value_transfer=value > 0 From 13dc9bf1ed829a22e8f92ca690324a43d14b0d25 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:04:27 +0200 Subject: [PATCH 53/55] docs(claude): add refund-cap, collision, and depth-loop lessons to enhance-ported-test --- .claude/commands/enhance-ported-test.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index b15231cb32b..a518ed9151d 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,31 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**Refund-cap derivations need the EIP-7623 kwarg.** The EIP-3529 cap's +base is the gas deducted before execution, which excludes the calldata +floor: pass `return_cost_deducted_prior_execution=True` to the intrinsic +calculator whenever the tx has calldata, or the derived `executed` (and +the cap) overstate. Validated on `test_refund_suicide50procent_cap`. + +**A CREATE address collision burns the child's gas allowance** (the +EIP-684 path): the withheld child grant is consumed, nothing is created, +and under EIP-8037 the new-account state charge is refunded. Useful to +build always-failing creator frames with predictable consumption. +Validated on `test_revert_depth_create_address_collision`. + +**Loop-to-depth-1024 cannot replace loop-to-OOG.** With 63/64 +attenuation, reaching depth 1024 needs ~e^16 × the terminal gas — no +legal budget gets there. For call-loop depth tests the honest shape is a +fixed named budget with per-gas-schedule-era pinned depth counts, each +shift explained (±1 frame ≈ 64·ln(cost ratio)). Validated on +`test_loop_calls_depth_then_revert`. + +**Framework wart: the SSTORE dirty-rewrite composite prices 100 on every +fork**, but Constantinople/Petersburg charge 5,000 for a dirty re-store — +a derived budget that must survive pre-Istanbul forks needs an explicit +headroom constant for it (named, commented). Observed on +`test_revert_depth_create_address_collision`'s ConstantinopleFix sweep. + **EIP-8037 repriced the code deposit's regular part — boundaries beware.** On 8037 forks the deposit charges only the keccak word cost (`OPCODE_KECCAK256_PER_WORD * ceil32(len)/32`, ~6 gas) as regular gas plus From 1fc547cef2635e6e89010114067ff2b602e6e5f6 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:23:34 +0200 Subject: [PATCH 54/55] fix(tests): reword docstring to satisfy codespell --- tests/ported_static/stAttackTest/test_crashing_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index d45d2a1b9fe..a7874ced2d1 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -1,6 +1,6 @@ """ Verify the Ropsten "crashing transaction" attack replay: a creation -transaction whose init code loops CREATEing children while more than +transaction whose init code CREATEs children in a loop while more than 50000 gas remains, then deposits its runtime code. Ported from: From bac43cc8d8751df389a9cb548d934399a8c42e03 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 30 Jul 2026 09:47:57 +0200 Subject: [PATCH 55/55] fix(tests): apply execution-gas rename in ported create tests --- .../stCreateTest/test_create_address_warm_after_fail.py | 2 +- .../stCreateTest/test_create_oog_from_call_refunds.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py index f9ab7c36933..0bd07162f29 100644 --- a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py +++ b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py @@ -408,7 +408,7 @@ def measured_call(frame: int, *, warm: bool, new: bool) -> int: value_transfer=bool(v), account_new=new and bool(v), ) - measured = frame + call.regular_cost(fork) + measured = frame + call.execution_cost(fork) if v: # The callee is empty (or STOP-only), so the stipend # forwarded with the value returns unused. diff --git a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py index c5ca79b876a..e410d9d9b7b 100644 --- a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py +++ b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py @@ -262,7 +262,7 @@ def test_create_oog_from_call_refunds( + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) ) # No init frame can receive enough to pay the OoG arms' deposit. - grant_bound = gas_limit - intrinsic - create_op.regular_cost(fork) + grant_bound = gas_limit - intrinsic - create_op.execution_cost(fork) assert grant_bound * 63 // 64 < deposit_price, ( "63/64 grant must stay below the OoG deposit price" )