From 3a75ec39d420bad360f34b3d86b0b04edfb2dfd7 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Fri, 24 Apr 2026 16:39:57 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20BAL=20corruptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_access_list/corruptions.py | 96 ++++++++++++++++ .../test_block_access_list_corruptions.py | 108 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py create mode 100644 packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py new file mode 100644 index 00000000000..a2e1bc80280 --- /dev/null +++ b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py @@ -0,0 +1,96 @@ +""" +Corruption catalog for EIP-7928 Block Access Lists. + +Derives the full set of *corruption cases* from a valid +`BlockAccessListExpectation`. Each case bundles: + + - a stable ``id`` for pytest parametrization, + - a ``modifier`` that, when applied to the real t8n-produced BAL at + fill time, yields a corrupted variant, + - the ``expected_exception`` the client must raise on that variant. + +The catalog is pure: it never touches a real BAL and has no side +effects. Higher-level nibbles wire these cases into pytest. + +## ID format + +:: + + ___ — account-targeted + _ — BAL-level + +```` is ``str(address)`` — the canonical 0x-prefixed +lowercase hex form. Using the address directly makes IDs independent of +dict iteration order or any external labeling scheme. + +## Prefixes + +- ``corrupt_`` — XOR-flip an existing write value (Correctness). +- ``omit_`` — remove an access-list or the whole account + (Exactness). +- ``duplicate_`` — repeat an existing access (Exactness). +- ``phantom_`` — inject an entry execution never produced (Exactness). +- ``swap_`` — swap a pair at account- or index-level (Sequence). +""" + +from typing import Callable, List, NamedTuple + +from execution_testing.exceptions import BlockException + +from .expectations import BlockAccessListExpectation +from .t8n import BlockAccessList + +Modifier = Callable[[BlockAccessList], BlockAccessList] + + +class CorruptionCase(NamedTuple): + """ + One corruption derived from a valid BAL expectation. + + Fields + ------ + id + Stable string for pytest parametrization. See module docstring + for the naming convention. + modifier + Callable that, applied to the real t8n-produced BAL at fill + time, yields the corrupted variant. + expected_exception + The ``BlockException`` the client must raise when verifying the + corrupted block. + """ + + id: str + modifier: Modifier + expected_exception: BlockException + + +def enumerate_corruptions( + expectation: BlockAccessListExpectation, +) -> List[CorruptionCase]: + r""" + Return every corruption case derivable from ``expectation``. + + Emits cases along three axes: + + - **Correctness** — ``corrupt_`` per existing change. + - **Exactness** — ``omit_*``, ``duplicate_*``, ``phantom_*``. + - **Sequence** — ``swap_accounts`` and ``swap_indices`` where + applicable. + + The total count is: + + .. math:: + + N = 2C + R + K + 6A + 1 + \\alpha + \\beta + + where :math:`A` = accounts, :math:`C` = total changes, :math:`R` = + total reads, :math:`K` = populated access-lists, :math:`\\alpha` = + 1 if :math:`A \\geq 2` else 0, :math:`\\beta` = 1 if there are + :math:`\\geq 2` distinct ``block_access_index`` values else 0. + + See the module docstring for the ID format. + """ + raise NotImplementedError( + "enumerate_corruptions is not yet implemented; " + ) diff --git a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py new file mode 100644 index 00000000000..eb4913c916c --- /dev/null +++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py @@ -0,0 +1,108 @@ +"""Unit tests for the BAL corruption catalog.""" + +from execution_testing.base_types import Address +from execution_testing.test_types.block_access_list import ( + BalAccountExpectation, + BalBalanceChange, + BalNonceChange, + BlockAccessListExpectation, +) +from execution_testing.test_types.block_access_list.corruptions import ( + enumerate_corruptions, +) + +ETH = 10**18 + +ALICE = Address(0xA11CE) +BOB = Address(0xB0B) + + +def test_enumerate_corruptions_alice_bob() -> None: + """ + Alice starts with 5 ETH and transfers 1 ETH to Bob. Gas is ignored + for simplicity — so Alice ends with 4 ETH and Bob with 1 ETH. + + Shape counts:: + + A = 2 (alice, bob) + C = 3 (alice.nonce, alice.balance, bob.balance) + R = 0 + K = 3 (alice.nonce_changes, alice.balance_changes, + bob.balance_changes) + α = 1 (A ≥ 2) + β = 0 (only block_access_index=1 appears) + + Total:: + + N = 2C + R + K + 6A + 1 + α + β + = 2(3) + 0 + 3 + 6(2) + 1 + 1 + 0 + = 23 + + Per-prefix breakdown:: + + corrupt_ = C = 3 + omit_ = K + A = 5 + duplicate_ = C + R = 3 + phantom_ = 1 + 5A = 11 + swap_ = α + β = 1 + """ + expectation = BlockAccessListExpectation( + account_expectations={ + ALICE: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + ], + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=4 * ETH + ), + ], + ), + BOB: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=1 * ETH + ), + ], + ), + } + ) + + cases = enumerate_corruptions(expectation) + + alice = str(ALICE) + bob = str(BOB) + + expected_ids = { + # corrupt_ (3) + f"{alice}__corrupt_nonce", + f"{alice}__corrupt_balance", + f"{bob}__corrupt_balance", + # omit_ (5) + f"{alice}__omit_nonce", + f"{alice}__omit_balance", + f"{bob}__omit_balance", + f"{alice}__omit_account", + f"{bob}__omit_account", + # duplicate_ (3) + f"{alice}__duplicate_nonce", + f"{alice}__duplicate_balance", + f"{bob}__duplicate_balance", + # phantom_ (11) + "phantom_account", + f"{alice}__phantom_nonce", + f"{alice}__phantom_balance", + f"{alice}__phantom_code", + f"{alice}__phantom_storage_write", + f"{alice}__phantom_storage_read", + f"{bob}__phantom_nonce", + f"{bob}__phantom_balance", + f"{bob}__phantom_code", + f"{bob}__phantom_storage_write", + f"{bob}__phantom_storage_read", + # swap_ (1) + "swap_accounts", + } + + assert len(cases) == 23 + assert {c.id for c in cases} == expected_ids From 206dfde31357bf93f035b7e40607cbcb32c52072 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Wed, 6 May 2026 15:53:16 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20feat:=20Corruption=20impl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_access_list/corruptions.py | 281 +++++++++++++++--- .../test_block_access_list_corruptions.py | 62 ++-- 2 files changed, 265 insertions(+), 78 deletions(-) diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py index a2e1bc80280..a9de577b2d3 100644 --- a/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py +++ b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py @@ -1,8 +1,8 @@ """ Corruption catalog for EIP-7928 Block Access Lists. -Derives the full set of *corruption cases* from a valid -`BlockAccessListExpectation`. Each case bundles: +Derives Correctness and Completeness corruption cases from a valid +``BlockAccessListExpectation``. Each case bundles: - a stable ``id`` for pytest parametrization, - a ``modifier`` that, when applied to the real t8n-produced BAL at @@ -16,28 +16,35 @@ :: - ___ — account-targeted - _ — BAL-level +
______ - nonce, balance, code +
____storage____ - storage_write +
____storage_read__ - storage_read (no bai) +
__missing - account-level missing -```` is ``str(address)`` — the canonical 0x-prefixed -lowercase hex form. Using the address directly makes IDs independent of -dict iteration order or any external labeling scheme. +``
`` is ``str(address)`` — the canonical 0x-prefixed lowercase +hex form. Using the address directly makes IDs independent of dict +iteration order. ```` is ``wrong`` (Correctness) or ``missing`` +(Completeness). -## Prefixes +## Cases -- ``corrupt_`` — XOR-flip an existing write value (Correctness). -- ``omit_`` — remove an access-list or the whole account - (Exactness). -- ``duplicate_`` — repeat an existing access (Exactness). -- ``phantom_`` — inject an entry execution never produced (Exactness). -- ``swap_`` — swap a pair at account- or index-level (Sequence). +- ``wrong`` — XOR-flip an existing value (Correctness). +- ``missing`` — drop an existing entry or whole account (Completeness). """ from typing import Callable, List, NamedTuple +from execution_testing.base_types import Address, ZeroPaddedHexNumber from execution_testing.exceptions import BlockException -from .expectations import BlockAccessListExpectation +from .expectations import BalAccountExpectation, BlockAccessListExpectation +from .modifiers import ( + modify_balance, + modify_code, + modify_nonce, + modify_storage, + remove_accounts, +) from .t8n import BlockAccessList Modifier = Callable[[BlockAccessList], BlockAccessList] @@ -65,32 +72,236 @@ class CorruptionCase(NamedTuple): expected_exception: BlockException -def enumerate_corruptions( - expectation: BlockAccessListExpectation, +_INVALID_BAL = BlockException.INVALID_BLOCK_ACCESS_LIST +_FLIP_MASK = 0xFF + + +def _flip(value: int) -> int: + """XOR-flip an integer to guarantee a distinct value.""" + return value ^ _FLIP_MASK + + +def _flip_bytes(value: bytes) -> bytes: + r"""XOR-flip the first byte of a bytes value, or return ``\\xff``.""" + if not value: + return b"\xff" + return bytes([value[0] ^ 0xFF]) + value[1:] + + +def _remove_change_at(address: Address, field_name: str, bai: int) -> Modifier: + """Drop one change in ``field_name`` at the given block_access_index.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for ac in bal.root: + if ac.address == address: + new_ac = ac.model_copy(deep=True) + old = getattr(new_ac, field_name) + setattr( + new_ac, + field_name, + [c for c in old if int(c.block_access_index) != bai], + ) + new_root.append(new_ac) + else: + new_root.append(ac) + return BlockAccessList(root=new_root) + + return transform + + +def _remove_slot_change_at(address: Address, slot: int, bai: int) -> Modifier: + """Drop one slot change in ``storage_changes[slot]`` at ``bai``.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for ac in bal.root: + if ac.address == address: + new_ac = ac.model_copy(deep=True) + for storage_slot in new_ac.storage_changes: + if int(storage_slot.slot) == slot: + storage_slot.slot_changes = [ + c + for c in storage_slot.slot_changes + if int(c.block_access_index) != bai + ] + break + new_root.append(new_ac) + else: + new_root.append(ac) + return BlockAccessList(root=new_root) + + return transform + + +def _remove_storage_read_at(address: Address, slot: int) -> Modifier: + """Drop a storage_read entry by slot key.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for ac in bal.root: + if ac.address == address: + new_ac = ac.model_copy(deep=True) + new_ac.storage_reads = [ + s for s in new_ac.storage_reads if int(s) != slot + ] + new_root.append(new_ac) + else: + new_root.append(ac) + return BlockAccessList(root=new_root) + + return transform + + +def _replace_storage_read_slot( + address: Address, old_slot: int, new_slot: int +) -> Modifier: + """Replace a storage_read's slot key with a flipped value.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for ac in bal.root: + if ac.address == address: + new_ac = ac.model_copy(deep=True) + reads = list(new_ac.storage_reads) + for i, s in enumerate(reads): + if int(s) == old_slot: + reads[i] = ZeroPaddedHexNumber(new_slot) + break + new_ac.storage_reads = reads + new_root.append(new_ac) + else: + new_root.append(ac) + return BlockAccessList(root=new_root) + + return transform + + +def _account_cases( + address: Address, account_exp: BalAccountExpectation ) -> List[CorruptionCase]: - r""" - Return every corruption case derivable from ``expectation``. + """Emit Correctness + Completeness cases for one account.""" + cases: List[CorruptionCase] = [] + addr = str(address) + + for nc in account_exp.nonce_changes: + bai = int(nc.block_access_index) + cases.append( + CorruptionCase( + id=f"{addr}__wrong__nonce__{bai}", + modifier=modify_nonce(address, bai, _flip(int(nc.post_nonce))), + expected_exception=_INVALID_BAL, + ) + ) + cases.append( + CorruptionCase( + id=f"{addr}__missing__nonce__{bai}", + modifier=_remove_change_at(address, "nonce_changes", bai), + expected_exception=_INVALID_BAL, + ) + ) - Emits cases along three axes: + for bc in account_exp.balance_changes: + bai = int(bc.block_access_index) + cases.append( + CorruptionCase( + id=f"{addr}__wrong__balance__{bai}", + modifier=modify_balance( + address, bai, _flip(int(bc.post_balance)) + ), + expected_exception=_INVALID_BAL, + ) + ) + cases.append( + CorruptionCase( + id=f"{addr}__missing__balance__{bai}", + modifier=_remove_change_at(address, "balance_changes", bai), + expected_exception=_INVALID_BAL, + ) + ) - - **Correctness** — ``corrupt_`` per existing change. - - **Exactness** — ``omit_*``, ``duplicate_*``, ``phantom_*``. - - **Sequence** — ``swap_accounts`` and ``swap_indices`` where - applicable. + for cc in account_exp.code_changes: + bai = int(cc.block_access_index) + cases.append( + CorruptionCase( + id=f"{addr}__wrong__code__{bai}", + modifier=modify_code( + address, bai, _flip_bytes(bytes(cc.new_code)) + ), + expected_exception=_INVALID_BAL, + ) + ) + cases.append( + CorruptionCase( + id=f"{addr}__missing__code__{bai}", + modifier=_remove_change_at(address, "code_changes", bai), + expected_exception=_INVALID_BAL, + ) + ) - The total count is: + for ss in account_exp.storage_changes: + slot = int(ss.slot) + for sc in ss.slot_changes: + bai = int(sc.block_access_index) + cases.append( + CorruptionCase( + id=f"{addr}__wrong__storage__{slot}__{bai}", + modifier=modify_storage( + address, bai, slot, _flip(int(sc.post_value)) + ), + expected_exception=_INVALID_BAL, + ) + ) + cases.append( + CorruptionCase( + id=f"{addr}__missing__storage__{slot}__{bai}", + modifier=_remove_slot_change_at(address, slot, bai), + expected_exception=_INVALID_BAL, + ) + ) - .. math:: + for sr in account_exp.storage_reads: + slot = int(sr) + cases.append( + CorruptionCase( + id=f"{addr}__wrong__storage_read__{slot}", + modifier=_replace_storage_read_slot( + address, slot, _flip(slot) + ), + expected_exception=_INVALID_BAL, + ) + ) + cases.append( + CorruptionCase( + id=f"{addr}__missing__storage_read__{slot}", + modifier=_remove_storage_read_at(address, slot), + expected_exception=_INVALID_BAL, + ) + ) - N = 2C + R + K + 6A + 1 + \\alpha + \\beta + cases.append( + CorruptionCase( + id=f"{addr}__missing", + modifier=remove_accounts(address), + expected_exception=_INVALID_BAL, + ) + ) + + return cases - where :math:`A` = accounts, :math:`C` = total changes, :math:`R` = - total reads, :math:`K` = populated access-lists, :math:`\\alpha` = - 1 if :math:`A \\geq 2` else 0, :math:`\\beta` = 1 if there are - :math:`\\geq 2` distinct ``block_access_index`` values else 0. - See the module docstring for the ID format. +def enumerate_corruptions( + expectation: BlockAccessListExpectation, +) -> List[CorruptionCase]: + """ + Return every Correctness and Completeness case derivable from + ``expectation``. + + See the module docstring for the ID format and the cases emitted. """ - raise NotImplementedError( - "enumerate_corruptions is not yet implemented; " - ) + cases: List[CorruptionCase] = [] + for address, account_exp in expectation.account_expectations.items(): + if account_exp is None: + continue + cases.extend(_account_cases(address, account_exp)) + return cases diff --git a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py index eb4913c916c..a2263b620a6 100644 --- a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py +++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py @@ -26,25 +26,18 @@ def test_enumerate_corruptions_alice_bob() -> None: A = 2 (alice, bob) C = 3 (alice.nonce, alice.balance, bob.balance) - R = 0 - K = 3 (alice.nonce_changes, alice.balance_changes, - bob.balance_changes) - α = 1 (A ≥ 2) - β = 0 (only block_access_index=1 appears) Total:: - N = 2C + R + K + 6A + 1 + α + β - = 2(3) + 0 + 3 + 6(2) + 1 + 1 + 0 - = 23 + N = 2C + A + = 2(3) + 2 + = 8 - Per-prefix breakdown:: + Per-property breakdown:: - corrupt_ = C = 3 - omit_ = K + A = 5 - duplicate_ = C + R = 3 - phantom_ = 1 + 5A = 11 - swap_ = α + β = 1 + wrong (Correctness) = C = 3 + missing per change (Compl.) = C = 3 + missing per account (Compl.)= A = 2 """ expectation = BlockAccessListExpectation( account_expectations={ @@ -74,35 +67,18 @@ def test_enumerate_corruptions_alice_bob() -> None: bob = str(BOB) expected_ids = { - # corrupt_ (3) - f"{alice}__corrupt_nonce", - f"{alice}__corrupt_balance", - f"{bob}__corrupt_balance", - # omit_ (5) - f"{alice}__omit_nonce", - f"{alice}__omit_balance", - f"{bob}__omit_balance", - f"{alice}__omit_account", - f"{bob}__omit_account", - # duplicate_ (3) - f"{alice}__duplicate_nonce", - f"{alice}__duplicate_balance", - f"{bob}__duplicate_balance", - # phantom_ (11) - "phantom_account", - f"{alice}__phantom_nonce", - f"{alice}__phantom_balance", - f"{alice}__phantom_code", - f"{alice}__phantom_storage_write", - f"{alice}__phantom_storage_read", - f"{bob}__phantom_nonce", - f"{bob}__phantom_balance", - f"{bob}__phantom_code", - f"{bob}__phantom_storage_write", - f"{bob}__phantom_storage_read", - # swap_ (1) - "swap_accounts", + # wrong (3) + f"{alice}__wrong__nonce__1", + f"{alice}__wrong__balance__1", + f"{bob}__wrong__balance__1", + # missing per change (3) + f"{alice}__missing__nonce__1", + f"{alice}__missing__balance__1", + f"{bob}__missing__balance__1", + # missing per account (2) + f"{alice}__missing", + f"{bob}__missing", } - assert len(cases) == 23 + assert len(cases) == 8 assert {c.id for c in cases} == expected_ids From 5516209d502ac61091b28d6021ad6acf75c2239e Mon Sep 17 00:00:00 2001 From: raxhvl Date: Wed, 6 May 2026 16:15:46 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A7=AA=20test:=20Complex=20BAL=20inte?= =?UTF-8?q?raction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_access_list/corruptions.py | 15 +-- .../test_block_access_list_corruptions.py | 115 ++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py index a9de577b2d3..0c6691a6c06 100644 --- a/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py +++ b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py @@ -22,8 +22,9 @@
__missing - account-level missing ``
`` is ``str(address)`` — the canonical 0x-prefixed lowercase -hex form. Using the address directly makes IDs independent of dict -iteration order. ```` is ``wrong`` (Correctness) or ``missing`` +hex form. ```` is the slot key as 0x-prefixed lowercase hex. +Using the address directly makes IDs independent of dict iteration +order. ```` is ``wrong`` (Correctness) or ``missing`` (Completeness). ## Cases @@ -245,7 +246,7 @@ def _account_cases( bai = int(sc.block_access_index) cases.append( CorruptionCase( - id=f"{addr}__wrong__storage__{slot}__{bai}", + id=f"{addr}__wrong__storage__{slot:#x}__{bai}", modifier=modify_storage( address, bai, slot, _flip(int(sc.post_value)) ), @@ -254,17 +255,17 @@ def _account_cases( ) cases.append( CorruptionCase( - id=f"{addr}__missing__storage__{slot}__{bai}", + id=f"{addr}__missing__storage__{slot:#x}__{bai}", modifier=_remove_slot_change_at(address, slot, bai), expected_exception=_INVALID_BAL, ) ) for sr in account_exp.storage_reads: - slot = int(sr) + slot = int.from_bytes(bytes(sr), "big") cases.append( CorruptionCase( - id=f"{addr}__wrong__storage_read__{slot}", + id=f"{addr}__wrong__storage_read__{slot:#x}", modifier=_replace_storage_read_slot( address, slot, _flip(slot) ), @@ -273,7 +274,7 @@ def _account_cases( ) cases.append( CorruptionCase( - id=f"{addr}__missing__storage_read__{slot}", + id=f"{addr}__missing__storage_read__{slot:#x}", modifier=_remove_storage_read_at(address, slot), expected_exception=_INVALID_BAL, ) diff --git a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py index a2263b620a6..aeb8941a9b2 100644 --- a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py +++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py @@ -7,6 +7,11 @@ BalNonceChange, BlockAccessListExpectation, ) +from execution_testing.test_types.block_access_list.account_changes import ( + BalCodeChange, + BalStorageChange, + BalStorageSlot, +) from execution_testing.test_types.block_access_list.corruptions import ( enumerate_corruptions, ) @@ -15,6 +20,7 @@ ALICE = Address(0xA11CE) BOB = Address(0xB0B) +ORACLE = Address(0x04AC1E) def test_enumerate_corruptions_alice_bob() -> None: @@ -82,3 +88,112 @@ def test_enumerate_corruptions_alice_bob() -> None: assert len(cases) == 8 assert {c.id for c in cases} == expected_ids + + +def test_enumerate_corruptions_complex() -> None: + """ + Two-transaction block exercising every change kind. Gas ignored. + + Tx 1: Alice deploys contract Oracle and sends 1 ETH on creation. + alice: nonce@1, balance@1 (5→4 ETH); + oracle: nonce@1 (EIP-161 contract-creation bump), + balance@1 (0→1 ETH), code@1. + Tx 2: Alice calls Oracle and sends 1 ETH. Oracle reads slot 0x42 and + writes slot 0x43@2. + alice: nonce@2, balance@2 (4→3 ETH); + oracle: balance@2 (1→2 ETH), storage@0x43, storage_read@0x42. + + Shape counts:: + + A = 2 (alice, oracle) + C = 10 (alice.nonce x2, alice.balance x2, + oracle.nonce, oracle.balance x2, oracle.code, + oracle.storage_write, oracle.storage_read) + + Total:: + + N = 2C + A + = 2(10) + 2 + = 22 + """ + expectation = BlockAccessListExpectation( + account_expectations={ + ALICE: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + BalNonceChange(block_access_index=2, post_nonce=2), + ], + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=4 * ETH + ), + BalBalanceChange( + block_access_index=2, post_balance=3 * ETH + ), + ], + ), + ORACLE: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + ], + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=1 * ETH + ), + BalBalanceChange( + block_access_index=2, post_balance=2 * ETH + ), + ], + code_changes=[ + BalCodeChange(block_access_index=1, new_code=b"\x60\x60"), + ], + storage_changes=[ + BalStorageSlot( + slot=0x43, + slot_changes=[ + BalStorageChange( + block_access_index=2, post_value=99 + ), + ], + ), + ], + storage_reads=[0x42], + ), + } + ) + + cases = enumerate_corruptions(expectation) + + alice = str(ALICE) + oracle = str(ORACLE) + + expected_ids = { + # wrong (10) + f"{alice}__wrong__nonce__1", + f"{alice}__wrong__nonce__2", + f"{alice}__wrong__balance__1", + f"{alice}__wrong__balance__2", + f"{oracle}__wrong__nonce__1", + f"{oracle}__wrong__balance__1", + f"{oracle}__wrong__balance__2", + f"{oracle}__wrong__code__1", + f"{oracle}__wrong__storage__0x43__2", + f"{oracle}__wrong__storage_read__0x42", + # missing per change (10) + f"{alice}__missing__nonce__1", + f"{alice}__missing__nonce__2", + f"{alice}__missing__balance__1", + f"{alice}__missing__balance__2", + f"{oracle}__missing__nonce__1", + f"{oracle}__missing__balance__1", + f"{oracle}__missing__balance__2", + f"{oracle}__missing__code__1", + f"{oracle}__missing__storage__0x43__2", + f"{oracle}__missing__storage_read__0x42", + # missing per account (2) + f"{alice}__missing", + f"{oracle}__missing", + } + + assert len(cases) == 22 + assert {c.id for c in cases} == expected_ids