Skip to content

Commit e6d7fe4

Browse files
committed
feat(spec-tools): teach Hardfork to clone forks
1 parent a4e2c0e commit e6d7fe4

File tree

9 files changed

+230
-84
lines changed

9 files changed

+230
-84
lines changed

src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
hex_to_u256,
2222
hex_to_uint,
2323
)
24+
from ethereum_spec_tools.forks import Hardfork
2425

2526
from .fork_loader import ForkLoad
2627
from .transaction_loader import TransactionLoad
@@ -52,10 +53,18 @@ class Load(BaseLoad):
5253
_fork_module: str
5354
fork: ForkLoad
5455

55-
def __init__(self, network: str, fork_module: str):
56+
def __init__(self, network: str, fork_module: str | Hardfork):
5657
self._network = network
57-
self._fork_module = fork_module
58-
self.fork = ForkLoad(fork_module)
58+
if isinstance(fork_module, Hardfork):
59+
self.fork = ForkLoad(fork_module)
60+
self._fork_module = fork_module.short_name
61+
else:
62+
self._fork_module = fork_module
63+
for fork in Hardfork.discover():
64+
if fork.short_name == fork_module:
65+
self.fork = ForkLoad(fork)
66+
return
67+
raise Exception(f"fork `{fork_module}` not found")
5968

6069
def json_to_state(self, raw: Any) -> Any:
6170
"""Converts json state data to a state object."""

src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py

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

5-
import importlib
6-
from typing import Any
5+
from typing import Any, Final
76

87
from ethereum_spec_tools.forks import Hardfork
98

@@ -13,46 +12,38 @@ class ForkLoad:
1312
Load the functions and classes from the relevant fork.
1413
"""
1514

16-
_fork_module: str
15+
hardfork: Final[Hardfork]
1716
_forks: list[Hardfork]
1817

19-
def __init__(self, fork_module: str):
20-
self._fork_module = fork_module
18+
def __init__(self, hardfork: Hardfork):
19+
self.hardfork = hardfork
2120
self._forks = Hardfork.discover()
2221

23-
@property
24-
def fork_module(self) -> str:
25-
"""Module that contains the fork code."""
26-
return self._fork_module
27-
2822
def _module(self, name: str) -> Any:
2923
"""Imports a module from the fork."""
30-
return importlib.import_module(
31-
f"ethereum.forks.{self._fork_module}.{name}"
32-
)
24+
return self.hardfork.module(name)
3325

3426
@property
3527
def proof_of_stake(self) -> bool:
3628
"""Whether the fork is proof of stake."""
37-
for fork in self._forks:
38-
if fork.name == "ethereum.forks." + self._fork_module:
39-
return fork.consensus.is_pos()
40-
raise Exception(f"fork {self._fork_module} not discovered")
29+
return self.hardfork.consensus.is_pos()
4130

4231
def is_after_fork(self, target_fork_name: str) -> bool:
4332
"""
4433
Check if the fork is after the target fork.
4534
4635
Accepts short fork names (e.g. "cancun") instead of full module paths.
4736
"""
48-
return_value = False
37+
target_fork: Hardfork | None = None
4938
for fork in self._forks:
50-
short_name = fork.short_name
51-
if short_name == target_fork_name:
52-
return_value = True
53-
if short_name == self._fork_module:
39+
if fork.short_name == target_fork_name:
40+
target_fork = fork
5441
break
55-
return return_value
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
5647

5748
@property
5849
def BEACON_ROOTS_ADDRESS(self) -> Any:

src/ethereum_spec_tools/evm_tools/t8n/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ..loaders.fork_loader import ForkLoad
2020
from ..utils import (
2121
FatalError,
22-
get_module_name,
22+
find_fork,
2323
get_stream_logger,
2424
parse_hex_or_int,
2525
)
@@ -92,7 +92,7 @@ def __init__(
9292
self.out_file = out_file
9393
self.in_file = in_file
9494
self.options = options
95-
self.forks = Hardfork.discover()
95+
forks = Hardfork.discover()
9696

9797
if "stdin" in (
9898
options.input_env,
@@ -103,9 +103,7 @@ def __init__(
103103
else:
104104
stdin = None
105105

106-
fork_module, self.fork_block = get_module_name(
107-
self.forks, self.options, stdin
108-
)
106+
fork_module, self.fork_block = find_fork(forks, self.options, stdin)
109107
self.fork = ForkLoad(fork_module)
110108

111109
tracers = GroupTracer()

src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None):
4747
data[address][key] = "0x" + hex(int(value))
4848

4949
state = t8n.json_to_state(data)
50-
if t8n.fork.fork_module == "dao_fork":
50+
if t8n.fork.hardfork.short_name == "dao_fork":
5151
t8n.fork.apply_dao(state)
5252

5353
self.state = state

src/ethereum_spec_tools/evm_tools/utils.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import logging
7+
import re
78
import sys
89
from typing import (
910
Any,
@@ -81,9 +82,9 @@ class FatalError(Exception):
8182
pass
8283

8384

84-
def get_module_name(
85+
def find_fork(
8586
forks: Sequence[Hardfork], options: Any, stdin: Any
86-
) -> Tuple[str, int]:
87+
) -> Tuple[Hardfork, int]:
8788
"""
8889
Get the module name and the fork block for the given state fork.
8990
"""
@@ -96,6 +97,13 @@ def get_module_name(
9697
except KeyError:
9798
pass
9899

100+
current_fork_block = 0
101+
current_fork_module = re.sub(
102+
r"(?<!^)(?=[A-Z])",
103+
"_",
104+
options.state_fork,
105+
).lower() # CamelCase to snake_case
106+
99107
if exception_config:
100108
if options.input_env == "stdin":
101109
assert stdin is not None
@@ -111,15 +119,9 @@ def get_module_name(
111119
current_fork_module = fork
112120
current_fork_block = fork_block
113121

114-
return current_fork_module, current_fork_block
115-
116-
# If the state fork is not an exception, use the fork name.
117122
for fork in forks:
118-
fork_module = fork.name.split(".")[-1]
119-
key = "".join(x.title() for x in fork_module.split("_"))
120-
121-
if key == options.state_fork:
122-
return fork_module, 0
123+
if current_fork_module == fork.short_name:
124+
return fork, current_fork_block
123125

124126
# Neither in exception nor a standard fork name.
125127
sys.exit(f"Unsupported state fork: {options.state_fork}")

src/ethereum_spec_tools/forks.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
import importlib.abc
99
import importlib.util
1010
import pkgutil
11+
import random
12+
from contextlib import AbstractContextManager
1113
from enum import Enum, auto
1214
from importlib.machinery import ModuleSpec
15+
from pathlib import Path
1316
from pkgutil import ModuleInfo
17+
from tempfile import TemporaryDirectory
1418
from types import ModuleType
1519
from typing import (
1620
TYPE_CHECKING,
@@ -21,12 +25,20 @@
2125
Optional,
2226
Type,
2327
TypeVar,
28+
Union,
29+
cast,
2430
)
2531

26-
from ethereum_types.numeric import U256, Uint
32+
from ethereum_types.numeric import U64, U256, Uint
33+
from typing_extensions import override
2734

2835
if TYPE_CHECKING:
29-
from ethereum.fork_criteria import ForkCriteria
36+
from ethereum.fork_criteria import (
37+
ByBlockNumber,
38+
ByTimestamp,
39+
ForkCriteria,
40+
Unscheduled,
41+
)
3042

3143

3244
class ConsensusType(Enum):
@@ -167,6 +179,84 @@ def load_from_json(cls: Type[H], json: Any) -> List[H]:
167179

168180
return cls.load(config)
169181

182+
@staticmethod
183+
def clone(
184+
template: H | str,
185+
fork_criteria: Union[
186+
"ByBlockNumber", "ByTimestamp", "Unscheduled", None
187+
] = None,
188+
target_blob_gas_per_block: U64 | None = None,
189+
gas_per_blob: U64 | None = None,
190+
min_blob_gasprice: Uint | None = None,
191+
blob_base_fee_update_fraction: Uint | None = None,
192+
max_blob_gas_per_block: U64 | None = None,
193+
blob_schedule_target: U64 | None = None,
194+
) -> "TemporaryHardfork":
195+
"""
196+
Create a temporary clone of an existing fork, optionally tweaking its
197+
parameters.
198+
"""
199+
from .new_fork.builder import ForkBuilder
200+
201+
maybe_directory: TemporaryDirectory | None = TemporaryDirectory()
202+
203+
try:
204+
assert maybe_directory is not None
205+
directory: TemporaryDirectory = maybe_directory
206+
207+
if isinstance(template, str):
208+
template_name = template
209+
else:
210+
template_name = template.short_name
211+
212+
clone_name = (
213+
f"{template_name}_clone{random.randrange(1_000_000_000)}"
214+
)
215+
216+
builder = ForkBuilder(template_name, clone_name)
217+
218+
builder.output = Path(directory.name)
219+
220+
if fork_criteria is not None:
221+
builder.fork_criteria = fork_criteria
222+
223+
if target_blob_gas_per_block is not None:
224+
builder.modify_target_blob_gas_per_block(
225+
target_blob_gas_per_block
226+
)
227+
228+
if gas_per_blob is not None:
229+
builder.modify_gas_per_blob(gas_per_blob)
230+
231+
if min_blob_gasprice is not None:
232+
builder.modify_min_blob_gasprice(min_blob_gasprice)
233+
234+
if blob_base_fee_update_fraction is not None:
235+
builder.modify_blob_base_fee_update_fraction(
236+
blob_base_fee_update_fraction
237+
)
238+
239+
if max_blob_gas_per_block is not None:
240+
builder.modify_max_blob_gas_per_block(max_blob_gas_per_block)
241+
242+
if blob_schedule_target is not None:
243+
builder.modify_blob_schedule_target(blob_schedule_target)
244+
245+
builder.build()
246+
247+
clone_forks = Hardfork.discover([directory.name])
248+
if len(clone_forks) != 1:
249+
raise Exception("len(clone_forks) != 1")
250+
if clone_forks[0].short_name != clone_name:
251+
raise Exception("found incorrect fork")
252+
253+
value = TemporaryHardfork(clone_forks[0].mod, directory)
254+
maybe_directory = None
255+
return value
256+
finally:
257+
if maybe_directory is not None:
258+
maybe_directory.cleanup()
259+
170260
def __init__(self, mod: ModuleType) -> None:
171261
self.mod = mod
172262

@@ -300,3 +390,28 @@ def walk_packages(self) -> Iterator[ModuleInfo]:
300390
raise ValueError(f"cannot walk {self.name}, path is None")
301391

302392
return pkgutil.walk_packages([self.path], self.name + ".")
393+
394+
395+
class TemporaryHardfork(Hardfork, AbstractContextManager):
396+
"""
397+
Short-lived `Hardfork` located in a temporary directory.
398+
"""
399+
400+
directory: TemporaryDirectory | None
401+
402+
def __init__(self, mod: ModuleType, directory: TemporaryDirectory) -> None:
403+
super().__init__(mod)
404+
self.directory = directory
405+
406+
@override
407+
def __exit__(self, *args: object, **kwargs: object) -> None:
408+
del args
409+
del kwargs
410+
411+
assert self.directory is not None
412+
self.directory.cleanup()
413+
self.directory = None
414+
415+
# Intentionally break ourselves. Once the directory is gone, imports
416+
# won't work.
417+
self.mod = cast(ModuleType, None)

0 commit comments

Comments
 (0)