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 e7d99ad0935..5b22e2e77a2 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,11 +28,13 @@ Header, Initcode, Op, + StateTestFiller, Transaction, TransactionException, add_kzg_version, compute_create_address, ) +from execution_testing import Macros as Om from .spec import ref_spec_7928 @@ -581,6 +583,118 @@ def test_bal_block_rewards( ) +@pytest.mark.parametrize( + "same_tx", [False, True], ids=["pre_deploy", "same_tx"] +) +def test_bal_selfdestruct_to_coinbase( + pre: Alloc, + state_test: StateTestFiller, + fork: Fork, + same_tx: bool, +) -> None: + """ + Ensure BAL records SELFDESTRUCT when the beneficiary is the coinbase. + + Post-Cancun (EIP-6780) the contract is only actually destroyed when + created in the same tx; the pre-deployed path only transfers balance + and preserves the contract. Both shapes must appear in BAL. + """ + alice = pre.fund_eoa() + coinbase = pre.fund_eoa(amount=0) + victim_balance = 100 + victim_code = Op.SELFDESTRUCT(Op.COINBASE) + + # Match gas_price to base_fee so the priority-fee tip is zero; + # coinbase's BAL entry then carries only the SELFDESTRUCT transfer. + base_fee_per_gas = 7 + env = Environment( + base_fee_per_gas=base_fee_per_gas, fee_recipient=coinbase + ) + + tx_gas_limit = fork.transaction_gas_limit_cap() + account_expectations: dict[Address, BalAccountExpectation] + + if same_tx: + initcode = Initcode(deploy_code=victim_code) + factory_code = Om.MSTORE(initcode, 0) + Op.CALL( + gas=Op.GAS, + address=Op.CREATE( + value=victim_balance, offset=0, size=len(initcode) + ), + ) + factory = pre.deploy_contract( + code=factory_code, balance=victim_balance + ) + victim = compute_create_address(address=factory, nonce=1) + tx_target = factory + post = { + factory: Account(balance=0), + victim: Account.NONEXISTENT, + coinbase: Account(balance=victim_balance), + } + account_expectations = { + factory: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=2) + ], + balance_changes=[ + BalBalanceChange(block_access_index=1, post_balance=0) + ], + ), + # Created and destroyed in the same tx — empty changes. + victim: BalAccountExpectation.empty(), + coinbase: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=victim_balance + ) + ], + ), + } + else: + victim = pre.deploy_contract(code=victim_code, balance=victim_balance) + tx_target = victim + # Pre-deployed and not same-tx: post-Cancun preserves the contract. + post = { + victim: Account(balance=0, code=victim_code), + coinbase: Account(balance=victim_balance), + } + account_expectations = { + victim: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(block_access_index=1, post_balance=0) + ], + ), + coinbase: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + block_access_index=1, post_balance=victim_balance + ) + ], + ), + } + + tx = Transaction( + sender=alice, + to=tx_target, + gas_limit=tx_gas_limit, + gas_price=base_fee_per_gas, + ) + + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + blockchain_test_header_verify=Header( + base_fee_per_gas=base_fee_per_gas + ), + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations, + ), + ) + + def test_bal_2930_account_listed_but_untouched( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -1239,6 +1353,188 @@ def test_bal_aborted_account_access( ) +@pytest.mark.parametrize( + "inner_action", + ["sstore", "sload", "balance", "extcodesize"], +) +@pytest.mark.parametrize( + "outer_abort", + [ + pytest.param(Op.REVERT(0, 0), id="outer_revert"), + pytest.param(Op.INVALID, id="outer_invalid"), + ], +) +def test_bal_parent_revert_state_access( + pre: Alloc, + state_test: StateTestFiller, + fork: Fork, + inner_action: str, + outer_abort: Op, +) -> None: + """Ensure BAL captures child-frame state access when the parent reverts.""" + alice = pre.fund_eoa() + extra_target = pre.deploy_contract(code=Op.STOP) + + if inner_action == "sstore": + # Write demoted to read by parent revert; pre-set 0xDEAD so the + # post-state confirms the slot was unchanged. + inner = pre.deploy_contract( + code=Op.SSTORE(1, 0x42) + Op.STOP, + storage={1: 0xDEAD}, + ) + elif inner_action == "sload": + inner = pre.deploy_contract( + code=Op.POP(Op.SLOAD(1)) + Op.STOP, + storage={1: 0xDEAD}, + ) + elif inner_action == "balance": + inner = pre.deploy_contract( + code=Op.POP(Op.BALANCE(extra_target)) + Op.STOP + ) + elif inner_action == "extcodesize": + inner = pre.deploy_contract( + code=Op.POP(Op.EXTCODESIZE(extra_target)) + Op.STOP + ) + else: + raise ValueError(f"unknown inner_action: {inner_action}") + outer = pre.deploy_contract( + code=Op.CALL(gas=Op.GAS, address=inner) + outer_abort + ) + + tx = Transaction( + sender=alice, to=outer, gas_limit=fork.transaction_gas_limit_cap() + ) + + account_expectations: dict[Address, BalAccountExpectation] + if inner_action in ("sstore", "sload"): + account_expectations = { + inner: BalAccountExpectation(storage_reads=[1]), + } + elif inner_action in ("balance", "extcodesize"): + account_expectations = { + inner: BalAccountExpectation.empty(), + extra_target: BalAccountExpectation.empty(), + } + else: + raise ValueError(f"unknown inner_action: {inner_action}") + + post: dict = {alice: Account(nonce=1)} + if inner_action in ("sstore", "sload"): + # Slot 1 stays at its pre-state value: SSTORE demoted to read, + # SLOAD never mutates. + post[inner] = Account(storage={1: 0xDEAD}) + + state_test( + pre=pre, + post=post, + tx=tx, + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations, + ), + ) + + +@pytest.mark.parametrize( + "inner_op", + [pytest.param("call", id="call"), pytest.param("create", id="create")], +) +def test_bal_outer_revert_with_inner_insufficient_funds( + pre: Alloc, + state_test: StateTestFiller, + fork: Fork, + inner_op: str, +) -> None: + """ + Outer REVERT + inner CALL/CREATE that fails on insufficient funds. + + Inner writes two slots and then attempts a value-bearing CALL or + CREATE that fails (balance=0 < value). The opcode's state-touching + costs are charged before the balance check, so the failed CALL's + target stays in BAL with empty changes, while CREATE fails before + `track_address` and the would-be address does not appear at all. + Inner's writes demote to reads under outer's REVERT; the post-state + checks confirm none of the rolled-back state leaked through. + """ + alice = pre.fund_eoa() + slot_a, slot_b = 1, 2 + insufficient_value = 100 + + if inner_op == "call": + target = pre.deploy_contract(code=Op.STOP) + inner = pre.deploy_contract( + code=( + Op.SSTORE(slot_a, 0x42) + + Op.SSTORE( + slot_b, + Op.CALL( + gas=100_000, + address=target, + value=insufficient_value, + ), + ) + + Op.STOP + ), + balance=0, + ) + extra_account = target + extra_bal: BalAccountExpectation | None = BalAccountExpectation.empty() + extra_post: Account | None = Account(balance=0) + elif inner_op == "create": + initcode_bytes = bytes(Initcode(deploy_code=Op.STOP)) + inner = pre.deploy_contract( + code=( + Op.MSTORE(0, Op.PUSH32(initcode_bytes)) + + Op.SSTORE(slot_a, 0x42) + + Op.SSTORE( + slot_b, + Op.CREATE( + value=insufficient_value, + offset=32 - len(initcode_bytes), + size=len(initcode_bytes), + ), + ) + + Op.STOP + ), + balance=0, + ) + extra_account = compute_create_address(address=inner, nonce=1) + extra_bal = None + extra_post = Account.NONEXISTENT + else: + raise ValueError(f"unknown inner_op: {inner_op}") + + outer = pre.deploy_contract( + code=Op.CALL(gas=Op.GAS, address=inner) + Op.REVERT(0, 0) + ) + + tx = Transaction( + sender=alice, to=outer, gas_limit=fork.transaction_gas_limit_cap() + ) + + state_test( + pre=pre, + post={ + alice: Account(nonce=1), + outer: Account(balance=0, storage={}), + inner: Account(balance=0, storage={}), + extra_account: extra_post, + }, + tx=tx, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1) + ], + ), + outer: BalAccountExpectation.empty(), + inner: BalAccountExpectation(storage_reads=[slot_a, slot_b]), + extra_account: extra_bal, + }, + ), + ) + + def test_bal_fully_unmutated_account( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2379,6 +2675,168 @@ def test_bal_cross_tx_deploy_then_call( ) +@pytest.mark.parametrize( + "failure_mode", + [ + pytest.param("none", id="no_failure"), + pytest.param("collision", id="mid_chain_collision"), + pytest.param("oog", id="mid_chain_oog"), + ], +) +@pytest.mark.pre_alloc_mutable() +def test_bal_cross_tx_factory_nonce_create_chain( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, + failure_mode: str, +) -> None: + """ + Cross-tx CREATE chain: 8 senders share a factory whose CREATE + address derives solely from `factory.nonce`. `collision` and `oog` + test opposite parallelization hazards mid-chain — collision still + bumps factory.nonce (later txs slide forward), OOG does not (later + txs slide backward, reusing the OOG'd slot). + """ + chain_length = 8 + failure_index = 3 if failure_mode in ("collision", "oog") else None + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.CREATE(0, 0, Op.CALLDATASIZE) + + Op.STOP + ) + factory = pre.deploy_contract(code=factory_code) + factory_pre_nonce = 1 + + deploy_code = Op.STOP + initcode = Initcode(deploy_code=deploy_code) + collision_code = Op.PUSH1(0x42) + Op.STOP + + targets = [ + compute_create_address(address=factory, nonce=factory_pre_nonce + k) + for k in range(chain_length) + ] + + if failure_mode == "collision": + assert failure_index is not None + pre[targets[failure_index]] = Account(code=collision_code) + + sequence: list[dict] = [] + factory_nonce = factory_pre_nonce + for i in range(chain_length): + block_idx = i + 1 + if failure_mode == "oog" and i == failure_index: + sequence.append( + {"block_idx": block_idx, "target_idx": None, "deployed": False} + ) + else: + target_idx = factory_nonce - factory_pre_nonce + factory_nonce += 1 + deployed = not (failure_mode == "collision" and i == failure_index) + sequence.append( + { + "block_idx": block_idx, + "factory_post_nonce": factory_nonce, + "target_idx": target_idx, + "deployed": deployed, + } + ) + + senders = [pre.fund_eoa() for _ in range(chain_length)] + # OOG tx: intrinsic + 1 — valid to include but no gas to run CREATE. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=bytes(initcode), contract_creation=False, access_list=[] + ) + txs = [ + Transaction( + sender=senders[i], + to=factory, + data=initcode, + gas_limit=( + intrinsic + 1 + if failure_mode == "oog" and i == failure_index + else fork.transaction_gas_limit_cap() + ), + ) + for i in range(chain_length) + ] + + account_expectations: dict = { + senders[i]: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=i + 1, post_nonce=1) + ], + ) + for i in range(chain_length) + } + # Factory: only txs that bumped its nonce contribute entries. + account_expectations[factory] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=s["block_idx"], + post_nonce=s["factory_post_nonce"], + ) + for s in sequence + if s["target_idx"] is not None + ], + ) + for s in sequence: + if s["target_idx"] is None: + continue + target = targets[s["target_idx"]] + if s["deployed"]: + account_expectations[target] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=s["block_idx"], post_nonce=1 + ) + ], + code_changes=[ + BalCodeChange( + block_access_index=s["block_idx"], + new_code=deploy_code, + ) + ], + ) + else: + # Collision: accessed during EIP-684 check, no state change. + account_expectations[target] = BalAccountExpectation.empty() + + touched_target_idxs = { + s["target_idx"] for s in sequence if s["target_idx"] is not None + } + final_factory_nonce = factory_pre_nonce + len(touched_target_idxs) + post: dict = { + factory: Account(nonce=final_factory_nonce), + **{sender: Account(nonce=1) for sender in senders}, + } + for s in sequence: + if s["target_idx"] is None: + continue + target = targets[s["target_idx"]] + post[target] = ( + Account(nonce=1, code=deploy_code) + if s["deployed"] + else Account(code=collision_code) + ) + for k, target in enumerate(targets): + if k not in touched_target_idxs: + post[target] = Account.NONEXISTENT + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=txs, + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post=post, + ) + + @pytest.mark.parametrize( "funding_method", ["direct_call", "selfdestruct"], 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 434c432c2cf..08ab12a9e79 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 @@ -133,70 +133,97 @@ def test_bal_7702_delegation_create( @pytest.mark.parametrize( - "self_funded", + "sender_pattern", [ - pytest.param(False, id="sponsored"), - pytest.param(True, id="self_funded"), + pytest.param("sponsored", id="sponsored"), + pytest.param("self_funded", id="self_funded"), + pytest.param("sponsored_cross_sender", id="sponsored_cross_sender"), ], ) def test_bal_7702_delegation_update( pre: Alloc, blockchain_test: BlockchainTestFiller, - self_funded: bool, + sender_pattern: str, ) -> None: - """Ensure BAL captures update of existing EOA delegation.""" + """ + Ensure BAL captures update of existing EOA delegation. The + `sponsored_cross_sender` variant uses a distinct relayer per tx so + the cross-tx auth-nonce dep can't be served by sender-nonce + serialization alone. + """ alice = pre.fund_eoa() bob = pre.fund_eoa(amount=0) - if not self_funded: - relayer = pre.fund_eoa() - sender = relayer - else: - sender = alice - oracle1 = pre.deploy_contract(code=Op.STOP) oracle2 = pre.deploy_contract(code=Op.STOP) - ## Perhaps create a pre-existing delegation, - ## see `test_bal_7702_delegated_storage_access` since - ## `test_bal_7702_delegation_create` already tests creation + if sender_pattern == "self_funded": + sender_create = alice + sender_update = alice + update_tx_nonce = 2 + create_auth_nonce = 1 + update_auth_nonce = 3 + alice_post_create = 2 + alice_post_update = 4 + elif sender_pattern == "sponsored": + relayer = pre.fund_eoa() + sender_create = relayer + sender_update = relayer + update_tx_nonce = 1 + create_auth_nonce = 0 + update_auth_nonce = 1 + alice_post_create = 1 + alice_post_update = 2 + elif sender_pattern == "sponsored_cross_sender": + relayer_create = pre.fund_eoa() + relayer_update = pre.fund_eoa() + sender_create = relayer_create + sender_update = relayer_update + update_tx_nonce = 0 + create_auth_nonce = 0 + update_auth_nonce = 1 + alice_post_create = 1 + alice_post_update = 2 + else: + raise ValueError(f"unknown sender_pattern: {sender_pattern}") + tx_create = Transaction( - sender=sender, + sender=sender_create, to=bob, value=10, gas_limit=1_000_000, authorization_list=[ AuthorizationTuple( address=oracle1, - nonce=1 if self_funded else 0, + nonce=create_auth_nonce, signer=alice, ) ], ) tx_update = Transaction( - nonce=2 if self_funded else 1, - sender=sender, + nonce=update_tx_nonce, + sender=sender_update, to=bob, value=10, gas_limit=1_000_000, authorization_list=[ AuthorizationTuple( address=oracle2, - nonce=3 if self_funded else 1, + nonce=update_auth_nonce, signer=alice, ) ], ) - account_expectations = { + account_expectations: dict = { alice: BalAccountExpectation( nonce_changes=[ BalNonceChange( - block_access_index=1, post_nonce=2 if self_funded else 1 + block_access_index=1, post_nonce=alice_post_create ), BalNonceChange( - block_access_index=2, post_nonce=4 if self_funded else 2 + block_access_index=2, post_nonce=alice_post_update ), ], code_changes=[ @@ -222,14 +249,24 @@ def test_bal_7702_delegation_update( oracle2: None, } - # For sponsored variant, relayer must also be included in BAL - if not self_funded: + if sender_pattern == "sponsored": account_expectations[relayer] = BalAccountExpectation( nonce_changes=[ BalNonceChange(block_access_index=1, post_nonce=1), BalNonceChange(block_access_index=2, post_nonce=2), ], ) + elif sender_pattern == "sponsored_cross_sender": + account_expectations[relayer_create] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1), + ], + ) + account_expectations[relayer_update] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=2, post_nonce=1), + ], + ) block = Block( txs=[tx_create, tx_update], @@ -238,19 +275,21 @@ def test_bal_7702_delegation_update( ), ) - post = { + post: dict = { # Finally Alice's account should be delegated to oracle2 alice: Account( - nonce=4 if self_funded else 2, + nonce=alice_post_update, code=Spec7702.delegation_designation(oracle2), ), # Bob receives 20 wei in total bob: Account(balance=20), } - # For sponsored variant, include relayer in post state - if not self_funded: - post.update({relayer: Account(nonce=2)}) + if sender_pattern == "sponsored": + post[relayer] = Account(nonce=2) + elif sender_pattern == "sponsored_cross_sender": + post[relayer_create] = Account(nonce=1) + post[relayer_update] = Account(nonce=1) blockchain_test( pre=pre, 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 671a84c6397..c7f0d49fcd4 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 @@ -466,7 +466,13 @@ def test_bal_extcodesize_and_oog( ) @pytest.mark.parametrize("value", [0, 1], ids=["no_value", "with_value"]) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_call_no_delegation_and_oog_before_target_access( pre: Alloc, @@ -476,13 +482,18 @@ def test_bal_call_no_delegation_and_oog_before_target_access( target_is_warm: bool, target_is_empty: bool, value: int, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ CALL without 7702 delegation - test SUCCESS and OOG before target access. When target_is_warm=True, we use EIP-2930 tx access list to warm the target. Access list warming does NOT add to BAL - only EVM access does. + + Memory expansion is parametrized independently for args (insize) and + ret (outsize) per #1910, surfacing client-impl asymmetry bugs in the + memory-cost calculator. """ alice = pre.fund_eoa() @@ -492,19 +503,21 @@ def test_bal_call_no_delegation_and_oog_before_target_access( else pre.deploy_contract(code=Op.STOP) ) - ret_size = 32 if memory_expansion else 0 + new_memory_size = max(args_size, ret_size) # Full gas metadata: includes create_cost when applicable call_code = Op.CALL( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=value > 0 and target_is_empty, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) caller = pre.deploy_contract(code=call_code, balance=value) @@ -524,12 +537,14 @@ def test_bal_call_no_delegation_and_oog_before_target_access( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) gas_limit = intrinsic_cost + call_static.gas_cost(fork) - 1 else: # SUCCESS @@ -602,7 +617,13 @@ def test_bal_call_no_delegation_and_oog_before_target_access( "target_is_warm", [False, True], ids=["cold_target", "warm_target"] ) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) @pytest.mark.eels_base_coverage def test_bal_call_no_delegation_oog_after_target_access( @@ -610,7 +631,8 @@ def test_bal_call_no_delegation_oog_after_target_access( blockchain_test: BlockchainTestFiller, fork: Fork, target_is_warm: bool, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ CALL without 7702 delegation - OOG after state access. @@ -629,6 +651,9 @@ def test_bal_call_no_delegation_oog_after_target_access( The create_cost (NEW_ACCOUNT = 25000) is charged only for value transfers to empty accounts, creating the gap tested here. + Memory expansion is parametrized independently for args (insize) and + ret (outsize) per #1910. + """ alice = pre.fund_eoa() @@ -637,8 +662,7 @@ def test_bal_call_no_delegation_oog_after_target_access( # value > 0 required for create_cost value = 1 - # memory expansion / no expansion - ret_size = 32 if memory_expansion else 0 + new_memory_size = max(args_size, ret_size) # Static gas (before state access): no create_cost # Pass static check, fail at second check due to create cost @@ -646,12 +670,14 @@ def test_bal_call_no_delegation_oog_after_target_access( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=True, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) caller = pre.deploy_contract(code=call_code, balance=value) @@ -717,7 +743,13 @@ def test_bal_call_no_delegation_oog_after_target_access( ) @pytest.mark.parametrize("value", [0, 1], ids=["no_value", "with_value"]) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_call_7702_delegation_and_oog( pre: Alloc, @@ -727,33 +759,37 @@ def test_bal_call_7702_delegation_and_oog( target_is_warm: bool, delegation_is_warm: bool, value: int, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ CALL with 7702 delegation - test all OOG boundaries. When target_is_warm or delegation_is_warm, we use EIP-2930 tx access list. Access list warming does NOT add targets to BAL - only EVM access does. + + Memory expansion is parametrized independently for args and ret per #1910. """ alice = pre.fund_eoa() delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) - # memory expansion / no expansion - ret_size = 32 if memory_expansion else 0 + new_memory_size = max(args_size, ret_size) # Full gas metadata: includes delegation cost call_code = Op.CALL( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, delegated_address=True, delegated_address_warm=delegation_is_warm, ) @@ -777,12 +813,14 @@ def test_bal_call_7702_delegation_and_oog( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: @@ -877,7 +915,13 @@ def test_bal_call_7702_delegation_and_oog( "target_is_warm", [False, True], ids=["cold_target", "warm_target"] ) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_delegatecall_no_delegation_and_oog_before_target_access( pre: Alloc, @@ -885,28 +929,32 @@ def test_bal_delegatecall_no_delegation_and_oog_before_target_access( fork: Fork, oog_boundary: OutOfGasBoundary, target_is_warm: bool, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ DELEGATECALL without 7702 delegation - test SUCCESS and OOG boundaries. When target_is_warm=True, we use EIP-2930 tx access list to warm the target. Access list warming does NOT add to BAL - only EVM access does. + + Memory expansion is parametrized independently for args and ret per #1910. """ alice = pre.fund_eoa() target = pre.deploy_contract(code=Op.STOP) - ret_size = 32 if memory_expansion else 0 - ret_offset = 0 + new_memory_size = max(args_size, ret_size) delegatecall_code = Op.DELEGATECALL( address=target, gas=0, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) caller = pre.deploy_contract(code=delegatecall_code) @@ -975,7 +1023,13 @@ def test_bal_delegatecall_no_delegation_and_oog_before_target_access( ids=["cold_delegation", "warm_delegation"], ) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_delegatecall_7702_delegation_and_oog( pre: Alloc, @@ -984,7 +1038,8 @@ def test_bal_delegatecall_7702_delegation_and_oog( oog_boundary: OutOfGasBoundary, target_is_warm: bool, delegation_is_warm: bool, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ DELEGATECALL with 7702 delegation - test all OOG boundaries. @@ -995,24 +1050,26 @@ def test_bal_delegatecall_7702_delegation_and_oog( For 7702 delegation, there's ALWAYS a gap between static gas and second check (delegation_cost) - all 3 scenarios produce distinct behaviors. + + Memory expansion is parametrized independently for args and ret per #1910. """ alice = pre.fund_eoa() delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) - # memory expansion / no expansion - ret_size = 32 if memory_expansion else 0 - ret_offset = 0 + new_memory_size = max(args_size, ret_size) # Full gas metadata: includes delegation cost delegatecall_code = Op.DELEGATECALL( gas=0, address=target, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, delegated_address=True, delegated_address_warm=delegation_is_warm, ) @@ -1036,10 +1093,12 @@ def test_bal_delegatecall_7702_delegation_and_oog( delegatecall_static = Op.DELEGATECALL( gas=0, address=target, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: @@ -1112,7 +1171,13 @@ def test_bal_delegatecall_7702_delegation_and_oog( ) @pytest.mark.parametrize("value", [0, 1], ids=["no_value", "with_value"]) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_callcode_no_delegation_and_oog_before_target_access( pre: Alloc, @@ -1121,7 +1186,8 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( oog_boundary: OutOfGasBoundary, target_is_warm: bool, value: int, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ CALLCODE without 7702 delegation - test SUCCESS and OOG boundaries. @@ -1129,23 +1195,27 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( When target_is_warm=True, we use EIP-2930 tx access list to warm the target. Access list warming does NOT add to BAL - only EVM access does. CALLCODE has no balance transfer to target (runs in caller's context). + + Memory expansion is parametrized independently for args and ret per #1910. """ alice = pre.fund_eoa() target = pre.deploy_contract(code=Op.STOP) - ret_size = 32 if memory_expansion else 0 + new_memory_size = max(args_size, ret_size) callcode_code = Op.CALLCODE( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) caller = pre.deploy_contract(code=callcode_code, balance=value) @@ -1221,7 +1291,13 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( ) @pytest.mark.parametrize("value", [0, 1], ids=["no_value", "with_value"]) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_callcode_7702_delegation_and_oog( pre: Alloc, @@ -1231,7 +1307,8 @@ def test_bal_callcode_7702_delegation_and_oog( target_is_warm: bool, delegation_is_warm: bool, value: int, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ CALLCODE with 7702 delegation - test all OOG boundaries. @@ -1242,26 +1319,29 @@ def test_bal_callcode_7702_delegation_and_oog( For 7702 delegation, there's ALWAYS a gap between static gas and second check (delegation_cost) - all 3 scenarios produce distinct behaviors. + + Memory expansion is parametrized independently for args and ret per #1910. """ alice = pre.fund_eoa() delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) - # memory expansion / no expansion - ret_size = 32 if memory_expansion else 0 + new_memory_size = max(args_size, ret_size) # Full gas metadata: includes delegation cost callcode_code = Op.CALLCODE( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, delegated_address=True, delegated_address_warm=delegation_is_warm, ) @@ -1285,12 +1365,14 @@ def test_bal_callcode_7702_delegation_and_oog( gas=0, address=target, value=value, + args_size=args_size, + args_offset=0, ret_size=ret_size, ret_offset=0, address_warm=target_is_warm, value_transfer=value > 0, account_new=False, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: @@ -1362,7 +1444,13 @@ def test_bal_callcode_7702_delegation_and_oog( "target_is_warm", [False, True], ids=["cold_target", "warm_target"] ) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_staticcall_no_delegation_and_oog_before_target_access( pre: Alloc, @@ -1370,7 +1458,8 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( fork: Fork, oog_boundary: OutOfGasBoundary, target_is_warm: bool, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ STATICCALL without 7702 delegation - test SUCCESS and OOG boundaries. @@ -1382,16 +1471,17 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( target = pre.deploy_contract(code=Op.STOP) - ret_size = 32 if memory_expansion else 0 - ret_offset = 0 + new_memory_size = max(args_size, ret_size) staticcall_code = Op.STATICCALL( address=target, gas=0, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) caller = pre.deploy_contract(code=staticcall_code) @@ -1460,7 +1550,13 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( ids=["cold_delegation", "warm_delegation"], ) @pytest.mark.parametrize( - "memory_expansion", [False, True], ids=["no_memory", "with_memory"] + "args_size,ret_size", + [ + pytest.param(0, 0, id="no_memory"), + pytest.param(4096, 0, id="args_large"), + pytest.param(0, 4096, id="ret_large"), + pytest.param(32, 32, id="both_small"), + ], ) def test_bal_staticcall_7702_delegation_and_oog( pre: Alloc, @@ -1469,7 +1565,8 @@ def test_bal_staticcall_7702_delegation_and_oog( oog_boundary: OutOfGasBoundary, target_is_warm: bool, delegation_is_warm: bool, - memory_expansion: bool, + args_size: int, + ret_size: int, ) -> None: """ STATICCALL with 7702 delegation - test all OOG boundaries. @@ -1486,25 +1583,23 @@ def test_bal_staticcall_7702_delegation_and_oog( delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) - # memory expansion / no expansion - ret_size = 32 if memory_expansion else 0 - ret_offset = 0 + new_memory_size = max(args_size, ret_size) - # Full gas metadata: includes delegation cost staticcall_code = Op.STATICCALL( gas=0, address=target, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, delegated_address=True, delegated_address_warm=delegation_is_warm, ) caller = pre.deploy_contract(code=staticcall_code) - # Build access list for warming access_list: list[AccessList] = [] if target_is_warm: access_list.append(AccessList(address=target, storage_keys=[])) @@ -1517,14 +1612,15 @@ def test_bal_staticcall_7702_delegation_and_oog( access_list=access_list ) - # Static gas (before state access): no delegation staticcall_static = Op.STATICCALL( gas=0, address=target, + args_size=args_size, + args_offset=0, ret_size=ret_size, - ret_offset=ret_offset, + ret_offset=0, address_warm=target_is_warm, - new_memory_size=ret_size, + new_memory_size=new_memory_size, ) if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: @@ -3268,25 +3364,30 @@ def test_bal_create2_selfdestruct_then_recreate_same_block( pre_balance: int, ) -> None: """ - Tx1 CREATE2+SELFDESTRUCT, Tx2 CREATE2 resurrection at same address. + Tx1 CREATE2+SSTORE+SELFDESTRUCT, Tx2 CREATE2 resurrection at same + address. Two identical txs invoke the same factory with the same initcode (same hash => same CREATE2 address A). The factory branches on its own storage slot 1: on the first tx, the slot is 0 so the factory - CREATE2's then CALLs A (runtime SELFDESTRUCTs) and records the - CALL's return code in slot 1; on the second tx, slot 1 is non-zero - so only CREATE2 runs and A persists with the runtime code. + CREATE2's then CALLs A (runtime SSTOREs to a target slot then + SELFDESTRUCTs) and records the CALL's return code in slot 1; on the + second tx, slot 1 is non-zero so only CREATE2 runs and A persists + with the runtime code (its runtime is never executed). Per EIP-7928 SELFDESTRUCT-in-tx semantics, Tx1's destructed A has no `nonce_changes` or `code_changes`; only `balance_changes` if it was - pre-funded. Tx2's fresh A has `nonce_changes` (post=1) and - `code_changes` (post=runtime). + pre-funded. The SSTORE is demoted to `storage_reads` because the + contract is destroyed in the same tx. Tx2's fresh A has + `nonce_changes` (post=1), `code_changes` (post=runtime), and empty + storage. """ alice = pre.fund_eoa() beneficiary = pre.fund_eoa(amount=0) salt = 0 + target_slot = 0x07 - runtime = Op.SELFDESTRUCT(beneficiary) + runtime = Op.SSTORE(target_slot, 0xCAFE) + Op.SELFDESTRUCT(beneficiary) runtime_bytes = bytes(runtime) initcode_bytes = bytes(Initcode(deploy_code=runtime)) @@ -3298,7 +3399,7 @@ def test_bal_create2_selfdestruct_then_recreate_same_block( ) + Conditional( condition=Op.ISZERO(Op.SLOAD(1)), - if_true=Op.SSTORE(1, Op.CALL(50_000, Op.SLOAD(0), 0, 0, 0, 0, 0)), + if_true=Op.SSTORE(1, Op.CALL(Op.GAS, Op.SLOAD(0), 0, 0, 0, 0, 0)), if_false=Op.STOP, ) + Op.STOP @@ -3349,8 +3450,10 @@ def test_bal_create2_selfdestruct_then_recreate_same_block( expected_block_access_list=BlockAccessListExpectation( account_expectations={ target_a: BalAccountExpectation( - # Tx1 destruction (EIP-7928 #165): no nonce/code changes. - # Tx2 resurrection: fresh contract with nonce=1, runtime. + # Tx1 destruction (EIP-7928 #165): no nonce/code changes; + # the SSTORE is demoted to a storage_read because A is + # destroyed same-tx. Tx2 resurrection: fresh contract + # with nonce=1, runtime, and untouched storage. nonce_changes=[ BalNonceChange(block_access_index=2, post_nonce=1), ], @@ -3361,7 +3464,7 @@ def test_bal_create2_selfdestruct_then_recreate_same_block( ], balance_changes=target_a_balance_changes, storage_changes=[], - storage_reads=[], + storage_reads=[target_slot], ), beneficiary: beneficiary_expectation, } @@ -3372,7 +3475,9 @@ def test_bal_create2_selfdestruct_then_recreate_same_block( pre=pre, blocks=[block], post={ - target_a: Account(nonce=1, balance=0, code=runtime_bytes), + target_a: Account( + nonce=1, balance=0, code=runtime_bytes, storage={} + ), beneficiary: Account(balance=pre_balance) if pre_balance > 0 else 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 c67ca1eaf3e..76f282706e6 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -13,13 +13,14 @@ | `test_selfdestruct_to_system_contract` (Cancun) | Ensure BAL captures SELFDESTRUCT success boundary for system contract beneficiaries | Victim executes `SELFDESTRUCT(system_contract)` at exact gas boundary. System contracts are always warm (no cold access charge) and always have code (no G_NEW_ACCOUNT charge). Gas = G_VERY_LOW + G_SELF_DESTRUCT. Parametrized: is_success (exact_gas/exact_gas_minus_1), all system contracts via `@pytest.mark.with_all_system_contracts`, same_tx (pre_deploy/same_tx), originator_balance (0/1). File: `tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py`. | exact_gas: System contract in BAL with `balance_changes` if originator had balance, victim destroyed (same_tx) or balance=0 (pre-existing). exact_gas_minus_1: OOG, system contract not in BAL (no state access). | ✅ Completed | | `test_initcode_selfdestruct_to_self` (TangerineWhistle) | Ensure BAL captures SELFDESTRUCT during initcode where beneficiary is self | Initcode executes `SELFDESTRUCT(ADDRESS)` during CREATE, before any code is deployed. Contract has nonce=1 (post-EIP-161), making it non-empty. Always warm (executing contract), no G_NEW_ACCOUNT (nonce > 0). Gas boundary testing not possible (CREATE uses all available gas). Parametrized: originator_balance (0/1). File: `tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py`. | Contract created and destroyed in same tx - victim has empty BAL changes. Caller has `nonce_changes` (incremented by CREATE) and `balance_changes` if originator had balance. Victim is NONEXISTENT in post state. | ✅ Completed | | `test_bal_account_access_target` | Ensure BAL captures target addresses of account access opcodes | Alice calls `Oracle` contract which uses account access opcodes (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`) on `TargetContract`. | BAL MUST include Alice, `Oracle`, and `TargetContract` with empty changes for `TargetContract` and nonce changes for Alice. | ✅ Completed | -| `test_bal_call_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated CALL | Parametrized: target warm/cold, target empty/existing, value 0/1, memory expansion, OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL with balance changes when value > 0. | ✅ Completed | -| `test_bal_call_no_delegation_oog_after_target_access` | Ensure BAL includes target but excludes value transfer when OOG after target access | Hardcoded: empty target, value=1 (required for create_cost gap). Parametrized: warm/cold, memory expansion. | Target always in BAL. No balance changes (value transfer fails after G_NEW_ACCOUNT check). | ✅ Completed | -| `test_bal_call_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for CALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, value 0/1, memory expansion, OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | +| `test_bal_call_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated CALL | Parametrized: target warm/cold, target empty/existing, value 0/1, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL with balance changes when value > 0. | ✅ Completed | +| `test_bal_call_no_delegation_oog_after_target_access` | Ensure BAL includes target but excludes value transfer when OOG after target access | Hardcoded: empty target, value=1 (required for create_cost gap). Parametrized: warm/cold, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)). | Target always in BAL. No balance changes (value transfer fails after G_NEW_ACCOUNT check). | ✅ Completed | +| `test_bal_call_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for CALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, value 0/1, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | | `test_bal_callcode_nested_value_transfer` | Ensure BAL captures balance changes from nested value transfers when CALLCODE executes target code that itself makes CALL with value | Alice calls `Oracle` contract (200 wei balance) which uses `CALLCODE` to execute `TargetContract`'s code; that code makes a nested CALL transferring 100 wei to Bob. | BAL MUST include Alice (nonce changes), `Oracle` (balance change to 100 wei), Bob (balance change to 100 wei), and `TargetContract` (empty changes). | ✅ Completed | | `test_bal_delegated_storage_writes` | Ensure BAL captures delegated storage writes via `DELEGATECALL` and `CALLCODE` | Alice calls `Oracle` contract which uses `DELEGATECALL`/`CALLCODE` to `TargetContract` that writes `0x42` to slot `0x01`. | BAL MUST include Alice (nonce changes), `Oracle` (storage changes for slot `0x01` = `0x42`), and `TargetContract` (empty changes). | ✅ Completed | | `test_bal_delegated_storage_reads` | Ensure BAL captures delegated storage reads via `DELEGATECALL` and `CALLCODE` | Alice calls `Oracle` contract (with slot `0x01` = `0x42`) which uses `DELEGATECALL`/`CALLCODE` to `TargetContract` that reads from slot `0x01`. | BAL MUST include Alice (nonce changes), `Oracle` (storage reads for slot `0x01`), and `TargetContract` (empty changes). | ✅ Completed | | `test_bal_block_rewards` | BAL tracks fee recipient balance changes from block rewards | Alice sends 100 wei to Bob with Charlie as fee recipient | BAL MUST include fee recipient Charlie with `balance_changes` reflecting transaction fees collected from the block. | ✅ Completed | +| `test_bal_selfdestruct_to_coinbase` | Ensure BAL records SELFDESTRUCT when the beneficiary is the coinbase address. Parametrized over `same_tx ∈ [False, True]`. Coinbase pre-funded with amount=0 (empty per EIP-161). `gas_price = base_fee_per_gas` so the priority-fee tip is zero and coinbase's entry carries only the SELFDESTRUCT transfer. | `pre_deploy`: pre-existing victim contract (balance 100) executes `SELFDESTRUCT(Op.COINBASE)`; post-Cancun (EIP-6780) the contract is preserved with balance 0. `same_tx`: factory creates victim with `CREATE(value=100, ...)` and CALLs it; victim's runtime SELFDESTRUCT runs in the same tx so the contract is actually destroyed. | `pre_deploy`: BAL MUST include victim with `balance_changes` 100→0 and coinbase with `balance_changes` 0→100. `same_tx`: BAL MUST include factory with `nonce_changes` (CREATE bumped nonce) + `balance_changes` 100→0, victim with empty changes (created+destroyed same-tx), and coinbase with `balance_changes` 0→100. | ✅ Completed | | `test_bal_2930_account_listed_but_untouched` | Ensure BAL excludes listed but untouched account | Alice sends a simple eth transfer tx to Bob with EIP-2930 access list including `Oracle` | BAL MUST NOT include any entry for `Oracle` because it wasn't accessed. | ✅ Completed | | `test_bal_2930_slot_listed_but_untouched` | Ensure BAL excludes listed but untouched storage slots | Alice sends tx with EIP-2930 access list including `(PureCalculator, slot=0x01)`; PureCalculator executes pure arithmetic (adding two numbers) without touching slot `0x01` | BAL MUST NOT include any entry for PureCalculator's slot `0x01` because it doesn't access state | ✅ Completed | | `test_bal_2930_slot_listed_and_unlisted_writes` | Ensure BAL includes storage writes regardless of access list presence | Alice sends tx with EIP-2930 access list including `(StorageWriter, slot=0x01)`; StorageWriter executes `SSTORE` to slots `0x01` and `0x02` | BAL MUST include `storage_changes` for StorageWriter's slots `0x01` and `0x02` | ✅ Completed | @@ -36,13 +37,17 @@ | `test_bal_system_contract_noop_filtering` | Ensure system contract post-execution calls filter net-zero storage writes | Simple transfer that doesn't interact with system contracts. Post-execution system calls read withdrawal/consolidation contract slots 0-3 but don't modify them. | Withdrawal and consolidation system contracts **MUST** have `storage_reads` for slots 0x00-0x03 but **MUST NOT** have `storage_changes` (no actual modifications occurred). | ✅ Completed | | `test_bal_aborted_storage_access` | Ensure BAL captures storage access in aborted transactions correctly | Alice calls contract that reads storage slot `0x01`, writes to slot `0x02`, then aborts with `REVERT`/`INVALID` | BAL MUST include storage_reads for slots `0x01` and `0x02` (aborted writes become reads), empty storage_changes. Only nonce changes for Alice. | ✅ Completed | | `test_bal_aborted_account_access` | Ensure BAL captures account access in aborted transactions for all account accessing opcodes | Alice calls `AbortContract` that performs account access operations (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`) on `TargetContract` and aborts via `REVERT`/`INVALID` | BAL MUST include Alice, `TargetContract`, and `AbortContract` in account_changes and nonce changes for Alice. | ✅ Completed | +| `test_bal_parent_revert_state_access` | Ensure BAL captures child-frame state access when the parent frame reverts (write-demoted-to-read across frame boundary). Parametrized: `inner_action ∈ [sstore, sload, balance, extcodesize]`, `outer_abort ∈ [REVERT, INVALID]`. | Outer contract CALLs an inner contract that performs one state-access op (SSTORE/SLOAD against its own storage, or BALANCE/EXTCODESIZE against a separate target). Inner's slot 1 is pre-set to `0xDEAD` so the post-state confirms the demoted SSTORE didn't overwrite it. Inner returns successfully; outer then aborts. | BAL MUST include inner with `storage_reads=[1]` (SSTORE/SLOAD cases — write demoted to read), or empty changes plus the separate target in BAL with empty changes (BALANCE/EXTCODESIZE cases). Post-state: `inner.storage[1] == 0xDEAD` for SSTORE/SLOAD cases. | ✅ Completed | +| `test_selfdestruct_balance_transfer_reverted` (Cancun) | Ensure BAL records SELFDESTRUCT touches even when the containing sub-call reverts. Multi-fork via `state_test`; BAL expectations gated on `fork.is_eip_enabled(7928)`. File: `tests/cancun/eip6780_selfdestruct/test_journal_revert.py`. | Outer → controller (CALL) → victim (CALL); victim executes `SELFDESTRUCT(beneficiary)`, controller REVERTs (rolling back the balance transfer), outer continues and observes balances. | When EIP-7928 is enabled, BAL MUST include victim and beneficiary with empty changes (touched in the reverted sub-call's SELFDESTRUCT plus outer's BALANCE reads; net balances unchanged). | ✅ Completed | +| `test_bal_outer_revert_with_inner_insufficient_funds` | Combine outer-frame REVERT with inner-frame insufficient-funds CALL or CREATE. Parametrized `inner_op ∈ [call, create]`. | Outer → inner (CALL). Inner does `SSTORE(slot_a, 0x42)`, then attempts CALL/CREATE with `value > balance=0`. Inner returns successfully (insufficient-balance check happens after the opcode's state-touching costs are charged, not via a REVERT). Outer then REVERTs. | Inner's two SSTOREs demote to `storage_reads`. For `call`: the failed call's target appears in BAL with empty changes (cold access charged before balance check). For `create`: the would-be address is NOT in BAL (early failure precedes `track_address`). | ✅ Completed | +| `test_create_insufficient_balance` (Berlin) | Ensure BAL records that a failed CREATE does NOT itself add the would-be address; only a subsequent BALANCE call does. Multi-fork via `state_test`; BAL gated on `fork.is_eip_enabled(7928)`. File: `tests/berlin/eip2929_gas_cost_increases/test_create.py`. | Entry → creator (CREATE with `value=1`, creator balance 0 → fails) → checker (cold BALANCE on the would-be address, measures gas). | Creator has real `storage_changes` (slot 0 = `1 → 0`). Checker has `storage_changes` (slot 1 = cold BALANCE gas cost). Would-be address appears in BAL with empty changes — accessed only via the BALANCE in the checker contract, NOT via the failed CREATE. | ✅ Completed | | `test_bal_pure_contract_call` | Ensure BAL captures contract access for pure computation calls | Alice calls `PureContract` that performs pure arithmetic (ADD operation) without storage or balance changes | BAL MUST include Alice and `PureContract` in `account_changes`, and `nonce_changes` for Alice. | ✅ Completed | | `test_bal_create_storage_op_then_selfdestruct_same_tx` | BAL correctly demotes ephemeral storage operations to `storage_reads` when a contract is created and destroyed in the same tx (combined coverage for `read` and `write` storage_op cases — replaces `test_bal_create2_to_A_read_then_selfdestruct` and `test_bal_create2_to_A_write_then_selfdestruct`). Parametrized: `@pytest.mark.with_all_create_opcodes` + `storage_op ∈ ["read", "write"]`. | Address A is pre-funded via `pre.fund_address`. Alice sends a single tx calling a factory that deploys a contract at A via the parametrized create opcode; init code is either `SLOAD(B)+SELFDESTRUCT` or `SSTORE(B, v)+SELFDESTRUCT` (beneficiary = separate EOA). | BAL **MUST** include **A** with `balance_changes` `[(1, 0)]` (outflow to beneficiary on destruction). Slot **B** **MUST** appear under `storage_reads`, **NOT** `storage_changes` (write demoted to read because the contract is destroyed same-tx). A **MUST NOT** have `nonce_changes` or `code_changes`. Beneficiary has `balance_changes` reflecting receipt of `fund` at index 1. | ✅ Completed | | `test_bal_precompile_funded` | BAL records precompile value transfer with or without balance change | Alice sends value to precompile (all precompiles) via direct transaction. Parameterized: (1) with value (1 ETH), (2) without value (0 ETH). | For with_value: BAL **MUST** include precompile with `balance_changes`. For no_value: BAL **MUST** include precompile with empty `balance_changes`. No `storage_changes` or `code_changes` in either case. | ✅ Completed | | `test_bal_precompile_call_opcode` | BAL records the precompile address regardless of call opcode. | Parametrized: `@pytest.mark.with_all_precompiles × @pytest.mark.with_all_call_opcodes`. Alice calls Oracle which invokes the precompile via the parametrized call opcode. For DELEGATECALL/CALLCODE the precompile provides the code but is not the call target, so its access has to be recorded explicitly rather than incidentally. | BAL **MUST** include the precompile address with empty changes for all four call opcodes. Oracle has empty changes (called but no state mutation), Alice has `nonce_changes`. | ✅ Completed | | `test_bal_7702_delegated_create` | BAL tracks EIP-7702 delegation indicator write and contract creation | Alice sends a type-4 (7702) tx authorizing herself to delegate to `Deployer` code which executes `CREATE` | BAL MUST include for **Alice**: `code_changes` (delegation indicator), `nonce_changes` (increment from 7702 processing), and `balance_changes` (post-gas). For **Child**: `code_changes` (runtime bytecode) and `nonce_changes = 1`. | ✅ Completed | | `test_bal_7702_delegation_create` | Ensure BAL captures creation of EOA delegation | Alice authorizes delegation to contract `Oracle`. Transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends 7702 tx herself. (2) Sponsored: `Relayer` sends 7702 tx on Alice's behalf. | BAL **MUST** include Alice: `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`),`nonce_changes` (increment). Bob: `balance_changes` (receives 10 wei). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes`.`Oracle` **MUST NOT** be present in BAL - the account is never accessed. | ✅ Completed | -| `test_bal_7702_delegation_update` | Ensure BAL captures update of existing EOA delegation | Alice first delegates to `Oracle1`, then in second tx updates delegation to `Oracle2`. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle1)`),`nonce_changes`. Second tx has`code_changes` (delegation designation `0xef0100\|\|address(Oracle2)`),`nonce_changes`. Bob:`balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes` for both transactions. `Oracle1` and `Oracle2` **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | +| `test_bal_7702_delegation_update` | Ensure BAL captures update of existing EOA delegation. Three variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: a single `Relayer` sends both txs on Alice's behalf. (3) `sponsored_cross_sender`: a distinct relayer per tx — exercises the cross-tx auth-nonce-chain dependency since sender-nonce serialization no longer trivializes the test. A client that parallel-verifies auth signatures must consult Alice's BAL `nonce_changes` to validate the second auth against her post-tx-1 nonce. | Alice first delegates to `Oracle1`, then in second tx updates delegation to `Oracle2`. Each transaction sends 10 wei to Bob. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle1)`),`nonce_changes`. Second tx has`code_changes` (delegation designation `0xef0100\|\|address(Oracle2)`),`nonce_changes`. Bob:`balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes` for both transactions; for `sponsored_cross_sender`, each relayer has one `nonce_changes` at its tx index. `Oracle1` and `Oracle2` **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | | `test_bal_7702_delegation_clear` | Ensure BAL captures clearing of EOA delegation | Alice first delegates to `Oracle`, then in second tx clears delegation by authorizing to `0x0` address. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`), `nonce_changes`. Second tx has `code_changes` (empty code - delegation cleared), `nonce_changes`. Bob: `balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`: `nonce_changes` for both transactions. `Oracle` and `0x0` address **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | | `test_bal_7702_delegated_storage_access` | Ensure BAL captures storage operations when calling a delegated EIP-7702 account | Alice has delegated her account to `Oracle`. `Oracle` contract contains code that reads from storage slot `0x01` and writes to storage slot `0x02`. Bob sends 10 wei to Alice (the delegated account), which executes `Oracle`'s code. | BAL **MUST** include Alice: `balance_changes` (receives 10 wei), `storage_changes` for slot `0x02` (write operation performed in Alice's storage), `storage_reads` for slot `0x01` (read operation from Alice's storage). Bob: `nonce_changes` (sender), `balance_changes` (loses 10 wei plus gas costs). `Oracle` (account access). | ✅ Completed | | `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | ✅ Completed | @@ -64,12 +69,12 @@ | `test_bal_balance_and_oog` | Ensure BAL handles OOG during BALANCE opcode execution correctly | Alice calls contract that attempts `BALANCE` opcode on cold target account. Parameterized: (1) OOG at BALANCE opcode (insufficient gas), (2) Successful BALANCE execution. | For OOG case: BAL **MUST NOT** include target account (wasn't accessed). For success case: BAL **MUST** include target account in `account_changes`. | ✅ Completed | | `test_bal_account_touch_system_address` | Ensure BAL includes `SYSTEM_ADDRESS` when a regular transaction touches it via any account-accessing opcode | Alice calls a contract that executes one of `BALANCE`, `EXTCODESIZE`, `EXTCODEHASH`, `EXTCODECOPY`, `CALL`, or `STATICCALL` against `SYSTEM_ADDRESS`. Parametrized over the six opcodes. | BAL **MUST** include `SYSTEM_ADDRESS` as an account-only entry for every opcode because the address experienced a real EVM state access. This is distinct from excluding the synthetic system-operation caller. | ✅ Completed | | `test_bal_extcodesize_and_oog` | Ensure BAL handles OOG during EXTCODESIZE opcode execution correctly | Alice calls contract that attempts `EXTCODESIZE` opcode on cold target contract. Parameterized: (1) OOG at EXTCODESIZE opcode (insufficient gas), (2) Successful EXTCODESIZE execution. | For OOG case: BAL **MUST NOT** include target contract (wasn't accessed). For success case: BAL **MUST** include target contract in `account_changes`. | ✅ Completed | -| `test_bal_delegatecall_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated DELEGATECALL | Parametrized: target warm/cold, memory expansion, OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | -| `test_bal_delegatecall_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for DELEGATECALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, memory expansion, OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | -| `test_bal_callcode_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated CALLCODE | Parametrized: target warm/cold, value 0/1, memory expansion, OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | -| `test_bal_callcode_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for CALLCODE to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, value 0/1, memory expansion, OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | -| `test_bal_staticcall_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated STATICCALL | Parametrized: target warm/cold, memory expansion, OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | -| `test_bal_staticcall_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for STATICCALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, memory expansion, OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | +| `test_bal_delegatecall_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated DELEGATECALL | Parametrized: target warm/cold, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | +| `test_bal_delegatecall_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for DELEGATECALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | +| `test_bal_callcode_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated CALLCODE | Parametrized: target warm/cold, value 0/1, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | +| `test_bal_callcode_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for CALLCODE to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, value 0/1, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | +| `test_bal_staticcall_no_delegation_and_oog_before_target_access` | Ensure BAL handles OOG before target access and success for non-delegated STATICCALL | Parametrized: target warm/cold, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/success). | OOG: target in BAL ONLY if pre-warmed. Success: target always in BAL. | ✅ Completed | +| `test_bal_staticcall_7702_delegation_and_oog` | Ensure BAL handles OOG at all 4 boundaries for STATICCALL to 7702 delegated accounts | Parametrized: target warm/cold, delegation warm/cold, `(args_size, ret_size)` pair (covers in-only and out-only expansion per [#1910](https://github.com/ethereum/execution-specs/issues/1910)), OOG boundary (before_target_access/after_target_access/success_minus_1/success). | OOG before: neither in BAL. OOG after & success_minus_1: target in BAL, delegation NOT in BAL (static check optimization). Success: all in BAL. | ✅ Completed | | `test_bal_extcodecopy_and_oog` | Ensure BAL handles OOG during EXTCODECOPY at various failure points | Alice calls contract that attempts `EXTCODECOPY` from cold target contract. Parameterized: (1) Successful EXTCODECOPY, (2) OOG at cold access (insufficient gas for account access), (3) OOG at memory expansion with large offset (64KB offset, gas covers cold access + copy but NOT memory expansion), (4) OOG at memory expansion boundary (256 byte offset, gas is exactly 1 less than needed). | For success case: BAL **MUST** include target contract. For all OOG cases: BAL **MUST NOT** include target contract. Gas for ALL components (cold access + copy + memory expansion) must be checked BEFORE recording account access. | ✅ Completed | | `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 | @@ -77,6 +82,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_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_factory_nonce_create_chain` | Verify clients propagate `factory.nonce_changes` across txs when later CREATE addresses derive from the factory's current nonce. The cross-tx dependency signal is solely `nonce_changes` — no storage or balance mutations exist anywhere. A scheduler that deprioritizes nonce_changes (or schedules CREATE-family txs by their resulting distinct addresses) would speculatively derive `addr(factory, N+1)` for every tx and produce only one successful deployment. Parametrized: `failure_mode ∈ ["none", "collision", "oog"]` — `collision` pre-populates the mid-chain target (factory.nonce still bumps; chain slides forward); `oog` gives the mid-chain tx `intrinsic+1` gas so CREATE never fires (factory.nonce does not bump; chain slides backward, reusing the slot). | 8 txs from 8 distinct senders each call a shared factory that does CREATE with identical minimal initcode (deploys `Op.STOP` as runtime). | `none`: factory has 8 sequential `nonce_changes` (post=`N+1`..`N+8`); each address has `nonce_changes`/`code_changes` at its `block_access_index`. `collision`: factory still has 8 sequential `nonce_changes`; the colliding target appears with `BalAccountExpectation.empty()` (accessed under EIP-684, no state change) and its pre-state code is preserved. `oog`: factory has only 7 `nonce_changes` (the OOG tx contributes none); subsequent post_nonce values are shifted -1; the OOG'd tx's would-be address slot is filled by the next tx; the final chain address is never touched (`NONEXISTENT`). Post-state: senders all `nonce=1`; factory `nonce=N+(7 or 8)` depending on mode. | ✅ 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`, `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 | @@ -122,7 +128,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 block_access_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at block_access_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at block_access_index=2 with `balance_changes` (100→0). **Oracle MUST NOT appear in tx2** - per EVM spec, SELFDESTRUCT transfers balance without executing recipient code, so delegation target is never accessed. | ✅ Completed | -| `test_bal_call_revert_insufficient_funds` | Ensure BAL handles value-transferring call failure due to insufficient balance (not OOG), with and without 7702 delegation | Caller contract (balance=100, storage slot 0x02=0xDEAD) executes: `SLOAD(0x01), call_opcode(target, value=1000), SSTORE(0x02, result)`. The call fails because 1000 > 100. Parametrized: (1) `call_opcode` over CALL and CALLCODE via `with_all_call_opcodes(selector=...)`, (2) `delegated` (target is plain EOA vs. 7702-delegated EOA pointing to `delegation_target`=STOP), (3) `target_is_warm` (cold/warm via EIP-2930 access list), (4) `delegation_is_warm` (only when delegated). | BAL **MUST** include: (1) Caller with `storage_reads` for slot 0x01, `storage_changes` for slot 0x02 (value=0, call returned failure). (2) Target with empty changes - accessed before balance check fails. (3) When delegated: `delegation_target` with empty changes - delegation is loaded before balance check fails (unlike the OOG cases in `test_bal_call_7702_delegation_and_oog` where the static-check optimization avoids the delegation load). Access-list warming does NOT add to BAL on its own, so the BAL is identical across warm/cold variants. | ✅ Completed | +| `test_bal_call_revert_insufficient_funds` | Ensure BAL handles value-transferring call failure due to insufficient balance (not OOG), with and without 7702 delegation | Caller contract (balance=100, storage slot 0x02=0xDEAD) executes: `SLOAD(0x01), call_opcode(target, value=1000), SSTORE(0x02, result)`. The call fails because 1000 > 100. Parametrized: (1) `call_opcode` over CALL and CALLCODE via `with_all_call_opcodes(selector=...)`, (2) `delegated` (target is plain EOA vs. 7702-delegated EOA pointing to `delegation_target`=STOP), (3) `target_is_warm` (cold/warm via EIP-2930 access list), (4) `delegation_is_warm` (only when delegated). | BAL **MUST** include: (1) Caller with `storage_reads` for slot 0x01, `storage_changes` for slot 0x02 (value=0, call returned failure). (2) Target with empty changes — accessed before the balance check fails. (3) When delegated: `delegation_target` **MUST NOT** appear in the BAL — the balance check fails before `generic_call` runs, so the delegation target's account is never read. Access-list warming does NOT add to BAL on its own, so the BAL is identical across warm/cold variants. | ✅ Completed | | `test_bal_lexicographic_address_ordering` | Ensure BAL enforces strict lexicographic byte-wise ordering | Pre-fund three addresses with specific byte patterns: `addr_low = 0x0000...0001`, `addr_mid = 0x0000...0100`, `addr_high = 0x0100...0000`. Contract touches them in reverse order: `BALANCE(addr_high), BALANCE(addr_low), BALANCE(addr_mid)`. Additionally, include two endian-trap addresses that are byte-reversals of each other: `addr_endian_low = 0x0100000000000000000000000000000000000002`, `addr_endian_high = 0x0200000000000000000000000000000000000001`. Note: `reverse(addr_endian_low) = addr_endian_high`. Correct lexicographic order: `addr_endian_low < addr_endian_high` (0x01 < 0x02 at byte 0). If implementation incorrectly reverses bytes before comparing, it would get `addr_endian_low > addr_endian_high` (wrong). | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high` < `addr_endian_low` < `addr_endian_high`, regardless of access order. The endian-trap addresses specifically catch byte-reversal bugs where addresses are compared with wrong byte order. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | ✅ Completed | | `test_bal_gas_limit_boundary` | Ensure BAL max items gas limit boundary is enforced for empty blocks | Empty block with gas limit set to the exact boundary where `bal_items <= block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM`. Parameterized: (1) at_boundary (gas limit = exact minimum, passes), (2) below_boundary (gas limit = exact minimum - 1, fails). | At boundary: block **MUST** be accepted. Below boundary: block **MUST** be rejected with `BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED` exception. | ✅ Completed | | `test_bal_transient_storage_not_tracked` | Ensure BAL excludes EIP-1153 transient storage operations | Contract executes: `TSTORE(0x01, 0x42)` (transient write), `TLOAD(0x01)` (transient read), `SSTORE(0x02, result)` (persistent write using transient value). | BAL **MUST** include slot 0x02 in `storage_changes` (persistent storage was modified). BAL **MUST NOT** include slot 0x01 in `storage_reads` or `storage_changes` (transient storage is not persisted, not needed for stateless execution). This verifies TSTORE/TLOAD don't pollute BAL. | ✅ Completed | @@ -163,7 +169,7 @@ | `test_bal_2935_query` | Ensure BAL captures storage reads when querying EIP-2935 historical block hashes (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 (empty, stores genesis hash via system call). Block 2: Oracle contract queries `HISTORY_STORAGE_ADDRESS` with block number. Two block number scenarios (valid=0 genesis hash, invalid=1042 out of range) and value (0 or 100 wei). Valid query (block_number=0): reads genesis hash slot, oracle writes returned value. If value > 0, history storage contract receives balance. Invalid query (block_number=1042, out of range): reverts before storage access, oracle has implicit SLOAD recorded, value stays in oracle (not transferred to history storage). | Block 2 BAL **MUST** include: Valid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with `storage_reads` [slot 0] and `balance_changes` if value > 0, oracle with `storage_changes` (empty `slot_changes`). Invalid case at `block_access_index=1`: `HISTORY_STORAGE_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, oracle with `storage_reads` [0], NO `storage_changes`, and `balance_changes` if value > 0 (value stays in oracle). Alice with `nonce_changes` at `block_access_index=1`. | ✅ Completed | | `test_bal_2935_selfdestruct_to_history_storage` | Ensure BAL captures `SELFDESTRUCT` to EIP-2935 history storage address | Single block: Transaction where Alice calls contract (pre-funded with 100 wei) that selfdestructs with `HISTORY_STORAGE_ADDRESS` as beneficiary. | BAL **MUST** include at `block_access_index=1`: Alice with `nonce_changes`, contract with `balance_changes` (100→0), `HISTORY_STORAGE_ADDRESS` with `balance_changes` (receives 100 wei). | ✅ Completed | | `test_bal_2935_invalid_calldata_size` | Ensure BAL correctly handles EIP-2935 queries with invalid calldata size (reverts before any storage access) | Parameterized test: Block 1 stores genesis hash via system call. Block 2: Oracle contract calls `HISTORY_STORAGE_ADDRESS` with invalid calldata sizes (0, 31, 33 bytes). EIP-2935 requires exactly 32 bytes calldata; any other size causes immediate revert before storage access. Optional value transfer (0 or 100 wei). | Block 2 BAL **MUST** include: `HISTORY_STORAGE_ADDRESS` with NO `storage_reads` (calldata size check fails before any SLOAD) and NO `balance_changes` (call reverts). Oracle with `storage_reads` [0] (implicit SLOAD from no-op SSTORE), NO `storage_changes`, and `balance_changes` if value > 0 (value stays in oracle on revert). Alice with `nonce_changes`. | ✅ Completed | -| `test_bal_create2_selfdestruct_then_recreate_same_block` | Ensure BAL handles **(tx1) CREATE2+SELFDESTRUCT** then **(tx2) CREATE2 "resurrection"** of the *same address* in the same block. Parametrized over `pre_balance: [0, 100]`. | Two identical txs invoke the same factory with the same initcode (same hash → same CREATE2 address A). The factory branches on its own `storage[1]`: on the first tx, slot 1 is 0 so the factory CREATE2's then CALLs A (runtime SELFDESTRUCTs to beneficiary) and records the CALL's return code in slot 1; on the second tx, slot 1 is non-zero so only CREATE2 runs and A persists. When `pre_balance > 0`, A is pre-funded so Tx1's SELFDESTRUCT transfers a real balance. | At `block_access_index=1` (destructed A): **MUST NOT** include `nonce_changes` or `code_changes` for A (EIP-7928 SELFDESTRUCT-in-tx semantics). `balance_changes` for A appears only when pre-funded. Beneficiary appears with `balance_changes` if pre-funded, otherwise `empty()` (SELFDESTRUCT touches the beneficiary even with 0 value). At `block_access_index=2` (resurrection): A has `nonce_changes` (post=1) and `code_changes` (post=runtime). Factory's `storage[1] = 1` confirms the Tx1 CALL went through. | ✅ Completed | +| `test_bal_create2_selfdestruct_then_recreate_same_block` | Ensure BAL handles **(tx1) CREATE2+SSTORE+SELFDESTRUCT** then **(tx2) CREATE2 "resurrection"** of the *same address* in the same block. Parametrized over `pre_balance: [0, 100]`. | Two identical txs invoke the same factory with the same initcode (same hash → same CREATE2 address A). The factory branches on its own `storage[1]`: on the first tx, slot 1 is 0 so the factory CREATE2's then CALLs A (runtime SSTOREs to a target slot then SELFDESTRUCTs to beneficiary) and records the CALL's return code in slot 1; on the second tx, slot 1 is non-zero so only CREATE2 runs and A persists with the runtime code (its runtime is never executed). When `pre_balance > 0`, A is pre-funded so Tx1's SELFDESTRUCT transfers a real balance. | At `block_access_index=1` (destructed A): **MUST NOT** include `nonce_changes` or `code_changes` for A (EIP-7928 SELFDESTRUCT-in-tx semantics); the SSTORE is demoted to `storage_reads` for the target slot (write demoted because A is destroyed same-tx). `balance_changes` for A appears only when pre-funded. Beneficiary appears with `balance_changes` if pre-funded, otherwise `empty()` (SELFDESTRUCT touches the beneficiary even with 0 value). At `block_access_index=2` (resurrection): A has `nonce_changes` (post=1) and `code_changes` (post=runtime). Post-state: A has empty `storage={}` (the tx1 SSTORE was wiped; tx2 never executed the runtime). Factory's `storage[1] = 1` confirms the Tx1 CALL went through. | ✅ Completed | | `test_bal_call_with_value_in_static_context` | CALL with nonzero value in static context: target NOT in BAL | Parametrized: `target_is_warm` (cold/warm via access list), `target_has_code` (EOA/contract). Static check must fire before account access. | `target` **MUST NOT** appear in BAL. Balances unchanged. | ✅ Completed | | `test_bal_create_in_static_context` | CREATE/CREATE2 in static context: created address NOT in BAL | Parametrized: `@pytest.mark.with_all_create_opcodes`, `value` (0/1). Static check must fire before balance check, address computation, or nonce increment. | Created address **MUST NOT** appear in BAL. Factory nonce unchanged. | ✅ Completed | | `test_bal_selfdestruct_in_static_context` | SELFDESTRUCT in static context: beneficiary NOT in BAL | Parametrized: `beneficiary_is_warm` (cold/warm via access list), `caller_balance` (0/100). Static check must fire before beneficiary access or balance transfer. | `beneficiary` **MUST NOT** appear in BAL. Balances unchanged. | ✅ Completed | @@ -171,7 +177,7 @@ | `test_bal_callcode_with_value_in_static_context` | CALLCODE with nonzero value succeeds in static context | EIP-214 explicitly excludes CALLCODE from write-protection. Caller invokes `CALLCODE(value=1, target)` inside STATICCALL. | `target` **MUST** appear in BAL. Ensures clients don't apply CALL-with-value restriction to CALLCODE. | ✅ Completed | | `test_bal_create_and_oog` | CREATE/CREATE2 OOG boundary test at three gas levels | Parametrized: `@pytest.mark.with_all_create_opcodes`, `OutOfGasBoundary` (OOG_BEFORE_TARGET_ACCESS, OOG_AFTER_TARGET_ACCESS, SUCCESS). BEFORE and AFTER differ by 1 gas, proving the static cost boundary. | OOG_BEFORE: created address **MUST NOT** appear in BAL. OOG_AFTER: created address IS in BAL as `empty()` (accessed, state reverted). SUCCESS: created address in BAL with `nonce_changes`/`code_changes`. | ✅ Completed | | `test_bal_fork_transition_happy_path` | Verify a BAL is produced at the Amsterdam activation block and absent before it. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. Uses `@pytest.mark.valid_at_transition_to("Amsterdam")` to run across the `BPO2 -> Amsterdam` boundary. | Two blocks: pre-fork (`timestamp=14_999`) with a simple Alice→Bob transfer, then activation block (`timestamp=15_000`) with the same kind of transfer. | Pre-fork block header **MUST NOT** carry `block_access_list_hash`. Activation block **MUST** include `block_access_list_hash` correctly derived from the BAL body, and the BAL body **MUST** record Alice's `nonce_changes` and Bob's `balance_changes` at `block_access_index=1`. | ✅ Completed | -| `test_invalid_pre_fork_block_with_bal_hash_field` | Verify clients reject a pre-Amsterdam block whose header carries `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=14_999` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Hash(0))` to inject the field into the pre-fork header schema. | Block **MUST** be rejected with `BlockException.INCORRECT_BLOCK_FORMAT` / `EngineAPIError.InvalidParams`. The new header / body field must not be accepted before activation. | ✅ Completed | -| `test_invalid_post_fork_block_without_bal_hash_field` | Verify clients reject an Amsterdam activation block whose header is missing `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=15_000` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Header.REMOVE_FIELD)` so the field is dropped from the header. | Block **MUST** be rejected with `BlockException.INCORRECT_BLOCK_FORMAT` / `EngineAPIError.InvalidParams`. The new header field becomes mandatory from the activation block onward. | ✅ Completed | +| `test_invalid_pre_fork_block_with_bal_hash_field` | Verify clients reject a pre-Amsterdam block whose header carries `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=14_999` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Hash(0))` to inject the field into the pre-fork header schema. | Block **MUST** be rejected with `BlockException.INVALID_BLOCK_HASH`: pre-fork clients compute the block hash without the injected field, mismatching the expected hash. | ✅ Completed | +| `test_invalid_post_fork_block_without_bal_hash_field` | Verify clients reject an Amsterdam activation block whose header is missing `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=15_000` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Header.REMOVE_FIELD)` so the field is dropped from the header. | Block **MUST** be rejected with `BlockException.INVALID_BAL_HASH` or `BlockException.INVALID_BLOCK_HASH`: clients re-derive the BAL hash from execution and detect the mismatch either at the BAL hash check or the header hash check. | ✅ Completed | | `test_fork_transition_bal_size_constraint` | Verify the BAL size constraint (`bal_items <= gas_limit // BLOCK_ACCESS_LIST_ITEM`) applies only on/after Amsterdam. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. Parametrized over `exceeds_limit_at_fork`: `at_fork_within_budget` (`gas_limit == empty_block_bal_item_count() * BLOCK_ACCESS_LIST_ITEM`) and `at_fork_over_budget` (`gas_limit` one wei below that). | Two empty blocks: pre-fork (`timestamp=14_999`) and activation block (`timestamp=15_000`). The same low `gas_limit` is used for both via `genesis_environment=Environment(gas_limit=...)`. | Pre-fork block **MUST** be accepted under both budgets (constraint not yet enforced). Activation block **MUST** be accepted at the exact budget and **MUST** be rejected with `BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED` one item over the budget. | ✅ Completed | diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py b/tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py index 8e524c8e68e..6be8458434c 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py @@ -12,7 +12,6 @@ BlockchainTestFiller, BlockException, EIPChecklist, - EngineAPIError, Environment, Hash, Header, @@ -94,9 +93,6 @@ def test_invalid_pre_fork_block_with_bal_hash_field( """ Reject a pre-Amsterdam block whose header carries `block_access_list_hash`. - - The field is not part of the pre-fork header schema; injecting it via the - Engine API must fail with `INCORRECT_BLOCK_FORMAT` / `InvalidParams`. """ sender = pre.fund_eoa() receiver = pre.fund_eoa(amount=0) @@ -111,8 +107,7 @@ def test_invalid_pre_fork_block_with_bal_hash_field( timestamp=FORK_TIMESTAMP - 1, txs=[tx], rlp_modifier=Header(block_access_list_hash=Hash(0)), - exception=BlockException.INCORRECT_BLOCK_FORMAT, - engine_api_error_code=EngineAPIError.InvalidParams, + exception=BlockException.INVALID_BLOCK_HASH, ), ], ) @@ -144,8 +139,10 @@ def test_invalid_post_fork_block_without_bal_hash_field( rlp_modifier=Header( block_access_list_hash=Header.REMOVE_FIELD, ), - exception=BlockException.INCORRECT_BLOCK_FORMAT, - engine_api_error_code=EngineAPIError.InvalidParams, + exception=[ + BlockException.INVALID_BAL_HASH, + BlockException.INVALID_BLOCK_HASH, + ], ), ], ) diff --git a/tests/berlin/eip2929_gas_cost_increases/test_create.py b/tests/berlin/eip2929_gas_cost_increases/test_create.py index 3e73407f5d9..f216a39242e 100644 --- a/tests/berlin/eip2929_gas_cost_increases/test_create.py +++ b/tests/berlin/eip2929_gas_cost_increases/test_create.py @@ -23,6 +23,11 @@ from execution_testing import ( Account, Alloc, + BalAccountExpectation, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, CodeGasMeasure, Environment, Fork, @@ -100,10 +105,11 @@ def test_create_insufficient_balance( + Op.STOP ) + sender = pre.fund_eoa() tx = Transaction( to=entry_address, gas_limit=1_000_000, - sender=pre.fund_eoa(), + sender=sender, ) post = { @@ -111,8 +117,63 @@ def test_create_insufficient_balance( creator_address: Account(storage={0: 0}), # BALANCE gas cost matches cold access checker_address: Account(storage={1: cold_balance.gas_cost(fork)}), + # Fail-proofing: confirm CREATE never deposited a contract. + contract_address: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + + # Under EIP-7928 (BAL): the failed CREATE itself does NOT add the + # would-be address to BAL (failure precedes `track_address`). The + # subsequent BALANCE call in `checker_address` is what brings it in, + # so it appears with empty changes. Creator/checker have real + # storage writes; entry is just a passthrough. + expected_bal = ( + BlockAccessListExpectation( + account_expectations={ + sender: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(block_access_index=1, post_nonce=1) + ], + ), + entry_address: BalAccountExpectation.empty(), + creator_address: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange( + block_access_index=1, post_value=0 + ) + ], + ) + ], + ), + checker_address: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=1, + slot_changes=[ + BalStorageChange( + block_access_index=1, + post_value=cold_balance.gas_cost(fork), + ) + ], + ) + ], + ), + contract_address: BalAccountExpectation.empty(), + } + ) + if fork.is_eip_enabled(7928) + else None + ) + + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + expected_block_access_list=expected_bal, + ) @pytest.mark.valid_from("Berlin") diff --git a/tests/cancun/eip6780_selfdestruct/test_journal_revert.py b/tests/cancun/eip6780_selfdestruct/test_journal_revert.py index 1ed5f267e5a..6dfe7fa5d57 100644 --- a/tests/cancun/eip6780_selfdestruct/test_journal_revert.py +++ b/tests/cancun/eip6780_selfdestruct/test_journal_revert.py @@ -6,6 +6,8 @@ from execution_testing import ( Account, Alloc, + BalAccountExpectation, + BlockAccessListExpectation, Environment, Fork, Op, @@ -74,6 +76,21 @@ def test_selfdestruct_balance_transfer_reverted( TransactionReceipt(logs=[]) if fork.is_eip_enabled(7708) else None ) + # Under EIP-7928 (BAL): victim and beneficiary are touched in the + # reverted sub-call's SELFDESTRUCT and again by outer's BALANCE reads. + # The balance transfer is undone, so they appear with empty changes + # (accessed but no net state change). + expected_bal = ( + BlockAccessListExpectation( + account_expectations={ + victim: BalAccountExpectation.empty(), + beneficiary: BalAccountExpectation.empty(), + } + ) + if fork.is_eip_enabled(7928) + else None + ) + state_test( env=env, pre=pre, @@ -90,4 +107,5 @@ def test_selfdestruct_balance_transfer_reverted( gas_limit=1_000_000, expected_receipt=expected_receipt, ), + expected_block_access_list=expected_bal, )