Skip to content

Commit 5856db5

Browse files
committed
feat(spec-tools): Support dynamic BPO forks
1 parent e6d7fe4 commit 5856db5

File tree

14 files changed

+365
-98
lines changed

14 files changed

+365
-98
lines changed

packages/testing/src/execution_testing/client_clis/clis/execution_specs.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
from io import StringIO
88
from pathlib import Path
99
from typing import Any, ClassVar, Dict, Optional
10+
from typing_extensions import override
1011

1112
import ethereum
1213
from ethereum_spec_tools.evm_tools import create_parser
13-
from ethereum_spec_tools.evm_tools.t8n import T8N
14+
from ethereum_spec_tools.evm_tools.t8n import T8N, ForkCache
1415
from ethereum_spec_tools.evm_tools.utils import get_supported_forks
1516

1617
from execution_testing.client_clis.cli_types import TransitionToolOutput
@@ -33,6 +34,9 @@
3334
class ExecutionSpecsTransitionTool(TransitionTool):
3435
"""Implementation of the EELS T8N for execution-spec-tests."""
3536

37+
supports_opcode_count: ClassVar[bool] = True
38+
supports_blob_params: ClassVar[bool] = True
39+
3640
def __init__(
3741
self,
3842
*,
@@ -44,6 +48,11 @@ def __init__(
4448
self.exception_mapper = ExecutionSpecsExceptionMapper()
4549
self.trace = trace
4650
self._info_metadata: Optional[Dict[str, Any]] = {}
51+
self.fork_cache = ForkCache()
52+
53+
@override
54+
def shutdown(self) -> None:
55+
self.fork_cache.__exit__()
4756

4857
def version(self) -> str:
4958
"""Version of the t8n tool."""
@@ -87,6 +96,9 @@ def evaluate(
8796
if transition_tool_data.state_test:
8897
t8n_args.append("--state-test")
8998

99+
if transition_tool_data.blob_params:
100+
t8n_args.append("--input.blobParams=stdin")
101+
90102
if self.trace:
91103
t8n_args.extend(
92104
[
@@ -103,7 +115,7 @@ def evaluate(
103115

104116
in_stream = StringIO(json.dumps(request_data_json["input"]))
105117

106-
t8n = T8N(t8n_options, out_stream, in_stream)
118+
t8n = T8N(t8n_options, out_stream, in_stream, self.fork_cache)
107119
t8n.run()
108120

109121
output_dict = json.loads(out_stream.getvalue())

src/ethereum_spec_tools/evm_tools/__init__.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import argparse
66
import subprocess
77
import sys
8+
from contextlib import ExitStack
89
from typing import Optional, Sequence, Text, TextIO
910

1011
from ethereum import __version__
1112

1213
from .b11r import B11R, b11r_arguments
1314
from .daemon import Daemon, daemon_arguments
1415
from .statetest import StateTest, state_test_arguments
15-
from .t8n import T8N, t8n_arguments
16+
from .t8n import T8N, ForkCache, t8n_arguments
1617
from .utils import get_supported_forks
1718

1819
DESCRIPTION = """
@@ -90,6 +91,7 @@ def main(
9091
args: Optional[Sequence[Text]] = None,
9192
out_file: Optional[TextIO] = None,
9293
in_file: Optional[TextIO] = None,
94+
fork_cache: Optional[ForkCache] = None,
9395
) -> int:
9496
"""Run the tools based on the given options."""
9597
parser = create_parser()
@@ -102,18 +104,23 @@ def main(
102104
if in_file is None:
103105
in_file = sys.stdin
104106

105-
if options.evm_tool == "t8n":
106-
t8n_tool = T8N(options, out_file, in_file)
107-
return t8n_tool.run()
108-
elif options.evm_tool == "b11r":
109-
b11r_tool = B11R(options, out_file, in_file)
110-
return b11r_tool.run()
111-
elif options.evm_tool == "daemon":
112-
daemon = Daemon(options)
113-
return daemon.run()
114-
elif options.evm_tool == "statetest":
115-
state_test = StateTest(options, out_file, in_file)
116-
return state_test.run()
117-
else:
118-
parser.print_help(file=out_file)
119-
return 0
107+
with ExitStack() as exit_stack:
108+
if fork_cache is None:
109+
fork_cache = ForkCache()
110+
exit_stack.push(fork_cache)
111+
112+
if options.evm_tool == "t8n":
113+
t8n_tool = T8N(options, out_file, in_file, fork_cache)
114+
return t8n_tool.run()
115+
elif options.evm_tool == "b11r":
116+
b11r_tool = B11R(options, out_file, in_file)
117+
return b11r_tool.run()
118+
elif options.evm_tool == "daemon":
119+
daemon = Daemon(options)
120+
return daemon.run()
121+
elif options.evm_tool == "statetest":
122+
state_test = StateTest(options, out_file, in_file)
123+
return state_test.run()
124+
else:
125+
parser.print_help(file=out_file)
126+
return 0

src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Loader for code from the relevant fork.
33
"""
44

5+
from inspect import signature
56
from typing import Any, Final
67

78
from ethereum_spec_tools.forks import Hardfork
@@ -28,28 +29,16 @@ def proof_of_stake(self) -> bool:
2829
"""Whether the fork is proof of stake."""
2930
return self.hardfork.consensus.is_pos()
3031

31-
def is_after_fork(self, target_fork_name: str) -> bool:
32-
"""
33-
Check if the fork is after the target fork.
34-
35-
Accepts short fork names (e.g. "cancun") instead of full module paths.
36-
"""
37-
target_fork: Hardfork | None = None
38-
for fork in self._forks:
39-
if fork.short_name == target_fork_name:
40-
target_fork = fork
41-
break
42-
43-
if target_fork is None:
44-
raise Exception(f"fork `{target_fork_name}` not found")
45-
46-
return self.hardfork.mod.FORK_CRITERIA >= target_fork.mod.FORK_CRITERIA
47-
4832
@property
4933
def BEACON_ROOTS_ADDRESS(self) -> Any:
5034
"""BEACON_ROOTS_ADDRESS of the given fork."""
5135
return self._module("fork").BEACON_ROOTS_ADDRESS
5236

37+
@property
38+
def has_beacon_roots_address(self) -> bool:
39+
"""Check if the fork has a `BEACON_ROOTS_ADDRESS` constant."""
40+
return hasattr(self._module("fork"), "BEACON_ROOTS_ADDRESS")
41+
5342
@property
5443
def HISTORY_STORAGE_ADDRESS(self) -> Any:
5544
"""HISTORY_STORAGE_ADDRESS of the given fork."""
@@ -80,11 +69,22 @@ def calculate_block_difficulty(self) -> Any:
8069
"""calculate_block_difficulty function of the given fork."""
8170
return self._module("fork").calculate_block_difficulty
8271

72+
@property
73+
def calculate_block_difficulty_arity(self) -> int:
74+
"""Number of parameters required by `calculate_block_difficulty`."""
75+
inspected = signature(self._module("fork").calculate_block_difficulty)
76+
return len(inspected.parameters)
77+
8378
@property
8479
def calculate_base_fee_per_gas(self) -> Any:
8580
"""calculate_base_fee_per_gas function of the given fork."""
8681
return self._module("fork").calculate_base_fee_per_gas
8782

83+
@property
84+
def has_calculate_base_fee_per_gas(self) -> bool:
85+
"""Check if the fork has a `calculate_base_fee_per_gas` function."""
86+
return hasattr(self._module("fork"), "calculate_base_fee_per_gas")
87+
8888
@property
8989
def logs_bloom(self) -> Any:
9090
"""logs_bloom function of the given fork."""
@@ -115,6 +115,11 @@ def signing_hash_155(self) -> Any:
115115
"""signing_hash_155 function of the fork."""
116116
return self._module("transactions").signing_hash_155
117117

118+
@property
119+
def has_signing_hash_155(self) -> bool:
120+
"""Check if the fork has a `signing_hash_155` function."""
121+
return hasattr(self._module("transactions"), "signing_hash_155")
122+
118123
@property
119124
def signing_hash_2930(self) -> Any:
120125
"""signing_hash_2930 function of the fork."""
@@ -160,6 +165,15 @@ def compute_requests_hash(self) -> Any:
160165
"""compute_requests_hash function of the fork."""
161166
return self._module("requests").compute_requests_hash
162167

168+
@property
169+
def has_compute_requests_hash(self) -> bool:
170+
"""Check if the fork has a `has_requests_hash` function."""
171+
try:
172+
module = self._module("requests")
173+
except ModuleNotFoundError:
174+
return False
175+
return hasattr(module, "compute_requests_hash")
176+
163177
@property
164178
def Bloom(self) -> Any:
165179
"""Bloom class of the fork."""
@@ -190,6 +204,14 @@ def LegacyTransaction(self) -> Any:
190204
"""Legacytransaction class of the fork."""
191205
return self._module("transactions").LegacyTransaction
192206

207+
@property
208+
def has_legacy_transaction(self) -> bool:
209+
"""
210+
Return `True` if the fork has a `LegacyTransaction` class, or `False`
211+
otherwise.
212+
"""
213+
return hasattr(self._module("transactions"), "LegacyTransaction")
214+
193215
@property
194216
def Access(self) -> Any:
195217
"""Access class of the fork."""
@@ -220,11 +242,21 @@ def Withdrawal(self) -> Any:
220242
"""Withdrawal class of the fork."""
221243
return self._module("blocks").Withdrawal
222244

245+
@property
246+
def has_withdrawal(self) -> bool:
247+
"""Check if the fork has a `Withdrawal` class."""
248+
return hasattr(self._module("blocks"), "Withdrawal")
249+
223250
@property
224251
def decode_transaction(self) -> Any:
225252
"""decode_transaction function of the fork."""
226253
return self._module("transactions").decode_transaction
227254

255+
@property
256+
def has_decode_transaction(self) -> bool:
257+
"""Check if this fork has a `decode_transaction`."""
258+
return hasattr(self._module("transactions"), "decode_transaction")
259+
228260
@property
229261
def State(self) -> Any:
230262
"""State class of the fork."""

src/ethereum_spec_tools/evm_tools/statetest/__init__.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from ethereum.utils.hexadecimal import hex_to_bytes
1515

16-
from ..t8n import T8N
16+
from ..t8n import T8N, ForkCache
1717
from ..t8n.t8n_types import Result
1818
from ..utils import get_supported_forks
1919

@@ -75,6 +75,7 @@ def read_test_cases(test_file_path: str) -> Iterable[TestCase]:
7575

7676
def run_test_case(
7777
test_case: TestCase,
78+
fork_cache: ForkCache,
7879
t8n_extra: Optional[List[str]] = None,
7980
output_basedir: Optional[str | TextIO] = None,
8081
) -> Result:
@@ -146,7 +147,7 @@ def run_test_case(
146147
if output_basedir is not None:
147148
t8n_options.output_basedir = output_basedir
148149

149-
t8n = T8N(t8n_options, out_stream, in_stream)
150+
t8n = T8N(t8n_options, out_stream, in_stream, fork_cache)
150151
t8n.run_state_test()
151152
return t8n.result
152153

@@ -212,12 +213,13 @@ def run(self) -> int:
212213
stream_handler.setFormatter(formatter)
213214
logger.addHandler(stream_handler)
214215

215-
if self.file is None:
216-
return self.run_many()
217-
else:
218-
return self.run_one(self.file)
216+
with ForkCache() as fork_cache:
217+
if self.file is None:
218+
return self.run_many(fork_cache)
219+
else:
220+
return self.run_one(self.file, fork_cache)
219221

220-
def run_one(self, path: str) -> int:
222+
def run_one(self, path: str, fork_cache: ForkCache) -> int:
221223
"""
222224
Execute state tests from a single file.
223225
"""
@@ -246,6 +248,7 @@ def run_one(self, path: str) -> int:
246248

247249
result = run_test_case(
248250
test_case,
251+
fork_cache,
249252
t8n_extra=t8n_extra,
250253
output_basedir=sys.stderr,
251254
)
@@ -279,13 +282,13 @@ def run_one(self, path: str) -> int:
279282
self.out_file.write("\n")
280283
return 0
281284

282-
def run_many(self) -> int:
285+
def run_many(self, fork_cache: ForkCache) -> int:
283286
"""
284287
Execute state tests from a line-delimited list of files provided from
285288
`self.in_file`.
286289
"""
287290
for line in self.in_file:
288-
result = self.run_one(line[:-1])
291+
result = self.run_one(line[:-1], fork_cache)
289292
if result != 0:
290293
return result
291294
return 0

0 commit comments

Comments
 (0)