Skip to content

Commit e48d4e0

Browse files
committed
fix: fix state leak in modifiers; add tests to catch this
1 parent 8dab8ee commit e48d4e0

2 files changed

Lines changed: 76 additions & 21 deletions

File tree

packages/testing/src/execution_testing/test_types/block_access_list/modifiers.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,9 @@ def _remove_field_from_accounts(
2828
) -> Callable[[BlockAccessList], BlockAccessList]:
2929
"""Abstracted helper to remove a field from specified accounts."""
3030
len_addresses = len(addresses)
31-
found_addresses = set()
3231

3332
def transform(bal: BlockAccessList) -> BlockAccessList:
34-
nonlocal found_addresses
33+
found_addresses: set[Address] = set()
3534
new_root = []
3635
for account_change in bal.root:
3736
if account_change.address in addresses:
@@ -70,11 +69,10 @@ def _modify_field_value(
7069
Abstracted helper to modify a field value for a specific account and
7170
transaction.
7271
"""
73-
found_address = False
74-
found_index = False
7572

7673
def transform(bal: BlockAccessList) -> BlockAccessList:
77-
nonlocal found_address, found_index
74+
found_address = False
75+
found_index = False
7876
new_root = []
7977
for account_change in bal.root:
8078
if account_change.address == address:
@@ -261,13 +259,12 @@ def swap_bal_indices(
261259
idx1: int, idx2: int
262260
) -> Callable[[BlockAccessList], BlockAccessList]:
263261
"""Swap block access indices throughout the BAL, modifying ordering."""
264-
nonce_indices = {idx1: False, idx2: False}
265-
balance_indices = nonce_indices.copy()
266-
storage_indices = nonce_indices.copy()
267-
code_indices = nonce_indices.copy()
268262

269263
def transform(bal: BlockAccessList) -> BlockAccessList:
270-
nonlocal nonce_indices, balance_indices, storage_indices, code_indices
264+
nonce_indices = {idx1: False, idx2: False}
265+
balance_indices = nonce_indices.copy()
266+
storage_indices = nonce_indices.copy()
267+
code_indices = nonce_indices.copy()
271268
new_root = []
272269
for account_change in bal.root:
273270
new_account = account_change.model_copy(deep=True)
@@ -399,10 +396,8 @@ def append_change(
399396
else:
400397
raise TypeError(f"Unsupported change type: {type(change)}")
401398

402-
found_address = False
403-
404399
def transform(bal: BlockAccessList) -> BlockAccessList:
405-
nonlocal found_address
400+
found_address = False
406401
new_root = []
407402
for account_change in bal.root:
408403
if account_change.address == account:
@@ -441,10 +436,9 @@ def append_storage(
441436
slot_changes
442437
- If change provided and slot new: creates new BalStorageSlot
443438
"""
444-
found_address = False
445439

446440
def transform(bal: BlockAccessList) -> BlockAccessList:
447-
nonlocal found_address
441+
found_address = False
448442
new_root = []
449443
for account_change in bal.root:
450444
if account_change.address == address:
@@ -491,10 +485,9 @@ def duplicate_account(
491485
address: Address,
492486
) -> Callable[[BlockAccessList], BlockAccessList]:
493487
"""Duplicate an account entry in the BAL."""
494-
address_present = False
495488

496489
def transform(bal: BlockAccessList) -> BlockAccessList:
497-
nonlocal address_present
490+
address_present = False
498491
new_root = []
499492
for account_change in bal.root:
500493
new_root.append(account_change)
@@ -528,15 +521,14 @@ def _duplicate_in_field(
528521
When sub_field and sub_match_fn are provided, find the parent entry
529522
via match_fn then duplicate within sub_field using sub_match_fn.
530523
"""
531-
found = False
532524

533525
def _copy(entry: Any) -> Any:
534526
if hasattr(entry, "model_copy"):
535527
return entry.model_copy(deep=True)
536528
return ZeroPaddedHexNumber(entry)
537529

538530
def transform(bal: BlockAccessList) -> BlockAccessList:
539-
nonlocal found
531+
found = False
540532
new_root = []
541533
for account_change in bal.root:
542534
if account_change.address == address:
@@ -677,10 +669,9 @@ def insert_storage_read(
677669
Useful for testing that a key must not appear in both
678670
storage_changes and storage_reads.
679671
"""
680-
found_address = False
681672

682673
def transform(bal: BlockAccessList) -> BlockAccessList:
683-
nonlocal found_address
674+
found_address = False
684675
new_root = []
685676
for account_change in bal.root:
686677
if account_change.address == address:

packages/testing/src/execution_testing/test_types/tests/test_block_access_list_modifiers.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Unit tests for BAL modifier functions."""
22

3+
from typing import Callable
4+
35
import pytest
46

57
from execution_testing.base_types import Address
@@ -13,6 +15,8 @@
1315
BlockAccessList,
1416
)
1517
from execution_testing.test_types.block_access_list.modifiers import (
18+
append_change,
19+
append_storage,
1620
duplicate_account,
1721
duplicate_balance_change,
1822
duplicate_code_change,
@@ -25,7 +29,9 @@
2529
modify_code,
2630
modify_nonce,
2731
modify_storage,
32+
remove_nonces,
2833
reorder_accounts,
34+
swap_bal_indices,
2935
)
3036

3137
ALICE = Address(0xA)
@@ -304,3 +310,61 @@ def test_reorder_accounts_out_of_range_raises(
304310
"""Raise when indices are not a valid permutation (skipped index)."""
305311
with pytest.raises(ValueError, match="valid permutation"):
306312
reorder_accounts([0, 2])(sample_bal)
313+
314+
315+
_EMPTY_BAL = BlockAccessList([])
316+
_ALICE_ONLY_BAL = BlockAccessList(
317+
[BalAccountChange(address=ALICE, nonce_changes=[])]
318+
)
319+
320+
321+
@pytest.mark.parametrize(
322+
"modifier_factory, missing_bal",
323+
[
324+
pytest.param(
325+
lambda: remove_nonces(ALICE), _EMPTY_BAL, id="remove_nonces"
326+
),
327+
pytest.param(
328+
lambda: swap_bal_indices(1, 1), _EMPTY_BAL, id="swap_bal_indices"
329+
),
330+
pytest.param(
331+
lambda: append_change(
332+
ALICE, BalNonceChange(block_access_index=2, post_nonce=5)
333+
),
334+
_EMPTY_BAL,
335+
id="append_change",
336+
),
337+
pytest.param(
338+
lambda: append_storage(CONTRACT, slot=7, read=True),
339+
_EMPTY_BAL,
340+
id="append_storage",
341+
),
342+
pytest.param(
343+
lambda: duplicate_account(ALICE),
344+
_EMPTY_BAL,
345+
id="duplicate_account",
346+
),
347+
pytest.param(
348+
lambda: duplicate_nonce_change(ALICE, 1),
349+
_ALICE_ONLY_BAL,
350+
id="duplicate_nonce_change",
351+
),
352+
pytest.param(
353+
lambda: insert_storage_read(CONTRACT, 99),
354+
_EMPTY_BAL,
355+
id="insert_storage_read",
356+
),
357+
],
358+
)
359+
def test_reused_callable_does_not_carry_found_state(
360+
sample_bal: BlockAccessList,
361+
modifier_factory: Callable[
362+
[], Callable[[BlockAccessList], BlockAccessList]
363+
],
364+
missing_bal: BlockAccessList,
365+
) -> None:
366+
"""A modifier's found-state must not persist across calls."""
367+
modifier = modifier_factory()
368+
modifier(sample_bal)
369+
with pytest.raises(ValueError, match="not found"):
370+
modifier(missing_bal)

0 commit comments

Comments
 (0)