diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/modifiers.py b/packages/testing/src/execution_testing/test_types/block_access_list/modifiers.py index 00903266251..2777f5c5e27 100644 --- a/packages/testing/src/execution_testing/test_types/block_access_list/modifiers.py +++ b/packages/testing/src/execution_testing/test_types/block_access_list/modifiers.py @@ -481,6 +481,41 @@ def transform(bal: BlockAccessList) -> BlockAccessList: return transform +def append_empty_slot( + address: Address, slot: int +) -> Callable[[BlockAccessList], BlockAccessList]: + """ + Append an empty BalStorageSlot (no changes) to an account's + storage_changes. Used by invalid-BAL tests to simulate a malformed + entry where a slot is recorded as changed but carries no actual change. + """ + + def transform(bal: BlockAccessList) -> BlockAccessList: + from . import BalStorageSlot + + found_address = False + new_root = [] + for account_change in bal.root: + if account_change.address == address: + found_address = True + new_account = account_change.model_copy(deep=True) + new_account.storage_changes.append( + BalStorageSlot(slot=slot, slot_changes=[]) + ) + new_root.append(new_account) + else: + new_root.append(account_change) + + if not found_address: + raise ValueError( + f"Address {address} not found in BAL to append empty slot" + ) + + return BlockAccessList(root=new_root) + + return transform + + def duplicate_account( address: Address, ) -> Callable[[BlockAccessList], BlockAccessList]: @@ -779,6 +814,7 @@ def transform(bal: BlockAccessList) -> BlockAccessList: "append_account", "append_change", "append_storage", + "append_empty_slot", "duplicate_account", "reverse_accounts", "keep_only", diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py index 52d366019d8..c0b16b3a08a 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py @@ -36,6 +36,7 @@ from execution_testing.test_types.block_access_list.modifiers import ( append_account, append_change, + append_empty_slot, append_storage, duplicate_account, duplicate_balance_change, @@ -1053,6 +1054,72 @@ def test_bal_invalid_extraneous_entries( ) +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +@pytest.mark.parametrize( + "pre_storage,oracle_expectation,slot_to_inject", + [ + pytest.param( + {}, + BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=1, post_value=0x42 + ) + ], + ) + ], + ), + 1, + id="unrelated_slot", + ), + pytest.param( + {0: 0x42}, + BalAccountExpectation(storage_reads=[0]), + 0, + id="demoted_noop", + ), + ], +) +def test_bal_invalid_empty_slot_changes( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + pre_storage: dict, + oracle_expectation: BalAccountExpectation, + slot_to_inject: int, +) -> None: + """Reject BAL containing a SlotChanges with an empty slot_changes list.""" + alice = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.SSTORE(0, 0x42), storage=pre_storage) + tx = Transaction(sender=alice, to=oracle, gas_limit=1_000_000) + + blockchain_test( + pre=pre, + post=pre, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=1, post_nonce=1 + ) + ], + ), + oracle: oracle_expectation, + } + ).modify(append_empty_slot(oracle, slot=slot_to_inject)), + ) + ], + ) + + @pytest.mark.valid_from("Amsterdam") @pytest.mark.exception_test @pytest.mark.parametrize( diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md index f814e9840ea..ec9fbed3529 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -157,6 +157,7 @@ | `test_bal_7002_request_invalid` | Ensure BAL correctly handles invalid withdrawal request scenarios | Parameterized test with 8 invalid scenarios: (1) insufficient_fee (fee=0), (2) calldata_too_short (55 bytes), (3) calldata_too_long (57 bytes), (4) oog (insufficient gas), (5-7) invalid_call_type (DELEGATECALL/STATICCALL/CALLCODE), (8) contract_reverts. Tests both EOA and contract-based withdrawal requests. | BAL **MUST** include sender with `nonce_changes` at `block_access_index=1`. BAL **MUST** include system contract with `storage_reads` for slots: excess (slot 0), count (slot 1), head (slot 2), tail (slot 3). System contract **MUST NOT** have `storage_changes` (transaction failed, no queue modification). | ✅ Completed | | `test_bal_invalid_extraneous_entries` | Verify clients reject blocks with any type of extraneous BAL entries | Alice sends 100 wei to Oracle contract (which reads storage slot 0). Charlie is uninvolved in this transaction. A valid BAL is created containing nonce change for Alice, balance change and storage read for Oracle. The BAL is corrupted by adding various extraneous entries: (1) extra_nonce, (2) extra_balance, (3) extra_code, (4) extra_storage_write_touched (slot 0 - already read), (5) extra_storage_write_untouched (slot 1 - not accessed), (6) extra_storage_write_uninvolved_account (Charlie - uninvolved account), (7) extra_account_access (Charlie), (8) extra_storage_read (slot 999). Each tested at block_access_index 1 (same tx), 2 (system tx), 3 (out of bounds). | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** detect any extraneous entries in BAL. | ✅ Completed | | `test_bal_invalid_duplicate_entries` | Verify clients reject blocks where BAL violates uniqueness constraints | Oracle writes storage, reads storage, and CREATEs a contract. BAL is corrupted with duplicate entries: (1) duplicate_nonce_change, (2) duplicate_balance_change, (3) duplicate_code_change, (4) duplicate_storage_slot, (5) duplicate_storage_read, (6) duplicate_slot_change, (7) storage_key_in_both_changes_and_reads. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Each `block_access_index` must appear at most once per change list, each storage key at most once in `storage_changes` and `storage_reads`, and no key in both. | ✅ Completed | +| `test_bal_invalid_empty_slot_changes` | Verify clients reject BAL containing a storage slot entry with no changes | Parametrized: (1) `unrelated_slot`: Oracle writes one slot; BAL is corrupted to include a second, unrelated slot with no changes. (2) `demoted_noop`: Oracle writes a slot back to its existing value (no-op), so the slot is recorded as a read; BAL is corrupted to also record the same slot as a change with no changes. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. A storage slot in `storage_changes` **MUST** have at least one recorded change; a slot accessed without any change belongs in `storage_reads`. | ✅ Completed | | `test_bal_invalid_missing_withdrawal_account` | Verify clients reject blocks where BAL is missing an account modified only by a withdrawal | Alice sends 5 wei to Bob (1 transaction). Charlie receives 10 gwei withdrawal. BAL modifier removes Charlie's entry entirely. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** detect that Charlie's balance was modified by the withdrawal but has no corresponding BAL entry. | ✅ Completed | | `test_bal_invalid_missing_withdrawal_account_empty_block` | Verify clients reject blocks where BAL is missing a withdrawal-modified account in an empty block | Charlie receives 10 gwei withdrawal in block with no transactions. BAL modifier removes Charlie's entry entirely. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** detect withdrawal-modified accounts even when no transactions are present. | ✅ Completed | | `test_bal_invalid_hash_mismatch` | Verify clients reject blocks where the BAL hash in the header does not match the actual BAL content | Alice sends value to Bob. BAL content is valid but header hash is overridden to a wrong value via `rlp_modifier`. Unlike other invalid BAL tests (which corrupt BAL content with matching hash), this keeps the BAL valid but injects a wrong header hash. | Block **MUST** be rejected with `INVALID_BAL_HASH` or `INVALID_BLOCK_HASH` exception. Clients **MUST** re-derive the BAL from block execution and compare its hash to the header, not just verify the BAL content is self-consistent. | ✅ Completed |