From f7d0078900081a3d97785a73aff3349351153edb Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 10 Apr 2026 13:24:04 -0600 Subject: [PATCH 1/8] feat: add more invalid BAL test cases; extend invalid case coverage --- .../test_block_access_lists_invalid.py | 225 ++++++++++++++++++ .../test_cases.md | 5 + 2 files changed, 230 insertions(+) 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 f632d4e78af..92eb1b6314f 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 @@ -21,6 +21,7 @@ BlockAccessListExpectation, BlockchainTestFiller, BlockException, + Hash, Initcode, Op, Storage, @@ -28,6 +29,7 @@ Withdrawal, compute_create_address, ) +from execution_testing.specs.blockchain import Header from execution_testing.test_types.block_access_list.modifiers import ( append_account, append_change, @@ -41,11 +43,15 @@ duplicate_storage_slot, insert_storage_read, modify_balance, + modify_code, modify_nonce, modify_storage, remove_accounts, remove_balances, + remove_code, remove_nonces, + remove_storage, + remove_storage_reads, reverse_accounts, swap_bal_indices, ) @@ -1101,3 +1107,222 @@ def test_bal_invalid_duplicate_entries( ) ], ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_hash_mismatch( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Test that clients reject blocks where the BAL hash in the header + does not match the actual BAL content. + + Unlike other invalid BAL tests which corrupt the BAL content while + keeping the header hash consistent with the corrupted data, this + test keeps the BAL valid but injects a wrong hash into the header + via rlp_modifier. + """ + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + rlp_modifier=Header(bal_hash=Hash(1)), + exception=[ + BlockException.INVALID_BAL_HASH, + BlockException.INVALID_BLOCK_HASH, + ], + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +@pytest.mark.parametrize( + "modifier", + [ + pytest.param( + lambda oracle, **_: remove_storage(oracle), + id="missing_storage_change", + ), + pytest.param( + lambda oracle, **_: remove_storage_reads(oracle), + id="missing_storage_read", + ), + pytest.param( + lambda created, **_: remove_code(created), + id="missing_code_change", + ), + pytest.param( + lambda created, **_: modify_code( + created, block_access_index=1, code=b"\xde\xad" + ), + id="wrong_code_value", + ), + ], +) +def test_bal_invalid_field_entries( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + modifier: Callable, +) -> None: + """ + Test that clients reject blocks with missing or incorrect + field-level BAL entries. + + Oracle writes storage slot 1, reads storage slot 2, and CREATEs a + small contract. A valid BAL is created containing all changes, then + corrupted by the parameterized modifier: + + - missing_storage_change: Oracle's storage writes removed. + - missing_storage_read: Oracle's storage reads removed. + - missing_code_change: Created contract's code change removed. + - wrong_code_value: Created contract's deployed bytecode wrong. + """ + alice = pre.fund_eoa() + deploy_code = b"\x13\x37" + initcode = Initcode(deploy_code=deploy_code) + initcode_word = int.from_bytes(bytes(initcode).ljust(32, b"\x00"), "big") + oracle = pre.deploy_contract( + code=( + Op.SSTORE(1, 0x42) + + Op.SLOAD(2) + + Op.MSTORE(0, initcode_word) + + Op.CREATE(0, 0, len(initcode)) + ), + storage={2: 0x84}, + ) + created = compute_create_address(address=oracle, nonce=1) + + tx = Transaction( + sender=alice, + to=oracle, + value=100, + gas_limit=2_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: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, + post_balance=100, + ), + ], + storage_changes=[ + BalStorageSlot( + slot=1, + slot_changes=[ + BalStorageChange( + block_access_index=1, + post_value=0x42, + ), + ], + ), + ], + storage_reads=[2], + ), + created: BalAccountExpectation( + code_changes=[ + BalCodeChange( + block_access_index=1, + new_code=deploy_code, + ), + ], + ), + } + ).modify( + modifier( + alice=alice, + oracle=oracle, + created=created, + ) + ), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_withdrawal_balance_value( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Test that clients reject blocks where BAL contains an incorrect + balance value for an account modified only by a withdrawal. + + Charlie receives a 10 gwei withdrawal in an empty block. + BAL is corrupted by changing Charlie's post-balance to 999 instead + of the correct 10_000_000_000 (10 gwei in wei). + """ + charlie = pre.fund_eoa(amount=0) + + blockchain_test( + pre=pre, + post={ + charlie: None, + }, + blocks=[ + Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=10, + ) + ], + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, + post_balance=10 * 10**9, + ) + ], + ), + } + ).modify( + modify_balance(charlie, block_access_index=1, balance=999) + ), + ) + ], + ) 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 7221c867d23..040624bf1b0 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -128,6 +128,11 @@ | `test_bal_7002_request_from_contract` | Ensure BAL captures withdrawal request from contract with correct source address | Alice calls `RelayContract` which internally calls EIP-7002 system contract with withdrawal request. Withdrawal request should have `source_address = RelayContract` (not Alice). | BAL **MUST** include Alice with `nonce_changes` at `block_access_index=1`. BAL **MUST** include `RelayContract` with `balance_changes` (fee paid to system contract) at `block_access_index=1`. BAL **MUST** include system contract with `balance_changes`, `storage_reads`, and `storage_changes` (queue modified). Source address in withdrawal request **MUST** be `RelayContract`. Clean sweep: count and tail reset to 0 at `block_access_index=2`. | ✅ Completed | | `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_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_BAL_MISSING_ACCOUNT` 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_BAL_MISSING_ACCOUNT` 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 | +| `test_bal_invalid_field_entries` | Verify clients reject blocks with missing or incorrect field-level BAL entries | Oracle writes storage slot 1, reads storage slot 2, and CREATEs a small contract. A valid BAL is created, then corrupted by parameterized modifier: (1) missing_storage_change: Oracle's storage writes removed, (2) missing_storage_read: Oracle's storage reads removed, (3) missing_code_change: created contract's code change removed, (4) wrong_code_value: created contract's deployed bytecode changed to wrong value. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate all field-level BAL entries: storage writes, storage reads, and code changes. | ✅ Completed | +| `test_bal_invalid_withdrawal_balance_value` | Verify clients reject blocks where BAL has an incorrect balance value for a withdrawal-modified account | Charlie receives 10 gwei withdrawal in an empty block. BAL modifier changes Charlie's post-balance from the correct 10_000_000_000 wei to 999. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate withdrawal balance values match actual withdrawal amounts (including gwei-to-wei conversion). | ✅ Completed | | `test_bal_2935_simple` | Ensure BAL captures EIP-2935 history storage writes during pre-execution system call alongside normal transactions | Block with 2 normal user transactions: Alice sends 10 wei to Charlie, Bob sends 10 wei to Charlie. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes` since parent hash is framework-computed); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. At `block_access_index=1`: Alice with `nonce_changes`, Charlie with `balance_changes` (10 wei). At `block_access_index=2`: Bob with `nonce_changes`, Charlie with `balance_changes` (20 wei total). | ✅ Completed | | `test_bal_2935_empty_block` | Ensure BAL captures EIP-2935 history storage writes in empty block | Block with no transactions. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes`); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. No transaction-related BAL entries. | ✅ Completed | | `test_bal_2935_query` | Ensure BAL captures storage reads when querying EIP-2935 historical block hashes (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 (empty, stores genesis hash via system call). Block 2: Oracle contract queries `HISTORY_STORAGE_ADDRESS` with block number. Two block number scenarios (valid=0 genesis hash, invalid=1042 out of range) and value (0 or 100 wei). Valid query (block_number=0): reads genesis hash slot, oracle writes returned value. If value > 0, history storage contract receives balance. Invalid query (block_number=1042, out of range): reverts before storage access, oracle has implicit SLOAD recorded, value stays in oracle (not transferred to history storage). | Block 2 BAL **MUST** include: Valid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with `storage_reads` [slot 0] and `balance_changes` if value > 0, oracle with `storage_changes` (empty `slot_changes`). Invalid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, oracle with `storage_reads` [0], NO `storage_changes`, and `balance_changes` if value > 0 (value stays in oracle). Alice with `nonce_changes` at `block_access_index=1`. | ✅ Completed | From 6a04383f3a4efb42326c8d4347c51b1615efcd29 Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 10 Apr 2026 14:00:51 -0600 Subject: [PATCH 2/8] chore: align renames with test_cases.md --- .../amsterdam/eip7928_block_level_access_lists/test_cases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 040624bf1b0..0cf5962d31b 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -45,7 +45,7 @@ | `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | ✅ Completed | | `test_bal_7702_invalid_chain_id_authorization` | Ensure BAL handles failed authorization due to wrong chain id | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect chain id, causing authorization failure before account access | BAL **MUST** include Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include Alice (authorization fails before loading account) or `Oracle` (authorization failed, no delegation) | ✅ Completed | | `test_bal_7702_delegated_via_call_opcode` | Ensure BAL captures delegation target when a contract uses *CALL opcodes to call a delegated account | Pre-deployed contract `Alice` delegated to `Oracle`. `Caller` contract uses CALL/CALLCODE/DELEGATECALL/STATICCALL to call `Alice`. Bob sends transaction to `Caller`. | BAL **MUST** include Bob: `nonce_changes`. `Caller`: empty changes (account access). `Alice`: empty changes (account access - delegated account being called). `Oracle`: empty changes (delegation target access). | ✅ Completed | -| `test_bal_7702_null_address_delegation` | Ensure BAL does not record spurious code changes for net-zero code operations | Alice sends transaction with authorization delegating to NULL_ADDRESS (0x0), which sets code to `b""` on an account that already has `b""` code. Transaction sends 10 wei to Bob. | BAL **MUST** include Alice with `nonce_changes` (tx nonce + auth nonce increment) but **MUST NOT** include `code_changes` (setting `b"" -> b""` is net-zero and filtered out). Bob: `balance_changes` (receives 10 wei). This ensures net-zero code change is not recorded. | ✅ Completed | +| `test_bal_7702_null_address_delegation_no_code_change` | Ensure BAL does not record spurious code changes for net-zero code operations | Alice sends transaction with authorization delegating to NULL_ADDRESS (0x0), which sets code to `b""` on an account that already has `b""` code. Transaction sends 10 wei to Bob. | BAL **MUST** include Alice with `nonce_changes` (tx nonce + auth nonce increment) but **MUST NOT** include `code_changes` (setting `b"" -> b""` is net-zero and filtered out). Bob: `balance_changes` (receives 10 wei). This ensures net-zero code change is not recorded. | ✅ Completed | | `test_bal_7702_double_auth_reset` | Ensure BAL tracks multiple 7702 nonce increments but filters net-zero code change | Single transaction contains two EIP-7702 authorizations for `Alice`: (1) first auth sets delegation `0xef0100\|\|Oracle`, (2) second auth clears delegation back to empty. Transaction sends 10 wei to `Bob`. Two variants: (a) Self-funded: `Alice` is tx sender (one tx nonce bump + two auth bumps → nonce 0→3). (b) Sponsored: `Relayer` is tx sender (`Alice` only in auths → nonce 0→2 for `Alice`, plus one nonce bump for `Relayer`). | Variant (a): BAL **MUST** include `Alice` with `nonce_changes` 0→3. Variant (b): BAL **MUST** include `Alice` with `nonce_changes` 0→2 and `Relayer` with its own `nonce_changes`. For both variants, BAL **MUST NOT** include `code_changes` for `Alice` (net code is empty), **MUST** include `Bob` with `balance_changes` (receives 10 wei), and `Oracle` **MUST NOT** appear in BAL. | ✅ Completed | | `test_bal_7702_double_auth_swap` | Ensure BAL captures final code when double auth swaps delegation targets | `Relayer` sends transaction with two authorizations for Alice: (1) First auth sets delegation to `CONTRACT_A` at nonce=0, (2) Second auth changes delegation to `CONTRACT_B` at nonce=1. Transaction sends 10 wei to Bob. Per EIP-7702, only the last authorization takes effect. | BAL **MUST** include Alice with `nonce_changes` (both auths increment nonce to 2) and `code_changes` (final code is delegation designation for `CONTRACT_B`, not `CONTRACT_A`). Bob: `balance_changes` (receives 10 wei). Relayer: `nonce_changes`. Neither `CONTRACT_A` nor `CONTRACT_B` appear in BAL during delegation setup (never accessed). This ensures BAL shows final state, not intermediate changes. | ✅ Completed | | `test_bal_sstore_and_oog` | Ensure BAL handles OOG during SSTORE execution at various gas boundaries (EIP-2200 stipend and implicit SLOAD) | Alice calls contract that attempts `SSTORE` to cold slot `0x01`. Parameterized: (1) OOG at EIP-2200 stipend check (2300 gas after PUSH opcodes) - fails before implicit SLOAD, (2) OOG at stipend + 1 (2301 gas) - passes stipend check but fails after implicit SLOAD, (3) OOG at exact gas - 1, (4) Successful SSTORE with exact gas. | For case (1): BAL **MUST NOT** include slot `0x01` in `storage_reads` or `storage_changes` (fails before implicit SLOAD). For cases (2) and (3): BAL **MUST** include slot `0x01` in `storage_reads` (implicit SLOAD occurred) but **MUST NOT** include in `storage_changes` (write didn't complete). For case (4): BAL **MUST** include slot `0x01` in `storage_changes` only (successful write; read is filtered by builder). | ✅ Completed | @@ -99,7 +99,7 @@ | `test_bal_storage_write_read_same_frame` | Ensure BAL captures write precedence over read in same call frame (writes shadow reads) | Alice calls `Oracle` which writes (`SSTORE`) value `0x42` to slot `0x01`, then reads (`SLOAD`) from slot `0x01` in the same call frame | BAL **MUST** include `Oracle` with slot `0x01` in `storage_changes` showing final value `0x42`. Slot `0x01` **MUST NOT** appear in `storage_reads` (write shadows the subsequent read in same frame). | ✅ Completed | | `test_bal_storage_write_read_cross_frame` | Ensure BAL captures write precedence over read across call frames (writes shadow reads cross-frame) | Alice calls `Oracle`. First call reads slot `0x01` (sees initial value), writes `0x42` to slot `0x01`, then calls itself (via `CALL`, `DELEGATECALL`, or `CALLCODE`). Second call reads slot `0x01` (sees `0x42`) and exits. | BAL **MUST** include `Oracle` with slot `0x01` in `storage_changes` showing final value `0x42`. Slot `0x01` **MUST NOT** appear in `storage_reads` (write shadows both the read before it in same frame and the read in the recursive call). | ✅ Completed | | `test_bal_create_transaction_empty_code` | Ensure BAL does not record spurious code changes for CREATE transaction deploying empty code | Alice sends CREATE transaction with empty initcode (deploys code `b""`). Contract address gets nonce = 1 and code = `b""`. | BAL **MUST** include Alice with `nonce_changes` and created contract with `nonce_changes` but **MUST NOT** include `code_changes` for contract (setting `b"" -> b""` is net-zero). | ✅ Completed | -| `test_bal_cross_block_precompile_state_leak` | Ensure internal EVM state for precompile handling does not leak between blocks | Block 1: Alice calls RIPEMD-160 (0x03) with zero value (RIPEMD-160 must be pre-funded). Block 2: Bob's transaction triggers an exception (stack underflow). | BAL for Block 1 **MUST** include RIPEMD-160. BAL for Block 2 **MUST NOT** include RIPEMD-160 (never accessed in Block 2). Internal state from Parity Touch Bug (EIP-161) handling must be reset between blocks. | ✅ Completed | +| `test_bal_cross_block_ripemd160_state_leak` | Ensure internal EVM state for precompile handling does not leak between blocks | Block 1: Alice calls RIPEMD-160 (0x03) with zero value (RIPEMD-160 must be pre-funded). Block 2: Bob's transaction triggers an exception (stack underflow). | BAL for Block 1 **MUST** include RIPEMD-160. BAL for Block 2 **MUST NOT** include RIPEMD-160 (never accessed in Block 2). Internal state from Parity Touch Bug (EIP-161) handling must be reset between blocks. | ✅ Completed | | `test_bal_all_transaction_types` | Ensure BAL correctly captures state changes from all transaction types in a single block | Single block with 5 transactions: Type 0 (Legacy), Type 1 (EIP-2930 Access List), Type 2 (EIP-1559), Type 3 (EIP-4844 Blob), Type 4 (EIP-7702 Set Code). Each tx writes to contract storage. Note: Access list addresses are pre-warmed but NOT recorded in BAL (no state access). | BAL **MUST** include: (1) All 5 senders with `nonce_changes`. (2) Contracts 0-3 with `storage_changes`. (3) Alice (7702 target) with `nonce_changes`, `code_changes` (delegation), `storage_changes`. (4) Oracle (delegation source) with empty changes. | ✅ Completed | | `test_bal_create2_collision` | Ensure BAL handles CREATE2 address collision correctly | Factory contract (nonce=1, storage slot 0=0xDEAD) executes `CREATE2(salt=0, initcode)` targeting address that already has `code=STOP, nonce=1`. Pre-deploy contract at calculated CREATE2 target address before factory deployment. | BAL **MUST** include: (1) Factory with `nonce_changes` (1→2, incremented even on failed CREATE2), `storage_changes` for slot 0 (0xDEAD→0, stores failure). (2) Collision address with empty changes (accessed during collision check, no state changes). CREATE2 returns 0. Collision address **MUST NOT** have `nonce_changes` or `code_changes`. | ✅ Completed | | `test_bal_create_selfdestruct_to_self_with_call` | Ensure BAL handles init code that calls external contract then selfdestructs to itself | Factory executes `CREATE2` with endowment=100. Init code (embedded in factory via CODECOPY): (1) `CALL(Oracle, 0)` - Oracle writes to its storage slot 0x01. (2) `SSTORE(0x01, 0x42)` - write to own storage. (3) `SELFDESTRUCT(SELF)` - selfdestruct to own address. Contract created and destroyed in same tx. | BAL **MUST** include: (1) Factory with `nonce_changes`, `balance_changes` (loses 100). (2) Oracle with `storage_changes` for slot 0x01 (external call succeeded). (3) Created address with `storage_reads` for slot 0x01 (aborted write becomes read) - **MUST NOT** have `nonce_changes`, `code_changes`, `storage_changes`, or `balance_changes` (ephemeral contract, balance burned via SELFDESTRUCT to self). | ✅ Completed | From 9426acd5cae1bdd73a94cc0796427b8692047461 Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 10 Apr 2026 14:04:49 -0600 Subject: [PATCH 3/8] chore: align test_cases.md with implementations from audit --- .../eip7928_block_level_access_lists/test_cases.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 0cf5962d31b..a3b69b58a90 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -30,6 +30,10 @@ | `test_bal_fully_unmutated_account` | Ensure BAL captures account that has zero net mutations | Alice sends 0 wei to `Oracle` which writes same pre-existing value to storage | BAL MUST include Alice with `nonce_changes` and balance changes (gas), `Oracle` with `storage_reads` for accessed slot but empty `storage_changes`. | ✅ Completed | | `test_bal_net_zero_balance_transfer` | BAL includes accounts with net-zero balance change but excludes them from balance changes | Contract receives and sends same amount to recipient using CALL or SELFDESTRUCT | BAL **MUST** include contract in `account_changes` without `balance_changes` (net zero). BAL **MUST** record non-zero `balance_changes` for recipient. | ✅ Completed | | `test_bal_system_dequeue_consolidations_eip7251` | BAL tracks post-exec system dequeues for consolidations | Pre-populate EIP-7251 consolidation requests; produce a block where dequeues occur | BAL MUST include the 7251 system contract with `storage_changes` (queue slots 0–3) using `block_access_index = len(txs)`. | ✅ Completed | +| `test_bal_withdrawal_contract_cross_index` | Ensure withdrawal system contract shows storage changes at both tx and post-execution indices | Alice sends withdrawal request to EIP-7002 system contract. Slots 0x01 (count) and 0x03 (tail) are incremented during tx (index 1) and reset during post-execution dequeue (index 2). | BAL **MUST** include withdrawal request contract with `storage_changes` for slots 0x01 and 0x03, each with two `slot_changes`: one at `block_access_index=1` (tx) and one at `block_access_index=2` (post-exec). | ✅ Completed | +| `test_bal_consolidation_contract_cross_index` | Ensure consolidation system contract shows storage changes at both tx and post-execution indices | Alice sends consolidation request to EIP-7251 system contract. Slots 0x01 (count) and 0x03 (tail) are incremented during tx (index 1) and reset during post-execution dequeue (index 2). | BAL **MUST** include consolidation request contract with `storage_changes` for slots 0x01 and 0x03, each with two `slot_changes`: one at `block_access_index=1` (tx) and one at `block_access_index=2` (post-exec). | ✅ Completed | +| `test_bal_noop_write_filtering` | Ensure BAL filters NOOP storage writes (writing same value or 0 to empty slot) | Contract writes 0 to uninitialized slot 1 (noop), 42 to slot 2 (real change), 100 to slot 3 (same as pre-state, noop), 200 to slot 4 (different from pre-state 150, real change). | BAL **MUST** include `storage_changes` only for slots 2 and 4 (actual changes). Slots 1 and 3 **MUST NOT** appear in `storage_changes` (no-op writes filtered). | ✅ Completed | +| `test_bal_system_contract_noop_filtering` | Ensure system contract post-execution calls filter net-zero storage writes | Simple transfer that doesn't interact with system contracts. Post-execution system calls read withdrawal/consolidation contract slots 0-3 but don't modify them. | Withdrawal and consolidation system contracts **MUST** have `storage_reads` for slots 0x00-0x03 but **MUST NOT** have `storage_changes` (no actual modifications occurred). | ✅ Completed | | `test_bal_aborted_storage_access` | Ensure BAL captures storage access in aborted transactions correctly | Alice calls contract that reads storage slot `0x01`, writes to slot `0x02`, then aborts with `REVERT`/`INVALID` | BAL MUST include storage_reads for slots `0x01` and `0x02` (aborted writes become reads), empty storage_changes. Only nonce changes for Alice. | ✅ Completed | | `test_bal_aborted_account_access` | Ensure BAL captures account access in aborted transactions for all account accessing opcodes | Alice calls `AbortContract` that performs account access operations (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`) on `TargetContract` and aborts via `REVERT`/`INVALID` | BAL MUST include Alice, `TargetContract`, and `AbortContract` in account_changes and nonce changes for Alice. | ✅ Completed | | `test_bal_pure_contract_call` | Ensure BAL captures contract access for pure computation calls | Alice calls `PureContract` that performs pure arithmetic (ADD operation) without storage or balance changes | BAL MUST include Alice and `PureContract` in `account_changes`, and `nonce_changes` for Alice. | ✅ Completed | @@ -106,6 +110,7 @@ | `test_bal_selfdestruct_to_7702_delegation` | Ensure BAL correctly handles SELFDESTRUCT to a 7702 delegated account (no code execution on recipient) | Tx1: Alice authorizes delegation to Oracle (sets code to `0xef0100\|\|Oracle`). Tx2: Victim contract (balance=100) executes `SELFDESTRUCT(Alice)`. Two separate transactions in same block. Note: Alice starts with initial balance which accumulates with selfdestruct. | BAL **MUST** include: (1) Alice at block_access_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at block_access_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at block_access_index=2 with `balance_changes` (100→0). **Oracle MUST NOT appear in tx2** - per EVM spec, SELFDESTRUCT transfers balance without executing recipient code, so delegation target is never accessed. | ✅ Completed | | `test_bal_call_revert_insufficient_funds` | Ensure BAL handles CALL failure due to insufficient balance (not OOG) | Contract (balance=100, storage slot 0x02=0xDEAD) executes: `SLOAD(0x01), CALL(target, value=1000), SSTORE(0x02, result)`. CALL fails because 1000 > 100. Target address 0xDEAD (pre-existing with non-zero balance to avoid pruning). Note: slot 0x02 must start non-zero so SSTORE(0) is a change. | BAL **MUST** include: (1) Contract with `storage_reads` for slot 0x01, `storage_changes` for slot 0x02 (value=0, CALL returned failure). (2) Target (0xDEAD) **MUST** appear in BAL with empty changes - target is accessed before balance check fails. | ✅ Completed | | `test_bal_lexicographic_address_ordering` | Ensure BAL enforces strict lexicographic byte-wise ordering | Pre-fund three addresses with specific byte patterns: `addr_low = 0x0000...0001`, `addr_mid = 0x0000...0100`, `addr_high = 0x0100...0000`. Contract touches them in reverse order: `BALANCE(addr_high), BALANCE(addr_low), BALANCE(addr_mid)`. Additionally, include two endian-trap addresses that are byte-reversals of each other: `addr_endian_low = 0x0100000000000000000000000000000000000002`, `addr_endian_high = 0x0200000000000000000000000000000000000001`. Note: `reverse(addr_endian_low) = addr_endian_high`. Correct lexicographic order: `addr_endian_low < addr_endian_high` (0x01 < 0x02 at byte 0). If implementation incorrectly reverses bytes before comparing, it would get `addr_endian_low > addr_endian_high` (wrong). | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high` < `addr_endian_low` < `addr_endian_high`, regardless of access order. The endian-trap addresses specifically catch byte-reversal bugs where addresses are compared with wrong byte order. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | ✅ Completed | +| `test_bal_gas_limit_boundary` | Ensure BAL max items gas limit boundary is enforced for empty blocks | Empty block with gas limit set to the exact boundary where `bal_items <= block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM`. Parameterized: (1) at_boundary (gas limit = exact minimum, passes), (2) below_boundary (gas limit = exact minimum - 1, fails). | At boundary: block **MUST** be accepted. Below boundary: block **MUST** be rejected with `BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED` exception. | ✅ Completed | | `test_bal_transient_storage_not_tracked` | Ensure BAL excludes EIP-1153 transient storage operations | Contract executes: `TSTORE(0x01, 0x42)` (transient write), `TLOAD(0x01)` (transient read), `SSTORE(0x02, result)` (persistent write using transient value). | BAL **MUST** include slot 0x02 in `storage_changes` (persistent storage was modified). BAL **MUST NOT** include slot 0x01 in `storage_reads` or `storage_changes` (transient storage is not persisted, not needed for stateless execution). This verifies TSTORE/TLOAD don't pollute BAL. | ✅ Completed | | `test_bal_withdrawal_to_7702_delegation` | Ensure BAL correctly handles withdrawal to a 7702 delegated account (no code execution on recipient) | Tx1: Alice authorizes delegation to Oracle (sets code to `0xef0100\|\|Oracle`). Withdrawal: 10 gwei sent to Alice. Single block with tx + withdrawal. | BAL **MUST** include: (1) Alice at block_access_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at block_access_index=2 with `balance_changes` (receives withdrawal). **Oracle MUST NOT appear** - withdrawals credit balance without executing recipient code, so delegation target is never accessed. This complements `test_bal_selfdestruct_to_7702_delegation` (selfdestruct) and `test_bal_withdrawal_no_evm_execution` (withdrawal to contract). | ✅ Completed | | `test_init_collision_create_tx` | Ensure BAL tracks CREATE collisions correctly (pre-Amsterdam test with BAL) | CREATE transaction targeting address with existing storage aborts | BAL **MUST** show empty expectations for collision address (no changes occur due to abort) | ✅ Completed | @@ -128,6 +133,7 @@ | `test_bal_7002_request_from_contract` | Ensure BAL captures withdrawal request from contract with correct source address | Alice calls `RelayContract` which internally calls EIP-7002 system contract with withdrawal request. Withdrawal request should have `source_address = RelayContract` (not Alice). | BAL **MUST** include Alice with `nonce_changes` at `block_access_index=1`. BAL **MUST** include `RelayContract` with `balance_changes` (fee paid to system contract) at `block_access_index=1`. BAL **MUST** include system contract with `balance_changes`, `storage_reads`, and `storage_changes` (queue modified). Source address in withdrawal request **MUST** be `RelayContract`. Clean sweep: count and tail reset to 0 at `block_access_index=2`. | ✅ Completed | | `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_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_BAL_MISSING_ACCOUNT` 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_BAL_MISSING_ACCOUNT` 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 | From 1d8d8e7dbbd12d7528788ba81b342546ea5eb91a Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 10 Apr 2026 14:26:28 -0600 Subject: [PATCH 4/8] feat(tests): add missing / invalid coinbase BAL tests --- .../test_block_access_lists_invalid.py | 229 +++++++++++++++++- .../test_cases.md | 3 + 2 files changed, 231 insertions(+), 1 deletion(-) 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 92eb1b6314f..edbfffa3d55 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 @@ -21,7 +21,10 @@ BlockAccessListExpectation, BlockchainTestFiller, BlockException, + Environment, + Fork, Hash, + Header, Initcode, Op, Storage, @@ -29,7 +32,6 @@ Withdrawal, compute_create_address, ) -from execution_testing.specs.blockchain import Header from execution_testing.test_types.block_access_list.modifiers import ( append_account, append_change, @@ -1326,3 +1328,228 @@ def test_bal_invalid_withdrawal_balance_value( ) ], ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_missing_coinbase( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Test that clients reject blocks where BAL is missing the + coinbase/fee recipient account. + + Alice sends 100 wei to Bob with gas_price > base_fee so the + coinbase (charlie) receives a non-zero tip. BAL is corrupted + by removing charlie's entry entirely. + """ + alice = pre.fund_eoa(amount=10**18) + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", + contract_creation=False, + access_list=[], + ) + gas_price = 0xA + + tx = Transaction( + sender=alice, + to=bob, + value=100, + gas_limit=intrinsic_gas + 1000, + gas_price=gas_price, + ) + + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + tip = (gas_price - base_fee_per_gas) * intrinsic_gas + + blockchain_test( + pre=pre, + post={}, + genesis_environment=genesis_env, + blocks=[ + Block( + txs=[tx], + fee_recipient=charlie, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + exception=BlockException.INVALID_BAL_MISSING_ACCOUNT, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=1, post_nonce=1 + ) + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=100 + ) + ], + ), + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=tip + ) + ], + ), + } + ).modify(remove_accounts(charlie)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_coinbase_balance_value( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Test that clients reject blocks where BAL contains an incorrect + balance value for the coinbase/fee recipient. + + Same setup as test_bal_invalid_missing_coinbase but the coinbase + entry is present with a wrong balance (999 instead of the + actual tip). + """ + alice = pre.fund_eoa(amount=10**18) + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", + contract_creation=False, + access_list=[], + ) + gas_price = 0xA + + tx = Transaction( + sender=alice, + to=bob, + value=100, + gas_limit=intrinsic_gas + 1000, + gas_price=gas_price, + ) + + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + tip = (gas_price - base_fee_per_gas) * intrinsic_gas + + blockchain_test( + pre=pre, + post={}, + genesis_environment=genesis_env, + blocks=[ + Block( + txs=[tx], + fee_recipient=charlie, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + 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 + ) + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=100 + ) + ], + ), + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=tip + ) + ], + ), + } + ).modify( + modify_balance(charlie, block_access_index=1, balance=999) + ), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +@pytest.mark.parametrize( + "has_withdrawal", + [ + pytest.param(False, id="empty_block"), + pytest.param(True, id="withdrawal_only"), + ], +) +def test_bal_invalid_extraneous_coinbase( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + has_withdrawal: bool, +) -> None: + """ + Test that clients reject blocks where BAL contains a spurious + coinbase entry when the coinbase received no fees. + + Coinbase is only included in BAL when it receives transaction tips. + In blocks with no transactions, the coinbase receives nothing — + even if withdrawals modify other accounts' balances. + + - empty_block: No txs, no withdrawals. Only system contracts. + - withdrawal_only: No txs, one withdrawal to a different address. + Withdrawals don't pay fees, so coinbase is still untouched. + """ + coinbase = pre.fund_eoa(amount=0) + + withdrawals = None + post: dict = {} + if has_withdrawal: + recipient = pre.fund_eoa(amount=0) + withdrawals = [ + Withdrawal( + index=0, + validator_index=0, + address=recipient, + amount=10, + ) + ] + post[recipient] = None + + blockchain_test( + pre=pre, + post=post, + blocks=[ + Block( + txs=[], + fee_recipient=coinbase, + withdrawals=withdrawals, + exception=BlockException.INVALID_BAL_EXTRA_ACCOUNT, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={coinbase: None} + ).modify(append_account(BalAccountChange(address=coinbase))), + ) + ], + ) 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 a3b69b58a90..fee6714d49c 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -139,6 +139,9 @@ | `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 | | `test_bal_invalid_field_entries` | Verify clients reject blocks with missing or incorrect field-level BAL entries | Oracle writes storage slot 1, reads storage slot 2, and CREATEs a small contract. A valid BAL is created, then corrupted by parameterized modifier: (1) missing_storage_change: Oracle's storage writes removed, (2) missing_storage_read: Oracle's storage reads removed, (3) missing_code_change: created contract's code change removed, (4) wrong_code_value: created contract's deployed bytecode changed to wrong value. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate all field-level BAL entries: storage writes, storage reads, and code changes. | ✅ Completed | | `test_bal_invalid_withdrawal_balance_value` | Verify clients reject blocks where BAL has an incorrect balance value for a withdrawal-modified account | Charlie receives 10 gwei withdrawal in an empty block. BAL modifier changes Charlie's post-balance from the correct 10_000_000_000 wei to 999. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate withdrawal balance values match actual withdrawal amounts (including gwei-to-wei conversion). | ✅ Completed | +| `test_bal_invalid_missing_coinbase` | Verify clients reject blocks where BAL is missing the coinbase/fee recipient | Alice sends 100 wei to Bob with gas_price > base_fee so coinbase (charlie) receives a non-zero tip. BAL modifier removes charlie's entry entirely. | Block **MUST** be rejected with `INVALID_BAL_MISSING_ACCOUNT` exception. Clients **MUST** include fee recipients that received tips in the BAL. | ✅ Completed | +| `test_bal_invalid_coinbase_balance_value` | Verify clients reject blocks where BAL has an incorrect balance for the coinbase/fee recipient | Same setup as test_bal_invalid_missing_coinbase. BAL modifier changes charlie's post-balance from the actual tip to 999. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate coinbase balance values match actual fee accounting (priority fee x gas used). | ✅ Completed | +| `test_bal_invalid_extraneous_coinbase` | Verify clients reject blocks with a spurious coinbase entry when coinbase received no fees | Parameterized: (1) empty_block: no txs, no withdrawals — only system contracts in valid BAL, (2) withdrawal_only: no txs, one withdrawal to a different address — withdrawals don't pay fees so coinbase is still untouched. BAL modifier appends spurious coinbase entry with empty changes. | Block **MUST** be rejected with `INVALID_BAL_EXTRA_ACCOUNT` exception. Coinbase **MUST NOT** appear in BAL when it receives no transaction tips, even if the block has other state-modifying activity (withdrawals). | ✅ Completed | | `test_bal_2935_simple` | Ensure BAL captures EIP-2935 history storage writes during pre-execution system call alongside normal transactions | Block with 2 normal user transactions: Alice sends 10 wei to Charlie, Bob sends 10 wei to Charlie. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes` since parent hash is framework-computed); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. At `block_access_index=1`: Alice with `nonce_changes`, Charlie with `balance_changes` (10 wei). At `block_access_index=2`: Bob with `nonce_changes`, Charlie with `balance_changes` (20 wei total). | ✅ Completed | | `test_bal_2935_empty_block` | Ensure BAL captures EIP-2935 history storage writes in empty block | Block with no transactions. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes`); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. No transaction-related BAL entries. | ✅ Completed | | `test_bal_2935_query` | Ensure BAL captures storage reads when querying EIP-2935 historical block hashes (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 (empty, stores genesis hash via system call). Block 2: Oracle contract queries `HISTORY_STORAGE_ADDRESS` with block number. Two block number scenarios (valid=0 genesis hash, invalid=1042 out of range) and value (0 or 100 wei). Valid query (block_number=0): reads genesis hash slot, oracle writes returned value. If value > 0, history storage contract receives balance. Invalid query (block_number=1042, out of range): reverts before storage access, oracle has implicit SLOAD recorded, value stays in oracle (not transferred to history storage). | Block 2 BAL **MUST** include: Valid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with `storage_reads` [slot 0] and `balance_changes` if value > 0, oracle with `storage_changes` (empty `slot_changes`). Invalid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, oracle with `storage_reads` [0], NO `storage_changes`, and `balance_changes` if value > 0 (value stays in oracle). Alice with `nonce_changes` at `block_access_index=1`. | ✅ Completed | From 6fd97fde516300e705e4d8a155989a3ae920d925 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Fri, 10 Apr 2026 23:33:00 +0000 Subject: [PATCH 5/8] chore: consolidate unused BAL exceptions --- .../client_clis/clis/besu.py | 11 +-------- .../client_clis/clis/erigon.py | 13 +++------- .../client_clis/clis/ethrex.py | 18 ++++---------- .../client_clis/clis/geth.py | 24 +++++++------------ .../client_clis/clis/nethermind.py | 9 +------ .../client_clis/clis/reth.py | 8 +------ .../exceptions/exceptions/block.py | 9 ------- .../test_block_access_lists_invalid.py | 12 +++++----- .../test_cases.md | 8 +++---- 9 files changed, 28 insertions(+), 84 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/besu.py b/packages/testing/src/execution_testing/client_clis/clis/besu.py index a48ec82f50e..ce0789cd399 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/besu.py +++ b/packages/testing/src/execution_testing/client_clis/clis/besu.py @@ -457,20 +457,11 @@ class BesuExceptionMapper(ExceptionMapper): r"Blob transaction has too many blobs: \d+|" r"Invalid Blob Count: \d+" ), - # BAL Exceptions: TODO - review once all clients completed. - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"Block access list hash mismatch, " - r"calculated:\s*(0x[a-f0-9]+)\s+header:\s*(0x[a-f0-9]+)|" - r"Block access list validation failed for block 0x[a-f0-9]+" - ), + # BAL Exceptions BlockException.INVALID_BAL_HASH: ( r"Block access list hash mismatch, " r"calculated:\s*(0x[a-f0-9]+)\s+header:\s*(0x[a-f0-9]+)" ), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"Block access list hash mismatch, " - r"calculated:\s*(0x[a-f0-9]+)\s+header:\s*(0x[a-f0-9]+)" - ), BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED: ( r"Block access list validation failed for block 0x[a-f0-9]+" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/erigon.py b/packages/testing/src/execution_testing/client_clis/clis/erigon.py index 699fd4f5744..89c60c6319a 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/erigon.py +++ b/packages/testing/src/execution_testing/client_clis/clis/erigon.py @@ -94,22 +94,15 @@ class ErigonExceptionMapper(ExceptionMapper): BlockException.INVALID_STATE_ROOT: "invalid block: wrong trie root", BlockException.INVALID_RECEIPTS_ROOT: "receiptHash mismatch", BlockException.INVALID_LOG_BLOOM: "invalid bloom", - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - "block access list mismatch" - ), BlockException.INCORRECT_BLOCK_FORMAT: "invalid block access list", - BlockException.INVALID_BAL_EXTRA_ACCOUNT: "invalid block access list", BlockException.GAS_USED_OVERFLOW: "block gas used overflow", } mapping_regex = { - BlockException.INVALID_BLOCK_ACCESS_LIST: ( + BlockException.INVALID_BAL_HASH: ( r"invalid block access list|block access list mismatch" ), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"block access list mismatch" - ), - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"invalid block access list" + BlockException.INVALID_BLOCK_ACCESS_LIST: ( + r"invalid block access list|block access list mismatch" ), BlockException.INCORRECT_BLOCK_FORMAT: (r"invalid block access list"), BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED: ( diff --git a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py index f3ad4741d79..d85a1e7ce86 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py +++ b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py @@ -41,14 +41,6 @@ class EthrexExceptionMapper(ExceptionMapper): "Block access list hash does not match the one in " "the header after executing" ), - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - "Block access list hash does not match the one in " - "the header after executing" - ), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - "Block access list hash does not match the one in " - "the header after executing" - ), BlockException.INCORRECT_BLOCK_FORMAT: ( "not in strictly ascending order for" ), @@ -175,11 +167,7 @@ class EthrexExceptionMapper(ExceptionMapper): BlockException.RLP_BLOCK_LIMIT_EXCEEDED: ( r"Maximum block size exceeded.*" ), - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"Block access list accounts not in strictly ascending order.*|" - r"BAL validation failed: account .* was never accessed.*" - ), - BlockException.INVALID_BAL_MISSING_ACCOUNT: (r"absent from BAL"), + BlockException.INVALID_BAL_HASH: r"BAL validation failed", BlockException.INVALID_BLOCK_ACCESS_LIST: ( r"Block access list contains index \d+ " r"exceeding max valid index \d+|" @@ -187,8 +175,10 @@ class EthrexExceptionMapper(ExceptionMapper): r"Block access list .+ not in strictly ascending order.*|" r"BAL validation failed for (tx \d+|system_tx|withdrawal): .*|" r"BAL validation failed: .*|" + r"absent from BAL|" r"Block access list slot .+ is in both " - r"storage_changes and storage_reads.*" + r"storage_changes and storage_reads.*|" + r"Invalid block hash" ), BlockException.INCORRECT_BLOCK_FORMAT: ( r"Block access list hash does not match " diff --git a/packages/testing/src/execution_testing/client_clis/clis/geth.py b/packages/testing/src/execution_testing/client_clis/clis/geth.py index 7fdedd348d6..25f0e0244e9 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/geth.py +++ b/packages/testing/src/execution_testing/client_clis/clis/geth.py @@ -111,12 +111,6 @@ class GethExceptionMapper(ExceptionMapper): BlockException.RLP_BLOCK_LIMIT_EXCEEDED: ( "block RLP-encoded size exceeds maximum" ), - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - "BAL change not reported in computed" - ), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - "additional mutations compared to BAL" - ), BlockException.INVALID_BLOCK_ACCESS_LIST: "unequal", BlockException.INVALID_BASEFEE_PER_GAS: "invalid baseFee", BlockException.INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT: ( @@ -157,19 +151,17 @@ class GethExceptionMapper(ExceptionMapper): # # EELS definition for `is_valid_deposit_event_data`: # https://github.com/ethereum/execution-specs/blob/5ddb904fa7ba27daeff423e78466744c51e8cb6a/src/ethereum/forks/prague/requests.py#L51 - # BAL Exceptions: TODO - review once all clients completed. - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"invalid block access list:" - ), + # BAL Exceptions BlockException.INVALID_BAL_HASH: (r"invalid block access list:"), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"computed state diff contained mutated accounts " - r"which weren't reported in BAL|" - r"invalid block access list:" - ), BlockException.INVALID_BLOCK_ACCESS_LIST: ( r"difference between computed state diff and " - r"BAL entry for account|invalid block access list:" + r"BAL entry for account|" + r"invalid block access list:|" + r"computed state diff contained mutated accounts " + r"which weren't reported in BAL|" + r"BAL change not reported in computed|" + r"additional mutations compared to BAL|" + r"[bB][aA][lL] validation fail" ), BlockException.INCORRECT_BLOCK_FORMAT: (r"invalid block access list:"), BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED: ( diff --git a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py index 90745b6ac6b..918dd535939 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py +++ b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py @@ -437,15 +437,8 @@ class NethermindExceptionMapper(ExceptionMapper): # BAL Exceptions — specific exceptions have unique patterns, but # INVALID_BLOCK_ACCESS_LIST and INCORRECT_BLOCK_FORMAT intentionally # overlap because the test framework requires `want in got` matching. + # BAL Exceptions BlockException.INVALID_BAL_HASH: (r"InvalidBlockLevelAccessListHash:"), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"InvalidBlockLevelAccessList:.*missing account" - ), - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"InvalidBlockLevelAccessList:.*surplus changes" - r"|could not be parsed as a block: " - r"Error decoding block access list:" - ), BlockException.INVALID_BLOCK_ACCESS_LIST: ( r"InvalidBlockLevelAccessListHash:" r"|InvalidBlockLevelAccessList:" diff --git a/packages/testing/src/execution_testing/client_clis/clis/reth.py b/packages/testing/src/execution_testing/client_clis/clis/reth.py index 6a70ebcc5e9..454d5f9ffcb 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/reth.py +++ b/packages/testing/src/execution_testing/client_clis/clis/reth.py @@ -109,14 +109,8 @@ class RethExceptionMapper(ExceptionMapper): BlockException.GAS_USED_OVERFLOW: ( r"transaction gas limit \w+ is more than blocks available gas \w+" ), - # BAL Exceptions: TODO - review once all clients completed. - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"block access list hash mismatch" - ), + # BAL Exceptions BlockException.INVALID_BAL_HASH: (r"block access list hash mismatch"), - BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"block access list hash mismatch" - ), BlockException.INVALID_BLOCK_ACCESS_LIST: ( r"block access list hash mismatch" ), diff --git a/packages/testing/src/execution_testing/exceptions/exceptions/block.py b/packages/testing/src/execution_testing/exceptions/exceptions/block.py index eceb98b6336..bf8fec8ba7e 100644 --- a/packages/testing/src/execution_testing/exceptions/exceptions/block.py +++ b/packages/testing/src/execution_testing/exceptions/exceptions/block.py @@ -177,15 +177,6 @@ class BlockException(ExceptionBase): """Block's access list is invalid.""" INVALID_BAL_HASH = auto() """Block header's BAL hash does not match the computed BAL hash.""" - INVALID_BAL_EXTRA_ACCOUNT = auto() - """ - Block BAL contains an account change that is not present in the computed - BAL. - """ - INVALID_BAL_MISSING_ACCOUNT = auto() - """ - Block BAL is missing an account change that is present in the computed BAL. - """ BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED = auto() """ Block access list exceeds the gas limit constraint (EIP-7928). 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 edbfffa3d55..93d198e3bad 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 @@ -339,7 +339,7 @@ def test_bal_invalid_account( blocks=[ Block( txs=[tx], - exception=BlockException.INVALID_BAL_EXTRA_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={ sender: BalAccountExpectation( @@ -595,7 +595,7 @@ def test_bal_invalid_missing_account( blocks=[ Block( txs=[tx], - exception=BlockException.INVALID_BAL_MISSING_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={ sender: BalAccountExpectation( @@ -664,7 +664,7 @@ def test_bal_invalid_missing_withdrawal_account( amount=10, ) ], - exception=BlockException.INVALID_BAL_MISSING_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={ alice: BalAccountExpectation( @@ -727,7 +727,7 @@ def test_bal_invalid_missing_withdrawal_account_empty_block( amount=10, ) ], - exception=BlockException.INVALID_BAL_MISSING_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={ charlie: BalAccountExpectation( @@ -1381,7 +1381,7 @@ def test_bal_invalid_missing_coinbase( txs=[tx], fee_recipient=charlie, header_verify=Header(base_fee_per_gas=base_fee_per_gas), - exception=BlockException.INVALID_BAL_MISSING_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={ alice: BalAccountExpectation( @@ -1546,7 +1546,7 @@ def test_bal_invalid_extraneous_coinbase( txs=[], fee_recipient=coinbase, withdrawals=withdrawals, - exception=BlockException.INVALID_BAL_EXTRA_ACCOUNT, + exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={coinbase: None} ).modify(append_account(BalAccountChange(address=coinbase))), 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 fee6714d49c..dfaefa7ccf4 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -134,14 +134,14 @@ | `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_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_BAL_MISSING_ACCOUNT` 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_BAL_MISSING_ACCOUNT` exception. Clients **MUST** detect withdrawal-modified accounts even when no transactions are present. | ✅ 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 | | `test_bal_invalid_field_entries` | Verify clients reject blocks with missing or incorrect field-level BAL entries | Oracle writes storage slot 1, reads storage slot 2, and CREATEs a small contract. A valid BAL is created, then corrupted by parameterized modifier: (1) missing_storage_change: Oracle's storage writes removed, (2) missing_storage_read: Oracle's storage reads removed, (3) missing_code_change: created contract's code change removed, (4) wrong_code_value: created contract's deployed bytecode changed to wrong value. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate all field-level BAL entries: storage writes, storage reads, and code changes. | ✅ Completed | | `test_bal_invalid_withdrawal_balance_value` | Verify clients reject blocks where BAL has an incorrect balance value for a withdrawal-modified account | Charlie receives 10 gwei withdrawal in an empty block. BAL modifier changes Charlie's post-balance from the correct 10_000_000_000 wei to 999. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate withdrawal balance values match actual withdrawal amounts (including gwei-to-wei conversion). | ✅ Completed | -| `test_bal_invalid_missing_coinbase` | Verify clients reject blocks where BAL is missing the coinbase/fee recipient | Alice sends 100 wei to Bob with gas_price > base_fee so coinbase (charlie) receives a non-zero tip. BAL modifier removes charlie's entry entirely. | Block **MUST** be rejected with `INVALID_BAL_MISSING_ACCOUNT` exception. Clients **MUST** include fee recipients that received tips in the BAL. | ✅ Completed | +| `test_bal_invalid_missing_coinbase` | Verify clients reject blocks where BAL is missing the coinbase/fee recipient | Alice sends 100 wei to Bob with gas_price > base_fee so coinbase (charlie) receives a non-zero tip. BAL modifier removes charlie's entry entirely. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** include fee recipients that received tips in the BAL. | ✅ Completed | | `test_bal_invalid_coinbase_balance_value` | Verify clients reject blocks where BAL has an incorrect balance for the coinbase/fee recipient | Same setup as test_bal_invalid_missing_coinbase. BAL modifier changes charlie's post-balance from the actual tip to 999. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate coinbase balance values match actual fee accounting (priority fee x gas used). | ✅ Completed | -| `test_bal_invalid_extraneous_coinbase` | Verify clients reject blocks with a spurious coinbase entry when coinbase received no fees | Parameterized: (1) empty_block: no txs, no withdrawals — only system contracts in valid BAL, (2) withdrawal_only: no txs, one withdrawal to a different address — withdrawals don't pay fees so coinbase is still untouched. BAL modifier appends spurious coinbase entry with empty changes. | Block **MUST** be rejected with `INVALID_BAL_EXTRA_ACCOUNT` exception. Coinbase **MUST NOT** appear in BAL when it receives no transaction tips, even if the block has other state-modifying activity (withdrawals). | ✅ Completed | +| `test_bal_invalid_extraneous_coinbase` | Verify clients reject blocks with a spurious coinbase entry when coinbase received no fees | Parameterized: (1) empty_block: no txs, no withdrawals — only system contracts in valid BAL, (2) withdrawal_only: no txs, one withdrawal to a different address — withdrawals don't pay fees so coinbase is still untouched. BAL modifier appends spurious coinbase entry with empty changes. | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Coinbase **MUST NOT** appear in BAL when it receives no transaction tips, even if the block has other state-modifying activity (withdrawals). | ✅ Completed | | `test_bal_2935_simple` | Ensure BAL captures EIP-2935 history storage writes during pre-execution system call alongside normal transactions | Block with 2 normal user transactions: Alice sends 10 wei to Charlie, Bob sends 10 wei to Charlie. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes` since parent hash is framework-computed); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. At `block_access_index=1`: Alice with `nonce_changes`, Charlie with `balance_changes` (10 wei). At `block_access_index=2`: Bob with `nonce_changes`, Charlie with `balance_changes` (20 wei total). | ✅ Completed | | `test_bal_2935_empty_block` | Ensure BAL captures EIP-2935 history storage writes in empty block | Block with no transactions. At block start (pre-execution), `SYSTEM_ADDRESS` calls `HISTORY_STORAGE_ADDRESS` to store parent block hash. | BAL **MUST** include `HISTORY_STORAGE_ADDRESS` with `storage_changes` (ring buffer slot 0, empty `slot_changes`); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. No transaction-related BAL entries. | ✅ Completed | | `test_bal_2935_query` | Ensure BAL captures storage reads when querying EIP-2935 historical block hashes (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 (empty, stores genesis hash via system call). Block 2: Oracle contract queries `HISTORY_STORAGE_ADDRESS` with block number. Two block number scenarios (valid=0 genesis hash, invalid=1042 out of range) and value (0 or 100 wei). Valid query (block_number=0): reads genesis hash slot, oracle writes returned value. If value > 0, history storage contract receives balance. Invalid query (block_number=1042, out of range): reverts before storage access, oracle has implicit SLOAD recorded, value stays in oracle (not transferred to history storage). | Block 2 BAL **MUST** include: Valid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with `storage_reads` [slot 0] and `balance_changes` if value > 0, oracle with `storage_changes` (empty `slot_changes`). Invalid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, oracle with `storage_reads` [0], NO `storage_changes`, and `balance_changes` if value > 0 (value stays in oracle). Alice with `nonce_changes` at `block_access_index=1`. | ✅ Completed | From ce4ca447b0e51c68b11e37e406da8fc7dcb7bd09 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 14 Apr 2026 11:15:21 -0600 Subject: [PATCH 6/8] fix(tests): changes from comments on PR #2653 --- .../test_block_access_lists_invalid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 93d198e3bad..98d406246ea 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 @@ -55,6 +55,7 @@ remove_storage, remove_storage_reads, reverse_accounts, + sort_accounts_by_address, swap_bal_indices, ) @@ -1268,7 +1269,6 @@ def test_bal_invalid_field_entries( } ).modify( modifier( - alice=alice, oracle=oracle, created=created, ) @@ -1549,7 +1549,10 @@ def test_bal_invalid_extraneous_coinbase( exception=BlockException.INVALID_BLOCK_ACCESS_LIST, expected_block_access_list=BlockAccessListExpectation( account_expectations={coinbase: None} - ).modify(append_account(BalAccountChange(address=coinbase))), + ).modify( + append_account(BalAccountChange(address=coinbase)), + sort_accounts_by_address(), + ), ) ], ) From e3281aeaeb30c84f7ee8afe52a0a08250b89eb5f Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 14 Apr 2026 15:28:43 -0600 Subject: [PATCH 7/8] fix(test,types): Fix bal_hash / block_access_list_hash mismatch for modifier --- .../src/execution_testing/specs/blockchain.py | 15 ++++++++++----- .../execution_testing/test_types/block_types.py | 2 +- .../test_block_access_lists_invalid.py | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index ae467bbfa8c..dc07361ca0f 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -164,7 +164,7 @@ class Header(CamelModel): excess_blob_gas: Removable | HexNumber | None = None parent_beacon_block_root: Removable | Hash | None = None requests_hash: Removable | Hash | None = None - bal_hash: Removable | Hash | None = None + block_access_list_hash: Removable | Hash | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """ @@ -334,7 +334,9 @@ def set_environment(self, env: Environment) -> Environment: not isinstance(self.requests_hash, Removable) and self.block_access_list is not None ): - new_env_values["bal_hash"] = self.block_access_list.keccak256() + new_env_values["block_access_list_hash"] = ( + self.block_access_list.keccak256() + ) new_env_values["block_access_list"] = self.block_access_list if ( not isinstance(self.block_access_list, Removable) @@ -723,11 +725,14 @@ def generate_block_data( "provided by the transition tool" ) - computed_bal_hash = Hash(t8n_bal.rlp.keccak256()) - assert computed_bal_hash == header.block_access_list_hash, ( + computed_block_access_list_hash = Hash(t8n_bal.rlp.keccak256()) + assert ( + computed_block_access_list_hash + == header.block_access_list_hash + ), ( "Block access list hash in header does not match the " f"computed hash from BAL: {header.block_access_list_hash} " - f"!= {computed_bal_hash}" + f"!= {computed_block_access_list_hash}" ) if block.rlp_modifier is not None: diff --git a/packages/testing/src/execution_testing/test_types/block_types.py b/packages/testing/src/execution_testing/test_types/block_types.py index 542df72440f..4d95a4e43f7 100644 --- a/packages/testing/src/execution_testing/test_types/block_types.py +++ b/packages/testing/src/execution_testing/test_types/block_types.py @@ -142,7 +142,7 @@ def strip_computed_fields(cls, data: Any) -> Any: extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True) # EIP-7928: Block-level access lists - bal_hash: Hash | None = Field(None) + block_access_list_hash: Hash | None = Field(None) block_access_lists: Bytes | None = Field(None) @computed_field # type: ignore[prop-decorator] 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 98d406246ea..5b86712e74b 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 @@ -1146,7 +1146,7 @@ def test_bal_invalid_hash_mismatch( blocks=[ Block( txs=[tx], - rlp_modifier=Header(bal_hash=Hash(1)), + rlp_modifier=Header(block_access_list_hash=Hash(1)), exception=[ BlockException.INVALID_BAL_HASH, BlockException.INVALID_BLOCK_HASH, From 3c5f635b13ac3187a772f149aa21f96e3177b303 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 14 Apr 2026 15:36:04 -0600 Subject: [PATCH 8/8] chore(test,types): If Header has a field and FixtureHeader doesn't, validate against this on apply --- .../src/execution_testing/specs/blockchain.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index dc07361ca0f..61ab40360fd 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -216,12 +216,18 @@ def apply(self, target: FixtureHeader) -> FixtureHeader: """ Produce a fixture header copy with the set values from the modifier. """ - return target.copy( - **{ - k: (v if v is not Header.REMOVE_FIELD else None) - for k, v in self.model_dump(exclude_none=True).items() - } - ) + overrides = { + k: (v if v is not Header.REMOVE_FIELD else None) + for k, v in self.model_dump(exclude_none=True).items() + } + unknown = overrides.keys() - target.__class__.model_fields.keys() + if unknown: + raise ValueError( + f"Header fields {unknown} do not exist on " + f"{target.__class__.__name__}. Check for field name " + f"mismatches between Header and {target.__class__.__name__}." + ) + return target.copy(**overrides) def verify(self, target: FixtureHeader) -> None: """Verify that the header fields from self are as expected."""