From 7f9d57aed791a9e4cb5896b55fc1be1706d8034d Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 13 May 2026 10:02:13 -0600 Subject: [PATCH 1/3] feat(test): inter-dependent tx tests for BAL parallelization --- .../test_block_access_lists.py | 539 ++++++++++++++++++ .../test_cases.md | 5 + 2 files changed, 544 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 13851dc6d0..cf3affaaab 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 @@ -20,16 +20,19 @@ BlockAccessListExpectation, BlockchainTestFiller, BlockException, + Conditional, Environment, Fork, Hash, Header, + Initcode, Op, Transaction, add_kzg_version, compute_create_address, ) +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .spec import ref_spec_7928 REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path @@ -2188,6 +2191,542 @@ def test_bal_cross_tx_storage_revert_to_zero( ) +def test_bal_cross_tx_storage_chain( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Verify clients apply BAL state changes from prior transactions before + executing later transactions in the same block. + + Each Tx i seeds slots 0 and 1 with `1`, then computes a + Fibonacci-style sum into slot i: `slot[i] = SLOAD(i-1) + SLOAD(i-2)`. + Every Tx i>=2 depends on the two immediately preceding writes, so + any parallelization that fails to apply a prior Tx's BAL storage + change cascades into a wrong slot value and a different state root. + """ + chain_length = 8 + # i<2 seeds slot i with 1; i>=2 computes the Fibonacci sum. + contract = pre.deploy_contract( + code=Conditional( + condition=Op.LT(Op.CALLDATALOAD(0), 2), + if_true=Op.SSTORE(Op.CALLDATALOAD(0), 1), + if_false=Op.SSTORE( + Op.CALLDATALOAD(0), + Op.ADD( + Op.SLOAD(Op.SUB(Op.CALLDATALOAD(0), 1)), + Op.SLOAD(Op.SUB(Op.CALLDATALOAD(0), 2)), + ), + ), + ), + ) + + fib = [1, 1] + for i in range(2, chain_length): + fib.append(fib[i - 1] + fib[i - 2]) + + txs = [] + senders = [] + for i in range(chain_length): + sender = pre.fund_eoa() + senders.append(sender) + txs.append( + Transaction( + sender=sender, + to=contract, + data=Hash(i), + gas_limit=100_000, + ) + ) + + account_expectations: dict = { + sender: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=i + 1, post_nonce=1) + ], + ) + for i, sender in enumerate(senders) + } + account_expectations[contract] = BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=i, + slot_changes=[ + BalStorageChange( + block_access_index=i + 1, post_value=fib[i] + ), + ], + ) + for i in range(chain_length) + ], + nonce_changes=[], + balance_changes=[], + code_changes=[], + storage_reads=[], + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=txs, + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + contract: Account( + storage={i: fib[i] for i in range(chain_length)} + ), + }, + ) + + +@pytest.mark.with_all_create_opcodes +def test_bal_cross_tx_deploy_then_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + create_opcode: Op, +) -> None: + """ + Verify clients apply Tx1's CREATE to their state view before + executing Tx2's CALL in the same block. Tx1 deploys a contract at a + deterministic address whose runtime code writes a sentinel to slot 0. + Tx2 CALLs that address. A client that parallelizes Tx2 without + applying Tx1's `code_changes` would hit an empty account, the CALL + would no-op, and slot 0 would remain 0. + """ + sentinel = 0x42 + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + runtime = Op.SSTORE(0, sentinel) + Op.STOP + initcode = Initcode(deploy_code=runtime) + initcode_bytes = bytes(initcode) + + salt = 0 + is_create2 = create_opcode == Op.CREATE2 + if is_create2: + deploy_op = Op.CREATE2( + value=0, offset=0, size=Op.CALLDATASIZE, salt=salt + ) + else: + deploy_op = Op.CREATE(value=0, offset=0, size=Op.CALLDATASIZE) + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(0, deploy_op) + + Op.STOP + ) + factory = pre.deploy_contract(code=factory_code) + target = compute_create_address( + address=factory, + nonce=1, + salt=salt, + initcode=initcode_bytes, + opcode=create_opcode, + ) + + tx_deploy = Transaction( + sender=alice, + to=factory, + data=initcode_bytes, + gas_limit=500_000, + ) + tx_call = Transaction( + sender=bob, + to=target, + gas_limit=100_000, + ) + + account_expectations = { + target: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)], + code_changes=[ + BalCodeChange(block_access_index=1, new_code=bytes(runtime)) + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=2, post_value=sentinel + ), + ], + ), + ], + ), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx_deploy, tx_call], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + target: Account( + nonce=1, code=bytes(runtime), storage={0: sentinel} + ), + factory: Account(nonce=2, storage={0: target}), + }, + ) + + +def test_bal_cross_tx_balance_dependency( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Verify clients apply Tx1's balance change before executing Tx2 in + the same block. Tx1 sends value to a contract; Tx2 invokes the + contract which records its `SELFBALANCE` to storage. A client that + parallelizes Tx2 without applying Tx1's `balance_changes` would + record the pre-block balance, yielding a different state root. + """ + transferred = 10**18 + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + # Any non-empty calldata triggers the SELFBALANCE record path; + # empty calldata is the value-receiver path. + contract = pre.deploy_contract( + code=Conditional( + condition=Op.ISZERO(Op.CALLDATASIZE), + if_true=Op.STOP, + if_false=Op.SSTORE(0, Op.SELFBALANCE), + ), + ) + + tx_send = Transaction( + sender=alice, + to=contract, + value=transferred, + gas_limit=100_000, + ) + tx_read = Transaction( + sender=bob, + to=contract, + data=b"\x01", + gas_limit=100_000, + ) + + account_expectations = { + contract: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=transferred + ), + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=2, post_value=transferred + ), + ], + ), + ], + ), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx_send, tx_read], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + contract: Account(balance=transferred, storage={0: transferred}), + }, + ) + + +def test_bal_cross_tx_7702_delegation_then_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Verify clients apply Tx1's EIP-7702 delegation before later txs CALL + the now-delegated EOA. Tx1 installs a delegation designator on `alice` + pointing to a contract that increments slot 0. Tx2 and Tx3 CALL alice, + so each later tx must observe both the installed code (Tx1) and the + prior increment (the immediately preceding tx) for the final slot 0 + to read 2. A client that parallelizes any tx against pre-block state + would see no code or a stale counter, yielding a different state root. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa() + charlie = pre.fund_eoa() + relayer = pre.fund_eoa() + + # Delegation target: increments slot 0 each time it's invoked. + counter = pre.deploy_contract( + code=Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)), + ) + + tx_delegate = Transaction( + sender=relayer, + to=bob, + value=0, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=counter, + nonce=0, + signer=alice, + ) + ], + ) + tx_call_1 = Transaction( + sender=bob, + to=alice, + gas_limit=200_000, + gas_price=0xA, + ) + tx_call_2 = Transaction( + sender=charlie, + to=alice, + gas_limit=200_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx_delegate, tx_call_1, tx_call_2], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + ], + code_changes=[ + BalCodeChange( + block_access_index=1, + new_code=Spec7702.delegation_designation(counter), + ) + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=2, post_value=1 + ), + BalStorageChange( + block_access_index=3, post_value=2 + ), + ], + ), + ], + ), + # Delegation target: loaded as execution target on each + # CALL to alice, but mutations land in alice's storage. + counter: BalAccountExpectation.empty(), + }, + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(counter), + storage={0: 2}, + ), + }, + ) + + +@pytest.mark.parametrize( + "eunice_oog", + [False, True], + ids=["success", "oog_minus_1"], +) +def test_bal_cross_tx_funding_chain( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, + eunice_oog: bool, +) -> None: + """ + Funding chain: alice → bob → charlie → dan → eunice → target. Each + intermediate starts empty and must receive the prior tx's forwarded + value to afford its own upfront gas + outgoing transfer. A client + that parallelizes any later tx against pre-block state would see a + zero balance on its sender and wrongly reject the block. In + `oog_minus_1`, eunice's tx is funded with exactly `gas_limit - 1` + worth of gas so the SSTORE OOGs at the boundary: the funding chain + must still resolve, eunice's nonce and balance updates persist, but + the target sees a `storage_reads` entry instead of a write. + """ + gas_price = 0xA + + target_code = Op.SSTORE( + 0, 0xC0FFEE, key_warm=False, original_value=0, new_value=0xC0FFEE + ) + target = pre.deploy_contract(code=target_code) + + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_calc() + eunice_exact_gas = intrinsic_gas + target_code.gas_cost(fork) + eunice_gas_limit = eunice_exact_gas - 1 if eunice_oog else eunice_exact_gas + eunice_upfront = eunice_gas_limit * gas_price + transfer_cost = intrinsic_gas * gas_price + + # Each sender (including alice) receives or starts with exactly what + # the next forward + its own gas demands; everyone ends at zero. + dan_value = eunice_upfront + charlie_value = transfer_cost + dan_value + bob_value = transfer_cost + charlie_value + alice_value = transfer_cost + bob_value + alice_pre_balance = transfer_cost + alice_value + + alice = pre.fund_eoa(amount=alice_pre_balance) + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) + dan = pre.fund_eoa(amount=0) + eunice = pre.fund_eoa(amount=0) + + txs = [ + Transaction( + sender=alice, + to=bob, + value=alice_value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ), + Transaction( + sender=bob, + to=charlie, + value=bob_value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ), + Transaction( + sender=charlie, + to=dan, + value=charlie_value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ), + Transaction( + sender=dan, + to=eunice, + value=dan_value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ), + Transaction( + sender=eunice, + to=target, + gas_limit=eunice_gas_limit, + gas_price=gas_price, + ), + ] + + if eunice_oog: + target_bal = BalAccountExpectation( + storage_reads=[0], + nonce_changes=[], + balance_changes=[], + code_changes=[], + storage_changes=[], + ) + target_post = Account(storage={}) + else: + target_bal = BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=5, post_value=0xC0FFEE + ), + ], + ), + ], + nonce_changes=[], + balance_changes=[], + code_changes=[], + storage_reads=[], + ) + target_post = Account(storage={0: 0xC0FFEE}) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)], + balance_changes=[ + BalBalanceChange(block_access_index=1, post_balance=0), + ], + ), + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=2, post_nonce=1)], + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=alice_value + ), + BalBalanceChange(block_access_index=2, post_balance=0), + ], + ), + charlie: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=3, post_nonce=1)], + balance_changes=[ + BalBalanceChange(block_access_index=2, post_balance=bob_value), + BalBalanceChange(block_access_index=3, post_balance=0), + ], + ), + dan: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=4, post_nonce=1)], + balance_changes=[ + BalBalanceChange( + block_access_index=3, post_balance=charlie_value + ), + BalBalanceChange(block_access_index=4, post_balance=0), + ], + ), + eunice: BalAccountExpectation( + nonce_changes=[BalNonceChange(block_access_index=5, post_nonce=1)], + balance_changes=[ + BalBalanceChange(block_access_index=4, post_balance=dan_value), + BalBalanceChange(block_access_index=5, post_balance=0), + ], + ), + target: target_bal, + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=txs, + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations, + ), + ) + ], + post={ + alice: Account(nonce=1, balance=0), + bob: Account(nonce=1, balance=0), + charlie: Account(nonce=1, balance=0), + dan: Account(nonce=1, balance=0), + eunice: Account(nonce=1, balance=0), + target: target_post, + }, + ) + + def test_bal_cross_block_ripemd160_state_leak( pre: Alloc, blockchain_test: BlockchainTestFiller, 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 73eaebe3c3..63fbb9a7e9 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -75,6 +75,11 @@ | `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_nested_delegatecall_storage_writes_net_zero` | Ensure BAL correctly filters net-zero storage changes across nested DELEGATECALL frames | Parametrized by nesting depth (1-3). Root contract has slot 0 = 1. Each frame writes a different intermediate value via DELEGATECALL chain, deepest frame writes back to original value (1). Example depth=2: 1 → 2 → 3 → 1 | BAL **MUST** include root contract with `storage_reads` for slot 0 but **MUST NOT** include `storage_changes` (net-zero). All delegate contracts **MUST** have empty changes. Tests that frame merging correctly removes parent's intermediate writes when child reverts to pre-tx value. | ✅ 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_cross_tx_storage_chain` | Verify clients apply BAL state changes from prior transactions before executing later transactions in the same block. Each later Tx depends on the two preceding writes (Fibonacci-style), so any tx skipped or run against pre-block state cascades into a wrong slot value and a different state root. | Fixed `chain_length=8`. Single branching contract: Tx i with `i<2` seeds slot i with `1`; Tx i with `i>=2` writes `slot[i] = SLOAD(i-1) + SLOAD(i-2)`. | BAL **MUST** include the contract with `storage_changes` for each slot i (post=`fib(i)` at `block_access_index=i+1`). Post-state: slots 0-7 equal `[1, 1, 2, 3, 5, 8, 13, 21]`. | ✅ Completed | +| `test_bal_cross_tx_deploy_then_call` | Verify clients apply Tx1's CREATE to their state view before executing Tx2's CALL in the same block. A client that parallelizes Tx2 without applying Tx1's `code_changes` would hit an empty account, the CALL would no-op, and slot 0 would remain 0. Parametrized over `@pytest.mark.with_all_create_opcodes` (CREATE and CREATE2). | Tx1 (Alice) calls a factory which CREATE/CREATE2s a contract whose runtime is `SSTORE(0, 0x42) + STOP` at a deterministic address. Tx2 (Bob) CALLs that address directly. | BAL **MUST** include the target contract with `nonce_changes` and `code_changes` at `block_access_index=1` (the deployment) and `storage_changes` for slot 0 (post=`0x42`) at `block_access_index=2` (Tx2's CALL through the deployed runtime). Post-state: target contract has runtime code and `slot[0] == 0x42`. | ✅ Completed | +| `test_bal_cross_tx_balance_dependency` | Verify clients apply Tx1's balance change before executing Tx2 in the same block. A client that parallelizes Tx2 without applying Tx1's `balance_changes` would record the pre-block balance via `SELFBALANCE`, yielding a different state root. | Tx1 (Alice) sends `10**18` wei to a branching contract (empty calldata → STOP). Tx2 (Bob) calls the same contract with non-empty calldata, taking the `SSTORE(0, SELFBALANCE)` path. | BAL **MUST** include the contract with `balance_changes` at `block_access_index=1` (post=`10**18`) and `storage_changes` for slot 0 (post=`10**18`) at `block_access_index=2`. Post-state: `contract.balance == contract.storage[0] == 10**18` proves Tx2 observed Tx1's balance change. | ✅ Completed | +| `test_bal_cross_tx_7702_delegation_then_call` | Verify clients apply Tx1's EIP-7702 delegation before later txs CALL the now-delegated EOA. Three-tx chain forces clients to apply both the code-install and each intermediate storage increment for the final value to be correct. | Tx1 (Relayer): sponsors an EIP-7702 auth that delegates Alice to a `SSTORE(0, SLOAD(0) + 1)` counter contract. Tx2 (Bob) and Tx3 (Charlie): both CALL Alice, which dispatches to the counter and increments her slot 0. | BAL **MUST** include Alice with `code_changes` at `block_access_index=1` (delegation designator), `nonce_changes` at index 1, and two `storage_changes` for slot 0 (post=1 at index 2, post=2 at index 3). Post-state: Alice has the delegation code and `slot[0] == 2`. | ✅ Completed | +| `test_bal_cross_tx_funding_chain` | Verify clients apply each tx's BAL `balance_changes` to the next sender's funds check. Five-tx chain across distinct senders, each intermediate starts empty and depends on the prior tx to be solvent. A parallelizing client validating any later tx against pre-block state would see zero balance on its sender and wrongly reject the block. Parametrized over `success` and `oog_minus_1` (eunice's tx OOGs at exact gas minus one). | Tx1 alice→bob, Tx2 bob→charlie, Tx3 charlie→dan, Tx4 dan→eunice. Each forwards exactly the next sender's upfront cost. Tx5 eunice→target with runtime `SSTORE(0, 0xC0FFEE)`. | BAL **MUST** include nonce/balance changes for each EOA at its tx's `block_access_index`, plus target's `storage_changes` at index 5 (`success`) or `storage_reads=[0]` (`oog_minus_1`). Post-state: all five EOAs end with `balance=0`; target has `slot[0]==0xC0FFEE` in `success` and empty storage in `oog_minus_1`. | ✅ Completed | | `test_bal_intra_tx_multiple_sstores_same_slot` | Ensure BAL coalesces consecutive SSTOREs to the same slot within one tx into a single storage change with the final post-value | Contract executes `SSTORE(0x01, 0xAA) + SSTORE(0x01, 0xBB) + SSTORE(0x01, 0xCC)` in one tx. Parametrized by `pre_value`: `slot_starts_empty` (0x00), `slot_starts_nonzero` (0x11), `intermediate_equals_pre` (0xBB, where the second write transiently matches the pre-state). | BAL **MUST** include contract with slot `0x01` having exactly one `slot_changes` entry: `txIndex=1, post_value=0xCC`. Intermediate values `0xAA` and `0xBB` **MUST NOT** appear as separate entries (enforced via `absent_values`). | ✅ Completed | | `test_bal_intra_tx_sstores_same_slot_net_zero` | Ensure BAL filters net-zero result when multiple SSTOREs to the same slot occur within one tx | Parametrized: `nonzero_pre_returns_to_pre` (pre=0xCC, writes 0xAA→0xBB→0xCC) and `empty_pre_ephemeral_writes` (pre=0x00, writes 0xAA→0xBB→0x00). Final value equals pre-state in both cases. | BAL **MUST** include contract with slot `0x01` in `storage_reads` (slot was accessed) and **MUST NOT** include slot `0x01` in `storage_changes` (net-zero). | ✅ 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 | From d021da230aff98687f6a823a808591db87ac390e Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 13 May 2026 12:24:04 -0600 Subject: [PATCH 2/3] chore: move 7702 test to 7702 BAL test file --- .../test_block_access_lists.py | 99 ------------------- .../test_block_access_lists_eip7702.py | 98 ++++++++++++++++++ .../test_cases.md | 2 +- 3 files changed, 99 insertions(+), 100 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 cf3affaaab..b10bb819d4 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 @@ -32,7 +32,6 @@ compute_create_address, ) -from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .spec import ref_spec_7928 REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path @@ -2451,104 +2450,6 @@ def test_bal_cross_tx_balance_dependency( ) -def test_bal_cross_tx_7702_delegation_then_call( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -) -> None: - """ - Verify clients apply Tx1's EIP-7702 delegation before later txs CALL - the now-delegated EOA. Tx1 installs a delegation designator on `alice` - pointing to a contract that increments slot 0. Tx2 and Tx3 CALL alice, - so each later tx must observe both the installed code (Tx1) and the - prior increment (the immediately preceding tx) for the final slot 0 - to read 2. A client that parallelizes any tx against pre-block state - would see no code or a stale counter, yielding a different state root. - """ - alice = pre.fund_eoa() - bob = pre.fund_eoa() - charlie = pre.fund_eoa() - relayer = pre.fund_eoa() - - # Delegation target: increments slot 0 each time it's invoked. - counter = pre.deploy_contract( - code=Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)), - ) - - tx_delegate = Transaction( - sender=relayer, - to=bob, - value=0, - gas_limit=1_000_000, - gas_price=0xA, - authorization_list=[ - AuthorizationTuple( - address=counter, - nonce=0, - signer=alice, - ) - ], - ) - tx_call_1 = Transaction( - sender=bob, - to=alice, - gas_limit=200_000, - gas_price=0xA, - ) - tx_call_2 = Transaction( - sender=charlie, - to=alice, - gas_limit=200_000, - gas_price=0xA, - ) - - block = Block( - txs=[tx_delegate, tx_call_1, tx_call_2], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[ - BalNonceChange(block_access_index=1, post_nonce=1), - ], - code_changes=[ - BalCodeChange( - block_access_index=1, - new_code=Spec7702.delegation_designation(counter), - ) - ], - storage_changes=[ - BalStorageSlot( - slot=0, - slot_changes=[ - BalStorageChange( - block_access_index=2, post_value=1 - ), - BalStorageChange( - block_access_index=3, post_value=2 - ), - ], - ), - ], - ), - # Delegation target: loaded as execution target on each - # CALL to alice, but mutations land in alice's storage. - counter: BalAccountExpectation.empty(), - }, - ), - ) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account( - nonce=1, - code=Spec7702.delegation_designation(counter), - storage={0: 2}, - ), - }, - ) - - @pytest.mark.parametrize( "eunice_oog", [False, True], 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 8824b61b97..9a776f797f 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 @@ -827,6 +827,104 @@ def test_bal_7702_multi_hop_delegation_chain( ) +def test_bal_7702_cross_tx_delegation_then_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Verify clients apply Tx1's EIP-7702 delegation before later txs CALL + the now-delegated EOA. Tx1 installs a delegation designator on `alice` + pointing to a contract that increments slot 0. Tx2 and Tx3 CALL alice, + so each later tx must observe both the installed code (Tx1) and the + prior increment (the immediately preceding tx) for the final slot 0 + to read 2. A client that parallelizes any tx against pre-block state + would see no code or a stale counter, yielding a different state root. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa() + charlie = pre.fund_eoa() + relayer = pre.fund_eoa() + + # Delegation target: increments slot 0 each time it's invoked. + counter = pre.deploy_contract( + code=Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)), + ) + + tx_delegate = Transaction( + sender=relayer, + to=bob, + value=0, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=counter, + nonce=0, + signer=alice, + ) + ], + ) + tx_call_1 = Transaction( + sender=bob, + to=alice, + gas_limit=200_000, + gas_price=0xA, + ) + tx_call_2 = Transaction( + sender=charlie, + to=alice, + gas_limit=200_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx_delegate, tx_call_1, tx_call_2], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + ], + code_changes=[ + BalCodeChange( + block_access_index=1, + new_code=Spec7702.delegation_designation(counter), + ) + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=2, post_value=1 + ), + BalStorageChange( + block_access_index=3, post_value=2 + ), + ], + ), + ], + ), + # Delegation target: loaded as execution target on each + # CALL to alice, but mutations land in alice's storage. + counter: BalAccountExpectation.empty(), + }, + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(counter), + storage={0: 2}, + ), + }, + ) + + def test_bal_7702_null_address_delegation_no_code_change( pre: Alloc, blockchain_test: BlockchainTestFiller, 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 63fbb9a7e9..355eddb3e2 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -78,7 +78,7 @@ | `test_bal_cross_tx_storage_chain` | Verify clients apply BAL state changes from prior transactions before executing later transactions in the same block. Each later Tx depends on the two preceding writes (Fibonacci-style), so any tx skipped or run against pre-block state cascades into a wrong slot value and a different state root. | Fixed `chain_length=8`. Single branching contract: Tx i with `i<2` seeds slot i with `1`; Tx i with `i>=2` writes `slot[i] = SLOAD(i-1) + SLOAD(i-2)`. | BAL **MUST** include the contract with `storage_changes` for each slot i (post=`fib(i)` at `block_access_index=i+1`). Post-state: slots 0-7 equal `[1, 1, 2, 3, 5, 8, 13, 21]`. | ✅ Completed | | `test_bal_cross_tx_deploy_then_call` | Verify clients apply Tx1's CREATE to their state view before executing Tx2's CALL in the same block. A client that parallelizes Tx2 without applying Tx1's `code_changes` would hit an empty account, the CALL would no-op, and slot 0 would remain 0. Parametrized over `@pytest.mark.with_all_create_opcodes` (CREATE and CREATE2). | Tx1 (Alice) calls a factory which CREATE/CREATE2s a contract whose runtime is `SSTORE(0, 0x42) + STOP` at a deterministic address. Tx2 (Bob) CALLs that address directly. | BAL **MUST** include the target contract with `nonce_changes` and `code_changes` at `block_access_index=1` (the deployment) and `storage_changes` for slot 0 (post=`0x42`) at `block_access_index=2` (Tx2's CALL through the deployed runtime). Post-state: target contract has runtime code and `slot[0] == 0x42`. | ✅ Completed | | `test_bal_cross_tx_balance_dependency` | Verify clients apply Tx1's balance change before executing Tx2 in the same block. A client that parallelizes Tx2 without applying Tx1's `balance_changes` would record the pre-block balance via `SELFBALANCE`, yielding a different state root. | Tx1 (Alice) sends `10**18` wei to a branching contract (empty calldata → STOP). Tx2 (Bob) calls the same contract with non-empty calldata, taking the `SSTORE(0, SELFBALANCE)` path. | BAL **MUST** include the contract with `balance_changes` at `block_access_index=1` (post=`10**18`) and `storage_changes` for slot 0 (post=`10**18`) at `block_access_index=2`. Post-state: `contract.balance == contract.storage[0] == 10**18` proves Tx2 observed Tx1's balance change. | ✅ Completed | -| `test_bal_cross_tx_7702_delegation_then_call` | Verify clients apply Tx1's EIP-7702 delegation before later txs CALL the now-delegated EOA. Three-tx chain forces clients to apply both the code-install and each intermediate storage increment for the final value to be correct. | Tx1 (Relayer): sponsors an EIP-7702 auth that delegates Alice to a `SSTORE(0, SLOAD(0) + 1)` counter contract. Tx2 (Bob) and Tx3 (Charlie): both CALL Alice, which dispatches to the counter and increments her slot 0. | BAL **MUST** include Alice with `code_changes` at `block_access_index=1` (delegation designator), `nonce_changes` at index 1, and two `storage_changes` for slot 0 (post=1 at index 2, post=2 at index 3). Post-state: Alice has the delegation code and `slot[0] == 2`. | ✅ Completed | +| `test_bal_7702_cross_tx_delegation_then_call` | Verify clients apply Tx1's EIP-7702 delegation before later txs CALL the now-delegated EOA. Three-tx chain forces clients to apply both the code-install and each intermediate storage increment for the final value to be correct. | Tx1 (Relayer): sponsors an EIP-7702 auth that delegates Alice to a `SSTORE(0, SLOAD(0) + 1)` counter contract. Tx2 (Bob) and Tx3 (Charlie): both CALL Alice, which dispatches to the counter and increments her slot 0. | BAL **MUST** include Alice with `code_changes` at `block_access_index=1` (delegation designator), `nonce_changes` at index 1, and two `storage_changes` for slot 0 (post=1 at index 2, post=2 at index 3). Post-state: Alice has the delegation code and `slot[0] == 2`. | ✅ Completed | | `test_bal_cross_tx_funding_chain` | Verify clients apply each tx's BAL `balance_changes` to the next sender's funds check. Five-tx chain across distinct senders, each intermediate starts empty and depends on the prior tx to be solvent. A parallelizing client validating any later tx against pre-block state would see zero balance on its sender and wrongly reject the block. Parametrized over `success` and `oog_minus_1` (eunice's tx OOGs at exact gas minus one). | Tx1 alice→bob, Tx2 bob→charlie, Tx3 charlie→dan, Tx4 dan→eunice. Each forwards exactly the next sender's upfront cost. Tx5 eunice→target with runtime `SSTORE(0, 0xC0FFEE)`. | BAL **MUST** include nonce/balance changes for each EOA at its tx's `block_access_index`, plus target's `storage_changes` at index 5 (`success`) or `storage_reads=[0]` (`oog_minus_1`). Post-state: all five EOAs end with `balance=0`; target has `slot[0]==0xC0FFEE` in `success` and empty storage in `oog_minus_1`. | ✅ Completed | | `test_bal_intra_tx_multiple_sstores_same_slot` | Ensure BAL coalesces consecutive SSTOREs to the same slot within one tx into a single storage change with the final post-value | Contract executes `SSTORE(0x01, 0xAA) + SSTORE(0x01, 0xBB) + SSTORE(0x01, 0xCC)` in one tx. Parametrized by `pre_value`: `slot_starts_empty` (0x00), `slot_starts_nonzero` (0x11), `intermediate_equals_pre` (0xBB, where the second write transiently matches the pre-state). | BAL **MUST** include contract with slot `0x01` having exactly one `slot_changes` entry: `txIndex=1, post_value=0xCC`. Intermediate values `0xAA` and `0xBB` **MUST NOT** appear as separate entries (enforced via `absent_values`). | ✅ Completed | | `test_bal_intra_tx_sstores_same_slot_net_zero` | Ensure BAL filters net-zero result when multiple SSTOREs to the same slot occur within one tx | Parametrized: `nonzero_pre_returns_to_pre` (pre=0xCC, writes 0xAA→0xBB→0xCC) and `empty_pre_ephemeral_writes` (pre=0x00, writes 0xAA→0xBB→0x00). Final value equals pre-state in both cases. | BAL **MUST** include contract with slot `0x01` in `storage_reads` (slot was accessed) and **MUST NOT** include slot `0x01` in `storage_changes` (net-zero). | ✅ Completed | From 98fc3410670b0fcc9335652af596b2d31eb461b6 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 13 May 2026 17:31:55 -0600 Subject: [PATCH 3/3] feat(test): extend test cases based on comments on PR #2851 --- .../test_block_access_lists.py | 125 ++++++++++++++---- .../test_cases.md | 4 +- 2 files changed, 103 insertions(+), 26 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 b10bb819d4..8dce480a1e 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 @@ -28,6 +28,7 @@ Initcode, Op, Transaction, + TransactionException, add_kzg_version, compute_create_address, ) @@ -2376,18 +2377,27 @@ def test_bal_cross_tx_deploy_then_call( ) +@pytest.mark.parametrize( + "funding_method", + ["direct_call", "selfdestruct"], +) def test_bal_cross_tx_balance_dependency( pre: Alloc, blockchain_test: BlockchainTestFiller, + funding_method: str, ) -> None: """ Verify clients apply Tx1's balance change before executing Tx2 in - the same block. Tx1 sends value to a contract; Tx2 invokes the + the same block. Tx1 routes value into a contract; Tx2 invokes the contract which records its `SELFBALANCE` to storage. A client that parallelizes Tx2 without applying Tx1's `balance_changes` would - record the pre-block balance, yielding a different state root. + record the pre-block balance, yielding a different state root. The + `selfdestruct` variant routes the funds via SELFDESTRUCT from a + pre-funded killer contract so the recipient's bytecode never runs + in Tx1 — catching any client optimization that ties balance + tracking to code execution. """ - transferred = 10**18 + transferred = 1 alice = pre.fund_eoa() bob = pre.fund_eoa() @@ -2401,12 +2411,34 @@ def test_bal_cross_tx_balance_dependency( ), ) - tx_send = Transaction( - sender=alice, - to=contract, - value=transferred, - gas_limit=100_000, - ) + if funding_method == "direct_call": + tx_send = Transaction( + sender=alice, + to=contract, + value=transferred, + gas_limit=100_000, + ) + send_expectations: dict = {} + elif funding_method == "selfdestruct": + killer = pre.deploy_contract( + code=Op.SELFDESTRUCT(contract), + balance=transferred, + ) + tx_send = Transaction( + sender=alice, + to=killer, + gas_limit=100_000, + ) + send_expectations = { + killer: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(block_access_index=1, post_balance=0), + ], + ), + } + else: + raise ValueError(f"unknown funding_method: {funding_method}") + tx_read = Transaction( sender=bob, to=contract, @@ -2432,6 +2464,7 @@ def test_bal_cross_tx_balance_dependency( ), ], ), + **send_expectations, } blockchain_test( @@ -2451,26 +2484,36 @@ def test_bal_cross_tx_balance_dependency( @pytest.mark.parametrize( - "eunice_oog", - [False, True], - ids=["success", "oog_minus_1"], + "eunice_outcome", + [ + pytest.param("success", id="success"), + pytest.param("oog_minus_1", id="oog_minus_1"), + pytest.param( + "insufficient_funds", + id="insufficient_funds", + marks=pytest.mark.exception_test, + ), + ], ) def test_bal_cross_tx_funding_chain( pre: Alloc, blockchain_test: BlockchainTestFiller, fork: Fork, - eunice_oog: bool, + eunice_outcome: str, ) -> None: """ Funding chain: alice → bob → charlie → dan → eunice → target. Each intermediate starts empty and must receive the prior tx's forwarded value to afford its own upfront gas + outgoing transfer. A client that parallelizes any later tx against pre-block state would see a - zero balance on its sender and wrongly reject the block. In - `oog_minus_1`, eunice's tx is funded with exactly `gas_limit - 1` - worth of gas so the SSTORE OOGs at the boundary: the funding chain - must still resolve, eunice's nonce and balance updates persist, but - the target sees a `storage_reads` entry instead of a write. + zero balance on its sender and wrongly reject the block. The + `oog_minus_1` variant funds eunice with exactly `gas_limit - 1` + worth of gas so her SSTORE OOGs at the boundary (target's BAL flips + from `storage_changes` to `storage_reads`). The `insufficient_funds` + variant has dan forward one wei short of eunice's `gas_limit * + gas_price`, so eunice's tx is rejected pre-execution and the entire + block MUST be rejected with `INSUFFICIENT_ACCOUNT_FUNDS` — a sanity + check on the off-by-one boundary of the upfront balance check. """ gas_price = 0xA @@ -2482,13 +2525,23 @@ def test_bal_cross_tx_funding_chain( intrinsic_calc = fork.transaction_intrinsic_cost_calculator() intrinsic_gas = intrinsic_calc() eunice_exact_gas = intrinsic_gas + target_code.gas_cost(fork) - eunice_gas_limit = eunice_exact_gas - 1 if eunice_oog else eunice_exact_gas + eunice_gas_limit = ( + eunice_exact_gas - 1 + if eunice_outcome == "oog_minus_1" + else eunice_exact_gas + ) eunice_upfront = eunice_gas_limit * gas_price transfer_cost = intrinsic_gas * gas_price - # Each sender (including alice) receives or starts with exactly what - # the next forward + its own gas demands; everyone ends at zero. - dan_value = eunice_upfront + # Each sender (including alice) starts with or receives exactly what + # the next forward + its own gas demands; everyone ends at zero in + # the success/oog variants. `insufficient_funds` shorts eunice by + # one wei via dan, leaving her unable to cover upfront gas. + dan_value = ( + eunice_upfront - 1 + if eunice_outcome == "insufficient_funds" + else eunice_upfront + ) charlie_value = transfer_cost + dan_value bob_value = transfer_cost + charlie_value alice_value = transfer_cost + bob_value @@ -2500,6 +2553,12 @@ def test_bal_cross_tx_funding_chain( dan = pre.fund_eoa(amount=0) eunice = pre.fund_eoa(amount=0) + eunice_error = ( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS + if eunice_outcome == "insufficient_funds" + else None + ) + txs = [ Transaction( sender=alice, @@ -2534,10 +2593,26 @@ def test_bal_cross_tx_funding_chain( to=target, gas_limit=eunice_gas_limit, gas_price=gas_price, + error=eunice_error, ), ] - if eunice_oog: + if eunice_outcome == "insufficient_funds": + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=txs, + exception=( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS + ), + ) + ], + post={}, + ) + return + + if eunice_outcome == "oog_minus_1": target_bal = BalAccountExpectation( storage_reads=[0], nonce_changes=[], @@ -2546,7 +2621,7 @@ def test_bal_cross_tx_funding_chain( storage_changes=[], ) target_post = Account(storage={}) - else: + elif eunice_outcome == "success": target_bal = BalAccountExpectation( storage_changes=[ BalStorageSlot( @@ -2564,6 +2639,8 @@ def test_bal_cross_tx_funding_chain( storage_reads=[], ) target_post = Account(storage={0: 0xC0FFEE}) + else: + raise ValueError(f"unknown eunice_outcome: {eunice_outcome}") account_expectations = { alice: BalAccountExpectation( 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 355eddb3e2..7d8faf6448 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -77,9 +77,9 @@ | `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_cross_tx_storage_chain` | Verify clients apply BAL state changes from prior transactions before executing later transactions in the same block. Each later Tx depends on the two preceding writes (Fibonacci-style), so any tx skipped or run against pre-block state cascades into a wrong slot value and a different state root. | Fixed `chain_length=8`. Single branching contract: Tx i with `i<2` seeds slot i with `1`; Tx i with `i>=2` writes `slot[i] = SLOAD(i-1) + SLOAD(i-2)`. | BAL **MUST** include the contract with `storage_changes` for each slot i (post=`fib(i)` at `block_access_index=i+1`). Post-state: slots 0-7 equal `[1, 1, 2, 3, 5, 8, 13, 21]`. | ✅ Completed | | `test_bal_cross_tx_deploy_then_call` | Verify clients apply Tx1's CREATE to their state view before executing Tx2's CALL in the same block. A client that parallelizes Tx2 without applying Tx1's `code_changes` would hit an empty account, the CALL would no-op, and slot 0 would remain 0. Parametrized over `@pytest.mark.with_all_create_opcodes` (CREATE and CREATE2). | Tx1 (Alice) calls a factory which CREATE/CREATE2s a contract whose runtime is `SSTORE(0, 0x42) + STOP` at a deterministic address. Tx2 (Bob) CALLs that address directly. | BAL **MUST** include the target contract with `nonce_changes` and `code_changes` at `block_access_index=1` (the deployment) and `storage_changes` for slot 0 (post=`0x42`) at `block_access_index=2` (Tx2's CALL through the deployed runtime). Post-state: target contract has runtime code and `slot[0] == 0x42`. | ✅ Completed | -| `test_bal_cross_tx_balance_dependency` | Verify clients apply Tx1's balance change before executing Tx2 in the same block. A client that parallelizes Tx2 without applying Tx1's `balance_changes` would record the pre-block balance via `SELFBALANCE`, yielding a different state root. | Tx1 (Alice) sends `10**18` wei to a branching contract (empty calldata → STOP). Tx2 (Bob) calls the same contract with non-empty calldata, taking the `SSTORE(0, SELFBALANCE)` path. | BAL **MUST** include the contract with `balance_changes` at `block_access_index=1` (post=`10**18`) and `storage_changes` for slot 0 (post=`10**18`) at `block_access_index=2`. Post-state: `contract.balance == contract.storage[0] == 10**18` proves Tx2 observed Tx1's balance change. | ✅ Completed | +| `test_bal_cross_tx_balance_dependency` | Verify clients apply Tx1's balance change before executing Tx2 in the same block. A client that parallelizes Tx2 without applying Tx1's `balance_changes` would record the pre-block balance via `SELFBALANCE`, yielding a different state root. Parametrized over `direct_call` and `selfdestruct` funding paths so a client can't tie balance tracking to recipient-code execution. | Tx1 (Alice) routes `1` wei into a branching contract (empty calldata → STOP). In `direct_call`, alice sends value directly; in `selfdestruct`, alice calls a pre-funded killer contract that `SELFDESTRUCT`s to the recipient (recipient bytecode never runs in Tx1). Tx2 (Bob) calls the contract with non-empty calldata, taking the `SSTORE(0, SELFBALANCE)` path. | BAL **MUST** include the contract with `balance_changes` at `block_access_index=1` (post=`1`) and `storage_changes` for slot 0 (post=`1`) at `block_access_index=2`. In `selfdestruct`, BAL also includes the killer with `balance_changes` (post=`0`) at index 1. Post-state: `contract.balance == contract.storage[0] == 1` proves Tx2 observed Tx1's balance change. | ✅ Completed | | `test_bal_7702_cross_tx_delegation_then_call` | Verify clients apply Tx1's EIP-7702 delegation before later txs CALL the now-delegated EOA. Three-tx chain forces clients to apply both the code-install and each intermediate storage increment for the final value to be correct. | Tx1 (Relayer): sponsors an EIP-7702 auth that delegates Alice to a `SSTORE(0, SLOAD(0) + 1)` counter contract. Tx2 (Bob) and Tx3 (Charlie): both CALL Alice, which dispatches to the counter and increments her slot 0. | BAL **MUST** include Alice with `code_changes` at `block_access_index=1` (delegation designator), `nonce_changes` at index 1, and two `storage_changes` for slot 0 (post=1 at index 2, post=2 at index 3). Post-state: Alice has the delegation code and `slot[0] == 2`. | ✅ Completed | -| `test_bal_cross_tx_funding_chain` | Verify clients apply each tx's BAL `balance_changes` to the next sender's funds check. Five-tx chain across distinct senders, each intermediate starts empty and depends on the prior tx to be solvent. A parallelizing client validating any later tx against pre-block state would see zero balance on its sender and wrongly reject the block. Parametrized over `success` and `oog_minus_1` (eunice's tx OOGs at exact gas minus one). | Tx1 alice→bob, Tx2 bob→charlie, Tx3 charlie→dan, Tx4 dan→eunice. Each forwards exactly the next sender's upfront cost. Tx5 eunice→target with runtime `SSTORE(0, 0xC0FFEE)`. | BAL **MUST** include nonce/balance changes for each EOA at its tx's `block_access_index`, plus target's `storage_changes` at index 5 (`success`) or `storage_reads=[0]` (`oog_minus_1`). Post-state: all five EOAs end with `balance=0`; target has `slot[0]==0xC0FFEE` in `success` and empty storage in `oog_minus_1`. | ✅ Completed | +| `test_bal_cross_tx_funding_chain` | Verify clients apply each tx's BAL `balance_changes` to the next sender's funds check. Five-tx chain across distinct senders, each intermediate starts empty and depends on the prior tx to be solvent. A parallelizing client validating any later tx against pre-block state would see zero balance on its sender and wrongly reject the block. Parametrized over `success`, `oog_minus_1` (eunice's tx OOGs at exact gas minus one), and `insufficient_funds` (dan forwards one wei short so eunice's upfront balance check fails, block rejected with `INSUFFICIENT_ACCOUNT_FUNDS`). | Tx1 alice→bob, Tx2 bob→charlie, Tx3 charlie→dan, Tx4 dan→eunice. Each forwards exactly the next sender's upfront cost. Tx5 eunice→target with runtime `SSTORE(0, 0xC0FFEE)`. | For `success`/`oog_minus_1`: BAL **MUST** include nonce/balance changes for each EOA at its tx's `block_access_index`, plus target's `storage_changes` at index 5 (`success`) or `storage_reads=[0]` (`oog_minus_1`). Post-state: all five EOAs end with `balance=0`; target has `slot[0]==0xC0FFEE` in `success` and empty storage in `oog_minus_1`. For `insufficient_funds`: block **MUST** be rejected with `TransactionException.INSUFFICIENT_ACCOUNT_FUNDS`. | ✅ Completed | | `test_bal_intra_tx_multiple_sstores_same_slot` | Ensure BAL coalesces consecutive SSTOREs to the same slot within one tx into a single storage change with the final post-value | Contract executes `SSTORE(0x01, 0xAA) + SSTORE(0x01, 0xBB) + SSTORE(0x01, 0xCC)` in one tx. Parametrized by `pre_value`: `slot_starts_empty` (0x00), `slot_starts_nonzero` (0x11), `intermediate_equals_pre` (0xBB, where the second write transiently matches the pre-state). | BAL **MUST** include contract with slot `0x01` having exactly one `slot_changes` entry: `txIndex=1, post_value=0xCC`. Intermediate values `0xAA` and `0xBB` **MUST NOT** appear as separate entries (enforced via `absent_values`). | ✅ Completed | | `test_bal_intra_tx_sstores_same_slot_net_zero` | Ensure BAL filters net-zero result when multiple SSTOREs to the same slot occur within one tx | Parametrized: `nonzero_pre_returns_to_pre` (pre=0xCC, writes 0xAA→0xBB→0xCC) and `empty_pre_ephemeral_writes` (pre=0x00, writes 0xAA→0xBB→0x00). Final value equals pre-state in both cases. | BAL **MUST** include contract with slot `0x01` in `storage_reads` (slot was accessed) and **MUST NOT** include slot `0x01` in `storage_changes` (net-zero). | ✅ 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 |