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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agents/skills/write-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Conventions and patterns for writing consensus tests. Run this skill before writ

- `pre.fund_eoa()` — create funded EOA, returns Address. Accepts `amount=`, `nonce=`. With `amount=0` nothing is added to the pre-alloc; you get a fresh, nonexistent address.
- `pre.deploy_contract(code=..., storage={...})` — deploy contract, returns Address
- Do not assign into `pre` directly. The one exception is overriding a predeploy's code, `pre[addr] = Account(...)` under `@pytest.mark.pre_alloc_mutable`. It replaces the whole account, and every field you leave out takes the `Account` default (nonce 0, zero balance, empty storage), so pass the ones the scenario needs. The marker skips the test in execute mode, so reserve it for scenarios a live chain cannot host.
- `pre` starts out holding the accounts the fork allocates at genesis, such as the system contracts, and the spec uses it as the genesis allocation unchanged.
- Do not assign into `pre` directly. The one exception is changing one of those genesis accounts under `@pytest.mark.pre_alloc_mutable`. `pre[addr] = Account(...)` replaces the whole account, and every field you leave out takes the `Account` default (nonce 0, zero balance, empty storage), so pass the ones the scenario needs. `del pre[addr]` removes it, for example so a deployment transaction can create it; an all-zero `Account` does not, because the empty account reaches genesis and is rejected. The marker skips the test in execute mode, so reserve it for scenarios a live chain cannot host.

## Bytecode Construction

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def blockchain_test_from_fuzzer(
)

return BlockchainTest(
pre=pre,
pre=pre.with_genesis_accounts(),
fork=fork,
blocks=blocks,
post={}, # Post-state verification can be added later
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def generate_fixture(self, test_params: Dict[str, Any]) -> Dict[str, Any]:
test = BlockchainTest(
genesis_environment=test_params["genesis_environment"],
fork=fork,
pre=test_params["pre"],
pre=test_params["pre"].with_genesis_accounts(),
post=test_params["post"],
blocks=test_params["blocks"],
chain_id=test_params["chain_id"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ def base_test_parametrizer_func(
fork: Fork | TransitionFork,
reference_spec: ReferenceSpec,
pre: Alloc,
fixture_format: Any,
output_dir: Path,
fixture_collector: FixtureCollector,
test_case_description: str,
Expand All @@ -1616,10 +1617,6 @@ def base_test_parametrizer_func(
as value.
"""
del fixed_opcode_count
if hasattr(request.node, "fixture_format"):
fixture_format = request.node.fixture_format
else:
fixture_format = request.param
assert isinstance(fixture_format, LabeledFixtureFormat) or issubclass(
fixture_format, BaseFixture
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import inspect
from functools import cache
from hashlib import sha256
from typing import Any, Dict, List, Literal
from typing import Any, Dict, List, Literal, Type

import pytest
from pydantic import PrivateAttr
Expand Down Expand Up @@ -76,9 +76,13 @@ def __init__(
flags: AllocFlags,
stub_accounts: Dict[str, Account] | None = None,
stub_eoas: Dict[str, EOA] | None = None,
genesis_seeded: bool = False,
**kwargs: Any,
) -> None:
"""Initialize the pre-alloc."""
"""
Initialize the pre-alloc, `genesis_seeded` when it starts out holding
the accounts the fork allocates at genesis.
"""
super().__init__(
*args,
fork=fork,
Expand All @@ -88,6 +92,8 @@ def __init__(
)
if stub_accounts is not None:
self._stub_accounts = stub_accounts
if genesis_seeded:
self._genesis_accounts = "seeded"

def get_next_account_salt(self, account_hash: Hash) -> int:
"""Retrieve the next salt for this account."""
Expand Down Expand Up @@ -486,11 +492,29 @@ def pre(
fork: Fork | TransitionFork,
stub_accounts: Dict[str, Account],
stub_eoas: Dict[str, EOA],
spec_type: Type[BaseTest] | None,
fixture_format: Any,
) -> Alloc:
"""Return default pre allocation for all tests (Empty alloc)."""
"""
Return the pre allocation of the test, seeded with the accounts the
genesis of the fixture being filled holds by default.

The spec uses the returned allocation as the genesis allocation
unchanged, so a test may inspect it, and may override or delete the
seeded accounts at its own risk.
"""
seed = (
spec_type.genesis_pre_allocation(
fork=fork, fixture_format=fixture_format
)
if spec_type is not None
else {}
)
return Alloc(
seed,
flags=alloc_flags,
fork=fork,
stub_accounts=stub_accounts,
stub_eoas=stub_eoas,
genesis_seeded=spec_type is not None,
)
Original file line number Diff line number Diff line change
Expand Up @@ -1037,12 +1037,12 @@ def test_fill_chain_id_reaches_state_test_fixtures(

test_module_benchmark = textwrap.dedent(
"""\
from execution_testing import Account, Environment, TestAddress, Transaction
from execution_testing import Environment, TestAddress, Transaction

def test_benchmark_one(state_test) -> None:
def test_benchmark_one(state_test, pre) -> None:
pre.fund_address(TestAddress, 1_000_000)
state_test(
env=Environment(),
pre={TestAddress: Account(balance=1_000_000)},
post={},
tx=Transaction(gas_limit=0x5208),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import textwrap
from pathlib import Path
from typing import Any, ClassVar, Dict, List
from typing import Any, ClassVar, Dict, List, Mapping
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -38,6 +38,12 @@ def __init__(
genesis_environment=genesis_environment,
)

@classmethod
def genesis_pre_allocation(cls, **kwargs: Any) -> Mapping:
"""Mock genesis pre-allocation method."""
del kwargs
return {}

def generate(self, *args: Any, **kwargs: Any) -> FillResult:
"""Mock generate method."""
raise NotImplementedError("This is a mock test class")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Test that a `pre` a test builds itself must opt into the genesis accounts.

The `pre` fixture starts out holding the accounts the fork allocates at
genesis. An allocation built outside it gets them only through
`with_genesis_accounts()`, and fails the fill without it, so a genesis never
silently lacks the system contracts.
"""

import json
import textwrap

import pytest

FORK = "Prague"

BEACON_ROOTS_ADDRESS = "0x000f3df6d732807ef1319fb7b8bb8522d0beac02"

TEST_MODULE_DIR = "tests/prague/dummy_test_module"

MODULE_TEMPLATE = textwrap.dedent(
"""\
import pytest

from execution_testing import Account, Alloc, Block, BlockchainTestFiller

@pytest.mark.valid_at("{fork}")
def test_case(blockchain_test: BlockchainTestFiller) -> None:
blockchain_test(pre={pre}, blocks=[Block()], post={{}})
"""
)


def fill(pytester: pytest.Pytester, pre: str) -> pytest.RunResult:
"""Fill a test that passes `pre` to `blockchain_test`."""
module_dir = pytester.path / TEST_MODULE_DIR
module_dir.mkdir(parents=True)
module = module_dir / "test_dummy.py"
module.write_text(MODULE_TEMPLATE.format(fork=FORK, pre=pre))
pytester.copy_example(
name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini"
)
return pytester.runpytest(
"-c",
"pytest-fill.ini",
"--fork",
FORK,
"-m",
"blockchain_test",
"--no-html",
"--output",
"fixtures",
str(module.relative_to(pytester.path)),
)


def test_pre_without_genesis_accounts_fails(pytester: pytest.Pytester) -> None:
"""A `pre` that never opted into the genesis accounts fails the fill."""
result = fill(pytester, pre="{0x1000: Account(balance=1)}")

result.assert_outcomes(failed=1)
output = "\n".join(result.outlines + result.errlines)
assert "lacks the accounts" in output, output


def test_pre_with_genesis_accounts_fills(pytester: pytest.Pytester) -> None:
"""The genesis holds both the test's accounts and the fork's."""
result = fill(
pytester,
pre="Alloc({0x1000: Account(balance=1)}).with_genesis_accounts()",
)

result.assert_outcomes(passed=1)
(fixture_file,) = (pytester.path / "fixtures" / "blockchain_tests").rglob(
"*.json"
)
(fixture,) = json.loads(fixture_file.read_text()).values()
assert BEACON_ROOTS_ADDRESS in fixture["pre"]
assert 0x1000 in [int(address, 16) for address in fixture["pre"]]
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest
from execution_testing import (
Account,
Alloc,
BlockException,
Block,
Environment,
Expand All @@ -21,7 +22,7 @@
@pytest.mark.valid_at("Cancun")
def test_verify_sync_default(blockchain_test) -> None:
blockchain_test(
pre={TestAddress: TEST_ADDRESS},
pre=Alloc({TestAddress: TEST_ADDRESS}).with_genesis_accounts(),
post={},
blocks=[Block(txs=[Transaction(gas_limit=0x5208)])]
)
Expand All @@ -31,7 +32,7 @@ def test_verify_sync_default(blockchain_test) -> None:
@pytest.mark.verify_sync
def test_verify_sync_with_marker(blockchain_test) -> None:
blockchain_test(
pre={TestAddress: TEST_ADDRESS},
pre=Alloc({TestAddress: TEST_ADDRESS}).with_genesis_accounts(),
post={},
blocks=[Block(txs=[Transaction(gas_limit=0x5208)])]
)
Expand All @@ -48,7 +49,7 @@ def test_verify_sync_with_marker(blockchain_test) -> None:
)
def test_verify_sync_with_param_marks(blockchain_test, has_exception) -> None:
blockchain_test(
pre={TestAddress: TEST_ADDRESS},
pre=Alloc({TestAddress: TEST_ADDRESS}).with_genesis_accounts(),
post={},
blocks=[
Block(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Shared pytest fixtures and hooks for EEST generation modes (fill and execute).
"""

from typing import Dict, List, Tuple
from typing import Any, Dict, List, Tuple, Type

import pytest
from pytest import StashKey
Expand All @@ -17,7 +17,11 @@
from execution_testing.test_types import EOA, Account, Alloc, ChainConfig

from ..shared.address_stubs import AddressStubs, StubEOA
from ..shared.helpers import get_rpc_endpoint, is_benchmark_test
from ..shared.helpers import (
get_rpc_endpoint,
get_spec_format_for_item,
is_benchmark_test,
)
from ..shared.pre_alloc import AllocFlags
from ..spec_version_checker.spec_version_checker import EIPSpecTestItem

Expand Down Expand Up @@ -399,6 +403,35 @@ def alloc_flags(
return alloc_flags_from_test_markers


def _spec_parameter(request: pytest.FixtureRequest) -> Tuple[Any, Any]:
"""
Return the spec type and format the test is parametrized with, or a
pair of `None` when the test requests no spec.
"""
callspec = getattr(request.node, "callspec", None)
if callspec is None:
return None, None
try:
return get_spec_format_for_item(callspec.params)
except ValueError:
return None, None


@pytest.fixture(scope="function")
def spec_type(request: pytest.FixtureRequest) -> Type[BaseTest] | None:
"""Return the spec type that fills or executes the test."""
return _spec_parameter(request)[0]


@pytest.fixture(scope="function")
def fixture_format(request: pytest.FixtureRequest) -> Any:
"""
Return the fixture format (when filling) or the execute format (when
executing) the test is parametrized with.
"""
return _spec_parameter(request)[1]


@pytest.fixture(scope="function")
def is_tx_gas_heavy_test(request: pytest.FixtureRequest) -> bool:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def test_t8n_support(fork: Fork, installed_t8n: TransitionTool) -> None:
test = BlockchainTest(
genesis_environment=env,
fork=fork,
pre=pre,
pre=pre.with_genesis_accounts(),
post=block_1.expected_post_state,
blocks=[block_1, block_2],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,12 +580,8 @@ def add_test_pre(
environment=environment,
group_salt=group_salt,
group_hash=pre_alloc_hash,
pre=Alloc.merge(
Alloc.model_validate(
fork.transitions_to().pre_allocation_blockchain()
),
pre,
),
# Plain `Alloc` copy, so later tests can be merged into it.
pre=Alloc(pre.root),
)
self.root[pre_alloc_hash] = group

Expand Down Expand Up @@ -620,6 +616,10 @@ class GroupPreAlloc(Alloc):
_cached_state_root: Hash | None = PrivateAttr(None)
_model_dump_cache: ModelDumpCache | None = PrivateAttr(None)
_pre_alloc_group_hash: AllocGroupHash | None = PrivateAttr(None)
_genesis_accounts: Literal["absent", "seeded", "deferred"] = PrivateAttr(
"seeded"
)
"""Built from the seeded `pre` of every test in the group."""

def structural_copy(self) -> Self:
"""
Expand Down
Loading
Loading