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..0c6691a6c06
--- /dev/null
+++ b/packages/testing/src/execution_testing/test_types/block_access_list/corruptions.py
@@ -0,0 +1,308 @@
+"""
+Corruption catalog for EIP-7928 Block Access Lists.
+
+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
+ 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
+
+::
+
+
______ - 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. ```` 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
+
+- ``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 BalAccountExpectation, BlockAccessListExpectation
+from .modifiers import (
+ modify_balance,
+ modify_code,
+ modify_nonce,
+ modify_storage,
+ remove_accounts,
+)
+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
+
+
+_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]:
+ """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,
+ )
+ )
+
+ 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,
+ )
+ )
+
+ 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,
+ )
+ )
+
+ 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:#x}__{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:#x}__{bai}",
+ modifier=_remove_slot_change_at(address, slot, bai),
+ expected_exception=_INVALID_BAL,
+ )
+ )
+
+ for sr in account_exp.storage_reads:
+ slot = int.from_bytes(bytes(sr), "big")
+ cases.append(
+ CorruptionCase(
+ id=f"{addr}__wrong__storage_read__{slot:#x}",
+ modifier=_replace_storage_read_slot(
+ address, slot, _flip(slot)
+ ),
+ expected_exception=_INVALID_BAL,
+ )
+ )
+ cases.append(
+ CorruptionCase(
+ id=f"{addr}__missing__storage_read__{slot:#x}",
+ modifier=_remove_storage_read_at(address, slot),
+ expected_exception=_INVALID_BAL,
+ )
+ )
+
+ cases.append(
+ CorruptionCase(
+ id=f"{addr}__missing",
+ modifier=remove_accounts(address),
+ expected_exception=_INVALID_BAL,
+ )
+ )
+
+ return cases
+
+
+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.
+ """
+ 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
new file mode 100644
index 00000000000..aeb8941a9b2
--- /dev/null
+++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_corruptions.py
@@ -0,0 +1,199 @@
+"""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.account_changes import (
+ BalCodeChange,
+ BalStorageChange,
+ BalStorageSlot,
+)
+from execution_testing.test_types.block_access_list.corruptions import (
+ enumerate_corruptions,
+)
+
+ETH = 10**18
+
+ALICE = Address(0xA11CE)
+BOB = Address(0xB0B)
+ORACLE = Address(0x04AC1E)
+
+
+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)
+
+ Total::
+
+ N = 2C + A
+ = 2(3) + 2
+ = 8
+
+ Per-property breakdown::
+
+ wrong (Correctness) = C = 3
+ missing per change (Compl.) = C = 3
+ missing per account (Compl.)= A = 2
+ """
+ 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 = {
+ # 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) == 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