Skip to content

Commit 2d66b26

Browse files
authored
feat(fill): add a plugin for optional execution witness generation (#2066)
* feat(fixtures): add `executionWitness` to `FixtureBlockBase` * feat(fill): add a plugin for execution witness generation * feat(fill): use the `generate_witness` fixture in the `filler` plugin * feat(fill,help): enable witness plugin help output to `fill --help`. * chore(fill): fix punctuation in witness plugin help string * chore(fill): add hotfix to generate_witness for Paris The `witness-filler` tool doesn't recognise Paris: Re-write `fixture.fork` to Merge if its value is Paris and then write it back to Paris afterwards. * docs: update changelog * fill: update witness-filler ref as requested by jsign * fixtures: change WitnessChunk from dataclass to CamelModel * fixtures: rename WitnessChunk json parser helper method * fill: add a WitnessFillerResult to validate witness-filler output * refactor(fill): improve `generate_witness` fixture & fork checks 1. Avoid `hasattr` by checking that the provided fixture is of the required pydantic model type (`BlockchainFixture`). 2. Add a clean solution for the `witness-filler` fork naming incompatibility by subclassing `Paris` as `Merge`. * chore(fixtures): remove unncessary pydantic field alias * chore(fill): exit on error if witness-filler is unavailable Don't build it using cargo install; this must be done by the user.
1 parent 5a51af9 commit 2d66b26

File tree

6 files changed

+157
-0
lines changed

6 files changed

+157
-0
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Users can select any of the artifacts depending on their testing needs for their
8787
- 💥 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)).
8888
- ✨ 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)).
8989
- ✨ 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)).
90+
- ✨ Generate and include execution witness data in blockchain fixtures if `--witness` is specified ([#2066](https://github.com/ethereum/execution-spec-tests/pull/2066)).
9091

9192
#### `consume`
9293

src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ addopts =
1212
-p pytest_plugins.concurrency
1313
-p pytest_plugins.filler.pre_alloc
1414
-p pytest_plugins.filler.filler
15+
-p pytest_plugins.filler.witness
1516
-p pytest_plugins.shared.execute_fill
1617
-p pytest_plugins.filler.ported_tests
1718
-p pytest_plugins.filler.static_filler

src/ethereum_test_fixtures/blockchain.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""BlockchainTest types."""
22

3+
import json
34
from functools import cached_property
45
from typing import (
56
Annotated,
@@ -401,13 +402,32 @@ def from_withdrawal(cls, w: WithdrawalGeneric) -> "FixtureWithdrawal":
401402
return cls(**w.model_dump())
402403

403404

405+
class WitnessChunk(CamelModel):
406+
"""Represents execution witness data for a block."""
407+
408+
state: List[str]
409+
codes: List[str]
410+
keys: List[str]
411+
headers: List[str]
412+
413+
@classmethod
414+
def parse_witness_chunks(cls, s: str) -> List["WitnessChunk"]:
415+
"""
416+
Parse multiple witness chunks from JSON string.
417+
418+
Returns a list of WitnessChunk instances parsed from the JSON array.
419+
"""
420+
return [cls(**obj) for obj in json.loads(s)]
421+
422+
404423
class FixtureBlockBase(CamelModel):
405424
"""Representation of an Ethereum block within a test Fixture without RLP bytes."""
406425

407426
header: FixtureHeader = Field(..., alias="blockHeader")
408427
txs: List[FixtureTransaction] = Field(default_factory=list, alias="transactions")
409428
ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders")
410429
withdrawals: List[FixtureWithdrawal] | None = None
430+
execution_witness: WitnessChunk | None = None
411431

412432
@computed_field(alias="blocknumber") # type: ignore[misc]
413433
@cached_property

src/pytest_plugins/filler/filler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@ def base_test_parametrizer_func(
11311131
test_case_description: str,
11321132
fixture_source_url: str,
11331133
gas_benchmark_value: int,
1134+
witness_generator,
11341135
):
11351136
"""
11361137
Fixture used to instantiate an auto-fillable BaseTest object from within
@@ -1214,6 +1215,10 @@ def __init__(self, *args, **kwargs):
12141215
_info_metadata=t8n._info_metadata,
12151216
)
12161217

1218+
# Generate witness data if witness functionality is enabled via the witness plugin
1219+
if witness_generator is not None:
1220+
witness_generator(fixture)
1221+
12171222
fixture_path = fixture_collector.add_fixture(
12181223
node_to_test_info(request.node),
12191224
fixture,

src/pytest_plugins/filler/witness.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Pytest plugin for witness functionality.
3+
4+
Provides --witness command-line option that checks for the witness-filler tool in PATH
5+
and generates execution witness data for blockchain test fixtures when enabled.
6+
"""
7+
8+
import shutil
9+
import subprocess
10+
from typing import Callable, List
11+
12+
import pytest
13+
14+
from ethereum_test_base_types import EthereumTestRootModel
15+
from ethereum_test_fixtures.blockchain import BlockchainFixture, FixtureBlock, WitnessChunk
16+
from ethereum_test_forks import Paris
17+
18+
19+
class WitnessFillerResult(EthereumTestRootModel[List[WitnessChunk]]):
20+
"""Model that defines the expected result from the `witness-filler` command."""
21+
22+
root: List[WitnessChunk]
23+
24+
25+
class Merge(Paris):
26+
"""
27+
Paris fork that serializes as 'Merge' for witness-filler compatibility.
28+
29+
IMPORTANT: This class MUST be named 'Merge' (not 'MergeForWitness' or similar)
30+
because the class name is used directly in Pydantic serialization, and
31+
witness-filler expects exactly 'Merge' for this fork.
32+
"""
33+
34+
pass
35+
36+
37+
def pytest_addoption(parser: pytest.Parser):
38+
"""Add witness command-line options to pytest."""
39+
witness_group = parser.getgroup("witness", "Arguments for witness functionality")
40+
witness_group.addoption(
41+
"--witness",
42+
"--witness-the-fitness",
43+
action="store_true",
44+
dest="witness",
45+
default=False,
46+
help=(
47+
"Generate execution witness data for blockchain test fixtures using the "
48+
"witness-filler tool (must be installed separately)."
49+
),
50+
)
51+
52+
53+
def pytest_configure(config):
54+
"""
55+
Pytest hook called after command line options have been parsed.
56+
57+
If --witness is enabled, checks that the witness-filler tool is available in PATH.
58+
"""
59+
if config.getoption("witness"):
60+
# Check if witness-filler binary is available in PATH
61+
if not shutil.which("witness-filler"):
62+
pytest.exit(
63+
"witness-filler tool not found in PATH. Please build and install witness-filler "
64+
"from https://github.com/kevaundray/reth.git before using --witness flag.\n"
65+
"Example: cargo install --git https://github.com/kevaundray/reth.git "
66+
"witness-filler",
67+
1,
68+
)
69+
70+
71+
@pytest.fixture
72+
def witness_generator(
73+
request: pytest.FixtureRequest,
74+
) -> Callable[[BlockchainFixture], None] | None:
75+
"""
76+
Provide a witness generator function if --witness is enabled.
77+
78+
Returns:
79+
None if witness functionality is disabled.
80+
Callable that generates witness data for a BlockchainFixture if enabled.
81+
82+
"""
83+
if not request.config.getoption("witness"):
84+
return None
85+
86+
def generate_witness(fixture: BlockchainFixture) -> None:
87+
"""Generate witness data for a blockchain fixture using the witness-filler tool."""
88+
if not isinstance(fixture, BlockchainFixture):
89+
return None
90+
91+
# Hotfix: witness-filler expects "Merge" but execution-spec-tests uses "Paris"
92+
original_fork = None
93+
if fixture.fork is Paris:
94+
original_fork = fixture.fork
95+
fixture.fork = Merge
96+
97+
try:
98+
result = subprocess.run(
99+
["witness-filler"],
100+
input=fixture.model_dump_json(by_alias=True),
101+
text=True,
102+
capture_output=True,
103+
)
104+
finally:
105+
if original_fork is not None:
106+
fixture.fork = original_fork
107+
108+
if result.returncode != 0:
109+
raise RuntimeError(
110+
f"witness-filler tool failed with exit code {result.returncode}. "
111+
f"stderr: {result.stderr}"
112+
)
113+
114+
try:
115+
result_model = WitnessFillerResult.model_validate_json(result.stdout)
116+
witnesses = result_model.root
117+
118+
for i, witness in enumerate(witnesses):
119+
if i < len(fixture.blocks):
120+
block = fixture.blocks[i]
121+
if isinstance(block, FixtureBlock):
122+
block.execution_witness = witness
123+
except Exception as e:
124+
raise RuntimeError(
125+
f"Failed to parse witness data from witness-filler tool. "
126+
f"Output was: {result.stdout[:500]}{'...' if len(result.stdout) > 500 else ''}"
127+
) from e
128+
129+
return generate_witness

src/pytest_plugins/help/help.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def pytest_configure(config):
8787
"defining debug",
8888
"pre-allocation behavior during test filling",
8989
"ported",
90+
"witness",
9091
],
9192
)
9293
elif config.getoption("show_consume_help"):

0 commit comments

Comments
 (0)