Skip to content

Commit e90abe3

Browse files
committed
feat(tests,tools): [WIP] initial invalid BAL tests
1 parent b6b5919 commit e90abe3

File tree

6 files changed

+1053
-14
lines changed

6 files changed

+1053
-14
lines changed

src/ethereum_test_forks/forks/forks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,6 +1810,11 @@ def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
18101810
class Amsterdam(Osaka):
18111811
"""Amsterdam fork."""
18121812

1813+
@classmethod
1814+
def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
1815+
"""From Amsterdam, header must contain block access list hash (EIP-7928)."""
1816+
return block_number > 0 # Not required in genesis block
1817+
18131818
@classmethod
18141819
def is_deployed(cls) -> bool:
18151820
"""Return True if this fork is deployed."""

src/ethereum_test_specs/blockchain.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ class Block(Header):
208208
An RLP modifying header which values would be used to override the ones
209209
returned by the `ethereum_clis.TransitionTool`.
210210
"""
211+
expected_block_access_list: BlockAccessListExpectation | None = None
212+
"""
213+
If set, the block access list will be verified and potentially corrupted for invalid tests.
214+
"""
211215
exception: BLOCK_EXCEPTION_TYPE = None
212216
"""
213217
If set, the block is expected to be rejected by the client.
@@ -624,6 +628,14 @@ def generate_block_data(
624628
header = block.rlp_modifier.apply(header)
625629
header.fork = fork # Deleted during `apply` because `exclude=True`
626630

631+
# Process block access list - apply transformer if present for invalid tests
632+
bal = transition_tool_output.result.block_access_list
633+
if block.expected_block_access_list is not None and bal is not None:
634+
# Use to_fixture_bal to validate and potentially transform the BAL
635+
bal = block.expected_block_access_list.to_fixture_bal(bal)
636+
# Don't update the header hash - leave it as the hash of the correct BAL
637+
# This creates a mismatch that should cause the block to be rejected
638+
627639
built_block = BuiltBlock(
628640
header=header,
629641
alloc=transition_tool_output.alloc,
@@ -636,7 +648,7 @@ def generate_block_data(
636648
expected_exception=block.exception,
637649
engine_api_error_code=block.engine_api_error_code,
638650
fork=fork,
639-
block_access_list=transition_tool_output.result.block_access_list,
651+
block_access_list=bal,
640652
)
641653

642654
try:
@@ -648,14 +660,20 @@ def generate_block_data(
648660
and block.rlp_modifier is None
649661
and block.requests is None
650662
and not block.skip_exception_verification
663+
and not (
664+
block.expected_block_access_list is not None
665+
and block.expected_block_access_list.modifier is not None
666+
)
651667
):
652668
# Only verify block level exception if:
653-
# - No transaction exception was raised, because these are not reported as block
654-
# exceptions.
655-
# - No RLP modifier was specified, because the modifier is what normally
656-
# produces the block exception.
657-
# - No requests were specified, because modified requests are also what normally
658-
# produces the block exception.
669+
# - No transaction exception was raised, because these are not
670+
# reported as block exceptions.
671+
# - No RLP modifier was specified, because the modifier is what
672+
# normally produces the block exception.
673+
# - No requests were specified, because modified requests are also
674+
# what normally produces the block exception.
675+
# - No BAL modifier was specified, because modified BAL also
676+
# produces block exceptions.
659677
built_block.verify_block_exception(
660678
transition_tool_exceptions_reliable=t8n.exception_mapper.reliable,
661679
)
@@ -740,11 +758,7 @@ def make_fixture(
740758
)
741759
fixture_blocks.append(built_block.get_fixture_block())
742760

743-
# Verify block access list if expected
744-
if self.expected_block_access_list is not None:
745-
self.verify_block_access_list(
746-
built_block.block_access_list, self.expected_block_access_list
747-
)
761+
# BAL verification already done in to_fixture_bal() if expected_block_access_list set
748762

749763
if block.exception is None:
750764
# Update env, alloc and last block hash for the next block.

src/ethereum_test_types/block_access_list.py renamed to src/ethereum_test_types/block_access_list/__init__.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
these are simple data classes that can be composed together.
66
"""
77

8-
from typing import Any, ClassVar, List
8+
from typing import Any, Callable, ClassVar, List, Optional
99

1010
import ethereum_rlp as eth_rlp
1111
from pydantic import Field
@@ -23,6 +23,20 @@
2323
from ethereum_test_base_types.serialization import to_serializable_element
2424

2525

26+
def compose(
27+
*modifiers: Callable[["BlockAccessList"], "BlockAccessList"],
28+
) -> Callable[["BlockAccessList"], "BlockAccessList"]:
29+
"""Compose multiple modifiers into a single modifier."""
30+
31+
def composed(bal: BlockAccessList) -> BlockAccessList:
32+
result = bal
33+
for modifier in modifiers:
34+
result = modifier(result)
35+
return result
36+
37+
return composed
38+
39+
2640
class BalNonceChange(CamelModel, RLPSerializable):
2741
"""Represents a nonce change in the block access list."""
2842

@@ -155,6 +169,60 @@ class BlockAccessListExpectation(CamelModel):
155169
default_factory=list, description="Expected account changes to verify"
156170
)
157171

172+
modifier: Optional[Callable[["BlockAccessList"], "BlockAccessList"]] = Field(
173+
None,
174+
exclude=True,
175+
description="Optional modifier to modify the BAL for invalid tests",
176+
)
177+
178+
def modify(
179+
self, *modifiers: Callable[["BlockAccessList"], "BlockAccessList"]
180+
) -> "BlockAccessListExpectation":
181+
"""
182+
Create a new expectation with a modifier for invalid test cases.
183+
184+
Args:
185+
modifiers: One or more functions that take and return a BlockAccessList
186+
187+
Returns:
188+
A new BlockAccessListExpectation instance with the modifiers applied
189+
190+
Example:
191+
from ethereum_test_types.block_access_list.modifiers import remove_nonces
192+
193+
expectation = BlockAccessListExpectation(
194+
account_changes=[...]
195+
).modify(remove_nonces(alice))
196+
197+
"""
198+
new_instance = self.model_copy(deep=True)
199+
new_instance.modifier = compose(*modifiers)
200+
return new_instance
201+
202+
def to_fixture_bal(self, t8n_bal: "BlockAccessList") -> "BlockAccessList":
203+
"""
204+
Convert t8n BAL to fixture BAL, optionally applying transformations.
205+
206+
1. First validates expectations are met (if any)
207+
2. Then applies modifier if specified (for invalid tests)
208+
209+
Args:
210+
t8n_bal: The BlockAccessList from t8n tool
211+
212+
Returns:
213+
The potentially transformed BlockAccessList for the fixture
214+
215+
"""
216+
# Only validate if we have expectations
217+
if self.account_changes:
218+
self.verify_against(t8n_bal)
219+
220+
# Apply modifier if present (for invalid tests)
221+
if self.modifier:
222+
return self.modifier(t8n_bal)
223+
224+
return t8n_bal
225+
158226
def verify_against(self, actual_bal: "BlockAccessList") -> None:
159227
"""
160228
Verify that the actual BAL from the client matches this expected BAL.
@@ -320,3 +388,17 @@ def _compare_change_lists(self, field_name: str, expected: List, actual: List) -
320388
raise AssertionError(msg)
321389

322390
return True
391+
392+
393+
__all__ = [
394+
# Core models
395+
"BlockAccessList",
396+
"BlockAccessListExpectation",
397+
# Change types
398+
"BalAccountChange",
399+
"BalNonceChange",
400+
"BalBalanceChange",
401+
"BalCodeChange",
402+
"BalStorageChange",
403+
"BalStorageSlot",
404+
]

0 commit comments

Comments
 (0)