Skip to content

Commit 607d1b6

Browse files
feat(forks,plugins): Fork transition test marker (#1081)
* feat(forks): Adds `supports_blobs` method * fix(forks): Transition fork comparisons * refactor,fix(plugins/forks): `fork_transition_test` marker * docs: add documentation, changelog * refactor(plugins/forks): Validity markers as classes * fix: fork set handling * fix(plugins): config variable usage in other plugins * fix(plugins/forks): Self-documenting code for validity marker classes * whitelist * refactor(forks): suggestions for fork transition marker changes (#1104) * refactor(forks): make `name()` an abstractmethod * refactor(forks): add helper methods to simplify fork comparisons * Update src/pytest_plugins/forks/forks.py Co-authored-by: danceratopz <[email protected]> * Update src/pytest_plugins/forks/forks.py Co-authored-by: danceratopz <[email protected]> * Update src/pytest_plugins/forks/forks.py Co-authored-by: danceratopz <[email protected]> * fixup --------- Co-authored-by: danceratopz <[email protected]>
1 parent 0609c7f commit 607d1b6

File tree

12 files changed

+607
-199
lines changed

12 files changed

+607
-199
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Release tarball changes:
7878
- 🐞 fix(consume): allow absolute paths with `--evm-bin` ([#1052](https://github.com/ethereum/execution-spec-tests/pull/1052)).
7979
- ✨ Disable EIP-7742 framework changes for Prague ([#1023](https://github.com/ethereum/execution-spec-tests/pull/1023)).
8080
- ✨ Allow verification of the transaction receipt on executed test transactions ([#1068](https://github.com/ethereum/execution-spec-tests/pull/1068)).
81+
- ✨ Modify `valid_at_transition_to` marker to add keyword arguments `subsequent_transitions` and `until` to fill a test using multiple transition forks ([#1081](https://github.com/ethereum/execution-spec-tests/pull/1081)).
8182
- 🐞 fix(consume): use `"HIVE_CHECK_LIVE_PORT"` to signal hive to wait for port 8551 (Engine API port) instead of the 8545 port when running `consume engine` ([#1095](https://github.com/ethereum/execution-spec-tests/pull/1095)).
8283

8384
### 🔧 EVM Tools

docs/writing_tests/test_markers.md

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,15 @@ These markers are used to specify the forks for which a test is valid.
1010

1111
### `@pytest.mark.valid_from("FORK_NAME")`
1212

13-
This marker is used to specify the fork from which the test is valid. The test will not be filled for forks before the specified fork.
14-
15-
```python
16-
import pytest
17-
18-
from ethereum_test_tools import Alloc, StateTestFiller
19-
20-
@pytest.mark.valid_from("London")
21-
def test_something_only_valid_after_london(
22-
state_test: StateTestFiller,
23-
pre: Alloc
24-
):
25-
pass
26-
```
27-
28-
In this example, the test will only be filled for the London fork and after, e.g. London, Paris, Shanghai, Cancun, etc.
13+
:::pytest_plugins.forks.forks.ValidFrom
2914

3015
### `@pytest.mark.valid_until("FORK_NAME")`
3116

32-
This marker is used to specify the fork until which the test is valid. The test will not be filled for forks after the specified fork.
33-
34-
```python
35-
import pytest
36-
37-
from ethereum_test_tools import Alloc, StateTestFiller
38-
39-
@pytest.mark.valid_until("London")
40-
def test_something_only_valid_until_london(
41-
state_test: StateTestFiller,
42-
pre: Alloc
43-
):
44-
pass
45-
```
46-
47-
In this example, the test will only be filled for the London fork and before, e.g. London, Berlin, Istanbul, etc.
17+
:::pytest_plugins.forks.forks.ValidUntil
4818

4919
### `@pytest.mark.valid_at_transition_to("FORK_NAME")`
5020

51-
This marker is used to specify that a test is only meant to be filled at the transition to the specified fork.
52-
53-
The test usually starts at the fork prior to the specified fork at genesis and at block 5 (for pre-merge forks) or at timestamp 15,000 (for post-merge forks) the fork transition occurs.
21+
:::pytest_plugins.forks.forks.ValidAtTransitionTo
5422

5523
## Fork Covariant Markers
5624

src/ethereum_test_forks/base_fork.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,29 +104,43 @@ def __call__(
104104
class BaseForkMeta(ABCMeta):
105105
"""Metaclass for BaseFork."""
106106

107+
@abstractmethod
107108
def name(cls) -> str:
108-
"""To be implemented by the fork base class."""
109-
return ""
109+
"""Return the name of the fork (e.g., Berlin), must be implemented by subclasses."""
110+
pass
110111

111112
def __repr__(cls) -> str:
112113
"""Print the name of the fork, instead of the class."""
113114
return cls.name()
114115

116+
@staticmethod
117+
def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta":
118+
"""Return the transitioned fork, if a transition fork, otherwise return `fork_cls`."""
119+
return fork_cls.transitions_to() if hasattr(fork_cls, "transitions_to") else fork_cls
120+
121+
@staticmethod
122+
def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool:
123+
"""Check if `a` is a subclass of `b`, taking fork transitions into account."""
124+
a = BaseForkMeta._maybe_transitioned(a)
125+
b = BaseForkMeta._maybe_transitioned(b)
126+
return issubclass(a, b)
127+
115128
def __gt__(cls, other: "BaseForkMeta") -> bool:
116-
"""Compare if a fork is newer than some other fork."""
117-
return cls != other and other.__subclasscheck__(cls)
129+
"""Compare if a fork is newer than some other fork (cls > other)."""
130+
return cls is not other and BaseForkMeta._is_subclass_of(cls, other)
118131

119132
def __ge__(cls, other: "BaseForkMeta") -> bool:
120-
"""Compare if a fork is newer than or equal to some other fork."""
121-
return other.__subclasscheck__(cls)
133+
"""Compare if a fork is newer than or equal to some other fork (cls >= other)."""
134+
return cls is other or BaseForkMeta._is_subclass_of(cls, other)
122135

123136
def __lt__(cls, other: "BaseForkMeta") -> bool:
124-
"""Compare if a fork is older than some other fork."""
125-
return cls != other and cls.__subclasscheck__(other)
137+
"""Compare if a fork is older than some other fork (cls < other)."""
138+
# "Older" means other is a subclass of cls, but not the same.
139+
return cls is not other and BaseForkMeta._is_subclass_of(other, cls)
126140

127141
def __le__(cls, other: "BaseForkMeta") -> bool:
128-
"""Compare if a fork is older than or equal to some other fork."""
129-
return cls.__subclasscheck__(other)
142+
"""Compare if a fork is older than or equal to some other fork (cls <= other)."""
143+
return cls is other or BaseForkMeta._is_subclass_of(other, cls)
130144

131145

132146
class BaseFork(ABC, metaclass=BaseForkMeta):
@@ -289,6 +303,12 @@ def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0
289303
"""Return the blob base fee update fraction at a given fork."""
290304
pass
291305

306+
@classmethod
307+
@abstractmethod
308+
def supports_blobs(cls, block_number: int = 0, timestamp: int = 0) -> bool:
309+
"""Return whether the given fork supports blobs or not."""
310+
pass
311+
292312
@classmethod
293313
@abstractmethod
294314
def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:

src/ethereum_test_forks/forks/forks.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,39 +215,46 @@ def blob_gas_price_calculator(
215215
cls, block_number: int = 0, timestamp: int = 0
216216
) -> BlobGasPriceCalculator:
217217
"""Return a callable that calculates the blob gas price at a given fork."""
218-
raise NotImplementedError("Blob gas price calculator is not supported in Frontier")
218+
raise NotImplementedError(f"Blob gas price calculator is not supported in {cls.name()}")
219219

220220
@classmethod
221221
def excess_blob_gas_calculator(
222222
cls, block_number: int = 0, timestamp: int = 0
223223
) -> ExcessBlobGasCalculator:
224224
"""Return a callable that calculates the excess blob gas for a block at a given fork."""
225-
raise NotImplementedError("Excess blob gas calculator is not supported in Frontier")
225+
raise NotImplementedError(f"Excess blob gas calculator is not supported in {cls.name()}")
226226

227227
@classmethod
228228
def min_base_fee_per_blob_gas(cls, block_number: int = 0, timestamp: int = 0) -> int:
229229
"""Return the amount of blob gas used per blob at a given fork."""
230-
raise NotImplementedError("Base fee per blob gas is not supported in Frontier")
230+
raise NotImplementedError(f"Base fee per blob gas is not supported in {cls.name()}")
231231

232232
@classmethod
233233
def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0) -> int:
234234
"""Return the blob base fee update fraction at a given fork."""
235-
raise NotImplementedError("Blob base fee update fraction is not supported in Frontier")
235+
raise NotImplementedError(
236+
f"Blob base fee update fraction is not supported in {cls.name()}"
237+
)
236238

237239
@classmethod
238240
def blob_gas_per_blob(cls, block_number: int = 0, timestamp: int = 0) -> int:
239241
"""Return the amount of blob gas used per blob at a given fork."""
240242
return 0
241243

244+
@classmethod
245+
def supports_blobs(cls, block_number: int = 0, timestamp: int = 0) -> bool:
246+
"""Blobs are not supported at Frontier."""
247+
return False
248+
242249
@classmethod
243250
def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
244251
"""Return the target number of blobs per block at a given fork."""
245-
raise NotImplementedError("Target blobs per block is not supported in Frontier")
252+
raise NotImplementedError(f"Target blobs per block is not supported in {cls.name()}")
246253

247254
@classmethod
248255
def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
249256
"""Return the max number of blobs per block at a given fork."""
250-
raise NotImplementedError("Max blobs per block is not supported in Frontier")
257+
raise NotImplementedError(f"Max blobs per block is not supported in {cls.name()}")
251258

252259
@classmethod
253260
def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
@@ -938,6 +945,11 @@ def blob_gas_per_blob(cls, block_number: int = 0, timestamp: int = 0) -> int:
938945
"""Blobs are enabled starting from Cancun."""
939946
return 2**17
940947

948+
@classmethod
949+
def supports_blobs(cls, block_number: int = 0, timestamp: int = 0) -> bool:
950+
"""At Cancun, blobs support is enabled."""
951+
return True
952+
941953
@classmethod
942954
def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
943955
"""Blobs are enabled starting from Cancun, with a static target of 3 blobs."""

src/ethereum_test_forks/helpers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,16 @@ def get_closest_fork_with_solc_support(fork: Fork, solc_version: Version) -> Opt
8181
)
8282

8383

84-
def get_transition_forks() -> List[Fork]:
84+
def get_transition_forks() -> Set[Fork]:
8585
"""Return all the transition forks."""
86-
transition_forks: List[Fork] = []
86+
transition_forks: Set[Fork] = set()
8787

8888
for fork_name in transition.__dict__:
8989
fork = transition.__dict__[fork_name]
9090
if not isinstance(fork, type):
9191
continue
9292
if issubclass(fork, TransitionBaseClass) and issubclass(fork, BaseFork):
93-
transition_forks.append(fork)
93+
transition_forks.add(fork)
9494

9595
return transition_forks
9696

@@ -164,14 +164,14 @@ def transition_fork_from_to(fork_from: Fork, fork_to: Fork) -> Fork | None:
164164
return None
165165

166166

167-
def transition_fork_to(fork_to: Fork) -> List[Fork]:
167+
def transition_fork_to(fork_to: Fork) -> Set[Fork]:
168168
"""Return transition fork that transitions to the specified fork."""
169-
transition_forks: List[Fork] = []
169+
transition_forks: Set[Fork] = set()
170170
for transition_fork in get_transition_forks():
171171
if not issubclass(transition_fork, TransitionBaseClass):
172172
continue
173173
if transition_fork.transitions_to() == fork_to:
174-
transition_forks.append(transition_fork)
174+
transition_forks.add(transition_fork)
175175

176176
return transition_forks
177177

src/ethereum_test_forks/tests/test_forks.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
Prague,
1818
Shanghai,
1919
)
20-
from ..forks.transition import BerlinToLondonAt5, ParisToShanghaiAtTime15k
20+
from ..forks.transition import (
21+
BerlinToLondonAt5,
22+
CancunToPragueAtTime15k,
23+
ParisToShanghaiAtTime15k,
24+
ShanghaiToCancunAtTime15k,
25+
)
2126
from ..helpers import (
2227
forks_from,
2328
forks_from_until,
@@ -40,7 +45,7 @@ def test_transition_forks():
4045
"""Test transition fork utilities."""
4146
assert transition_fork_from_to(Berlin, London) == BerlinToLondonAt5
4247
assert transition_fork_from_to(Berlin, Paris) is None
43-
assert transition_fork_to(Shanghai) == [ParisToShanghaiAtTime15k]
48+
assert transition_fork_to(Shanghai) == {ParisToShanghaiAtTime15k}
4449

4550
# Test forks transitioned to and from
4651
assert BerlinToLondonAt5.transitions_to() == London
@@ -119,6 +124,9 @@ def test_forks():
119124
assert cast(Fork, ParisToShanghaiAtTime15k).header_withdrawals_required(0, 15_000) is True
120125
assert cast(Fork, ParisToShanghaiAtTime15k).header_withdrawals_required() is True
121126

127+
128+
def test_fork_comparison():
129+
"""Test fork comparison operators."""
122130
# Test fork comparison
123131
assert Paris > Berlin
124132
assert not Berlin > Paris
@@ -153,6 +161,50 @@ def test_forks():
153161
assert fork == Berlin
154162

155163

164+
def test_transition_fork_comparison():
165+
"""
166+
Test comparing to a transition fork.
167+
168+
The comparison logic is based on the logic we use to generate the tests.
169+
170+
E.g. given transition fork A->B, when filling, and given the from/until markers,
171+
we expect the following logic:
172+
173+
Marker Comparison A->B Included
174+
--------- ------------ ---------------
175+
From A fork >= A True
176+
Until A fork <= A False
177+
From B fork >= B True
178+
Until B fork <= B True
179+
"""
180+
assert BerlinToLondonAt5 >= Berlin
181+
assert not BerlinToLondonAt5 <= Berlin
182+
assert BerlinToLondonAt5 >= London
183+
assert BerlinToLondonAt5 <= London
184+
185+
# Comparisons between transition forks is done against the `transitions_to` fork
186+
assert BerlinToLondonAt5 < ParisToShanghaiAtTime15k
187+
assert ParisToShanghaiAtTime15k > BerlinToLondonAt5
188+
assert BerlinToLondonAt5 == BerlinToLondonAt5
189+
assert BerlinToLondonAt5 != ParisToShanghaiAtTime15k
190+
assert BerlinToLondonAt5 <= ParisToShanghaiAtTime15k
191+
assert ParisToShanghaiAtTime15k >= BerlinToLondonAt5
192+
193+
assert sorted(
194+
{
195+
CancunToPragueAtTime15k,
196+
ParisToShanghaiAtTime15k,
197+
ShanghaiToCancunAtTime15k,
198+
BerlinToLondonAt5,
199+
}
200+
) == [
201+
BerlinToLondonAt5,
202+
ParisToShanghaiAtTime15k,
203+
ShanghaiToCancunAtTime15k,
204+
CancunToPragueAtTime15k,
205+
]
206+
207+
156208
def test_get_forks(): # noqa: D103
157209
all_forks = get_forks()
158210
assert all_forks[0] == FIRST_DEPLOYED

src/pytest_plugins/execute/execute.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ def pytest_configure(config):
9595
command_line_args = "fill " + " ".join(config.invocation_params.args)
9696
config.stash[metadata_key]["Command-line args"] = f"<code>{command_line_args}</code>"
9797

98-
if len(config.fork_set) != 1:
98+
if len(config.selected_fork_set) != 1:
9999
pytest.exit(
100100
f"""
101-
Expected exactly one fork to be specified, got {len(config.fork_set)}.
101+
Expected exactly one fork to be specified, got {len(config.selected_fork_set)}.
102102
Make sure to specify exactly one fork using the --fork command line argument.
103103
""",
104104
returncode=pytest.ExitCode.USAGE_ERROR,

0 commit comments

Comments
 (0)