5
5
these are simple data classes that can be composed together.
6
6
"""
7
7
8
- from typing import Any , Callable , ClassVar , List
8
+ from functools import cached_property
9
+ from typing import Any , Callable , ClassVar , Dict , List
9
10
10
11
import ethereum_rlp as eth_rlp
11
12
from pydantic import Field
@@ -138,10 +139,40 @@ def to_list(self) -> List[Any]:
138
139
"""Return the list for RLP encoding per EIP-7928."""
139
140
return to_serializable_element (self .root )
140
141
142
+ @cached_property
141
143
def rlp (self ) -> Bytes :
142
144
"""Return the RLP encoded block access list for hash verification."""
143
145
return Bytes (eth_rlp .encode (self .to_list ()))
144
146
147
+ @cached_property
148
+ def rlp_hash (self ) -> Bytes :
149
+ """Return the hash of the RLP encoded block access list."""
150
+ return self .rlp .keccak256 ()
151
+
152
+
153
+ class BalAccountExpectation (CamelModel ):
154
+ """
155
+ Represents expected changes to a specific account in a block.
156
+
157
+ Same as BalAccountChange but without the address field, used for expectations.
158
+ """
159
+
160
+ nonce_changes : List [BalNonceChange ] = Field (
161
+ default_factory = list , description = "List of expected nonce changes"
162
+ )
163
+ balance_changes : List [BalBalanceChange ] = Field (
164
+ default_factory = list , description = "List of expected balance changes"
165
+ )
166
+ code_changes : List [BalCodeChange ] = Field (
167
+ default_factory = list , description = "List of expected code changes"
168
+ )
169
+ storage_changes : List [BalStorageSlot ] = Field (
170
+ default_factory = list , description = "List of expected storage changes"
171
+ )
172
+ storage_reads : List [StorageKey ] = Field (
173
+ default_factory = list , description = "List of expected read storage slots"
174
+ )
175
+
145
176
146
177
class BlockAccessListExpectation (CamelModel ):
147
178
"""
@@ -151,22 +182,23 @@ class BlockAccessListExpectation(CamelModel):
151
182
- Partial validation (only checks explicitly set fields)
152
183
- Convenient test syntax with named parameters
153
184
- Verification against actual BAL from t8n
185
+ - Explicit exclusion of addresses (using None values)
154
186
155
187
Example:
156
188
# In test definition
157
189
expected_block_access_list = BlockAccessListExpectation(
158
- account_changes=[
159
- BalAccountChange(
160
- address=alice,
190
+ account_expectations={
191
+ alice: BalAccountExpectation(
161
192
nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)]
162
- )
163
- ]
193
+ ),
194
+ bob: None, # Bob should NOT be in the BAL
195
+ }
164
196
)
165
197
166
198
"""
167
199
168
- account_changes : List [ BalAccountChange ] = Field (
169
- default_factory = list , description = "Expected account changes to verify"
200
+ account_expectations : Dict [ Address , BalAccountExpectation | None ] = Field (
201
+ default_factory = dict , description = "Expected account changes or exclusions to verify"
170
202
)
171
203
172
204
modifier : Callable [["BlockAccessList" ], "BlockAccessList" ] | None = Field (
@@ -191,7 +223,7 @@ def modify(
191
223
from ethereum_test_types.block_access_list.modifiers import remove_nonces
192
224
193
225
expectation = BlockAccessListExpectation(
194
- account_changes=[ ...]
226
+ account_expectations={ ...}
195
227
).modify(remove_nonces(alice))
196
228
197
229
"""
@@ -213,8 +245,7 @@ def to_fixture_bal(self, t8n_bal: "BlockAccessList") -> "BlockAccessList":
213
245
The potentially transformed BlockAccessList for the fixture
214
246
215
247
"""
216
- # Only validate if we have expectations
217
- if self .account_changes :
248
+ if self .account_expectations :
218
249
self .verify_against (t8n_bal )
219
250
220
251
# Apply modifier if present (for invalid tests)
@@ -235,49 +266,42 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None:
235
266
236
267
"""
237
268
actual_accounts_by_addr = {acc .address : acc for acc in actual_bal .root }
238
- expected_accounts_by_addr = {acc .address : acc for acc in self .account_changes }
239
269
240
- # Check for missing accounts
241
- missing_accounts = set (expected_accounts_by_addr .keys ()) - set (
242
- actual_accounts_by_addr .keys ()
243
- )
244
- if missing_accounts :
245
- raise Exception (
246
- "Expected accounts not found in actual BAL: "
247
- f"{ ', ' .join (str (a ) for a in missing_accounts )} "
248
- )
249
-
250
- # Verify each expected account
251
- for address , expected_account in expected_accounts_by_addr .items ():
252
- actual_account = actual_accounts_by_addr [address ]
253
-
254
- try :
255
- self ._compare_account_changes (expected_account , actual_account )
256
- except AssertionError as e :
257
- raise Exception (f"Account { address } : { str (e )} " ) from e
258
-
259
- def _compare_account_changes (
260
- self , expected : BalAccountChange , actual : BalAccountChange
270
+ for address , expectation in self .account_expectations .items ():
271
+ if expectation is None :
272
+ # check explicit exclusion of address when set to `None`
273
+ if address in actual_accounts_by_addr :
274
+ raise Exception (f"Address { address } should not be in BAL but was found" )
275
+ else :
276
+ # Address should be in BAL with expected values
277
+ if address not in actual_accounts_by_addr :
278
+ raise Exception (f"Expected address { address } not found in actual BAL" )
279
+
280
+ actual_account = actual_accounts_by_addr [address ]
281
+ try :
282
+ self ._compare_account_expectations (expectation , actual_account )
283
+ except AssertionError as e :
284
+ raise Exception (f"Account { address } : { str (e )} " ) from e
285
+
286
+ def _compare_account_expectations (
287
+ self , expected : BalAccountExpectation , actual : BalAccountChange
261
288
) -> None :
262
289
"""
263
- Compare two BalAccountChange models with detailed error reporting .
290
+ Compare expected account changes with actual BAL account entry .
264
291
265
292
Only validates fields that were explicitly set in the expected model,
266
293
using model_fields_set to determine what was intentionally specified.
267
294
"""
268
295
# Only check fields that were explicitly set in the expected model
269
296
for field_name in expected .model_fields_set :
270
- if field_name == "address" :
271
- continue # Already matched by account lookup
272
-
273
297
expected_value = getattr (expected , field_name )
274
298
actual_value = getattr (actual , field_name )
275
299
276
- # If we explicitly set a field to None, verify it's None/empty
300
+ # explicit check for None
277
301
if expected_value is None :
278
302
if actual_value is not None and actual_value != []:
279
303
raise AssertionError (
280
- f"Expected { field_name } to be empty/ None but found: { actual_value } "
304
+ f"Expected { field_name } to be ` None` but found: { actual_value } "
281
305
)
282
306
continue
283
307
@@ -314,7 +338,8 @@ def _compare_account_changes(
314
338
# The comparison method will raise with details
315
339
pass
316
340
317
- def _compare_change_lists (self , field_name : str , expected : List , actual : List ) -> bool :
341
+ @staticmethod
342
+ def _compare_change_lists (field_name : str , expected : List , actual : List ) -> bool :
318
343
"""Compare lists of change objects using set operations for better error messages."""
319
344
if field_name == "storage_changes" :
320
345
# Storage changes are nested (slot -> changes)
@@ -390,6 +415,7 @@ def _compare_change_lists(self, field_name: str, expected: List, actual: List) -
390
415
# Core models
391
416
"BlockAccessList" ,
392
417
"BlockAccessListExpectation" ,
418
+ "BalAccountExpectation" ,
393
419
# Change types
394
420
"BalAccountChange" ,
395
421
"BalNonceChange" ,
0 commit comments