diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a6d9e2f2df2..ff099700426 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -87,6 +87,7 @@ Users can select any of the artifacts depending on their testing needs for their - 💥 Flag `--flat-output` has been removed due to having been unneeded for an extended period of time ([#2018](https://github.com/ethereum/execution-spec-tests/pull/2018)). - ✨ Add support for `BlockchainEngineSyncFixture` format for tests marked with `pytest.mark.verify_sync` to enable client synchronization testing via `consume sync` command ([#2007](https://github.com/ethereum/execution-spec-tests/pull/2007)). - ✨ Framework is updated to include BPO ([EIP-7892](https://eips.ethereum.org/EIPS/eip-7892)) fork markers to enable the filling of BPO tests ([#2050](https://github.com/ethereum/execution-spec-tests/pull/2050)). +- ✨ Generate and include execution witness data in blockchain fixtures if `--witness` is specified ([#2066](https://github.com/ethereum/execution-spec-tests/pull/2066)). #### `consume` diff --git a/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini b/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini index dcdab775d66..84915897eb4 100644 --- a/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini +++ b/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini @@ -12,6 +12,7 @@ addopts = -p pytest_plugins.concurrency -p pytest_plugins.filler.pre_alloc -p pytest_plugins.filler.filler + -p pytest_plugins.filler.witness -p pytest_plugins.shared.execute_fill -p pytest_plugins.filler.ported_tests -p pytest_plugins.filler.static_filler diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 3cd0b848d17..61688a4c3dc 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -1,5 +1,6 @@ """BlockchainTest types.""" +import json from functools import cached_property from typing import ( Annotated, @@ -401,6 +402,24 @@ def from_withdrawal(cls, w: WithdrawalGeneric) -> "FixtureWithdrawal": return cls(**w.model_dump()) +class WitnessChunk(CamelModel): + """Represents execution witness data for a block.""" + + state: List[str] + codes: List[str] + keys: List[str] + headers: List[str] + + @classmethod + def parse_witness_chunks(cls, s: str) -> List["WitnessChunk"]: + """ + Parse multiple witness chunks from JSON string. + + Returns a list of WitnessChunk instances parsed from the JSON array. + """ + return [cls(**obj) for obj in json.loads(s)] + + class FixtureBlockBase(CamelModel): """Representation of an Ethereum block within a test Fixture without RLP bytes.""" @@ -408,6 +427,7 @@ class FixtureBlockBase(CamelModel): txs: List[FixtureTransaction] = Field(default_factory=list, alias="transactions") ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders") withdrawals: List[FixtureWithdrawal] | None = None + execution_witness: WitnessChunk | None = None @computed_field(alias="blocknumber") # type: ignore[misc] @cached_property diff --git a/src/pytest_plugins/filler/filler.py b/src/pytest_plugins/filler/filler.py index b96ec252a34..83d8e94812e 100644 --- a/src/pytest_plugins/filler/filler.py +++ b/src/pytest_plugins/filler/filler.py @@ -1140,6 +1140,7 @@ def base_test_parametrizer_func( test_case_description: str, fixture_source_url: str, gas_benchmark_value: int, + witness_generator, ): """ Fixture used to instantiate an auto-fillable BaseTest object from within @@ -1223,6 +1224,10 @@ def __init__(self, *args, **kwargs): _info_metadata=t8n._info_metadata, ) + # Generate witness data if witness functionality is enabled via the witness plugin + if witness_generator is not None: + witness_generator(fixture) + fixture_path = fixture_collector.add_fixture( node_to_test_info(request.node), fixture, diff --git a/src/pytest_plugins/filler/witness.py b/src/pytest_plugins/filler/witness.py new file mode 100644 index 00000000000..fc21e3dceeb --- /dev/null +++ b/src/pytest_plugins/filler/witness.py @@ -0,0 +1,129 @@ +""" +Pytest plugin for witness functionality. + +Provides --witness command-line option that checks for the witness-filler tool in PATH +and generates execution witness data for blockchain test fixtures when enabled. +""" + +import shutil +import subprocess +from typing import Callable, List + +import pytest + +from ethereum_test_base_types import EthereumTestRootModel +from ethereum_test_fixtures.blockchain import BlockchainFixture, FixtureBlock, WitnessChunk +from ethereum_test_forks import Paris + + +class WitnessFillerResult(EthereumTestRootModel[List[WitnessChunk]]): + """Model that defines the expected result from the `witness-filler` command.""" + + root: List[WitnessChunk] + + +class Merge(Paris): + """ + Paris fork that serializes as 'Merge' for witness-filler compatibility. + + IMPORTANT: This class MUST be named 'Merge' (not 'MergeForWitness' or similar) + because the class name is used directly in Pydantic serialization, and + witness-filler expects exactly 'Merge' for this fork. + """ + + pass + + +def pytest_addoption(parser: pytest.Parser): + """Add witness command-line options to pytest.""" + witness_group = parser.getgroup("witness", "Arguments for witness functionality") + witness_group.addoption( + "--witness", + "--witness-the-fitness", + action="store_true", + dest="witness", + default=False, + help=( + "Generate execution witness data for blockchain test fixtures using the " + "witness-filler tool (must be installed separately)." + ), + ) + + +def pytest_configure(config): + """ + Pytest hook called after command line options have been parsed. + + If --witness is enabled, checks that the witness-filler tool is available in PATH. + """ + if config.getoption("witness"): + # Check if witness-filler binary is available in PATH + if not shutil.which("witness-filler"): + pytest.exit( + "witness-filler tool not found in PATH. Please build and install witness-filler " + "from https://github.com/kevaundray/reth.git before using --witness flag.\n" + "Example: cargo install --git https://github.com/kevaundray/reth.git " + "witness-filler", + 1, + ) + + +@pytest.fixture +def witness_generator( + request: pytest.FixtureRequest, +) -> Callable[[BlockchainFixture], None] | None: + """ + Provide a witness generator function if --witness is enabled. + + Returns: + None if witness functionality is disabled. + Callable that generates witness data for a BlockchainFixture if enabled. + + """ + if not request.config.getoption("witness"): + return None + + def generate_witness(fixture: BlockchainFixture) -> None: + """Generate witness data for a blockchain fixture using the witness-filler tool.""" + if not isinstance(fixture, BlockchainFixture): + return None + + # Hotfix: witness-filler expects "Merge" but execution-spec-tests uses "Paris" + original_fork = None + if fixture.fork is Paris: + original_fork = fixture.fork + fixture.fork = Merge + + try: + result = subprocess.run( + ["witness-filler"], + input=fixture.model_dump_json(by_alias=True), + text=True, + capture_output=True, + ) + finally: + if original_fork is not None: + fixture.fork = original_fork + + if result.returncode != 0: + raise RuntimeError( + f"witness-filler tool failed with exit code {result.returncode}. " + f"stderr: {result.stderr}" + ) + + try: + result_model = WitnessFillerResult.model_validate_json(result.stdout) + witnesses = result_model.root + + for i, witness in enumerate(witnesses): + if i < len(fixture.blocks): + block = fixture.blocks[i] + if isinstance(block, FixtureBlock): + block.execution_witness = witness + except Exception as e: + raise RuntimeError( + f"Failed to parse witness data from witness-filler tool. " + f"Output was: {result.stdout[:500]}{'...' if len(result.stdout) > 500 else ''}" + ) from e + + return generate_witness diff --git a/src/pytest_plugins/help/help.py b/src/pytest_plugins/help/help.py index aaa55960bd4..b70c0357a55 100644 --- a/src/pytest_plugins/help/help.py +++ b/src/pytest_plugins/help/help.py @@ -87,6 +87,7 @@ def pytest_configure(config): "defining debug", "pre-allocation behavior during test filling", "ported", + "witness", ], ) elif config.getoption("show_consume_help"):