diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py index 653e50af3e5..9fdbd45933e 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py @@ -22,6 +22,8 @@ Op, Storage, Transaction, + TransactionException, + TransactionReceipt, ) from .spec import ref_spec_8037 @@ -466,3 +468,170 @@ def test_multi_block_dimension_flip( ], post=post_2, ) + + +@pytest.mark.exception_test +@pytest.mark.valid_from("EIP8037") +def test_tx_rejected_when_regular_gas_exceeds_block_limit_small( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Reject a small-gas tx whose regular gas overflows the block.""" + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + + block_gas_limit = intrinsic_gas * 2 + + filler = pre.deploy_contract(code=Op.STOP) + filler_tx = Transaction( + to=filler, + gas_limit=intrinsic_gas, + sender=pre.fund_eoa(), + ) + + rejected_gas_limit = intrinsic_gas + 1 + assert rejected_gas_limit < gas_limit_cap + rejected = pre.deploy_contract(code=Op.STOP) + rejected_tx = Transaction( + to=rejected, + gas_limit=rejected_gas_limit, + sender=pre.fund_eoa(), + error=TransactionException.GAS_ALLOWANCE_EXCEEDED, + ) + + blockchain_test( + genesis_environment=Environment(gas_limit=block_gas_limit), + pre=pre, + blocks=[ + Block( + txs=[filler_tx, rejected_tx], + gas_limit=block_gas_limit, + exception=TransactionException.GAS_ALLOWANCE_EXCEEDED, + ) + ], + post={}, + ) + + +@pytest.mark.parametrize( + "tx2_gas_limit_equals_block_gas_limit", + [ + pytest.param(True, id="tx_gas_limit_equals_block_limit"), + pytest.param(False, id="tx_gas_limit_just_above_remaining"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_block_2d_gas_tx_gas_limit_exceeds_regular_remaining( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + tx2_gas_limit_equals_block_gas_limit: bool, +) -> None: + """ + Verify a block is valid when a later tx's gas_limit exceeds the + regular budget remaining but its capped regular contribution fits. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + env = Environment() + block_gas_limit = int(env.gas_limit) + + if tx2_gas_limit_equals_block_gas_limit: + tx2_gas_limit = block_gas_limit + else: + tx2_gas_limit = block_gas_limit - intrinsic_gas + 1 + + assert tx2_gas_limit > gas_limit_cap + assert tx2_gas_limit > block_gas_limit - intrinsic_gas + + stop_contract = pre.deploy_contract(code=Op.STOP) + + storage = Storage() + sstore_contract = pre.deploy_contract( + code=Op.SSTORE(storage.store_next(1), 1), + ) + + tx1_regular = intrinsic_gas + tx2_regular, tx2_state = sstore_tx_gas(fork) + expected_gas_used = max(tx1_regular + tx2_regular, tx2_state) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[ + Transaction( + to=stop_contract, + gas_limit=intrinsic_gas, + sender=pre.fund_eoa(), + ), + Transaction( + to=sstore_contract, + gas_limit=tx2_gas_limit, + sender=pre.fund_eoa(), + ), + ], + header_verify=Header(gas_used=expected_gas_used), + ), + ], + post={sstore_contract: Account(storage=storage)}, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_receipt_cumulative_differs_from_header_gas_used( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify receipt cumulative_gas_used can diverge from header + gas_used under 2D accounting when state gas dominates. + """ + tx_regular, tx_state = sstore_tx_gas(fork) + num_txs = 3 + + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + tx_gas_limit = gas_limit_cap + fork.sstore_state_gas() + per_tx_gas_used = tx_regular + tx_state + + txs: list[Transaction] = [] + post: dict = {} + for i in range(num_txs): + storage = Storage() + contract = pre.deploy_contract( + code=Op.SSTORE(storage.store_next(1), 1) + Op.STOP, + ) + txs.append( + Transaction( + to=contract, + gas_limit=tx_gas_limit, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=(i + 1) * per_tx_gas_used, + ), + ) + ) + post[contract] = Account(storage=storage) + + block_regular = num_txs * tx_regular + block_state = num_txs * tx_state + header_gas_used = max(block_regular, block_state) + + assert block_state > block_regular + assert header_gas_used < num_txs * per_tx_gas_used + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=txs, + header_verify=Header(gas_used=header_gas_used), + ), + ], + post=post, + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py index 68b94b244fa..1b8ab0dc53d 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py @@ -29,6 +29,7 @@ StateTestFiller, Storage, Transaction, + TransactionReceipt, compute_create2_address, compute_create_address, ) @@ -1289,3 +1290,283 @@ def test_call_value_to_pre_existing_selfdestructed_account( ], post={}, ) + + +@pytest.mark.parametrize( + "reservoir_delta", + [ + pytest.param(-1, id="reservoir_one_short"), + pytest.param(0, id="reservoir_exact"), + pytest.param(1, id="reservoir_one_over"), + ], +) +@pytest.mark.parametrize( + "child_termination", + [ + pytest.param("revert", id="child_revert"), + pytest.param("halt", id="child_halt"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_top_level_halt_preserves_restored_reservoir( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + child_termination: str, + reservoir_delta: int, +) -> None: + """ + Verify the reservoir is refunded on a top-level halt after a + failing child restored state gas to the parent frame. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + sstore_state_gas = fork.sstore_state_gas() + + if child_termination == "revert": + child_code: Bytecode = Op.SSTORE(0, 1) + Op.REVERT(0, 0) + else: + child_code = Op.SSTORE(0, 1) + Op.INVALID + + child = pre.deploy_contract(code=child_code) + + parent = pre.deploy_contract( + code=(Op.POP(Op.CALL(gas=500_000, address=child)) + Op.INVALID), + ) + + tx = Transaction( + to=parent, + gas_limit=gas_limit_cap + sstore_state_gas + reservoir_delta, + sender=pre.fund_eoa(), + ) + + # When the reservoir is one short of the child's SSTORE, the + # spill from regular gas is restored on the child's failure, + # lowering the block regular total by the same amount. + expected_gas_used = gas_limit_cap + min(reservoir_delta, 0) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_gas_used), + ), + ], + post={child: Account(storage={0: 0})}, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_callcode_value_no_new_account_state_gas( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify CALLCODE with value does not charge new-account state + gas, since the value stays with the caller. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + sstore_state_gas = fork.sstore_state_gas() + + target = pre.fund_eoa(amount=0) + + storage = Storage() + contract = pre.deploy_contract( + code=( + Op.POP( + Op.CALLCODE( + gas=Op.GAS, + address=target, + value=1, + ) + ) + + Op.SSTORE(storage.store_next(1, "reservoir_ok"), 1) + ), + balance=10**18, + ) + + tx = Transaction( + to=contract, + gas_limit=gas_limit_cap + sstore_state_gas, + sender=pre.fund_eoa(), + ) + + post = { + contract: Account(storage=storage), + target: Account.NONEXISTENT, + } + state_test(pre=pre, post=post, tx=tx) + + +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_create_oog_during_state_gas_charge( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, +) -> None: + """ + Verify the parent reservoir is refunded when a child's CREATE + OOGs while charging account-creation state gas. The grandchild + SSTORE is forwarded only its regular stipend, so it succeeds + only if the refund landed in the reservoir (not in `gas_left`). + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + gas_costs = fork.gas_costs() + sstore_state_gas = fork.sstore_state_gas() + + init_code = Op.STOP + inner_create_call = ( + create_opcode(value=0, offset=31, size=1, salt=0) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=31, size=1) + ) + + inner = pre.deploy_contract( + code=( + Op.MSTORE( + 0, + int.from_bytes(bytes(init_code), "big") << 248, + ) + + Op.POP(inner_create_call) + ), + ) + + grandchild = pre.deploy_contract(code=Op.SSTORE(0, 1)) + + push_cost = 2 * gas_costs.GAS_VERY_LOW + sstore_regular = gas_costs.GAS_COLD_STORAGE_WRITE + grandchild_stipend = push_cost + sstore_regular + + parent = pre.deploy_contract( + code=( + Op.POP(Op.CALL(gas=20_000, address=inner)) + + Op.POP(Op.CALL(gas=grandchild_stipend, address=grandchild)) + ), + ) + + tx = Transaction( + to=parent, + gas_limit=gas_limit_cap + sstore_state_gas, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={grandchild: Account(storage={0: 1})}, + tx=tx, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_call_new_account_no_regular_account_creation_cost( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify CALL with value to a non-existent account does not + charge a regular account-creation cost on top of state gas. + """ + gas_costs = fork.gas_costs() + new_account_state_gas = gas_costs.GAS_NEW_ACCOUNT + + target = pre.fund_eoa(amount=0) + + caller_code = Op.POP(Op.CALL(gas=0, address=target, value=1)) + Op.STOP + caller = pre.deploy_contract(code=caller_code, balance=1) + + # Tight budget: slack is less than the old pre-Amsterdam regular + # account-creation cost, so any extra regular draw would OOG. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + tx = Transaction( + to=caller, + gas_limit=( + intrinsic + + caller_code.gas_cost(fork) + + gas_costs.GAS_CALL_VALUE + + new_account_state_gas + + 20_000 + ), + sender=pre.fund_eoa(), + ) + + state_test(pre=pre, post={target: Account(balance=1)}, tx=tx) + + +@pytest.mark.parametrize( + "call_opcode", + [ + pytest.param(Op.CALL, id="call"), + pytest.param(Op.DELEGATECALL, id="delegatecall"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_opcode: Op, +) -> None: + """ + Verify state gas from a failing child is restored to the + reservoir (not regular gas), so a grandchild SSTORE can draw + from it under a tight regular stipend. Parametrized across CALL + (grandchild writes to its own storage) and DELEGATECALL + (grandchild writes to the parent's storage via shared context). + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + gas_costs = fork.gas_costs() + sstore_state_gas = fork.sstore_state_gas() + + grandchild = pre.deploy_contract(code=Op.SSTORE(0, 1)) + + child = pre.deploy_contract(code=Op.SSTORE(0, 1) + Op.REVERT(0, 0)) + + # Tight stipend: just enough regular gas for the grandchild's + # SSTORE opcode plus its two stack pushes, leaving no slack to + # absorb a state-gas spill. + push_cost = 2 * gas_costs.GAS_VERY_LOW + sstore_regular = gas_costs.GAS_COLD_STORAGE_WRITE + grandchild_stipend = push_cost + sstore_regular + + parent = pre.deploy_contract( + code=( + Op.POP(call_opcode(gas=Op.GAS, address=child)) + + Op.POP(call_opcode(gas=grandchild_stipend, address=grandchild)) + ), + ) + + # Empirical per-tx cumulative. Pinning this catches a mutation + # that correctly restores the reservoir but also double-refunds + # to regular gas (or otherwise leaks extra gas to the sender), + # which the storage probe alone cannot discriminate. + expected_cumulative = { + Op.CALL: 73_831, + Op.DELEGATECALL: 73_825, + }[call_opcode] + + tx = Transaction( + to=parent, + gas_limit=gas_limit_cap + sstore_state_gas, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative, + ), + ) + + # DELEGATECALL executes the callee in the caller's storage + # context, so grandchild's SSTORE lands on `parent` instead of + # `grandchild`. + if call_opcode == Op.DELEGATECALL: + post: dict = {parent: Account(storage={0: 1})} + else: + post = {grandchild: Account(storage={0: 1})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index a0332895b0a..ef32e4bbbc4 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -1533,3 +1533,613 @@ def test_create_code_deposit_oog_refunds_state_gas( ) state_test(pre=pre, post={factory: Account(storage=storage)}, tx=tx) + + +@pytest.mark.parametrize( + "init_code", + [ + pytest.param(Op.REVERT(0, 0), id="revert"), + pytest.param(Op.INVALID, id="halt"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_failed_create_tx_state_gas_dominates( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + init_code: Bytecode, +) -> None: + """ + Verify the header gas is set by intrinsic state gas when a + creation tx fails with a tight regular budget. + """ + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + create_state_gas = fork.create_state_gas(code_size=0) + + intrinsic_total = intrinsic_calc( + calldata=bytes(init_code), contract_creation=True + ) + intrinsic_regular = intrinsic_total - create_state_gas + gas_limit = intrinsic_total + 1000 + + assert intrinsic_regular + 1000 < create_state_gas, ( + "tight gas budget must keep block_regular below create_state_gas" + ) + + tx = Transaction( + to=None, + data=init_code, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=create_state_gas), + ), + ], + post={}, + ) + + +@pytest.mark.parametrize( + "initcode_size_delta", + [ + pytest.param(0, id="at_max"), + pytest.param(1, id="over_max", marks=pytest.mark.exception_test), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_oversized_initcode_tx_no_state_gas( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + initcode_size_delta: int, +) -> None: + """ + Verify a creation tx with oversized initcode is rejected before + any state gas is charged. + """ + max_size = fork.max_initcode_size() + size = max_size + initcode_size_delta + initcode = Initcode(deploy_code=Op.STOP, initcode_length=size) + + sender = pre.fund_eoa() + create_address = compute_create_address(address=sender, nonce=0) + + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + create_state_gas = fork.create_state_gas(code_size=len(Op.STOP)) + + tx = Transaction( + sender=sender, + to=None, + data=initcode, + gas_limit=gas_limit_cap + create_state_gas, + ) + + if initcode_size_delta > 0: + tx.error = TransactionException.INITCODE_SIZE_EXCEEDED + post: dict = {create_address: Account.NONEXISTENT} + else: + post = {create_address: Account(code=Op.STOP)} + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + exception=( + TransactionException.INITCODE_SIZE_EXCEEDED + if initcode_size_delta > 0 + else None + ), + ), + ], + post=post, + ) + + +@pytest.mark.parametrize( + "initcode_size_delta", + [ + pytest.param(0, id="at_max"), + pytest.param(1, id="over_max"), + ], +) +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_oversized_initcode_opcode_no_state_gas( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + initcode_size_delta: int, +) -> None: + """ + Verify CREATE/CREATE2 with oversized initcode fails the size + check before any state gas is charged. + """ + max_size = fork.max_initcode_size() + size = max_size + initcode_size_delta + initcode = Initcode(deploy_code=Op.STOP, initcode_length=size) + initcode_bytes = bytes(initcode) + + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + gas_costs = fork.gas_costs() + create_state_gas = gas_costs.GAS_NEW_ACCOUNT + + create_call = ( + create_opcode( + value=0, + offset=0, + size=Op.CALLDATASIZE, + salt=0, + init_code_size=len(initcode_bytes), + ) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + factory = pre.deploy_contract( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.SSTORE(0, create_call) + ) + + create_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=initcode, + opcode=create_opcode, + ) + + storage = Storage() + storage[0] = create_address if initcode_size_delta == 0 else 0 + + tx = Transaction( + sender=pre.fund_eoa(), + to=factory, + data=initcode_bytes, + gas_limit=gas_limit_cap + create_state_gas, + ) + + post: dict = {factory: Account(storage=storage)} + if initcode_size_delta == 0: + post[create_address] = Account(code=Op.STOP) + else: + post[create_address] = Account.NONEXISTENT + + blockchain_test( + pre=pre, + blocks=[Block(txs=[tx])], + post=post, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_selfdestruct_in_create_tx_initcode( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify state gas accounting when a creation tx's initcode + immediately SELFDESTRUCTs to a new beneficiary. + """ + gas_costs = fork.gas_costs() + create_state_gas = fork.create_state_gas(code_size=0) + + beneficiary = 0xDEAD + initcode = Op.SELFDESTRUCT(beneficiary) + + sender = pre.fund_eoa() + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_total = intrinsic_calc( + calldata=bytes(initcode), contract_creation=True + ) + + expected_state = create_state_gas + + initcode_gas = initcode.gas_cost(fork) + gas_limit = ( + intrinsic_total + initcode_gas + gas_costs.GAS_NEW_ACCOUNT + 1000 + ) + + tx = Transaction( + sender=sender, + to=None, + data=initcode, + value=1, + gas_limit=gas_limit, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_state), + ), + ], + post={}, + ) + + +@pytest.mark.parametrize( + "outer_outcome", + [ + pytest.param("succeeds", id="outer_succeeds"), + pytest.param("reverts", id="outer_reverts"), + pytest.param("halts", id="outer_halts"), + ], +) +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_inner_create_succeeds_code_deposit_state_gas( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + outer_outcome: str, +) -> None: + """ + Verify state gas accumulation and top-level failure refund in a + creation tx whose initcode runs a successful inner CREATE. + """ + gas_costs = fork.gas_costs() + outer_state_gas = fork.create_state_gas(code_size=0) + inner_code_deposit = fork.code_deposit_state_gas(code_size=1) + inner_state_gas = gas_costs.GAS_NEW_ACCOUNT + inner_code_deposit + + deploy_code = Op.STOP + inner_initcode = Op.MSTORE( + 0, + int.from_bytes(bytes(deploy_code), "big") << 248, + ) + Op.RETURN(31, 1) + inner_bytes = bytes(inner_initcode) + + setup = Op.MSTORE( + 0, + int.from_bytes(inner_bytes, "big") << (256 - 8 * len(inner_bytes)), + ) + if create_opcode == Op.CREATE2: + inner_create = Op.POP(Op.CREATE2(0, 0, len(inner_bytes), 0)) + else: + inner_create = Op.POP(Op.CREATE(0, 0, len(inner_bytes))) + + if outer_outcome == "succeeds": + termination = Op.RETURN(0, 0) + elif outer_outcome == "reverts": + termination = Op.REVERT(0, 0) + else: + termination = Op.INVALID + + initcode = setup + inner_create + termination + + sender = pre.fund_eoa() + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_total = intrinsic_calc( + calldata=bytes(initcode), contract_creation=True + ) + + # Static cost excludes inner code-deposit, so add it to give + # the initcode enough to reach RETURN in the child frame. + initcode_gas = initcode.gas_cost(fork) + gas_limit = intrinsic_total + initcode_gas + inner_code_deposit + 1000 + + if outer_outcome == "succeeds": + expected_state = outer_state_gas + inner_state_gas + else: + expected_state = outer_state_gas + + create_address = compute_create_address(address=sender, nonce=0) + + tx = Transaction( + sender=sender, + to=None, + data=initcode, + gas_limit=gas_limit, + ) + + if outer_outcome == "succeeds": + post: dict = {create_address: Account(code=b"")} + else: + post = {create_address: Account.NONEXISTENT} + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_state), + ), + ], + post=post, + ) + + +@pytest.mark.parametrize( + "parent_reverts", + [ + pytest.param(True, id="parent_reverts"), + pytest.param(False, id="parent_succeeds"), + ], +) +@pytest.mark.parametrize( + "child_failure", + [ + pytest.param("revert", id="child_revert"), + pytest.param("halt", id="child_halt"), + ], +) +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_nested_create_fail_parent_revert_state_gas( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + parent_reverts: bool, + child_failure: str, + create_opcode: Op, +) -> None: + """ + Verify factory nonce is rolled back when the factory reverts after + a failed inner CREATE, and preserved when the factory returns. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + gas_costs = fork.gas_costs() + create_state_gas = gas_costs.GAS_NEW_ACCOUNT + + if child_failure == "revert": + init_code = Op.REVERT(0, 0) + else: + init_code = Op.INVALID + + create_call = ( + create_opcode(value=0, offset=0, size=len(init_code), salt=0) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=len(init_code)) + ) + + factory = pre.deploy_contract( + code=( + Op.MSTORE( + 0, + int.from_bytes(bytes(init_code), "big") + << (256 - 8 * len(init_code)), + ) + + Op.POP(create_call) + + (Op.REVERT(0, 0) if parent_reverts else Op.STOP) + ), + ) + + # Nested CALL required so the child-error path has a parent + # frame to receive the restored state gas. + caller = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=500_000, address=factory)), + ) + + tx = Transaction( + to=caller, + gas_limit=gas_limit_cap + create_state_gas, + sender=pre.fund_eoa(), + ) + + inner_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=bytes(init_code), + opcode=create_opcode, + ) + + if parent_reverts: + post = { + factory: Account(nonce=1), + inner_address: Account.NONEXISTENT, + } + else: + post = { + factory: Account(nonce=2), + inner_address: Account.NONEXISTENT, + } + + blockchain_test( + pre=pre, + blocks=[Block(txs=[tx])], + post=post, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_create_stack_depth_state_gas_consumed( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify the state gas reservoir survives a deep recursion of + nested CALLs that silently fail on gas or depth exhaustion. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + sstore_state_gas = fork.sstore_state_gas() + + storage = Storage() + recursive = pre.deploy_contract( + code=( + Op.POP(Op.CALL(Op.GAS, Op.ADDRESS, 0, 0, 0, 0, 0)) + + Op.SSTORE(storage.store_next(1, "reservoir_ok"), 1) + ), + ) + + tx = Transaction( + to=recursive, + gas_limit=gas_limit_cap + sstore_state_gas, + sender=pre.fund_eoa(), + ) + + post = {recursive: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +@pytest.mark.parametrize( + "num_inner_ops", + [ + pytest.param(1, id="single"), + pytest.param(3, id="accumulate"), + ], +) +@pytest.mark.parametrize( + "outer_outcome", + [ + pytest.param("succeeds", id="outer_succeeds"), + pytest.param("reverts", id="outer_reverts"), + ], +) +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_inner_create_fail_refunds_in_creation_tx( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + outer_outcome: str, + num_inner_ops: int, +) -> None: + """ + Verify failed inner CREATEs inside a creation tx refund state + gas so only the outer intrinsic state gas remains. + """ + gas_costs = fork.gas_costs() + outer_state_gas = fork.create_state_gas(code_size=0) + + inner_initcode = bytes(Op.REVERT(0, 0)) + + setup = Op.MSTORE( + 0, + int.from_bytes(inner_initcode, "big") + << (256 - 8 * len(inner_initcode)), + ) + + inner_ops = Bytecode() + for i in range(num_inner_ops): + if create_opcode == Op.CREATE2: + inner_ops += Op.POP(Op.CREATE2(0, 0, len(inner_initcode), i)) + else: + inner_ops += Op.POP(Op.CREATE(0, 0, len(inner_initcode))) + + if outer_outcome == "succeeds": + termination = Op.RETURN(0, 0) + else: + termination = Op.REVERT(0, 0) + + initcode = setup + inner_ops + termination + + sender = pre.fund_eoa() + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_total = intrinsic_calc( + calldata=bytes(initcode), contract_creation=True + ) + + initcode_gas = initcode.gas_cost(fork) + per_inner_slack = 2_000 + gas_limit = ( + intrinsic_total + + initcode_gas + + num_inner_ops * (gas_costs.GAS_NEW_ACCOUNT + per_inner_slack) + ) + + expected_state = outer_state_gas + + create_address = compute_create_address(address=sender, nonce=0) + + tx = Transaction( + sender=sender, + to=None, + data=initcode, + gas_limit=gas_limit, + ) + + if outer_outcome == "succeeds": + post: dict = {create_address: Account(code=b"")} + else: + post = {create_address: Account.NONEXISTENT} + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_state), + ), + ], + post=post, + ) + + +@pytest.mark.pre_alloc_mutable +@pytest.mark.with_all_create_opcodes() +@pytest.mark.valid_from("EIP8037") +def test_create_collision_burned_gas_counted_in_block_regular( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, +) -> None: + """ + Verify gas burned by a CREATE/CREATE2 address collision counts + toward block regular gas used in the header. + """ + init_code = Op.STOP + mstore_value, size = init_code_at_high_bytes(init_code) + salt = 0 + + create_call = ( + create_opcode(value=0, offset=0, size=size, salt=salt) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=size) + ) + factory_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) + Op.STOP + factory = pre.deploy_contract(code=factory_code) + + collision_target = compute_create_address( + address=factory, + nonce=1, + salt=salt, + initcode=bytes(init_code), + opcode=create_opcode, + ) + pre.deploy_contract(code=Op.STOP, address=collision_target) + + # Fixed-size budget so the forwarded create_message_gas is + # deterministic and the empirical baseline below is reproducible. + gas_limit = 250_000 + + tx = Transaction( + to=factory, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) + + # Empirical baseline: block_state_gas is zero for this tx, so + # header.gas_used equals the regular-gas total. A mutation that + # drops the burned create_message_gas from regular accounting + # would reduce this value. + baseline_gas_used = 0x01C98C + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=baseline_gas_used), + ), + ], + post={}, + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py index e969606f933..f1ed432ada1 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py @@ -15,6 +15,7 @@ import pytest from execution_testing import ( + AccessList, Account, Alloc, Block, @@ -1020,3 +1021,109 @@ def test_top_level_failure_refunds_state_gas_propagated_from_child( ) state_test(pre=pre, post={child: Account(storage={})}, tx=tx) + + +@pytest.mark.parametrize( + "num_access_list_entries", + [ + pytest.param(1, id="one_entry"), + pytest.param(10, id="ten_entries"), + ], +) +@pytest.mark.parametrize( + "slots_per_entry", + [ + pytest.param(0, id="addresses_only"), + pytest.param(3, id="with_storage_keys"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_access_list_gas_is_regular_not_state( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + num_access_list_entries: int, + slots_per_entry: int, +) -> None: + """Verify EIP-2930 access list gas counts as regular, not state.""" + contract = pre.deploy_contract(code=Op.STOP) + + access_list = [] + for _ in range(num_access_list_entries): + target = pre.fund_eoa(amount=0) + storage_keys = list(range(slots_per_entry)) + access_list.append( + AccessList(address=target, storage_keys=storage_keys) + ) + + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + gas_needed = intrinsic_calc(access_list=access_list) + + tx = Transaction( + to=contract, + gas_limit=gas_needed, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=gas_needed), + ), + ], + post={}, + ) + + +@pytest.mark.valid_from("EIP8037") +def test_access_list_warm_savings_stay_regular( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Verify access-list warm savings stay in regular gas.""" + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + sstore_state_gas = fork.sstore_state_gas() + + contract = pre.deploy_contract( + code=Op.SSTORE(0, Op.SLOAD(0)), + storage={0: 1}, + ) + + access_list = [AccessList(address=contract, storage_keys=[0])] + + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_calc(access_list=access_list) + + contract_code = Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=1, + new_value=1, + )(0, Op.SLOAD.with_metadata(key_warm=True)(0)) + evm_gas = contract_code.gas_cost(fork) + + expected_gas_used = intrinsic_gas + evm_gas + gas_limit = gas_limit_cap + sstore_state_gas + + tx = Transaction( + to=contract, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_gas_used), + ), + ], + post={contract: Account(storage={0: 1})}, + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py index 89cb8bbf398..19f15ba76fb 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py @@ -412,6 +412,82 @@ def test_create_selfdestruct_refunds_code_deposit_state_gas( ) +@pytest.mark.valid_from("EIP8037") +def test_create_selfdestruct_code_deposit_refund_header_check( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify block header gas reflects the code-deposit state-gas + refund on a same-tx CREATE plus SELFDESTRUCT. + """ + gas_limit_cap = fork.transaction_gas_limit_cap() + assert gas_limit_cap is not None + gas_costs = fork.gas_costs() + new_account_state_gas = gas_costs.GAS_NEW_ACCOUNT + + # Deployed code is sized so the code-deposit state gas would + # dominate block regular gas if the refund did not land. + selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) + sd_len = len(bytes(selfdestruct)) + code_size = 256 + assert code_size >= sd_len + deployed = bytes(selfdestruct) + b"\x00" * (code_size - sd_len) + initcode = Initcode(deploy_code=deployed) + initcode_len = len(initcode) + code_deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) + + factory_code = Op.CALLDATACOPY( + 0, + 0, + Op.CALLDATASIZE, + data_size=initcode_len, + new_memory_size=initcode_len, + ) + Op.POP( + Op.CALL( + gas=Op.GAS, + address=Op.CREATE( + value=0, + offset=0, + size=Op.CALLDATASIZE, + init_code_size=initcode_len, + ), + ) + ) + factory = pre.deploy_contract(code=factory_code) + created_address = compute_create_address(address=factory, nonce=1) + + total_state_refund = new_account_state_gas + code_deposit_state_gas + tx = Transaction( + to=factory, + data=bytes(initcode), + gas_limit=gas_limit_cap + total_state_refund, + sender=pre.fund_eoa(), + ) + + # Empirical baseline: block_state_gas refunds to zero so the + # header reports block regular only. Baseline regular must stay + # below the code-deposit state gas so a missing refund would + # push the header above this value. + baseline_block_regular = 0x8EAE + assert baseline_block_regular < code_deposit_state_gas, ( + "Baseline regular must be below code_deposit_state_gas so " + "the mutation's un-refunded state_gas dominates the header." + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=baseline_block_regular), + ), + ], + post={created_address: Account.NONEXISTENT}, + ) + + @pytest.mark.valid_from("EIP8037") def test_create_selfdestruct_no_double_refund_with_sstore_restoration( blockchain_test: BlockchainTestFiller, @@ -634,3 +710,38 @@ def test_selfdestruct_via_delegatecall_chain( factory: Account(storage=factory_storage), }, ) + + +@pytest.mark.valid_from("EIP8037") +def test_selfdestruct_new_beneficiary_no_regular_account_creation_cost( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify SELFDESTRUCT to a new beneficiary does not charge a + regular account-creation cost on top of state gas. + """ + gas_costs = fork.gas_costs() + new_account_state_gas = gas_costs.GAS_NEW_ACCOUNT + + beneficiary = pre.fund_eoa(amount=0) + + victim_code = Op.SELFDESTRUCT(beneficiary) + victim = pre.deploy_contract(code=victim_code, balance=1) + + # Tight budget: slack is less than the old pre-Amsterdam regular + # account-creation cost, so any extra regular draw would OOG. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + tx = Transaction( + to=victim, + gas_limit=( + intrinsic + + victim_code.gas_cost(fork) + + new_account_state_gas + + 20_000 + ), + sender=pre.fund_eoa(), + ) + + state_test(pre=pre, post={beneficiary: Account(balance=1)}, tx=tx)