Skip to content

Commit d6368b6

Browse files
committed
refactor(bal): Split BAL into two classes
- Use a root model class for the t8n BAL model. This lets us stick closer to the EIP definition where the BAL itself is a list. - The class doesn't need to be `RLPSerializable`, just have simple instructions to serialize its elements (`rlp()`). - Create a `BlockAccessListExpectation` class to represent the expected BAL for a given block. This class can describe different expectations for the test cases, such as defining mutations to be used for invalid tests.
1 parent bc2e123 commit d6368b6

File tree

6 files changed

+65
-57
lines changed

6 files changed

+65
-57
lines changed

src/ethereum_clis/types.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
TransactionException,
1313
UndefinedException,
1414
)
15-
from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt
16-
from ethereum_test_types.block_access_list import BlockAccessList
15+
from ethereum_test_types import (
16+
Alloc,
17+
BlockAccessList,
18+
Environment,
19+
Transaction,
20+
TransactionReceipt,
21+
)
1722

1823

1924
class TransactionExceptionWithMessage(ExceptionWithMessage[TransactionException]):

src/ethereum_test_specs/blockchain.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from ethereum_test_fixtures.common import FixtureBlobSchedule
5353
from ethereum_test_forks import Fork
5454
from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal
55-
from ethereum_test_types.block_access_list import BlockAccessList
55+
from ethereum_test_types.block_access_list import BlockAccessList, BlockAccessListExpectation
5656

5757
from .base import BaseTest, OpMode, verify_result
5858
from .debugging import print_traces
@@ -426,11 +426,11 @@ class BlockchainTest(BaseTest):
426426
Exclude the post state from the fixture output.
427427
In this case, the state verification is only performed based on the state root.
428428
"""
429-
expected_block_access_list: Any | None = None # Will be BAL type
429+
expected_block_access_list: BlockAccessListExpectation | None = None
430430
"""
431431
Expected block access list for verification.
432432
If set, verifies that the block access list returned by the client matches expectations.
433-
Use the BAL builder API to construct expectations.
433+
Use BlockAccessListExpectation to define partial validation expectations.
434434
"""
435435

436436
supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [
@@ -691,7 +691,7 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None
691691
raise e
692692

693693
def verify_block_access_list(
694-
self, actual_bal: BlockAccessList | None, expected_bal: Any | None
694+
self, actual_bal: BlockAccessList | None, expected_bal: BlockAccessListExpectation | None
695695
):
696696
"""
697697
Verify that the actual block access list matches expectations.
@@ -704,15 +704,9 @@ def verify_block_access_list(
704704
if expected_bal is None:
705705
return
706706

707-
if not isinstance(expected_bal, BlockAccessList):
708-
raise Exception(
709-
f"Invalid expected_bal type: {type(expected_bal)}. Use BlockAccessList."
710-
)
711-
712707
if actual_bal is None:
713708
raise Exception("Expected block access list but got none.")
714709

715-
# Verify using the BlockAccessList's verification method
716710
try:
717711
expected_bal.verify_against(actual_bal)
718712
except Exception as e:

src/ethereum_test_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
BalStorageChange,
1111
BalStorageSlot,
1212
BlockAccessList,
13+
BlockAccessListExpectation,
1314
)
1415
from .block_types import (
1516
Environment,
@@ -52,6 +53,7 @@
5253
"BalStorageSlot",
5354
"Blob",
5455
"BlockAccessList",
56+
"BlockAccessListExpectation",
5557
"ConsolidationRequest",
5658
"DepositRequest",
5759
"Environment",

src/ethereum_test_types/block_access_list.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
these are simple data classes that can be composed together.
66
"""
77

8-
from typing import Any, ClassVar, Dict, List
8+
from typing import Any, ClassVar, List
99

10+
import ethereum_rlp as eth_rlp
1011
from pydantic import Field
1112

1213
from ethereum_test_base_types import (
1314
Address,
1415
Bytes,
1516
CamelModel,
17+
EthereumTestRootModel,
1618
HexNumber,
1719
Number,
1820
RLPSerializable,
@@ -98,56 +100,61 @@ class BalAccountChange(CamelModel, RLPSerializable):
98100
]
99101

100102

101-
class BlockAccessList(CamelModel, RLPSerializable):
103+
class BlockAccessList(EthereumTestRootModel[List[BalAccountChange]]):
102104
"""
103-
Expected Block Access List for verification.
105+
Block Access List for t8n tool communication and fixtures.
106+
107+
This model represents the BAL exactly as defined in EIP-7928 - it is itself a list
108+
of account changes (root model), not a container. Used for:
109+
- Communication with t8n tools
110+
- Fixture generation
111+
- RLP encoding for hash verification
104112
105113
Example:
106-
expected_block_access_list = BlockAccessList(
114+
bal = BlockAccessList([
115+
BalAccountChange(address=alice, nonce_changes=[...]),
116+
BalAccountChange(address=bob, balance_changes=[...])
117+
])
118+
119+
"""
120+
121+
root: List[BalAccountChange] = Field(default_factory=list)
122+
123+
def to_list(self) -> List[Any]:
124+
"""Return the list for RLP encoding per EIP-7928."""
125+
return to_serializable_element(self.root)
126+
127+
def rlp(self) -> Bytes:
128+
"""Return the RLP encoded block access list for hash verification."""
129+
return Bytes(eth_rlp.encode(self.to_list()))
130+
131+
132+
class BlockAccessListExpectation(CamelModel):
133+
"""
134+
Block Access List expectation model for test writing.
135+
136+
This model is used to define expected BAL values in tests. It supports:
137+
- Partial validation (only checks explicitly set fields)
138+
- Convenient test syntax with named parameters
139+
- Verification against actual BAL from t8n
140+
141+
Example:
142+
# In test definition
143+
expected_block_access_list = BlockAccessListExpectation(
107144
account_changes=[
108145
BalAccountChange(
109146
address=alice,
110-
nonce_changes=[
111-
BalNonceChange(tx_index=0, post_nonce=1)
112-
],
113-
balance_changes=[
114-
BalBalanceChange(tx_index=0, post_balance=9000)
115-
]
116-
),
117-
BalAccountChange(
118-
address=bob,
119-
balance_changes=[
120-
BalBalanceChange(tx_index=0, post_balance=100)
121-
],
122-
code_changes=[
123-
BalCodeChange(tx_index=0, new_code=b"0x1234")
124-
],
125-
),
147+
nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)]
148+
)
126149
]
127150
)
128151
129152
"""
130153

131154
account_changes: List[BalAccountChange] = Field(
132-
default_factory=list, description="List of account changes in the block"
155+
default_factory=list, description="Expected account changes to verify"
133156
)
134157

135-
rlp_fields: ClassVar[List[str]] = ["account_changes"]
136-
137-
def to_list(self, signing: bool = False) -> List[Any]:
138-
"""
139-
Override to_list to return the account changes list directly.
140-
141-
The BlockAccessList IS the list of account changes, not a container
142-
that contains a list, per EIP-7928.
143-
"""
144-
# Return the list of accounts directly, not wrapped in another list
145-
return to_serializable_element(self.account_changes)
146-
147-
def to_dict(self) -> Dict[str, Any]:
148-
"""Convert to dictionary for serialization."""
149-
return self.model_dump(exclude_none=True)
150-
151158
def verify_against(self, actual_bal: "BlockAccessList") -> None:
152159
"""
153160
Verify that the actual BAL from the client matches this expected BAL.
@@ -159,7 +166,7 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None:
159166
Exception: If verification fails
160167
161168
"""
162-
actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.account_changes}
169+
actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.root}
163170
expected_accounts_by_addr = {acc.address: acc for acc in self.account_changes}
164171

165172
# Check for missing accounts

src/pytest_plugins/eels_resolutions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@
5555
"Amsterdam": {
5656
"git_url": "https://github.com/fselmo/execution-specs.git",
5757
"branch": "feat/amsterdam-fork-and-block-access-lists",
58-
"commit": "59ff2946ca30879079b2ef2b601ae09106ca08f7"
58+
"commit": "f4c49309e90e74b51d043b06c045735b54e997b0"
5959
}
6060
}

tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
BalNonceChange,
1919
BalStorageChange,
2020
BalStorageSlot,
21-
BlockAccessList,
21+
BlockAccessListExpectation,
2222
)
2323

2424
from .spec import ref_spec_7928
@@ -51,7 +51,7 @@ def test_bal_nonce_changes(
5151
alice: Account(nonce=1),
5252
bob: Account(balance=100),
5353
},
54-
expected_block_access_list=BlockAccessList(
54+
expected_block_access_list=BlockAccessListExpectation(
5555
account_changes=[
5656
BalAccountChange(
5757
address=alice,
@@ -103,7 +103,7 @@ def test_bal_balance_changes(
103103
alice: Account(nonce=1, balance=alice_final_balance),
104104
bob: Account(balance=100),
105105
},
106-
expected_block_access_list=BlockAccessList(
106+
expected_block_access_list=BlockAccessListExpectation(
107107
account_changes=[
108108
BalAccountChange(
109109
address=alice,
@@ -146,7 +146,7 @@ def test_bal_storage_writes(
146146
alice: Account(nonce=1),
147147
storage_contract: Account(storage={0x01: 0x42}),
148148
},
149-
expected_block_access_list=BlockAccessList(
149+
expected_block_access_list=BlockAccessListExpectation(
150150
account_changes=[
151151
BalAccountChange(
152152
address=storage_contract,
@@ -189,7 +189,7 @@ def test_bal_storage_reads(
189189
alice: Account(nonce=1),
190190
storage_contract: Account(storage={0x01: 0x42}),
191191
},
192-
expected_block_access_list=BlockAccessList(
192+
expected_block_access_list=BlockAccessListExpectation(
193193
account_changes=[
194194
BalAccountChange(
195195
address=storage_contract,
@@ -259,7 +259,7 @@ def test_bal_code_changes(
259259
storage={},
260260
),
261261
},
262-
expected_block_access_list=BlockAccessList(
262+
expected_block_access_list=BlockAccessListExpectation(
263263
account_changes=[
264264
BalAccountChange(
265265
address=alice,

0 commit comments

Comments
 (0)