From 6aaa41d6d3e2a23a7f50798a94cdbfc653f0c453 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 10:09:15 +0100 Subject: [PATCH 01/11] rebase onto upstream --- .../eip7928_block_level_access_lists/test_cases.md | 11 +++++++++-- 1 file changed, 9 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 9ddca8a362..9329a7f0a2 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -90,9 +90,16 @@ | `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. | ✅ 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_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ 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: (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. Note: Uses EXTCODECOPY from template contract for init code (too long for PUSH32). | BAL **MUST** include: (1) Factory with `nonce_changes`, `balance_changes` (loses 100). (2) Oracle with `storage_changes` for slot 0x01 (external call succeeded). (3) Template contract (EXTCODECOPY source). (4) 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, SELFDESTRUCT to self = net zero). | ✅ Completed | +| `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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_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_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) with empty changes. Per EVM, CALL loads target address before balance check, so target **MUST** appear in BAL even though transfer failed. No `balance_changes` for target. | ✅ 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)`. | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high`, regardless of access order. This catches endianness bugs where addresses are compared as big integers instead of byte arrays. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | ✅ 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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. Note: Uses caller contract to trigger selfdestruct (post-Cancun SELFDESTRUCT only transfers balance on CALL). | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ 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 | | `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | ✅ Completed | | `test_selfdestruct_created_in_same_tx_with_revert` | Ensure BAL tracks selfdestruct with revert correctly (pre-Amsterdam test with BAL) | Contract created and selfdestructed in same tx with nested revert | BAL **MUST** track storage reads and balance changes for selfdestruct even with reverts | ✅ Completed | | `test_value_transfer_gas_calculation` | Ensure BAL correctly tracks OOG scenarios for CALL/CALLCODE/DELEGATECALL/STATICCALL (pre-Amsterdam test with BAL) | Nested calls with precise gas limits to test OOG behavior. For CALL with OOG: target account is read. For CALLCODE/DELEGATECALL/STATICCALL with OOG: target account **NOT** read (OOG before state access) | For CALL: target in BAL even with OOG. For CALLCODE/DELEGATECALL/STATICCALL: target **NOT** in BAL when OOG (state access deferred until after gas check) | ✅ 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_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ Completed | From 1946b26d6c845c06fe8a46a664a730e1b7e16057 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 28 Nov 2025 13:24:22 +0100 Subject: [PATCH 02/11] chore(fix) balance check in call before target access --- .../forks/amsterdam/vm/instructions/system.py | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index e5e2ec306f..de667b6a29 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -427,6 +427,19 @@ def call(evm: Evm) -> None: evm, access_gas_cost + transfer_gas_cost + extend_memory.cost, ) + # Early balance check - before accessing target state. + # This ensures target is NOT in BAL when call fails due to insufficient funds. + sender_balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + if sender_balance < value: + # Only charge transfer + memory (not cold access, since we didn't access) + charge_gas(evm, transfer_gas_cost + extend_memory.cost) + evm.memory += b"\x00" * extend_memory.expand_by + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.pc += Uint(1) + return # need to access account to check if account is alive, check gas before create_gas_cost = GAS_NEW_ACCOUNT @@ -477,30 +490,23 @@ def call(evm: Evm) -> None: if evm.message.is_static and value != U256(0): raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by - sender_balance = get_account( - evm.message.block_env.state, evm.message.current_target - ).balance - if sender_balance < value: - push(evm.stack, U256(0)) - evm.return_data = b"" - evm.gas_left += message_call_gas.sub_call - else: - generic_call( - evm, - message_call_gas.sub_call, - value, - evm.message.current_target, - to, - code_address, - True, - False, - memory_input_start_position, - memory_input_size, - memory_output_start_position, - memory_output_size, - code, - disable_precompiles, - ) + + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) # PROGRAM COUNTER evm.pc += Uint(1) From 2d363c48795e9019840862b0ac9d1785a85cdd45 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 10:15:51 +0100 Subject: [PATCH 03/11] merge test cases with usptream --- .../amsterdam/eip7928_block_level_access_lists/test_cases.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 9329a7f0a2..5e6ae802f6 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -58,6 +58,7 @@ | `test_bal_cross_tx_storage_revert_to_zero` | Ensure BAL captures storage changes when tx2 reverts slot back to original value (blobhash regression test) | Alice sends tx1 writing slot 0=0xABCD (from 0x0), then tx2 writing slot 0=0x0 (back to original) | BAL **MUST** include contract with slot 0 having two `slot_changes`: txIndex=1 (0xABCD) and txIndex=2 (0x0). Cross-transaction net-zero **MUST NOT** be filtered. | ✅ Completed | | `test_bal_create_contract_init_revert` | Ensure BAL correctly handles CREATE when parent call reverts | Caller calls factory, factory executes CREATE (succeeds), then factory REVERTs rolling back the CREATE | BAL **MUST** include Alice with `nonce_changes`. Caller and factory with no changes (reverted). Created contract address appears in BAL but **MUST NOT** have `nonce_changes` or `code_changes` (CREATE was rolled back). Contract address **MUST NOT** exist in post-state. | ✅ Completed | | `test_bal_create_oog_code_deposit` | Ensure BAL correctly handles CREATE OOG during code deposit | Alice calls factory contract that executes CREATE with init code returning 10,000 bytes. Transaction has insufficient gas for code deposit. Factory nonce increments, CREATE returns 0 and stores in slot 1. | BAL **MUST** include Alice with `nonce_changes`. Factory with `nonce_changes` (incremented by CREATE) and `storage_changes` (slot 1 = 0). Contract address with empty changes (read during collision check). **MUST NOT** include nonce or code changes for contract address (rolled back on OOG). Contract address **MUST NOT** exist in post-state. | ✅ Completed | +| `test_bal_create_early_failure` | Ensure BAL correctly handles CREATE that fails before accessing contract address | Factory (balance=50) attempts CREATE(value=100). CREATE fails due to insufficient endowment (100 > 50). Factory stores CREATE result (0) in slot 0. | BAL **MUST** include Alice with `nonce_changes`. Factory with `storage_changes` (slot 0 = 0) but **MUST NOT** have `nonce_changes` (CREATE failed before nonce increment). Contract address **MUST NOT** appear in BAL (never accessed - CREATE failed before `track_address`). This is distinct from collision/OOG failures where contract address IS in BAL. | 🟡 Planned | | `test_bal_invalid_missing_nonce` | Verify clients reject blocks with BAL missing required nonce changes | Alice sends transaction to Bob; BAL modifier removes Alice's nonce change entry | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate that all sender accounts have nonce changes recorded. | ✅ Completed | | `test_bal_invalid_nonce_value` | Verify clients reject blocks with incorrect nonce values in BAL | Alice sends transaction to Bob; BAL modifier changes Alice's nonce to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate nonce values match actual state transitions. | ✅ Completed | | `test_bal_invalid_storage_value` | Verify clients reject blocks with incorrect storage values in BAL | Alice calls contract that writes to storage; BAL modifier changes storage value to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate storage change values match actual state transitions. | ✅ Completed | @@ -91,7 +92,7 @@ | `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. | ✅ 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_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ 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. | 🟡 Planned | | `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: (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. Note: Uses EXTCODECOPY from template contract for init code (too long for PUSH32). | BAL **MUST** include: (1) Factory with `nonce_changes`, `balance_changes` (loses 100). (2) Oracle with `storage_changes` for slot 0x01 (external call succeeded). (3) Template contract (EXTCODECOPY source). (4) 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, SELFDESTRUCT to self = net zero). | ✅ Completed | | `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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_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 | @@ -99,6 +100,8 @@ | `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)`. | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high`, regardless of access order. This catches endianness bugs where addresses are compared as big integers instead of byte arrays. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | ✅ 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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. Note: Uses caller contract to trigger selfdestruct (post-Cancun SELFDESTRUCT only transfers balance on CALL). | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed | +| `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ 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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_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). | 🟡 Planned | | `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 | | `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | ✅ Completed | | `test_selfdestruct_created_in_same_tx_with_revert` | Ensure BAL tracks selfdestruct with revert correctly (pre-Amsterdam test with BAL) | Contract created and selfdestructed in same tx with nested revert | BAL **MUST** track storage reads and balance changes for selfdestruct even with reverts | ✅ Completed | From 7003e68d1e38d3a9f504b8eabf925da26563f265 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 10:24:08 +0100 Subject: [PATCH 04/11] merge test cases with usptream --- .../test_block_access_lists.py | 922 ++++++++++++++++++ .../test_block_access_lists_eip7702.py | 244 +++++ 2 files changed, 1166 insertions(+) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 50fb9e0ad2..05c56e84af 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -8,6 +8,7 @@ Account, Address, Alloc, + AuthorizationTuple, BalAccountExpectation, BalBalanceChange, BalCodeChange, @@ -23,6 +24,7 @@ Header, Op, Transaction, + add_kzg_version, compute_create_address, ) @@ -2145,3 +2147,923 @@ def test_bal_cross_block_ripemd160_state_leak( ripemd160_addr: Account(balance=1), }, ) + + +def test_bal_all_transaction_types( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with all 5 tx types in single block: Legacy, EIP-2930, EIP-1559, Blob, EIP-7702. + + Each tx writes to contract storage. Access list addresses are pre-warmed but NOT in BAL. + + Expected BAL: + - All 5 senders: nonce_changes + - Contracts 0-3: storage_changes + - Alice (7702): nonce_changes, code_changes (delegation), storage_changes + - Oracle: empty (delegation target, accessed) + """ + from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + + # Create senders for each transaction type + sender_0 = pre.fund_eoa() # Type 0 - Legacy + sender_1 = pre.fund_eoa() # Type 1 - Access List + sender_2 = pre.fund_eoa() # Type 2 - EIP-1559 + sender_3 = pre.fund_eoa() # Type 3 - Blob + sender_4 = pre.fund_eoa() # Type 4 - EIP-7702 + + # Create contracts for each tx type (except 7702 which uses delegation) + contract_code = Op.SSTORE(0x01, Op.CALLDATALOAD(0)) + Op.STOP + contract_0 = pre.deploy_contract(code=contract_code) + contract_1 = pre.deploy_contract(code=contract_code) + contract_2 = pre.deploy_contract(code=contract_code) + contract_3 = pre.deploy_contract(code=contract_code) + + # For Type 4 (EIP-7702): Alice delegates to Oracle + alice = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x05) + Op.STOP) + + # Dummy address to warm in access list + warmed_address = Address("0x0000000000000000000000000000000000001234") + pre[warmed_address] = Account(balance=1) + + # TX1: Type 0 - Legacy transaction + tx_type_0 = Transaction( + ty=0, + sender=sender_0, + to=contract_0, + gas_limit=100_000, + gas_price=10, + data=Hash(0x01), # Value to store + ) + + # TX2: Type 1 - Access List transaction (EIP-2930) + tx_type_1 = Transaction( + ty=1, + sender=sender_1, + to=contract_1, + gas_limit=100_000, + gas_price=10, + data=Hash(0x02), + access_list=[ + AccessList( + address=warmed_address, + storage_keys=[], + ) + ], + ) + + # TX3: Type 2 - EIP-1559 Dynamic fee transaction + tx_type_2 = Transaction( + ty=2, + sender=sender_2, + to=contract_2, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + data=Hash(0x03), + ) + + # TX4: Type 3 - Blob transaction (EIP-4844) + # Blob versioned hashes need KZG version prefix (0x01) + blob_hashes = add_kzg_version([Hash(0xBEEF)], 1) + tx_type_3 = Transaction( + ty=3, + sender=sender_3, + to=contract_3, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + max_fee_per_blob_gas=10, + blob_versioned_hashes=blob_hashes, + data=Hash(0x04), + ) + + # TX5: Type 4 - EIP-7702 Set Code transaction + tx_type_4 = Transaction( + ty=4, + sender=sender_4, + to=alice, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx_type_0, tx_type_1, tx_type_2, tx_type_3, tx_type_4], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Type 0 sender + sender_0: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Type 1 sender + sender_1: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=2, post_nonce=1)], + ), + # Type 2 sender + sender_2: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=3, post_nonce=1)], + ), + # Type 3 sender + sender_3: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=4, post_nonce=1)], + ), + # Type 4 sender + sender_4: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=5, post_nonce=1)], + ), + # Contract touched by Type 0 + contract_0: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x01)], + ) + ], + ), + # Contract touched by Type 1 + contract_1: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=2, post_value=0x02)], + ) + ], + ), + # Note: warmed_address from access_list is NOT in BAL because + # access lists pre-warm but don't record in BAL (no state access) + # Contract touched by Type 2 + contract_2: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=3, post_value=0x03)], + ) + ], + ), + # Contract touched by Type 3 + contract_3: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=4, post_value=0x04)], + ) + ], + ), + # Alice (Type 4 delegation target, executes oracle code) + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=5, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=5, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=5, post_value=0x05)], + ) + ], + ), + # Oracle (accessed via delegation) + oracle: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + sender_0: Account(nonce=1), + sender_1: Account(nonce=1), + sender_2: Account(nonce=1), + sender_3: Account(nonce=1), + sender_4: Account(nonce=1), + contract_0: Account(storage={0x01: 0x01}), + contract_1: Account(storage={0x01: 0x02}), + contract_2: Account(storage={0x01: 0x03}), + contract_3: Account(storage={0x01: 0x04}), + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + storage={0x01: 0x05}, + ), + }, + ) + + +def test_bal_create2_collision( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE2 collision against pre-existing contract (code=STOP, nonce=1). + + Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting occupied address. + + Expected BAL: + - Factory: nonce_changes (1→2), storage_changes slot 0 (0xDEAD→0) + - Collision address: empty (accessed during collision check, no state changes) + - Collision address MUST NOT have nonce_changes or code_changes + """ + alice = pre.fund_eoa() + + # Init code that deploys simple STOP contract + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE2(value=0, salt=0, initcode) and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE2(...)) - stores CREATE2 result in slot 0 + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=0, + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + salt=0, + ), + ) + + Op.STOP + ) + + # Deploy factory - it starts with nonce=1 by default + factory = pre.deploy_contract( + code=factory_code, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate the CREATE2 target address + collision_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + # Set up the collision by pre-populating the target address + # This contract has code (STOP) and nonce=1, causing collision + pre[collision_address] = Account( + code=Op.STOP, + nonce=1, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # Nonce incremented 1→2 even on failed CREATE2 + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Storage changes: slot 0 = 0xDEAD → 0 (CREATE2 returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Collision address: empty changes (accessed but no state changes) + # Explicitly verify ALL fields are empty to avoid false positives + collision_address: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[], # MUST NOT have balance changes + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, storage={0x00: 0}), + # Collision address unchanged - contract still exists + collision_address: Account(code=bytes(Op.STOP), nonce=1), + }, + ) + + +def test_bal_create_selfdestruct_to_self_with_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with init code that CALLs Oracle, writes storage, then SELFDESTRUCTs to self. + + Factory CREATE2(endowment=100). Init: CALL(Oracle)→SSTORE(0x01)→SELFDESTRUCT(SELF). + + Expected BAL: + - Factory: nonce_changes, balance_changes (loses 100) + - Oracle: storage_changes slot 0x01 + - Created address: storage_reads [0x01] (aborted write→read), + MUST NOT have nonce/code/storage/balance changes (ephemeral) + """ + alice = pre.fund_eoa() + factory_balance = 1000 + + # Oracle contract that writes to slot 0x01 when called + oracle_code = Op.SSTORE(0x01, 0x42) + Op.STOP + oracle = pre.deploy_contract(code=oracle_code) + + endowment = 100 + + # Init code that: + # 1. Calls Oracle (which writes to its slot 0x01) + # 2. Writes 0x42 to own slot 0x01 + # 3. Selfdestructs to self + initcode_runtime = ( + # CALL(gas, Oracle, value=0, ...) + Op.CALL(100_000, oracle, 0, 0, 0, 0, 0) + + Op.POP + # Write to own storage slot 0x01 + + Op.SSTORE(0x01, 0x42) + # SELFDESTRUCT to self (ADDRESS returns own address) + + Op.SELFDESTRUCT(Op.ADDRESS) + ) + init_code = Initcode(deploy_code=Op.STOP, initcode_prefix=initcode_runtime) + init_code_bytes = bytes(init_code) + init_code_size = len(init_code_bytes) + + # Factory code with embedded initcode (no template contract needed) + # Structure: [execution code] [initcode bytes] + # CODECOPY copies initcode from factory's own code to memory + # + # Two-pass approach: build with placeholder, measure, rebuild with actual size + placeholder_offset = 0xFF # Placeholder (same byte size as final value) + factory_execution_template = ( + Op.CODECOPY(0, placeholder_offset, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Measure execution code size + execution_code_size = len(bytes(factory_execution_template)) + + # Rebuild with actual offset value + factory_execution = ( + Op.CODECOPY(0, execution_code_size, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Combine execution code with embedded initcode + factory_code = bytes(factory_execution) + init_code_bytes + + factory = pre.deploy_contract(code=factory_code, balance=factory_balance) + + # Calculate the CREATE2 target address + created_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Balance changes: loses endowment (100) + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=factory_balance - endowment + ) + ], + ), + # Oracle: storage changes for slot 0x01 + oracle: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + ), + # Created address: ephemeral (created and destroyed same tx) + # - storage_reads for slot 0x01 (aborted write becomes read) + # - NO nonce_changes, code_changes, storage_changes, balance_changes + created_address: BalAccountExpectation( + storage_reads=[0x01], + storage_changes=[], + nonce_changes=[], + code_changes=[], + balance_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, balance=factory_balance - endowment), + oracle: Account(storage={0x01: 0x42}), + # Created address doesn't exist - destroyed in same tx + created_address: Account.NONEXISTENT, + }, + ) + + +def test_bal_revert_insufficient_funds( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CALL failure due to insufficient balance (not OOG). + + Contract (balance=100): SLOAD(0x01)→CALL(target, value=1000)→SSTORE(0x02, result). + CALL fails because 1000 > 100. Target is 0xDEAD (pre-existing). + + Expected BAL: + - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) + - Target: MUST NOT appear (balance check fails before target access) + """ + alice = pre.fund_eoa() + + contract_balance = 100 + transfer_amount = 1000 # More than contract has + + # Target address that should be warmed but not receive funds + # Give it a small balance so it's not considered "empty" and pruned + target_balance = 1 + target_address = Address("0x000000000000000000000000000000000000DEAD") + pre[target_address] = Account(balance=target_balance) + + # Contract that: + # 1. SLOAD slot 0x01 + # 2. CALL target with value=1000 (will fail - insufficient funds) + # 3. SSTORE slot 0x02 with CALL result (0 = failure) + contract_code = ( + Op.SLOAD(0x01) # Read from slot 0x01, push to stack + + Op.POP # Discard value + # CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize) + + Op.CALL(100_000, target_address, transfer_amount, 0, 0, 0, 0) + # CALL result is on stack (0 = failure, 1 = success) + # Stack: [result] + + Op.PUSH1(0x02) # Push slot number + # Stack: [0x02, result] + + Op.SSTORE # SSTORE pops slot (0x02), then value (result) + + Op.STOP + ) + + contract = pre.deploy_contract( + code=contract_code, + balance=contract_balance, + storage={0x02: 0xDEAD}, # Non-zero initial value so SSTORE(0) is a change + ) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Storage read for slot 0x01 + storage_reads=[0x01], + # Storage change for slot 0x02 (CALL result = 0) + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Target MUST NOT appear in BAL - balance check fails before + # target state is accessed, so no witness is needed for target + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account( + balance=contract_balance, # Unchanged - transfer failed + storage={0x02: 0}, # CALL returned 0 (failure) + ), + target_address: Account(balance=target_balance), # Unchanged + }, + ) + + +def test_bal_lexicographic_address_ordering( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL enforces strict lexicographic byte-wise address ordering. + + Addresses: addr_low (0x...01), addr_mid (0x...0100), addr_high (0x01...00). + Endian-trap: addr_endian_low (0x01...02), addr_endian_high (0x02...01). + Contract touches them in reverse order to verify sorting. + + Expected BAL order: addr_low < addr_mid < addr_high < addr_endian_low < addr_endian_high + Catches endianness bugs in address comparison. + """ + alice = pre.fund_eoa() + + # Create addresses with specific byte patterns for lexicographic testing + # In lexicographic (byte-wise) order: low < mid < high + # addr_low: 0x0000000000000000000000000000000000000001 (rightmost byte = 0x01) + # addr_mid: 0x0000000000000000000000000000000000000100 (second-rightmost byte = 0x01) + # addr_high: 0x0100000000000000000000000000000000000000 (leftmost byte = 0x01) + addr_low = Address("0x0000000000000000000000000000000000000001") + addr_mid = Address("0x0000000000000000000000000000000000000100") + addr_high = Address("0x0100000000000000000000000000000000000000") + + # Endian-trap addresses: byte-reversals of each other to catch byte-order bugs + # addr_endian_low: 0x01...02 (0x01 at byte 0, 0x02 at byte 19) + # addr_endian_high: 0x02...01 (0x02 at byte 0, 0x01 at byte 19) + # Note: reverse(addr_endian_low) = addr_endian_high + # Correct order: addr_endian_low < addr_endian_high (0x01 < 0x02 at byte 0) + # If bytes are reversed before comparing: would incorrectly get opposite order + addr_endian_low = Address("0x0100000000000000000000000000000000000002") + addr_endian_high = Address("0x0200000000000000000000000000000000000001") + + # Give each address a balance so they exist + addr_balance = 100 + pre[addr_low] = Account(balance=addr_balance) + pre[addr_mid] = Account(balance=addr_balance) + pre[addr_high] = Account(balance=addr_balance) + pre[addr_endian_low] = Account(balance=addr_balance) + pre[addr_endian_high] = Account(balance=addr_balance) + + # Contract that accesses addresses in REVERSE lexicographic order + # to verify sorting is applied correctly + contract_code = ( + Op.BALANCE(addr_high) # Access high first + + Op.POP + + Op.BALANCE(addr_low) # Access low second + + Op.POP + + Op.BALANCE(addr_mid) # Access mid third + + Op.POP + # Access endian-trap addresses in reverse order + + Op.BALANCE(addr_endian_high) # Access endian_high before endian_low + + Op.POP + + Op.BALANCE(addr_endian_low) + + Op.POP + + Op.STOP + ) + + contract = pre.deploy_contract(code=contract_code) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + # BAL must be sorted lexicographically by address bytes + # The order should be: addr_low < addr_mid < addr_high < addr_endian_low < addr_endian_high + # (sorted by raw address bytes, regardless of access order) + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation.empty(), + # These addresses appear in BAL due to BALANCE access + # The expectation framework will verify they're in correct order + addr_low: BalAccountExpectation.empty(), + addr_mid: BalAccountExpectation.empty(), + addr_high: BalAccountExpectation.empty(), + # Endian-trap addresses: must be sorted correctly despite being + # byte-reversals of each other + addr_endian_low: BalAccountExpectation.empty(), + addr_endian_high: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account(), + addr_low: Account(balance=addr_balance), + addr_mid: Account(balance=addr_balance), + addr_high: Account(balance=addr_balance), + addr_endian_low: Account(balance=addr_balance), + addr_endian_high: Account(balance=addr_balance), + }, + ) + + +def test_bal_transient_storage_not_tracked( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL excludes EIP-1153 transient storage (TSTORE/TLOAD). + + Contract: TSTORE(0x01, 0x42)→TLOAD(0x01)→SSTORE(0x02, result). + + Expected BAL: + - storage_changes: slot 0x02 (persistent) + - MUST NOT include slot 0x01 (transient storage not persisted) + """ + alice = pre.fund_eoa() + + # Contract that uses transient storage then persists to regular storage + contract_code = ( + # TSTORE slot 0x01 with value 0x42 (transient storage) + Op.TSTORE(0x01, 0x42) + # TLOAD slot 0x01 (transient storage read) + + Op.TLOAD(0x01) + # Result (0x42) is on stack, store it in persistent slot 0x02 + + Op.PUSH1(0x02) + + Op.SSTORE # SSTORE pops slot (0x02), then value (0x42) + + Op.STOP + ) + + contract = pre.deploy_contract(code=contract_code) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Persistent storage change for slot 0x02 + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + # MUST NOT include slot 0x01 in storage_reads + # Transient storage operations don't pollute BAL + storage_reads=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account(storage={0x02: 0x42}), + }, + ) + + +def test_bal_selfdestruct_to_precompile( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01) as beneficiary. + + Victim (balance=100) selfdestructs to precompile 0x01. + + Expected BAL: + - Victim: balance_changes (100→0) + - Precompile 0x01: balance_changes (0→100), MUST NOT have code/nonce changes + """ + alice = pre.fund_eoa() + + contract_balance = 100 + ecrecover_precompile = Address(1) # 0x0000...0001 + + # Contract that selfdestructs to ecrecover precompile + victim_code = Op.SELFDESTRUCT(ecrecover_precompile) + + victim = pre.deploy_contract(code=victim_code, balance=contract_balance) + + # Caller that triggers the selfdestruct + caller_code = Op.CALL(100_000, victim, 0, 0, 0, 0, 0) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx = Transaction( + sender=alice, + to=caller, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + caller: BalAccountExpectation.empty(), + # Victim (selfdestructing contract): balance changes 100→0 + # Explicitly verify ALL fields to avoid false positives + victim: BalAccountExpectation( + nonce_changes=[], # Contract nonce unchanged + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], + code_changes=[], # Code unchanged (post-Cancun SELFDESTRUCT) + storage_changes=[], # No storage changes + storage_reads=[], # No storage reads + ), + # Precompile receives selfdestruct balance + # Explicitly verify ALL fields to avoid false positives + ecrecover_precompile: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=contract_balance) + ], + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + caller: Account(), + # Victim still exists with 0 balance (post-Cancun SELFDESTRUCT) + victim: Account(balance=0), + # Precompile has received the balance + ecrecover_precompile: Account(balance=contract_balance), + }, + ) + + +def test_bal_create_early_failure( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE failure due to insufficient endowment (before track_address). + + Factory (balance=50) attempts CREATE(value=100). Fails before nonce increment. + Distinct from collision where address IS accessed and nonce IS incremented. + + Expected BAL: + - Alice: nonce_changes + - Factory: storage_changes slot 0 (0xDEAD→0), NO nonce_changes + - Contract address: MUST NOT appear (never accessed) + """ + alice = pre.fund_eoa() + + factory_balance = 50 + endowment = 100 # More than factory has + + # Simple init code that deploys STOP + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE(value=endowment) and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE(value, offset, size)) + + Op.SSTORE( + 0x00, + Op.CREATE( + value=endowment, # 100 > 50, will fail + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + ), + ) + + Op.STOP + ) + + # Deploy factory with insufficient balance for the CREATE endowment + factory = pre.deploy_contract( + code=factory_code, + balance=factory_balance, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate what the contract address WOULD be (but it won't be created) + would_be_contract_address = compute_create_address(address=factory, nonce=1) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # NO nonce_changes - CREATE failed before increment_nonce + nonce_changes=[], + # Storage changes: slot 0 = 0xDEAD → 0 (CREATE returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Contract address MUST NOT appear in BAL - never accessed + # (CREATE failed before track_address was called) + would_be_contract_address: None, + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + # Factory nonce unchanged (still 1), balance unchanged + factory: Account(nonce=1, balance=factory_balance, storage={0x00: 0}), + # Contract was never created + would_be_contract_address: Account.NONEXISTENT, + }, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index e9bed5df85..7a39b59259 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -16,6 +16,7 @@ BlockchainTestFiller, Op, Transaction, + Withdrawal, ) from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 @@ -828,3 +829,246 @@ def test_bal_7702_double_auth_swap( relayer: Account(nonce=1), }, ) + + +def test_bal_selfdestruct_to_7702_delegation( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with SELFDESTRUCT to 7702 delegated account. + + Tx1: Alice delegates to Oracle. Tx2: Victim (balance=100) selfdestructs to Alice. + SELFDESTRUCT transfers balance without executing recipient code. + + Expected BAL: + - Alice tx1: code_changes (delegation), nonce_changes + - Alice tx2: balance_changes (+100) + - Victim tx2: balance_changes (100→0) + - Oracle: MUST NOT appear (SELFDESTRUCT doesn't execute recipient code) + """ + # Alice (EOA) will receive delegation then receive selfdestruct balance + # Use explicit initial balance for clarity + alice_initial_balance = 10**18 # 1 ETH default + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) # Just to be the recipient of tx + + # Oracle contract that Alice will delegate to + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + + victim_balance = 100 + + # Victim contract that selfdestructs to Alice + victim = pre.deploy_contract( + code=Op.SELFDESTRUCT(alice), + balance=victim_balance, + ) + + # Relayer for tx1 (delegation) + relayer = pre.fund_eoa() + + # Tx1: Alice authorizes delegation to Oracle + tx1 = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + # Caller contract that triggers selfdestruct on victim + caller = pre.deploy_contract(code=Op.CALL(100_000, victim, 0, 0, 0, 0, 0)) + + # Tx2: Trigger selfdestruct on victim (victim sends balance to Alice) + tx2 = Transaction( + nonce=1, + sender=relayer, + to=caller, + gas_limit=1_000_000, + gas_price=0xA, + ) + + alice_final_balance = alice_initial_balance + victim_balance + + account_expectations = { + alice: BalAccountExpectation( + # tx1: nonce change for auth, code change for delegation + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=1, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + # tx2: balance change from selfdestruct + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=alice_final_balance) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ), + caller: BalAccountExpectation.empty(), + # Victim (selfdestructing contract): balance changes to 0 + # Explicitly verify ALL fields to avoid false positives + victim: BalAccountExpectation( + nonce_changes=[], # Contract nonce unchanged + balance_changes=[BalBalanceChange(tx_index=2, post_balance=0)], + code_changes=[], # Code unchanged (post-Cancun SELFDESTRUCT) + storage_changes=[], # No storage changes + storage_reads=[], # No storage reads + ), + # Oracle MUST NOT appear in tx2 - SELFDESTRUCT doesn't execute + # recipient code, so delegation target is never accessed + oracle: None, + } + + block = Block( + txs=[tx1, tx2], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + balance=alice_final_balance, + ), + bob: Account(balance=10), + relayer: Account(nonce=2), + # Victim still exists but with 0 balance (post-Cancun SELFDESTRUCT) + victim: Account(balance=0), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +GWEI = 10**9 + + +def test_bal_withdrawal_to_7702_delegation( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with withdrawal to 7702 delegated account. + + Tx1: Alice delegates to Oracle. Withdrawal: 10 gwei to Alice. + Withdrawals credit balance without executing code. + + Expected BAL: + - Alice tx1: code_changes (delegation), nonce_changes + - Alice tx2: balance_changes (+10 gwei) + - Oracle: MUST NOT appear (withdrawals don't execute recipient code) + """ + # Alice (EOA) will receive delegation then receive withdrawal + alice_initial_balance = 10**18 # 1 ETH default + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) # Recipient of tx value + + # Oracle contract that Alice will delegate to + # If delegation were followed, this would write to storage + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + + # Relayer for the delegation tx + relayer = pre.fund_eoa() + + withdrawal_amount_gwei = 10 + + # Tx1: Alice authorizes delegation to Oracle + tx1 = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + alice_final_balance = alice_initial_balance + (withdrawal_amount_gwei * GWEI) + + account_expectations = { + alice: BalAccountExpectation( + # tx1: nonce change for auth, code change for delegation + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=1, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + # tx2 (withdrawal): balance change + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=alice_final_balance) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle MUST NOT appear - withdrawals don't execute recipient code, + # so delegation target is never accessed + oracle: None, + } + + block = Block( + txs=[tx1], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=alice, + amount=withdrawal_amount_gwei, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + balance=alice_final_balance, + ), + bob: Account(balance=10), + relayer: Account(nonce=1), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + + + From d584517b17c73edb87b9e59ac341020975c17df5 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 28 Nov 2025 15:37:08 +0100 Subject: [PATCH 05/11] chore(fix) format with ruff --- .../test_block_access_lists.py | 43 ++++++++++++++----- .../test_block_access_lists_eip7702.py | 8 ++-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 05c56e84af..20323f026d 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -2286,7 +2286,9 @@ def test_bal_all_transaction_types( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[BalStorageChange(tx_index=1, post_value=0x01)], + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x01) + ], ) ], ), @@ -2295,7 +2297,9 @@ def test_bal_all_transaction_types( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[BalStorageChange(tx_index=2, post_value=0x02)], + slot_changes=[ + BalStorageChange(tx_index=2, post_value=0x02) + ], ) ], ), @@ -2306,7 +2310,9 @@ def test_bal_all_transaction_types( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[BalStorageChange(tx_index=3, post_value=0x03)], + slot_changes=[ + BalStorageChange(tx_index=3, post_value=0x03) + ], ) ], ), @@ -2315,7 +2321,9 @@ def test_bal_all_transaction_types( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[BalStorageChange(tx_index=4, post_value=0x04)], + slot_changes=[ + BalStorageChange(tx_index=4, post_value=0x04) + ], ) ], ), @@ -2331,7 +2339,9 @@ def test_bal_all_transaction_types( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[BalStorageChange(tx_index=5, post_value=0x05)], + slot_changes=[ + BalStorageChange(tx_index=5, post_value=0x05) + ], ) ], ), @@ -2582,7 +2592,8 @@ def test_bal_create_selfdestruct_to_self_with_call( # Balance changes: loses endowment (100) balance_changes=[ BalBalanceChange( - tx_index=1, post_balance=factory_balance - endowment + tx_index=1, + post_balance=factory_balance - endowment, ) ], ), @@ -2669,7 +2680,9 @@ def test_bal_revert_insufficient_funds( contract = pre.deploy_contract( code=contract_code, balance=contract_balance, - storage={0x02: 0xDEAD}, # Non-zero initial value so SSTORE(0) is a change + storage={ + 0x02: 0xDEAD + }, # Non-zero initial value so SSTORE(0) is a change ) tx = Transaction( @@ -2939,7 +2952,9 @@ def test_bal_selfdestruct_to_precompile( # Explicitly verify ALL fields to avoid false positives victim: BalAccountExpectation( nonce_changes=[], # Contract nonce unchanged - balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=0) + ], code_changes=[], # Code unchanged (post-Cancun SELFDESTRUCT) storage_changes=[], # No storage changes storage_reads=[], # No storage reads @@ -2949,7 +2964,9 @@ def test_bal_selfdestruct_to_precompile( ecrecover_precompile: BalAccountExpectation( nonce_changes=[], # MUST NOT have nonce changes balance_changes=[ - BalBalanceChange(tx_index=1, post_balance=contract_balance) + BalBalanceChange( + tx_index=1, post_balance=contract_balance + ) ], code_changes=[], # MUST NOT have code changes storage_changes=[], # MUST NOT have storage changes @@ -3021,7 +3038,9 @@ def test_bal_create_early_failure( ) # Calculate what the contract address WOULD be (but it won't be created) - would_be_contract_address = compute_create_address(address=factory, nonce=1) + would_be_contract_address = compute_create_address( + address=factory, nonce=1 + ) tx = Transaction( sender=alice, @@ -3062,7 +3081,9 @@ def test_bal_create_early_failure( post={ alice: Account(nonce=1), # Factory nonce unchanged (still 1), balance unchanged - factory: Account(nonce=1, balance=factory_balance, storage={0x00: 0}), + factory: Account( + nonce=1, balance=factory_balance, storage={0x00: 0} + ), # Contract was never created would_be_contract_address: Account.NONEXISTENT, }, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index 7a39b59259..d006a49162 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -1010,7 +1010,9 @@ def test_bal_withdrawal_to_7702_delegation( ], ) - alice_final_balance = alice_initial_balance + (withdrawal_amount_gwei * GWEI) + alice_final_balance = alice_initial_balance + ( + withdrawal_amount_gwei * GWEI + ) account_expectations = { alice: BalAccountExpectation( @@ -1068,7 +1070,3 @@ def test_bal_withdrawal_to_7702_delegation( blocks=[block], post=post, ) - - - - From 0280c56c5fd5b76e2289fb0d36ce7b14c8ecdde9 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 28 Nov 2025 17:20:36 +0100 Subject: [PATCH 06/11] chore(fix) revert call changes and add target to bal --- .../forks/amsterdam/vm/instructions/system.py | 54 +++++++++---------- .../test_block_access_lists.py | 6 +-- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index de667b6a29..e5e2ec306f 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -427,19 +427,6 @@ def call(evm: Evm) -> None: evm, access_gas_cost + transfer_gas_cost + extend_memory.cost, ) - # Early balance check - before accessing target state. - # This ensures target is NOT in BAL when call fails due to insufficient funds. - sender_balance = get_account( - evm.message.block_env.state, evm.message.current_target - ).balance - if sender_balance < value: - # Only charge transfer + memory (not cold access, since we didn't access) - charge_gas(evm, transfer_gas_cost + extend_memory.cost) - evm.memory += b"\x00" * extend_memory.expand_by - push(evm.stack, U256(0)) - evm.return_data = b"" - evm.pc += Uint(1) - return # need to access account to check if account is alive, check gas before create_gas_cost = GAS_NEW_ACCOUNT @@ -490,23 +477,30 @@ def call(evm: Evm) -> None: if evm.message.is_static and value != U256(0): raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by - - generic_call( - evm, - message_call_gas.sub_call, - value, - evm.message.current_target, - to, - code_address, - True, - False, - memory_input_start_position, - memory_input_size, - memory_output_start_position, - memory_output_size, - code, - disable_precompiles, - ) + sender_balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 20323f026d..6e93d1dd75 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -2647,7 +2647,7 @@ def test_bal_revert_insufficient_funds( Expected BAL: - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) - - Target: MUST NOT appear (balance check fails before target access) + - Target: appears in BAL (accessed before balance check fails) """ alice = pre.fund_eoa() @@ -2711,8 +2711,8 @@ def test_bal_revert_insufficient_funds( ) ], ), - # Target MUST NOT appear in BAL - balance check fails before - # target state is accessed, so no witness is needed for target + # Target appears in BAL - accessed before balance check fails + target_address: BalAccountExpectation.empty(), } ), ) From 4f29f6d961135dbedc4a69833ad9a08901c5579b Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 10:40:35 +0100 Subject: [PATCH 07/11] merge test cases with usptream --- .../eip7928_block_level_access_lists/test_cases.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 5e6ae802f6..b15ebd1236 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -94,13 +94,14 @@ | `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_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. | 🟡 Planned | | `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: (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. Note: Uses EXTCODECOPY from template contract for init code (too long for PUSH32). | BAL **MUST** include: (1) Factory with `nonce_changes`, `balance_changes` (loses 100). (2) Oracle with `storage_changes` for slot 0x01 (external call succeeded). (3) Template contract (EXTCODECOPY source). (4) 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, SELFDESTRUCT to self = net zero). | ✅ 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 | | `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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_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_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) with empty changes. Per EVM, CALL loads target address before balance check, so target **MUST** appear in BAL even though transfer failed. No `balance_changes` for target. | ✅ 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)`. | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high`, regardless of access order. This catches endianness bugs where addresses are compared as big integers instead of byte arrays. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | ✅ Completed | +| `test_bal_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_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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. Note: Uses caller contract to trigger selfdestruct (post-Cancun SELFDESTRUCT only transfers balance on CALL). | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed | | `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ Completed | +| `test_bal_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ 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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_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). | 🟡 Planned | | `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 | | `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | ✅ Completed | From 67bfd84494d0fe6d12a9af6d986eebb641d6e1ba Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 11:00:29 +0100 Subject: [PATCH 08/11] improve wording --- tests/amsterdam/eip7928_block_level_access_lists/test_cases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b15ebd1236..032e98f94a 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -101,7 +101,7 @@ | `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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. Note: Uses caller contract to trigger selfdestruct (post-Cancun SELFDESTRUCT only transfers balance on CALL). | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed | | `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ Completed | -| `test_bal_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed | +| `test_bal_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Caller triggers victim contract (balance=100) to execute `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ 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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_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). | 🟡 Planned | | `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 | | `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | ✅ Completed | From 320670f27df1104697f228e072cf724691885fb3 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Dec 2025 11:08:10 +0100 Subject: [PATCH 09/11] chore(formate) fix formatting and line length --- .../test_block_access_lists.py | 69 ++++++++++--------- .../test_block_access_lists_eip7702.py | 3 +- .../test_cases.md | 12 ++-- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 6e93d1dd75..7d87d3ad72 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -22,6 +22,7 @@ Fork, Hash, Header, + Initcode, Op, Transaction, add_kzg_version, @@ -2154,9 +2155,11 @@ def test_bal_all_transaction_types( blockchain_test: BlockchainTestFiller, ) -> None: """ - Test BAL with all 5 tx types in single block: Legacy, EIP-2930, EIP-1559, Blob, EIP-7702. + Test BAL with all 5 tx types in single block. - Each tx writes to contract storage. Access list addresses are pre-warmed but NOT in BAL. + Types: Legacy, EIP-2930, EIP-1559, Blob, EIP-7702. + Each tx writes to contract storage. Access list addresses are pre-warmed + but NOT in BAL. Expected BAL: - All 5 senders: nonce_changes @@ -2303,8 +2306,8 @@ def test_bal_all_transaction_types( ) ], ), - # Note: warmed_address from access_list is NOT in BAL because - # access lists pre-warm but don't record in BAL (no state access) + # Note: warmed_address from access_list is NOT in BAL + # because access lists pre-warm but don't record in BAL # Contract touched by Type 2 contract_2: BalAccountExpectation( storage_changes=[ @@ -2378,13 +2381,14 @@ def test_bal_create2_collision( blockchain_test: BlockchainTestFiller, ) -> None: """ - Test BAL with CREATE2 collision against pre-existing contract (code=STOP, nonce=1). + Test BAL with CREATE2 collision against pre-existing contract. - Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting occupied address. + Pre-existing contract has code=STOP, nonce=1. + Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting it. Expected BAL: - Factory: nonce_changes (1→2), storage_changes slot 0 (0xDEAD→0) - - Collision address: empty (accessed during collision check, no state changes) + - Collision address: empty (accessed during collision check) - Collision address MUST NOT have nonce_changes or code_changes """ alice = pre.fund_eoa() @@ -2393,7 +2397,7 @@ def test_bal_create2_collision( init_code = Initcode(deploy_code=Op.STOP) init_code_bytes = bytes(init_code) - # Factory code: CREATE2(value=0, salt=0, initcode) and store result in slot 0 + # Factory code: CREATE2 and store result in slot 0 factory_code = ( # Push init code to memory Op.MSTORE(0, Op.PUSH32(init_code_bytes)) @@ -2458,8 +2462,8 @@ def test_bal_create2_collision( ) ], ), - # Collision address: empty changes (accessed but no state changes) - # Explicitly verify ALL fields are empty to avoid false positives + # Collision address: empty (accessed but no state changes) + # Explicitly verify ALL fields are empty collision_address: BalAccountExpectation( nonce_changes=[], # MUST NOT have nonce changes balance_changes=[], # MUST NOT have balance changes @@ -2488,9 +2492,11 @@ def test_bal_create_selfdestruct_to_self_with_call( blockchain_test: BlockchainTestFiller, ) -> None: """ - Test BAL with init code that CALLs Oracle, writes storage, then SELFDESTRUCTs to self. + Test BAL with init code that CALLs Oracle, writes storage, then + SELFDESTRUCTs to self. - Factory CREATE2(endowment=100). Init: CALL(Oracle)→SSTORE(0x01)→SELFDESTRUCT(SELF). + Factory CREATE2(endowment=100). + Init: CALL(Oracle)→SSTORE(0x01)→SELFDESTRUCT(SELF). Expected BAL: - Factory: nonce_changes, balance_changes (loses 100) @@ -2528,7 +2534,7 @@ def test_bal_create_selfdestruct_to_self_with_call( # Structure: [execution code] [initcode bytes] # CODECOPY copies initcode from factory's own code to memory # - # Two-pass approach: build with placeholder, measure, rebuild with actual size + # Two-pass approach: build with placeholder, measure, rebuild placeholder_offset = 0xFF # Placeholder (same byte size as final value) factory_execution_template = ( Op.CODECOPY(0, placeholder_offset, init_code_size) @@ -2610,7 +2616,7 @@ def test_bal_create_selfdestruct_to_self_with_call( ), # Created address: ephemeral (created and destroyed same tx) # - storage_reads for slot 0x01 (aborted write becomes read) - # - NO nonce_changes, code_changes, storage_changes, balance_changes + # - NO nonce/code/storage/balance changes created_address: BalAccountExpectation( storage_reads=[0x01], storage_changes=[], @@ -2642,8 +2648,8 @@ def test_bal_revert_insufficient_funds( """ Test BAL with CALL failure due to insufficient balance (not OOG). - Contract (balance=100): SLOAD(0x01)→CALL(target, value=1000)→SSTORE(0x02, result). - CALL fails because 1000 > 100. Target is 0xDEAD (pre-existing). + Contract (balance=100): SLOAD(0x01)→CALL(target, value=1000)→SSTORE(0x02). + CALL fails because 1000 > 100. Target is 0xDEAD. Expected BAL: - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) @@ -2742,26 +2748,26 @@ def test_bal_lexicographic_address_ordering( Endian-trap: addr_endian_low (0x01...02), addr_endian_high (0x02...01). Contract touches them in reverse order to verify sorting. - Expected BAL order: addr_low < addr_mid < addr_high < addr_endian_low < addr_endian_high + Expected BAL order: low < mid < high < endian_low < endian_high. Catches endianness bugs in address comparison. """ alice = pre.fund_eoa() # Create addresses with specific byte patterns for lexicographic testing # In lexicographic (byte-wise) order: low < mid < high - # addr_low: 0x0000000000000000000000000000000000000001 (rightmost byte = 0x01) - # addr_mid: 0x0000000000000000000000000000000000000100 (second-rightmost byte = 0x01) - # addr_high: 0x0100000000000000000000000000000000000000 (leftmost byte = 0x01) + # addr_low: 0x00...01 (rightmost byte = 0x01) + # addr_mid: 0x00...0100 (second-rightmost byte = 0x01) + # addr_high: 0x01...00 (leftmost byte = 0x01) addr_low = Address("0x0000000000000000000000000000000000000001") addr_mid = Address("0x0000000000000000000000000000000000000100") addr_high = Address("0x0100000000000000000000000000000000000000") - # Endian-trap addresses: byte-reversals of each other to catch byte-order bugs + # Endian-trap addresses: byte-reversals to catch byte-order bugs # addr_endian_low: 0x01...02 (0x01 at byte 0, 0x02 at byte 19) # addr_endian_high: 0x02...01 (0x02 at byte 0, 0x01 at byte 19) # Note: reverse(addr_endian_low) = addr_endian_high - # Correct order: addr_endian_low < addr_endian_high (0x01 < 0x02 at byte 0) - # If bytes are reversed before comparing: would incorrectly get opposite order + # Correct order: endian_low < endian_high (0x01 < 0x02 at byte 0) + # Reversed bytes would incorrectly get opposite order addr_endian_low = Address("0x0100000000000000000000000000000000000002") addr_endian_high = Address("0x0200000000000000000000000000000000000001") @@ -2799,7 +2805,7 @@ def test_bal_lexicographic_address_ordering( ) # BAL must be sorted lexicographically by address bytes - # The order should be: addr_low < addr_mid < addr_high < addr_endian_low < addr_endian_high + # Order: low < mid < high < endian_low < endian_high # (sorted by raw address bytes, regardless of access order) block = Block( txs=[tx], @@ -2810,7 +2816,7 @@ def test_bal_lexicographic_address_ordering( ), contract: BalAccountExpectation.empty(), # These addresses appear in BAL due to BALANCE access - # The expectation framework will verify they're in correct order + # The expectation framework verifies correct order addr_low: BalAccountExpectation.empty(), addr_mid: BalAccountExpectation.empty(), addr_high: BalAccountExpectation.empty(), @@ -2912,13 +2918,13 @@ def test_bal_selfdestruct_to_precompile( blockchain_test: BlockchainTestFiller, ) -> None: """ - Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01) as beneficiary. + Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01). Victim (balance=100) selfdestructs to precompile 0x01. Expected BAL: - Victim: balance_changes (100→0) - - Precompile 0x01: balance_changes (0→100), MUST NOT have code/nonce changes + - Precompile 0x01: balance_changes (0→100), no code/nonce changes """ alice = pre.fund_eoa() @@ -2955,7 +2961,7 @@ def test_bal_selfdestruct_to_precompile( balance_changes=[ BalBalanceChange(tx_index=1, post_balance=0) ], - code_changes=[], # Code unchanged (post-Cancun SELFDESTRUCT) + code_changes=[], # Code unchanged (post-Cancun) storage_changes=[], # No storage changes storage_reads=[], # No storage reads ), @@ -2995,10 +3001,11 @@ def test_bal_create_early_failure( blockchain_test: BlockchainTestFiller, ) -> None: """ - Test BAL with CREATE failure due to insufficient endowment (before track_address). + Test BAL with CREATE failure due to insufficient endowment. - Factory (balance=50) attempts CREATE(value=100). Fails before nonce increment. - Distinct from collision where address IS accessed and nonce IS incremented. + Factory (balance=50) attempts CREATE(value=100). + Fails before nonce increment (before track_address). + Distinct from collision where address IS accessed. Expected BAL: - Alice: nonce_changes diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index d006a49162..a0a6a1e121 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -838,7 +838,8 @@ def test_bal_selfdestruct_to_7702_delegation( """ Test BAL with SELFDESTRUCT to 7702 delegated account. - Tx1: Alice delegates to Oracle. Tx2: Victim (balance=100) selfdestructs to Alice. + Tx1: Alice delegates to Oracle. + Tx2: Victim (balance=100) selfdestructs to Alice. SELFDESTRUCT transfers balance without executing recipient code. Expected BAL: 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 032e98f94a..b639e7d5d2 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -54,11 +54,10 @@ | `test_bal_oog_7702_delegated_warm_cold` | Ensure BAL handles OOG during EIP-7702 delegated account loading when first account is warm, second is cold | Alice calls warm delegated account Bob (7702) which delegates to cold `TargetContract` with insufficient gas for second cold load | BAL **MUST** include Bob in `account_changes` (warm load succeeds) but **MUST NOT** include `TargetContract` (cold load fails due to OOG) | 🟡 Planned | | `test_bal_multiple_balance_changes_same_account` | Ensure BAL tracks multiple balance changes to same account across transactions | Alice funds Bob (starts at 0) in tx0 with exact amount needed. Bob spends everything in tx1 to Charlie. Bob's balance: 0 → funding_amount → 0 | BAL **MUST** include Bob with two `balance_changes`: one at txIndex=1 (receives funds) and one at txIndex=2 (balance returns to 0). This tests balance tracking across two transactions. | ✅ Completed | | `test_bal_multiple_storage_writes_same_slot` | Ensure BAL tracks multiple writes to same storage slot across transactions | Alice calls contract 3 times in same block. Contract increments slot 1 on each call: 0 → 1 → 2 → 3 | BAL **MUST** include contract with slot 1 having three `slot_changes`: txIndex=1 (value 1), txIndex=2 (value 2), txIndex=3 (value 3). Each transaction's write must be recorded separately. | ✅ 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_tx_storage_revert_to_zero` | Ensure BAL captures storage changes when tx2 reverts slot back to original value (blobhash regression test) | Alice sends tx1 writing slot 0=0xABCD (from 0x0), then tx2 writing slot 0=0x0 (back to original) | BAL **MUST** include contract with slot 0 having two `slot_changes`: txIndex=1 (0xABCD) and txIndex=2 (0x0). Cross-transaction net-zero **MUST NOT** be filtered. | ✅ Completed | | `test_bal_create_contract_init_revert` | Ensure BAL correctly handles CREATE when parent call reverts | Caller calls factory, factory executes CREATE (succeeds), then factory REVERTs rolling back the CREATE | BAL **MUST** include Alice with `nonce_changes`. Caller and factory with no changes (reverted). Created contract address appears in BAL but **MUST NOT** have `nonce_changes` or `code_changes` (CREATE was rolled back). Contract address **MUST NOT** exist in post-state. | ✅ Completed | | `test_bal_create_oog_code_deposit` | Ensure BAL correctly handles CREATE OOG during code deposit | Alice calls factory contract that executes CREATE with init code returning 10,000 bytes. Transaction has insufficient gas for code deposit. Factory nonce increments, CREATE returns 0 and stores in slot 1. | BAL **MUST** include Alice with `nonce_changes`. Factory with `nonce_changes` (incremented by CREATE) and `storage_changes` (slot 1 = 0). Contract address with empty changes (read during collision check). **MUST NOT** include nonce or code changes for contract address (rolled back on OOG). Contract address **MUST NOT** exist in post-state. | ✅ Completed | -| `test_bal_create_early_failure` | Ensure BAL correctly handles CREATE that fails before accessing contract address | Factory (balance=50) attempts CREATE(value=100). CREATE fails due to insufficient endowment (100 > 50). Factory stores CREATE result (0) in slot 0. | BAL **MUST** include Alice with `nonce_changes`. Factory with `storage_changes` (slot 0 = 0) but **MUST NOT** have `nonce_changes` (CREATE failed before nonce increment). Contract address **MUST NOT** appear in BAL (never accessed - CREATE failed before `track_address`). This is distinct from collision/OOG failures where contract address IS in BAL. | 🟡 Planned | +| `test_bal_create_early_failure` | Ensure BAL correctly handles CREATE that fails before accessing contract address | Factory (balance=50) attempts CREATE(value=100). CREATE fails due to insufficient endowment (100 > 50). Factory stores CREATE result (0) in slot 0. | BAL **MUST** include Alice with `nonce_changes`. Factory with `storage_changes` (slot 0 = 0) but **MUST NOT** have `nonce_changes` (CREATE failed before nonce increment). Contract address **MUST NOT** appear in BAL (never accessed - CREATE failed before `track_address`). This is distinct from collision/OOG failures where contract address IS in BAL. | ✅ Completed | | `test_bal_invalid_missing_nonce` | Verify clients reject blocks with BAL missing required nonce changes | Alice sends transaction to Bob; BAL modifier removes Alice's nonce change entry | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate that all sender accounts have nonce changes recorded. | ✅ Completed | | `test_bal_invalid_nonce_value` | Verify clients reject blocks with incorrect nonce values in BAL | Alice sends transaction to Bob; BAL modifier changes Alice's nonce to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate nonce values match actual state transitions. | ✅ Completed | | `test_bal_invalid_storage_value` | Verify clients reject blocks with incorrect storage values in BAL | Alice calls contract that writes to storage; BAL modifier changes storage value to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate storage change values match actual state transitions. | ✅ Completed | @@ -90,19 +89,18 @@ | `test_bal_nonexistent_account_access_value_transfer` | Ensure BAL captures non-existent account accessed via CALL/CALLCODE with value transfers | Alice calls `Oracle` contract which uses `CALL` or `CALLCODE` on non-existent account Bob. Tests both zero and positive value transfers. | BAL **MUST** include Alice with `nonce_changes`. For CALL with positive value: `Oracle` with `balance_changes` (loses value), Bob with `balance_changes` (receives value). For CALLCODE with value or zero value transfers: `Oracle` and Bob with empty changes (CALLCODE self-transfer = net zero). | ✅ Completed | | `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. | ✅ 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_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. | 🟡 Planned | +| `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 | | `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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_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_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_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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Contract with balance=100 wei executes `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. Note: Uses caller contract to trigger selfdestruct (post-Cancun SELFDESTRUCT only transfers balance on CALL). | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed | -| `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ Completed | | `test_bal_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Caller triggers victim contract (balance=100) to execute `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ 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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_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). | 🟡 Planned | +| `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | ✅ 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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_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 | | `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | ✅ Completed | | `test_selfdestruct_created_in_same_tx_with_revert` | Ensure BAL tracks selfdestruct with revert correctly (pre-Amsterdam test with BAL) | Contract created and selfdestructed in same tx with nested revert | BAL **MUST** track storage reads and balance changes for selfdestruct even with reverts | ✅ Completed | From b4f8109684ab780ec77c0c47fb3b9a678461bb86 Mon Sep 17 00:00:00 2001 From: fselmo Date: Mon, 8 Dec 2025 13:28:55 -0700 Subject: [PATCH 10/11] refactor(test-tests): Use pre API where possible; explicit check for none in BAL --- .../test_block_access_lists.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 7d87d3ad72..b9e4d474c4 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -2188,8 +2188,7 @@ def test_bal_all_transaction_types( oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x05) + Op.STOP) # Dummy address to warm in access list - warmed_address = Address("0x0000000000000000000000000000000000001234") - pre[warmed_address] = Account(balance=1) + warmed_address = pre.fund_eoa(amount=1) # TX1: Type 0 - Legacy transaction tx_type_0 = Transaction( @@ -2309,6 +2308,7 @@ def test_bal_all_transaction_types( # Note: warmed_address from access_list is NOT in BAL # because access lists pre-warm but don't record in BAL # Contract touched by Type 2 + warmed_address: None, # explicit check contract_2: BalAccountExpectation( storage_changes=[ BalStorageSlot( @@ -2663,8 +2663,7 @@ def test_bal_revert_insufficient_funds( # Target address that should be warmed but not receive funds # Give it a small balance so it's not considered "empty" and pruned target_balance = 1 - target_address = Address("0x000000000000000000000000000000000000DEAD") - pre[target_address] = Account(balance=target_balance) + target_address = pre.fund_eoa(amount=target_balance) # Contract that: # 1. SLOAD slot 0x01 From 1518655b855fbdbb1094a27073a5a94175047cfc Mon Sep 17 00:00:00 2001 From: fselmo Date: Mon, 8 Dec 2025 13:49:47 -0700 Subject: [PATCH 11/11] refactor(test-tests): Refactor opcode tests to bal opcodes test file --- .../test_block_access_lists.py | 615 ------------------ .../test_block_access_lists_opcodes.py | 614 +++++++++++++++++ .../test_cases.md | 2 +- 3 files changed, 615 insertions(+), 616 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index b9e4d474c4..b17935e1e3 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -22,7 +22,6 @@ Fork, Hash, Header, - Initcode, Op, Transaction, add_kzg_version, @@ -2376,366 +2375,6 @@ def test_bal_all_transaction_types( ) -def test_bal_create2_collision( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL with CREATE2 collision against pre-existing contract. - - Pre-existing contract has code=STOP, nonce=1. - Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting it. - - Expected BAL: - - Factory: nonce_changes (1→2), storage_changes slot 0 (0xDEAD→0) - - Collision address: empty (accessed during collision check) - - Collision address MUST NOT have nonce_changes or code_changes - """ - alice = pre.fund_eoa() - - # Init code that deploys simple STOP contract - init_code = Initcode(deploy_code=Op.STOP) - init_code_bytes = bytes(init_code) - - # Factory code: CREATE2 and store result in slot 0 - factory_code = ( - # Push init code to memory - Op.MSTORE(0, Op.PUSH32(init_code_bytes)) - # SSTORE(0, CREATE2(...)) - stores CREATE2 result in slot 0 - + Op.SSTORE( - 0x00, - Op.CREATE2( - value=0, - offset=32 - len(init_code_bytes), - size=len(init_code_bytes), - salt=0, - ), - ) - + Op.STOP - ) - - # Deploy factory - it starts with nonce=1 by default - factory = pre.deploy_contract( - code=factory_code, - storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works - ) - - # Calculate the CREATE2 target address - collision_address = compute_create_address( - address=factory, - nonce=1, - salt=0, - initcode=init_code_bytes, - opcode=Op.CREATE2, - ) - - # Set up the collision by pre-populating the target address - # This contract has code (STOP) and nonce=1, causing collision - pre[collision_address] = Account( - code=Op.STOP, - nonce=1, - ) - - tx = Transaction( - sender=alice, - to=factory, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - factory: BalAccountExpectation( - # Nonce incremented 1→2 even on failed CREATE2 - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], - # Storage changes: slot 0 = 0xDEAD → 0 (CREATE2 returned 0) - storage_changes=[ - BalStorageSlot( - slot=0x00, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0) - ], - ) - ], - ), - # Collision address: empty (accessed but no state changes) - # Explicitly verify ALL fields are empty - collision_address: BalAccountExpectation( - nonce_changes=[], # MUST NOT have nonce changes - balance_changes=[], # MUST NOT have balance changes - code_changes=[], # MUST NOT have code changes - storage_changes=[], # MUST NOT have storage changes - storage_reads=[], # MUST NOT have storage reads - ), - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - factory: Account(nonce=2, storage={0x00: 0}), - # Collision address unchanged - contract still exists - collision_address: Account(code=bytes(Op.STOP), nonce=1), - }, - ) - - -def test_bal_create_selfdestruct_to_self_with_call( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL with init code that CALLs Oracle, writes storage, then - SELFDESTRUCTs to self. - - Factory CREATE2(endowment=100). - Init: CALL(Oracle)→SSTORE(0x01)→SELFDESTRUCT(SELF). - - Expected BAL: - - Factory: nonce_changes, balance_changes (loses 100) - - Oracle: storage_changes slot 0x01 - - Created address: storage_reads [0x01] (aborted write→read), - MUST NOT have nonce/code/storage/balance changes (ephemeral) - """ - alice = pre.fund_eoa() - factory_balance = 1000 - - # Oracle contract that writes to slot 0x01 when called - oracle_code = Op.SSTORE(0x01, 0x42) + Op.STOP - oracle = pre.deploy_contract(code=oracle_code) - - endowment = 100 - - # Init code that: - # 1. Calls Oracle (which writes to its slot 0x01) - # 2. Writes 0x42 to own slot 0x01 - # 3. Selfdestructs to self - initcode_runtime = ( - # CALL(gas, Oracle, value=0, ...) - Op.CALL(100_000, oracle, 0, 0, 0, 0, 0) - + Op.POP - # Write to own storage slot 0x01 - + Op.SSTORE(0x01, 0x42) - # SELFDESTRUCT to self (ADDRESS returns own address) - + Op.SELFDESTRUCT(Op.ADDRESS) - ) - init_code = Initcode(deploy_code=Op.STOP, initcode_prefix=initcode_runtime) - init_code_bytes = bytes(init_code) - init_code_size = len(init_code_bytes) - - # Factory code with embedded initcode (no template contract needed) - # Structure: [execution code] [initcode bytes] - # CODECOPY copies initcode from factory's own code to memory - # - # Two-pass approach: build with placeholder, measure, rebuild - placeholder_offset = 0xFF # Placeholder (same byte size as final value) - factory_execution_template = ( - Op.CODECOPY(0, placeholder_offset, init_code_size) - + Op.SSTORE( - 0x00, - Op.CREATE2( - value=endowment, - offset=0, - size=init_code_size, - salt=0, - ), - ) - + Op.STOP - ) - # Measure execution code size - execution_code_size = len(bytes(factory_execution_template)) - - # Rebuild with actual offset value - factory_execution = ( - Op.CODECOPY(0, execution_code_size, init_code_size) - + Op.SSTORE( - 0x00, - Op.CREATE2( - value=endowment, - offset=0, - size=init_code_size, - salt=0, - ), - ) - + Op.STOP - ) - # Combine execution code with embedded initcode - factory_code = bytes(factory_execution) + init_code_bytes - - factory = pre.deploy_contract(code=factory_code, balance=factory_balance) - - # Calculate the CREATE2 target address - created_address = compute_create_address( - address=factory, - nonce=1, - salt=0, - initcode=init_code_bytes, - opcode=Op.CREATE2, - ) - - tx = Transaction( - sender=alice, - to=factory, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - factory: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], - # Balance changes: loses endowment (100) - balance_changes=[ - BalBalanceChange( - tx_index=1, - post_balance=factory_balance - endowment, - ) - ], - ), - # Oracle: storage changes for slot 0x01 - oracle: BalAccountExpectation( - storage_changes=[ - BalStorageSlot( - slot=0x01, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0x42) - ], - ) - ], - ), - # Created address: ephemeral (created and destroyed same tx) - # - storage_reads for slot 0x01 (aborted write becomes read) - # - NO nonce/code/storage/balance changes - created_address: BalAccountExpectation( - storage_reads=[0x01], - storage_changes=[], - nonce_changes=[], - code_changes=[], - balance_changes=[], - ), - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - factory: Account(nonce=2, balance=factory_balance - endowment), - oracle: Account(storage={0x01: 0x42}), - # Created address doesn't exist - destroyed in same tx - created_address: Account.NONEXISTENT, - }, - ) - - -def test_bal_revert_insufficient_funds( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL with CALL failure due to insufficient balance (not OOG). - - Contract (balance=100): SLOAD(0x01)→CALL(target, value=1000)→SSTORE(0x02). - CALL fails because 1000 > 100. Target is 0xDEAD. - - Expected BAL: - - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) - - Target: appears in BAL (accessed before balance check fails) - """ - alice = pre.fund_eoa() - - contract_balance = 100 - transfer_amount = 1000 # More than contract has - - # Target address that should be warmed but not receive funds - # Give it a small balance so it's not considered "empty" and pruned - target_balance = 1 - target_address = pre.fund_eoa(amount=target_balance) - - # Contract that: - # 1. SLOAD slot 0x01 - # 2. CALL target with value=1000 (will fail - insufficient funds) - # 3. SSTORE slot 0x02 with CALL result (0 = failure) - contract_code = ( - Op.SLOAD(0x01) # Read from slot 0x01, push to stack - + Op.POP # Discard value - # CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize) - + Op.CALL(100_000, target_address, transfer_amount, 0, 0, 0, 0) - # CALL result is on stack (0 = failure, 1 = success) - # Stack: [result] - + Op.PUSH1(0x02) # Push slot number - # Stack: [0x02, result] - + Op.SSTORE # SSTORE pops slot (0x02), then value (result) - + Op.STOP - ) - - contract = pre.deploy_contract( - code=contract_code, - balance=contract_balance, - storage={ - 0x02: 0xDEAD - }, # Non-zero initial value so SSTORE(0) is a change - ) - - tx = Transaction( - sender=alice, - to=contract, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - contract: BalAccountExpectation( - # Storage read for slot 0x01 - storage_reads=[0x01], - # Storage change for slot 0x02 (CALL result = 0) - storage_changes=[ - BalStorageSlot( - slot=0x02, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0) - ], - ) - ], - ), - # Target appears in BAL - accessed before balance check fails - target_address: BalAccountExpectation.empty(), - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - contract: Account( - balance=contract_balance, # Unchanged - transfer failed - storage={0x02: 0}, # CALL returned 0 (failure) - ), - target_address: Account(balance=target_balance), # Unchanged - }, - ) - - def test_bal_lexicographic_address_ordering( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2840,257 +2479,3 @@ def test_bal_lexicographic_address_ordering( addr_endian_high: Account(balance=addr_balance), }, ) - - -def test_bal_transient_storage_not_tracked( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL excludes EIP-1153 transient storage (TSTORE/TLOAD). - - Contract: TSTORE(0x01, 0x42)→TLOAD(0x01)→SSTORE(0x02, result). - - Expected BAL: - - storage_changes: slot 0x02 (persistent) - - MUST NOT include slot 0x01 (transient storage not persisted) - """ - alice = pre.fund_eoa() - - # Contract that uses transient storage then persists to regular storage - contract_code = ( - # TSTORE slot 0x01 with value 0x42 (transient storage) - Op.TSTORE(0x01, 0x42) - # TLOAD slot 0x01 (transient storage read) - + Op.TLOAD(0x01) - # Result (0x42) is on stack, store it in persistent slot 0x02 - + Op.PUSH1(0x02) - + Op.SSTORE # SSTORE pops slot (0x02), then value (0x42) - + Op.STOP - ) - - contract = pre.deploy_contract(code=contract_code) - - tx = Transaction( - sender=alice, - to=contract, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - contract: BalAccountExpectation( - # Persistent storage change for slot 0x02 - storage_changes=[ - BalStorageSlot( - slot=0x02, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0x42) - ], - ) - ], - # MUST NOT include slot 0x01 in storage_reads - # Transient storage operations don't pollute BAL - storage_reads=[], - ), - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - contract: Account(storage={0x02: 0x42}), - }, - ) - - -def test_bal_selfdestruct_to_precompile( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01). - - Victim (balance=100) selfdestructs to precompile 0x01. - - Expected BAL: - - Victim: balance_changes (100→0) - - Precompile 0x01: balance_changes (0→100), no code/nonce changes - """ - alice = pre.fund_eoa() - - contract_balance = 100 - ecrecover_precompile = Address(1) # 0x0000...0001 - - # Contract that selfdestructs to ecrecover precompile - victim_code = Op.SELFDESTRUCT(ecrecover_precompile) - - victim = pre.deploy_contract(code=victim_code, balance=contract_balance) - - # Caller that triggers the selfdestruct - caller_code = Op.CALL(100_000, victim, 0, 0, 0, 0, 0) + Op.STOP - caller = pre.deploy_contract(code=caller_code) - - tx = Transaction( - sender=alice, - to=caller, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - caller: BalAccountExpectation.empty(), - # Victim (selfdestructing contract): balance changes 100→0 - # Explicitly verify ALL fields to avoid false positives - victim: BalAccountExpectation( - nonce_changes=[], # Contract nonce unchanged - balance_changes=[ - BalBalanceChange(tx_index=1, post_balance=0) - ], - code_changes=[], # Code unchanged (post-Cancun) - storage_changes=[], # No storage changes - storage_reads=[], # No storage reads - ), - # Precompile receives selfdestruct balance - # Explicitly verify ALL fields to avoid false positives - ecrecover_precompile: BalAccountExpectation( - nonce_changes=[], # MUST NOT have nonce changes - balance_changes=[ - BalBalanceChange( - tx_index=1, post_balance=contract_balance - ) - ], - code_changes=[], # MUST NOT have code changes - storage_changes=[], # MUST NOT have storage changes - storage_reads=[], # MUST NOT have storage reads - ), - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - caller: Account(), - # Victim still exists with 0 balance (post-Cancun SELFDESTRUCT) - victim: Account(balance=0), - # Precompile has received the balance - ecrecover_precompile: Account(balance=contract_balance), - }, - ) - - -def test_bal_create_early_failure( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Test BAL with CREATE failure due to insufficient endowment. - - Factory (balance=50) attempts CREATE(value=100). - Fails before nonce increment (before track_address). - Distinct from collision where address IS accessed. - - Expected BAL: - - Alice: nonce_changes - - Factory: storage_changes slot 0 (0xDEAD→0), NO nonce_changes - - Contract address: MUST NOT appear (never accessed) - """ - alice = pre.fund_eoa() - - factory_balance = 50 - endowment = 100 # More than factory has - - # Simple init code that deploys STOP - init_code = Initcode(deploy_code=Op.STOP) - init_code_bytes = bytes(init_code) - - # Factory code: CREATE(value=endowment) and store result in slot 0 - factory_code = ( - # Push init code to memory - Op.MSTORE(0, Op.PUSH32(init_code_bytes)) - # SSTORE(0, CREATE(value, offset, size)) - + Op.SSTORE( - 0x00, - Op.CREATE( - value=endowment, # 100 > 50, will fail - offset=32 - len(init_code_bytes), - size=len(init_code_bytes), - ), - ) - + Op.STOP - ) - - # Deploy factory with insufficient balance for the CREATE endowment - factory = pre.deploy_contract( - code=factory_code, - balance=factory_balance, - storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works - ) - - # Calculate what the contract address WOULD be (but it won't be created) - would_be_contract_address = compute_create_address( - address=factory, nonce=1 - ) - - tx = Transaction( - sender=alice, - to=factory, - gas_limit=1_000_000, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - factory: BalAccountExpectation( - # NO nonce_changes - CREATE failed before increment_nonce - nonce_changes=[], - # Storage changes: slot 0 = 0xDEAD → 0 (CREATE returned 0) - storage_changes=[ - BalStorageSlot( - slot=0x00, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0) - ], - ) - ], - ), - # Contract address MUST NOT appear in BAL - never accessed - # (CREATE failed before track_address was called) - would_be_contract_address: None, - } - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - # Factory nonce unchanged (still 1), balance unchanged - factory: Account( - nonce=1, balance=factory_balance, storage={0x00: 0} - ), - # Contract was never created - would_be_contract_address: Account.NONEXISTENT, - }, - ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py index bae1544eee..1c12cec89e 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py @@ -1266,3 +1266,617 @@ def test_bal_create_contract_init_revert( created_address: Account.NONEXISTENT, }, ) + + +def test_bal_call_revert_insufficient_funds( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CALL failure due to insufficient balance (not OOG). + + Contract (balance=100): SLOAD(0x01)→CALL(target, value=1000)→SSTORE(0x02). + CALL fails because 1000 > 100. Target is 0xDEAD. + + Expected BAL: + - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) + - Target: appears in BAL (accessed before balance check fails) + """ + alice = pre.fund_eoa() + + contract_balance = 100 + transfer_amount = 1000 # More than contract has + + # Target address that should be warmed but not receive funds + # Give it a small balance so it's not considered "empty" and pruned + target_balance = 1 + target_address = pre.fund_eoa(amount=target_balance) + + # Contract that: + # 1. SLOAD slot 0x01 + # 2. CALL target with value=1000 (will fail - insufficient funds) + # 3. SSTORE slot 0x02 with CALL result (0 = failure) + contract_code = ( + Op.SLOAD(0x01) # Read from slot 0x01, push to stack + + Op.POP # Discard value + # CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize) + + Op.CALL(100_000, target_address, transfer_amount, 0, 0, 0, 0) + # CALL result is on stack (0 = failure, 1 = success) + # Stack: [result] + + Op.PUSH1(0x02) # Push slot number + # Stack: [0x02, result] + + Op.SSTORE # SSTORE pops slot (0x02), then value (result) + + Op.STOP + ) + + contract = pre.deploy_contract( + code=contract_code, + balance=contract_balance, + storage={ + 0x02: 0xDEAD + }, # Non-zero initial value so SSTORE(0) is a change + ) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Storage read for slot 0x01 + storage_reads=[0x01], + # Storage change for slot 0x02 (CALL result = 0) + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Target appears in BAL - accessed before balance check fails + target_address: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account( + balance=contract_balance, # Unchanged - transfer failed + storage={0x02: 0}, # CALL returned 0 (failure) + ), + target_address: Account(balance=target_balance), # Unchanged + }, + ) + + +def test_bal_create_selfdestruct_to_self_with_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with init code that CALLs Oracle, writes storage, then + SELFDESTRUCTs to self. + + Factory CREATE2(endowment=100). + Init: CALL(Oracle)→SSTORE(0x01)→SELFDESTRUCT(SELF). + + Expected BAL: + - Factory: nonce_changes, balance_changes (loses 100) + - Oracle: storage_changes slot 0x01 + - Created address: storage_reads [0x01] (aborted write→read), + MUST NOT have nonce/code/storage/balance changes (ephemeral) + """ + alice = pre.fund_eoa() + factory_balance = 1000 + + # Oracle contract that writes to slot 0x01 when called + oracle_code = Op.SSTORE(0x01, 0x42) + Op.STOP + oracle = pre.deploy_contract(code=oracle_code) + + endowment = 100 + + # Init code that: + # 1. Calls Oracle (which writes to its slot 0x01) + # 2. Writes 0x42 to own slot 0x01 + # 3. Selfdestructs to self + initcode_runtime = ( + # CALL(gas, Oracle, value=0, ...) + Op.CALL(100_000, oracle, 0, 0, 0, 0, 0) + + Op.POP + # Write to own storage slot 0x01 + + Op.SSTORE(0x01, 0x42) + # SELFDESTRUCT to self (ADDRESS returns own address) + + Op.SELFDESTRUCT(Op.ADDRESS) + ) + init_code = Initcode(deploy_code=Op.STOP, initcode_prefix=initcode_runtime) + init_code_bytes = bytes(init_code) + init_code_size = len(init_code_bytes) + + # Factory code with embedded initcode (no template contract needed) + # Structure: [execution code] [initcode bytes] + # CODECOPY copies initcode from factory's own code to memory + # + # Two-pass approach: build with placeholder, measure, rebuild + placeholder_offset = 0xFF # Placeholder (same byte size as final value) + factory_execution_template = ( + Op.CODECOPY(0, placeholder_offset, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Measure execution code size + execution_code_size = len(bytes(factory_execution_template)) + + # Rebuild with actual offset value + factory_execution = ( + Op.CODECOPY(0, execution_code_size, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Combine execution code with embedded initcode + factory_code = bytes(factory_execution) + init_code_bytes + + factory = pre.deploy_contract(code=factory_code, balance=factory_balance) + + # Calculate the CREATE2 target address + created_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Balance changes: loses endowment (100) + balance_changes=[ + BalBalanceChange( + tx_index=1, + post_balance=factory_balance - endowment, + ) + ], + ), + # Oracle: storage changes for slot 0x01 + oracle: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + ), + # Created address: ephemeral (created and destroyed same tx) + # - storage_reads for slot 0x01 (aborted write becomes read) + # - NO nonce/code/storage/balance changes + created_address: BalAccountExpectation( + storage_reads=[0x01], + storage_changes=[], + nonce_changes=[], + code_changes=[], + balance_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, balance=factory_balance - endowment), + oracle: Account(storage={0x01: 0x42}), + # Created address doesn't exist - destroyed in same tx + created_address: Account.NONEXISTENT, + }, + ) + + +def test_bal_create2_collision( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE2 collision against pre-existing contract. + + Pre-existing contract has code=STOP, nonce=1. + Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting it. + + Expected BAL: + - Factory: nonce_changes (1→2), storage_changes slot 0 (0xDEAD→0) + - Collision address: empty (accessed during collision check) + - Collision address MUST NOT have nonce_changes or code_changes + """ + alice = pre.fund_eoa() + + # Init code that deploys simple STOP contract + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE2 and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE2(...)) - stores CREATE2 result in slot 0 + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=0, + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + salt=0, + ), + ) + + Op.STOP + ) + + # Deploy factory - it starts with nonce=1 by default + factory = pre.deploy_contract( + code=factory_code, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate the CREATE2 target address + collision_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + # Set up the collision by pre-populating the target address + # This contract has code (STOP) and nonce=1, causing collision + pre[collision_address] = Account( + code=Op.STOP, + nonce=1, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # Nonce incremented 1→2 even on failed CREATE2 + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Storage changes: slot 0 = 0xDEAD → 0 (CREATE2 returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Collision address: empty (accessed but no state changes) + # Explicitly verify ALL fields are empty + collision_address: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[], # MUST NOT have balance changes + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, storage={0x00: 0}), + # Collision address unchanged - contract still exists + collision_address: Account(code=bytes(Op.STOP), nonce=1), + }, + ) + + +def test_bal_transient_storage_not_tracked( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL excludes EIP-1153 transient storage (TSTORE/TLOAD). + + Contract: TSTORE(0x01, 0x42)→TLOAD(0x01)→SSTORE(0x02, result). + + Expected BAL: + - storage_changes: slot 0x02 (persistent) + - MUST NOT include slot 0x01 (transient storage not persisted) + """ + alice = pre.fund_eoa() + + # Contract that uses transient storage then persists to regular storage + contract_code = ( + # TSTORE slot 0x01 with value 0x42 (transient storage) + Op.TSTORE(0x01, 0x42) + # TLOAD slot 0x01 (transient storage read) + + Op.TLOAD(0x01) + # Result (0x42) is on stack, store it in persistent slot 0x02 + + Op.PUSH1(0x02) + + Op.SSTORE # SSTORE pops slot (0x02), then value (0x42) + + Op.STOP + ) + + contract = pre.deploy_contract(code=contract_code) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Persistent storage change for slot 0x02 + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + # MUST NOT include slot 0x01 in storage_reads + # Transient storage operations don't pollute BAL + storage_reads=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account(storage={0x02: 0x42}), + }, + ) + + +def test_bal_selfdestruct_to_precompile( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01). + + Victim (balance=100) selfdestructs to precompile 0x01. + + Expected BAL: + - Victim: balance_changes (100→0) + - Precompile 0x01: balance_changes (0→100), no code/nonce changes + """ + alice = pre.fund_eoa() + + contract_balance = 100 + ecrecover_precompile = Address(1) # 0x0000...0001 + + # Contract that selfdestructs to ecrecover precompile + victim_code = Op.SELFDESTRUCT(ecrecover_precompile) + + victim = pre.deploy_contract(code=victim_code, balance=contract_balance) + + # Caller that triggers the selfdestruct + caller_code = Op.CALL(100_000, victim, 0, 0, 0, 0, 0) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx = Transaction( + sender=alice, + to=caller, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + caller: BalAccountExpectation.empty(), + # Victim (selfdestructing contract): balance changes 100→0 + # Explicitly verify ALL fields to avoid false positives + victim: BalAccountExpectation( + nonce_changes=[], # Contract nonce unchanged + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=0) + ], + code_changes=[], # Code unchanged (post-Cancun) + storage_changes=[], # No storage changes + storage_reads=[], # No storage reads + ), + # Precompile receives selfdestruct balance + # Explicitly verify ALL fields to avoid false positives + ecrecover_precompile: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=contract_balance + ) + ], + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + caller: Account(), + # Victim still exists with 0 balance (post-Cancun SELFDESTRUCT) + victim: Account(balance=0), + # Precompile has received the balance + ecrecover_precompile: Account(balance=contract_balance), + }, + ) + + +def test_bal_create_early_failure( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE failure due to insufficient endowment. + + Factory (balance=50) attempts CREATE(value=100). + Fails before nonce increment (before track_address). + Distinct from collision where address IS accessed. + + Expected BAL: + - Alice: nonce_changes + - Factory: storage_changes slot 0 (0xDEAD→0), NO nonce_changes + - Contract address: MUST NOT appear (never accessed) + """ + alice = pre.fund_eoa() + + factory_balance = 50 + endowment = 100 # More than factory has + + # Simple init code that deploys STOP + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE(value=endowment) and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE(value, offset, size)) + + Op.SSTORE( + 0x00, + Op.CREATE( + value=endowment, # 100 > 50, will fail + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + ), + ) + + Op.STOP + ) + + # Deploy factory with insufficient balance for the CREATE endowment + factory = pre.deploy_contract( + code=factory_code, + balance=factory_balance, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate what the contract address WOULD be (but it won't be created) + would_be_contract_address = compute_create_address( + address=factory, nonce=1 + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # NO nonce_changes - CREATE failed before increment_nonce + nonce_changes=[], + # Storage changes: slot 0 = 0xDEAD → 0 (CREATE returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Contract address MUST NOT appear in BAL - never accessed + # (CREATE failed before track_address was called) + would_be_contract_address: None, + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + # Factory nonce unchanged (still 1), balance unchanged + factory: Account( + nonce=1, balance=factory_balance, storage={0x00: 0} + ), + # Contract was never created + would_be_contract_address: Account.NONEXISTENT, + }, + ) 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 b639e7d5d2..07d184d957 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -95,7 +95,7 @@ | `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 | | `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 tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_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_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_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_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_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Caller triggers victim contract (balance=100) to execute `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100→0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0→100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | ✅ Completed |