From 86dac146a9356327f5575f41ed7ba01f7704a80b Mon Sep 17 00:00:00 2001 From: marioevz Date: Wed, 22 Jul 2026 11:44:08 +0300 Subject: [PATCH 1/5] fix(test-forks): Add `call_value_stipend` helper --- .../src/execution_testing/forks/base_fork.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 911867deec8..f24bc228d77 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -826,6 +826,20 @@ def transaction_top_frame_state_gas( del contract_creation, sends_value, recipient_type, authorizations return 0 + @classmethod + def call_value_stipend(cls) -> int: + """ + Return the gas stipend forwarded to the callee of a value-bearing + CALL/CALLCODE. + + The stipend is added to the child frame's gas and returned to the + caller when the callee does not consume it, so tests that pin + value-call gas at an exact boundary subtract it from the charged + total. Exposed as a named accessor so tests need not read + ``gas_costs().CALL_STIPEND`` directly. + """ + return cls.gas_costs().CALL_STIPEND + @classmethod def system_call_gas_limit(cls) -> int: """ From 834779a85d5bfe2141eaa6ed78da3b3c8f19e275 Mon Sep 17 00:00:00 2001 From: marioevz Date: Tue, 14 Jul 2026 18:50:26 +0300 Subject: [PATCH 2/5] fix(tests): Remove unnecessary gas_costs checks from EIP-8037, EIP-8038 tests --- .../test_block_2d_gas_accounting.py | 15 +- .../test_state_gas_call.py | 329 ++++++++++-------- .../test_state_gas_calldata_floor.py | 42 +-- .../test_state_gas_create.py | 230 +++++------- .../test_state_gas_delegation_pointer.py | 18 +- .../test_state_gas_multi_block.py | 27 +- .../test_state_gas_ordering.py | 100 ++---- .../test_state_gas_pricing.py | 22 +- .../test_state_gas_reservoir.py | 15 +- .../test_state_gas_selfdestruct.py | 68 ++-- .../test_state_gas_set_code.py | 1 - .../test_state_gas_sstore.py | 47 +-- .../test_access_list_gas.py | 56 +-- .../test_call_gas.py | 156 +++------ .../test_create_gas.py | 67 ++-- .../test_eip_mainnet.py | 2 +- .../test_exact_balance_no_fallback.py | 33 +- .../test_ext_code_opcodes_gas.py | 40 +-- .../test_fork_transition.py | 66 +--- .../test_selfdestruct_gas.py | 43 +-- .../test_set_code_auth_gas.py | 59 +++- .../test_sstore_gas.py | 4 +- .../test_sstore_refunds.py | 25 +- .../test_transient_storage_regression.py | 25 +- 24 files changed, 567 insertions(+), 923 deletions(-) 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 42775006b8c..662af4980f2 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 @@ -368,24 +368,27 @@ def test_block_gas_used_call_new_account( GAS_NEW_ACCOUNT state gas) then SSTORE. Combined with a STOP tx, the 2D max must reflect state gas from account creation. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) target = pre.fund_eoa(amount=0) + call = Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) parent_storage = Storage() parent = pre.deploy_contract( - code=( - Op.CALL(gas=100_000, address=target, value=1) - + Op.SSTORE(parent_storage.store_next(1), 1) - ), + code=(call + Op.SSTORE(parent_storage.store_next(1), 1)), balance=10**18, ) txs = [ Transaction( to=parent, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=call.state_cost(fork) + sstore_state_gas, sender=pre.fund_eoa(), ), ] + stop_txs(pre, fork, 1) 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 62f3a91eeca..170217fa7cf 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 @@ -433,26 +433,36 @@ def test_call_value_transfer_new_account( A CALL that transfers value to a non-existent account creates a new account, charging new-account state gas of state gas. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - # Target address that doesn't exist in pre-state - target = 0xDEAD + target = pre.nonexistent_account() parent_storage = Storage() - parent = pre.deploy_contract( - code=( - Op.SSTORE( - parent_storage.store_next(1), - Op.CALL(gas=100_000, address=target, value=1), - ) + # Capture the CALL result in a pre-existing slot (2 -> 1) so the + # instrumentation SSTORE modifies rather than creates a key and + # adds no state gas; the reservoir then covers exactly the CALL's + # new-account charge. + slot = parent_storage.store_next(1) + parent_code = Op.SSTORE( + slot, + Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, ), - balance=1, + original_value=2, + current_value=2, + new_value=1, + key_warm=False, + ) + parent = pre.deploy_contract( + code=parent_code, balance=1, storage={slot: 2} ) tx = Transaction( to=parent, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=parent_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -803,7 +813,6 @@ def test_call_pre_charged_costs_excluded_from_forwarding( pre-charged costs (access gas, memory expansion, or both) causes the child to OOG and the SSTORE to revert. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: SSTORE(0, 1) as proof of execution @@ -817,9 +826,9 @@ def test_call_pre_charged_costs_excluded_from_forwarding( ret_size = 512 * 32 # 512 words memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - extra_gas = gas_costs.COLD_ACCOUNT_ACCESS # cold call, value=0 - - # Wrapper: CALL child requesting max gas with memory expansion + # Wrapper: CALL child requesting max gas with memory expansion. The + # memory metadata makes `wrapper_code.regular_cost(fork)` fold the + # cold access, the 7 argument pushes and the memory expansion. wrapper_code = Op.CALL( gas=0xFFFFFFFF, address=child, @@ -828,17 +837,16 @@ def test_call_pre_charged_costs_excluded_from_forwarding( args_size=0, ret_offset=0, ret_size=ret_size, + new_memory_size=ret_size, ) wrapper = pre.deploy_contract(wrapper_code) - wrapper_pushes = 7 * gas_costs.VERY_LOW # 7 CALL args - - # After the pre-charge of extra_gas + memory_cost, the wrapper has - # gas_remaining left. The 63/64 rule should forward - # gas_remaining * 63/64 to the child — just enough for its SSTORE. + # After the up-front pre-charge, the wrapper has gas_remaining left. + # The 63/64 rule should forward gas_remaining * 63/64 to the child — + # just enough for its SSTORE. gas_remaining = child_regular_gas * 64 // 63 + memory_cost // 2 - wrapper_gas = wrapper_pushes + extra_gas + memory_cost + gas_remaining + wrapper_gas = wrapper_code.regular_cost(fork) + gas_remaining caller = pre.deploy_contract( Op.POP(Op.CALL(gas=wrapper_gas, address=wrapper)) @@ -871,25 +879,35 @@ def test_call_new_account_header_gas_used( GAS_NEW_ACCOUNT state gas. The block must be accepted with correct 2D max(regular, state) accounting in the header. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - target = pre.fund_eoa(amount=0) storage = Storage() - contract = pre.deploy_contract( - code=( - Op.SSTORE( - storage.store_next(1, "call_succeeds"), - Op.CALL(gas=100_000, address=target, value=1), - ) + # Capture the CALL result in a pre-existing slot (2 -> 1) so the + # instrumentation SSTORE modifies rather than creates a key and + # adds no state gas; the reservoir then covers exactly the CALL's + # new-account charge. + slot = storage.store_next(1, "call_succeeds") + contract_code = Op.SSTORE( + slot, + Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, ), - balance=1, + original_value=2, + current_value=2, + new_value=1, + key_warm=False, + ) + contract = pre.deploy_contract( + code=contract_code, balance=1, storage={slot: 2} ) tx = Transaction( to=contract, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=contract_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -929,34 +947,29 @@ def test_call_value_to_self_destructed_same_tx_account( the no charge behavior lives in `test_call_value_to_self_destructed_header_gas_used`. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) storage = Storage() - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.SSTORE( - storage.store_next(1, "call_succeeds"), - Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1), - ) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.SSTORE( + storage.store_next(1, "call_succeeds"), + Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1), + ) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -998,8 +1011,6 @@ def test_call_value_to_self_destructed_header_gas_used( targeted itself or an external beneficiary, so the no charge behavior holds across both cases. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - if selfdestruct_beneficiary == "self": inner_code = Op.SELFDESTRUCT(Op.ADDRESS) else: @@ -1009,24 +1020,22 @@ def test_call_value_to_self_destructed_header_gas_used( inner_code = Op.SELFDESTRUCT(alive_beneficiary) mstore_value, size = init_code_at_high_bytes(inner_code) - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1)) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=1)) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1069,31 +1078,29 @@ def test_call_value_to_self_destructed_burns_value( address. At the end of the transaction the account is removed and the accumulated balance is lost. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) initial_balance = 2 * call_value - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(call_value, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(call_value, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP( - Op.CALL( - gas=Op.GAS, - address=Op.MLOAD(0x20), - value=call_value, - ) + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(call_value, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(call_value, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP( + Op.CALL( + gas=Op.GAS, + address=Op.MLOAD(0x20), + value=call_value, ) - ), - balance=initial_balance, + ) + ) + orchestrator = pre.deploy_contract( + code=orchestrator_code, balance=initial_balance ) created_address = compute_create_address( address=orchestrator, @@ -1105,7 +1112,7 @@ def test_call_value_to_self_destructed_burns_value( tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1147,29 +1154,25 @@ def test_call_zero_value_to_self_destructed_same_tx_account( value CALL (value gate broken) would double the state gas component. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - inner_code = Op.SELFDESTRUCT(Op.ADDRESS) mstore_value, size = init_code_at_high_bytes(inner_code) - orchestrator = pre.deploy_contract( - code=( - Op.MSTORE(0, mstore_value) - + ( - Op.CREATE2(1, 0, size, 0) - if create_opcode == Op.CREATE2 - else Op.CREATE(1, 0, size) - ) - + Op.MSTORE(0x20, Op.DUP1) - + Op.POP - + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=0)) - ), - balance=3, + orchestrator_code = ( + Op.MSTORE(0, mstore_value) + + ( + Op.CREATE2(1, 0, size, 0) + if create_opcode == Op.CREATE2 + else Op.CREATE(1, 0, size) + ) + + Op.MSTORE(0x20, Op.DUP1) + + Op.POP + + Op.POP(Op.CALL(gas=Op.GAS, address=Op.MLOAD(0x20), value=0)) ) + orchestrator = pre.deploy_contract(code=orchestrator_code, balance=3) tx = Transaction( to=orchestrator, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=orchestrator_code.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1455,12 +1458,20 @@ def test_call_new_account_no_regular_account_creation_cost( 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.NEW_ACCOUNT - target = pre.fund_eoa(amount=0) - caller_code = Op.POP(Op.CALL(gas=0, address=target, value=1)) + Op.STOP + caller_code = ( + Op.POP( + Op.CALL( + gas=0, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) + ) + + Op.STOP + ) caller = pre.deploy_contract(code=caller_code, balance=1) # Tight budget: slack is less than the old pre-Amsterdam regular @@ -1468,13 +1479,7 @@ def test_call_new_account_no_regular_account_creation_cost( intrinsic = fork.transaction_intrinsic_cost_calculator()() tx = Transaction( to=caller, - gas_limit=( - intrinsic - + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - + new_account_state_gas - + 20_000 - ), + gas_limit=(intrinsic + caller_code.gas_cost(fork) + 20_000), sender=pre.fund_eoa(), ) @@ -1498,20 +1503,26 @@ def test_call_new_account_state_gas_boundary( materialized; one gas short the caller frame goes out of gas, so nothing is created and the value transfer is rolled back. """ - gas_costs = fork.gas_costs() - target = 0xDEAD - caller_code = Op.CALL(gas=0, address=target, value=1) + Op.STOP + target = pre.nonexistent_account() + caller_code = ( + Op.CALL( + gas=0, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) + + Op.STOP + ) caller = pre.deploy_contract(code=caller_code, balance=1) exact_fit = ( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - + gas_costs.NEW_ACCOUNT ) post: dict if gas_delta == 0: - gas_used = exact_fit - gas_costs.CALL_STIPEND + gas_used = exact_fit - fork.call_value_stipend() post = {target: Account(balance=1), caller: Account(balance=0)} else: gas_used = exact_fit + gas_delta @@ -1555,7 +1566,6 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( tight regular stipend. Covers SSTORE and CALL-value (new account) state-gas charge paths. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) probe_storage = Storage() @@ -1564,14 +1574,19 @@ def test_child_failure_refunds_state_gas_to_reservoir_not_gas_left( if charge_via == "sstore": child_code: Bytecode = Op.SSTORE(0, 1) + Op.REVERT(0, 0) child_balance = 0 - child_state_charge = sstore_state_gas else: fresh_target = pre.fund_eoa(amount=0) child_code = Op.POP( - Op.CALL(gas=Op.GAS, address=fresh_target, value=1) + Op.CALL( + gas=Op.GAS, + address=fresh_target, + value=1, + value_transfer=True, + account_new=True, + ) ) + Op.REVERT(0, 0) child_balance = 1 - child_state_charge = gas_costs.NEW_ACCOUNT + child_state_charge = child_code.state_cost(fork) child = pre.deploy_contract(code=child_code, balance=child_balance) probe = pre.deploy_contract(probe_code) @@ -1620,9 +1635,7 @@ def test_call_insufficient_balance_refunds_new_account_state_gas( Refill NEW_ACCOUNT state gas on a value CALL that fails the balance check before the child frame. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT probe_storage = Storage() probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) @@ -1632,14 +1645,22 @@ def test_call_insufficient_balance_refunds_new_account_state_gas( non_existent_account = pre.nonexistent_account() + value_call = Op.CALL( + gas=Op.GAS, + address=non_existent_account, + value=1, + value_transfer=True, + account_new=True, + ) parent = pre.deploy_contract( code=( - Op.POP(Op.CALL(gas=Op.GAS, address=non_existent_account, value=1)) + Op.POP(value_call) + Op.POP(Op.CALL(gas=probe_stipend, address=probe)) ), balance=0, ) + new_account_state_gas = value_call.state_cost(fork) assert new_account_state_gas >= sstore_state_gas reservoir = new_account_state_gas @@ -1663,9 +1684,7 @@ def test_call_value_precompile_halt_refunds_new_account_state_gas( Refill NEW_ACCOUNT state gas on a value CALL to an unfunded precompile that halts in the child frame. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT probe_storage = Storage() probe_code = Op.SSTORE(probe_storage.store_next(1, "probe_ran"), 1) @@ -1675,14 +1694,18 @@ def test_call_value_precompile_halt_refunds_new_account_state_gas( ecpairing = 0x08 + value_call = Op.CALL( + 1, ecpairing, 1, 0, 0, 0, 0, value_transfer=True, account_new=True + ) parent = pre.deploy_contract( code=( - Op.POP(Op.CALL(1, ecpairing, 1, 0, 0, 0, 0)) + Op.POP(value_call) + Op.POP(Op.CALL(gas=probe_stipend, address=probe)) ), balance=1, ) + new_account_state_gas = value_call.state_cost(fork) assert new_account_state_gas >= sstore_state_gas reservoir = new_account_state_gas @@ -1734,10 +1757,17 @@ def test_call_value_new_account_state_gas_consumed_on_caller_halt( if target_kind == "precompile" else pre.nonexistent_account() ) - caller = pre.deploy_contract( - code=Op.CALL(gas=0, address=target, value=value) + Op.INVALID, - balance=value, + caller_code = ( + Op.CALL( + gas=0, + address=target, + value=value, + value_transfer=True, + account_new=True, + ) + + Op.INVALID ) + caller = pre.deploy_contract(code=caller_code, balance=value) sender = pre.fund_eoa() gas_limit_cap = fork.transaction_gas_limit_cap() @@ -1745,7 +1775,7 @@ def test_call_value_new_account_state_gas_consumed_on_caller_halt( if reservoir == "over_cap": # The excess over the EIP-7825 cap becomes the reservoir. - gas_limit = gas_limit_cap + fork.gas_costs().NEW_ACCOUNT // 2 + gas_limit = gas_limit_cap + caller_code.state_cost(fork) // 2 expected_gas_used = gas_limit_cap else: gas_limit = 1_000_000 @@ -1787,27 +1817,32 @@ def test_call_value_new_account_state_gas_returned_on_caller_revert( """ value = 1 target = pre.nonexistent_account() - caller_code = Op.CALL(gas=0, address=target, value=value) + Op.REVERT(0, 0) + caller_code = Op.CALL( + gas=0, + address=target, + value=value, + value_transfer=True, + account_new=True, + ) + Op.REVERT(0, 0) caller = pre.deploy_contract(code=caller_code, balance=value) sender = pre.fund_eoa() - gas_costs = fork.gas_costs() - # Only regular execution is billed: the spilled and reservoir-funded parts - # of the NEW_ACCOUNT charge are both refunded, so the cost matches in-cap - # and over-cap. `gas_cost` covers the pushes and cold access; the value - # transfer is added on top and the empty child returns its stipend unused. + # Only regular execution is billed: the spilled and reservoir-funded + # parts of the NEW_ACCOUNT charge are both refunded, so the cost + # matches in-cap and over-cap. `regular_cost` covers the pushes, cold + # access and the value transfer (NEW_ACCOUNT lands in the state + # dimension); the empty child returns its stipend unused. expected_gas_used = ( fork.transaction_intrinsic_cost_calculator()() - + caller_code.gas_cost(fork) - + gas_costs.CALL_VALUE - - gas_costs.CALL_STIPEND + + caller_code.regular_cost(fork) + - fork.call_value_stipend() ) receipt = TransactionReceipt(cumulative_gas_used=expected_gas_used) gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None gas_limit = ( - gas_limit_cap + gas_costs.NEW_ACCOUNT // 2 + gas_limit_cap + caller_code.state_cost(fork) // 2 if reservoir == "over_cap" else 1_000_000 ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py index d0c8f7e4ed9..2a58f8e6d55 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_calldata_floor.py @@ -157,33 +157,29 @@ def test_calldata_floor_exceeding_tx_gas_limit_cap( exceeds_cap: one byte more tips the floor over the cap — transaction rejected. """ - gas_costs = fork.gas_costs() cap = fork.transaction_gas_limit_cap() assert cap is not None floor_cost = fork.transaction_data_floor_cost_calculator() - floor_token = gas_costs.TX_DATA_TOKEN_FLOOR - # EIP-2780 anchors the floor on the decomposed intrinsic base; the tx - # targets a contract, so the base includes the recipient-access charge. - floor_base = gas_costs.TX_BASE + gas_costs.COLD_ACCOUNT_ACCESS - max_tokens = (cap - floor_base) // floor_token - - if fork.is_eip_enabled(7976): - # EIP-7976: all bytes contribute 4 floor tokens regardless of - # value, so the token count is len(data) * 4. - tokens_per_byte = 4 - max_bytes = max_tokens // tokens_per_byte - if exceeds_cap: - max_bytes += 1 - calldata = b"\x01" * max_bytes - else: - # EIP-7623: non-zero bytes contribute 4 tokens, zero bytes 1. - tokens_per_nonzero = 4 - nonzero_bytes = max_tokens // tokens_per_nonzero - zero_bytes = max_tokens - nonzero_bytes * tokens_per_nonzero - if exceeds_cap: - zero_bytes += 1 - calldata = b"\x01" * nonzero_bytes + b"\x00" * zero_bytes + # Binary-search the largest all-nonzero calldata whose floor cost fits + # within the gas cap; `exceeds_cap` adds one more byte to tip the floor + # over. Driven by the floor calculator directly so it tracks the + # per-byte token pricing across forks. + def floor_fits(num_bytes: int) -> bool: + return floor_cost(data=b"\x01" * num_bytes) <= cap + + high = 1 + while floor_fits(high): + high *= 2 + low = high // 2 + while low < high: + mid = (low + high + 1) // 2 + if floor_fits(mid): + low = mid + else: + high = mid - 1 + max_bytes = low + 1 if exceeds_cap else low + calldata = b"\x01" * max_bytes contract = pre.deploy_contract(Op.STOP) floor = floor_cost(data=calldata) 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 882872cff2d..b398f230f0f 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 @@ -97,9 +97,6 @@ def test_create_with_reservoir( Provide gas above TX_MAX_GAS_LIMIT so the new account state gas is drawn from the reservoir rather than gas_left. """ - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - storage = Storage() init_code = Op.STOP @@ -124,7 +121,7 @@ def test_create_with_reservoir( tx = Transaction( to=contract, - state_gas_reservoir=create_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -266,18 +263,14 @@ def test_code_deposit_state_gas_exact_fit_boundary( ``gas_left`` and burns it all, billing the full ``gas_limit``. The scaling tests assert success only. """ - gas_costs = fork.gas_costs() cap = fork.transaction_gas_limit_cap() assert cap is not None code_size = fork.max_code_size() if funding == "reservoir" else 1000 - words = (code_size + 31) // 32 - memory_gas = gas_costs.MEMORY_PER_WORD * words + words * words // 512 - init_code = Op.RETURN(0, code_size) - init_exec_regular = init_code.regular_cost(fork) + memory_gas - keccak_gas = gas_costs.OPCODE_KECCAK256_PER_WORD * words - deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) + init_code = Op.RETURN( + 0, code_size, code_deposit_size=code_size, new_memory_size=code_size + ) intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( calldata=bytes(init_code), @@ -285,13 +278,13 @@ def test_code_deposit_state_gas_exact_fit_boundary( return_cost_deducted_prior_execution=True, ) # The fresh target's NEW_ACCOUNT is a top-frame state charge under - # EIP-2780, no longer folded into the intrinsic. + # EIP-2780, no longer folded into the intrinsic. The RETURN metadata + # folds the memory expansion, code-hash keccak and code-deposit state + # gas into `init_code`'s own cost. exact_fit_gas = ( intrinsic_regular - + gas_costs.NEW_ACCOUNT - + init_exec_regular - + keccak_gas - + deposit_state_gas + + fork.transaction_top_frame_state_gas(contract_creation=True) + + init_code.gas_cost(fork) ) if funding == "reservoir": assert exact_fit_gas > cap @@ -469,6 +462,7 @@ def test_create_insufficient_state_gas( returning 0. """ init_code = Op.STOP + create_call = Op.CREATE(0, 0, len(init_code)) storage = Storage() contract = pre.deploy_contract( @@ -480,17 +474,15 @@ def test_create_insufficient_state_gas( ) + Op.SSTORE( storage.store_next(0), # CREATE returns 0 on OOG - Op.CREATE(0, 0, len(init_code)), + create_call, ) ), ) # Tight gas — enough for intrinsic + CREATE regular gas but not # enough for the new account state gas - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - regular_create_gas = gas_costs.OPCODE_CREATE_BASE - gas_limit = intrinsic_cost() + regular_create_gas + 10_000 + gas_limit = intrinsic_cost() + create_call.regular_cost(fork) + 10_000 tx = Transaction( to=contract, @@ -658,14 +650,17 @@ def test_code_deposit_oog_preserves_parent_reservoir( CREATE proves the reservoir was not inflated by a spill-then-halt refund. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Small deploy size; code deposit state gas will exceed the # limited gas available in the CREATE child frame. deploy_size = 4096 init_code = Op.RETURN(0, deploy_size) + create_call = Op.CREATE( + value=0, + offset=32 - len(init_code), + size=len(init_code), + ) # Limited regular gas forwarded to the factory. After CREATE # takes 63/64, the factory retains ~23 K for its SSTOREs. @@ -677,11 +672,7 @@ def test_code_deposit_oog_preserves_parent_reservoir( Op.MSTORE(0, Op.PUSH32(bytes(init_code))) + Op.SSTORE( factory_storage.store_next(0, "create_fails"), - Op.CREATE( - value=0, - offset=32 - len(init_code), - size=len(init_code), - ), + create_call, ) # Reservoir must be fully preserved after failed CREATE; # parent can still perform its own SSTORE. @@ -702,7 +693,7 @@ def test_code_deposit_oog_preserves_parent_reservoir( # gas_left, which the limited CALL gas cannot cover. tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas + sstore_state_gas, + state_gas_reservoir=create_call.state_cost(fork) + sstore_state_gas, sender=pre.fund_eoa(), ) @@ -754,26 +745,34 @@ def test_parent_state_gas_after_child_failure( """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT initcode = Op.SSTORE(0, 1, original_value=0, new_value=1) + failure_op + create_call = Op.CREATE( + value=0, + offset=32 - len(initcode), + size=len(initcode), + init_code_size=len(initcode), + ) + factory_storage = Storage() - factory_code = ( - Op.MSTORE(0, Op.PUSH32(bytes(initcode))) - + Op.SSTORE( - factory_storage.store_next(0, "create_fails"), - Op.CREATE( - value=0, - offset=32 - len(initcode), - size=len(initcode), - ), - original_value=0, - new_value=0, + # Split the factory into the CREATE run (memory setup + CREATE, whose + # result is left on the stack) and the post-CREATE stores, so each + # step's regular gas is read off `.regular_cost(fork)` rather than + # rebuilt from constants. + factory_create_code = ( + Op.MSTORE(0, Op.PUSH32(bytes(initcode)), new_memory_size=32) + + create_call + ) + factory_post_create_code = ( + # Store the CREATE result (0 on failure): a cold 0 -> 0 no-op. + Op.PUSH1(factory_storage.store_next(0, "create_fails")) + + Op.SSTORE.with_metadata(original_value=0, new_value=0)( + unchecked=True ) + # Factory's own cold 0 -> 1 SSTORE. + Op.SSTORE( factory_storage.store_next(1, "post_create"), 1, @@ -781,50 +780,16 @@ def test_parent_state_gas_after_child_failure( new_value=1, ) ) + factory_code = factory_create_code + factory_post_create_code factory = pre.deploy_contract(code=factory_code) + new_account_state_gas = create_call.state_cost(fork) gas_limit = ( gas_limit_cap + new_account_state_gas + sstore_state_gas * 2 if with_reservoir else 5_000_000 ) - # `bytecode.gas_cost(fork)` accounts for opcode base costs and - # state-gas charges, but does NOT track memory-expansion or CREATE - # init-code word costs. Add those back to recover runtime regular - # gas consumption. - init_code_word_count = (len(initcode) + 31) // 32 - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD * init_code_word_count - mstore_memory_expansion = gas_costs.MEMORY_PER_WORD # 1 word - gas_cost_helper_extras = init_code_word_cost + mstore_memory_expansion - - # Factory bytecode shape costs, derived from fork.gas_costs(): - # pre-CREATE: PUSH32 + PUSH1 + MSTORE (with 1-word expansion) - # + 3 PUSHes for CREATE inputs - # post-CREATE: PUSH key + SSTORE (cold no-op: access cost only) - # + 2 PUSHes + SSTORE (cold zero-to-nonzero: - # access + write, the compound COLD_STORAGE_WRITE) - factory_pre_create_regular = ( - gas_costs.VERY_LOW * 2 - + gas_costs.OPCODE_MSTORE_BASE - + mstore_memory_expansion - + gas_costs.VERY_LOW * 3 - ) - factory_post_create_regular = ( - gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_ACCESS - + gas_costs.VERY_LOW * 2 - + gas_costs.COLD_STORAGE_WRITE - ) - - factory_regular = ( - factory_code.gas_cost(fork) - - new_account_state_gas - - sstore_state_gas - + gas_cost_helper_extras - ) - initcode_regular_revert = initcode.gas_cost(fork) - sstore_state_gas - if failure_op == Op.INVALID: # Simulate runtime gas for HALT under EIP-8037 LIFO refills: # 1. Regular pool capped by transaction_gas_limit_cap. The @@ -846,8 +811,9 @@ def test_parent_state_gas_after_child_failure( sim_gas_left = min(regular_budget, execution_gas) sim_state_gas_left = execution_gas - sim_gas_left - sim_gas_left -= factory_pre_create_regular - sim_gas_left -= gas_costs.OPCODE_CREATE_BASE + init_code_word_cost + # Memory setup, the CREATE arg pushes and the CREATE regular + # cost are all consumed before the 63/64 split. + sim_gas_left -= factory_create_code.regular_cost(fork) # CREATE new_account state gas: reservoir first, spill tracked. new_account_from_reservoir = min( @@ -871,7 +837,7 @@ def test_parent_state_gas_after_child_failure( sim_gas_left += new_account_spill sim_state_gas_left += new_account_from_reservoir - sim_gas_left -= factory_post_create_regular + sim_gas_left -= factory_post_create_code.regular_cost(fork) # Factory post-CREATE SSTORE: reservoir first, spill otherwise. if sim_state_gas_left >= sstore_state_gas: @@ -887,8 +853,9 @@ def test_parent_state_gas_after_child_failure( # factory's own post-CREATE SSTORE consumes net state gas. expected_cumulative = ( intrinsic_cost - + factory_regular - + initcode_regular_revert + + factory_create_code.regular_cost(fork) + + factory_post_create_code.regular_cost(fork) + + initcode.regular_cost(fork) + sstore_state_gas ) @@ -922,9 +889,8 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( code deposit after init code runs. The CREATE increments the factory nonce but code deposit fails, so no contract is deployed. """ - init_code = Op.RETURN(0, 1) - gas_costs = fork.gas_costs() - code_deposit_state = fork.code_deposit_state_gas(code_size=1) + init_code = Op.RETURN(0, 1, new_memory_size=32) + code_deposit_state = Op.RETURN(0, 1, code_deposit_size=1).state_cost(fork) factory_mstore = Op.MSTORE( 0, Op.PUSH32(bytes(init_code)), new_memory_size=32 @@ -942,7 +908,7 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( # Init code child execution: PUSH1 + PUSH1 + RETURN's mem_exp. # Code deposit (keccak + state) is charged AFTER the child returns. - init_cost = 2 * gas_costs.VERY_LOW + gas_costs.MEMORY_PER_WORD + init_cost = init_code.regular_cost(fork) # Target child: enough for init, not enough for code deposit state. target_child = (init_cost + code_deposit_state) // 2 # Invert EIP-150 63/64ths rule: ceil(target_child * 64 / 63). @@ -955,7 +921,7 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas( intrinsic_cost + factory_mstore.regular_cost(fork) + factory_create.regular_cost(fork) - + gas_costs.NEW_ACCOUNT + + factory_create.state_cost(fork) + factory_remaining ) @@ -1337,7 +1303,6 @@ def test_create_tx_header_gas_used( regular intrinsic and the floor, and fails if a stray NEW_ACCOUNT is charged. """ - gas_costs = fork.gas_costs() initcode = Op.STOP create_state_gas = fork.create_state_gas(code_size=1) @@ -1381,7 +1346,9 @@ def test_create_tx_header_gas_used( else: # For a minimal CREATE tx deploying Op.STOP (1 byte), # state gas (new account) dominates regular gas. - expected_gas_used = gas_costs.NEW_ACCOUNT + expected_gas_used = fork.transaction_top_frame_state_gas( + contract_creation=True + ) blockchain_test( pre=pre, @@ -1581,7 +1548,6 @@ def test_create_silent_failure_refunds_state_gas( balance) refund `GAS_NEW_ACCOUNT` to the reservoir. Block state gas reflects only the probe SSTORE, not the refunded CREATE. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() @@ -1607,13 +1573,8 @@ def test_create_silent_failure_refunds_state_gas( # CREATE's GAS_NEW_ACCOUNT is refunded (silent failure, no child # spawned). SSTORE's state portion is tracked separately in - # tx_state. - tx_regular = ( - intrinsic_cost - + factory_code.gas_cost(fork) - - gas_costs.NEW_ACCOUNT - - sstore_state_gas - ) + # tx_state, so only the regular dimension remains here. + tx_regular = intrinsic_cost + factory_code.regular_cost(fork) tx_state = sstore_state_gas expected = max(tx_regular, tx_state) blockchain_test( @@ -1651,7 +1612,6 @@ def test_create_child_revert_refunds_state_gas( """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() @@ -1689,9 +1649,7 @@ def test_create_child_revert_refunds_state_gas( # incorporate_child_on_error. tx_regular = ( intrinsic_cost - + factory_code.gas_cost(fork) - - gas_costs.NEW_ACCOUNT - - sstore_state_gas + + factory_code.regular_cost(fork) + init_code.gas_cost(fork) ) tx_state = sstore_state_gas @@ -1730,9 +1688,7 @@ def test_create_child_halt_refunds_state_gas( but not enough to spill the state portion, so the probe SSTORE can only succeed via the refunded reservoir. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT init_code: Op | Bytecode if failure_mode == "initcode_halt": @@ -1766,9 +1722,9 @@ def test_create_child_halt_refunds_state_gas( # regular fits but state gas spillover from `gas_left` under # the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular # Reservoir sized for CREATE charge only — SSTORE must pull @@ -1778,7 +1734,7 @@ def test_create_child_halt_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1860,9 +1816,7 @@ def test_create_collision_refunds_state_gas( probe SSTORE can only succeed via the refunded reservoir, not by spilling state gas from `gas_left`. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT init_code = Op.STOP mstore_value, size = init_code_at_high_bytes(init_code) @@ -1897,9 +1851,9 @@ def test_create_collision_refunds_state_gas( # the probe SSTORE regular fits but state gas spillover from # `gas_left` under the old behavior OOGs. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular # Reservoir sized for CREATE charge only — SSTORE must pull from @@ -1909,7 +1863,7 @@ def test_create_collision_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -1933,9 +1887,7 @@ def test_create_code_deposit_oog_refunds_state_gas( `gas_left` so the probe SSTORE can only succeed via the refunded reservoir, not by spilling state gas from `gas_left`. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = gas_costs.NEW_ACCOUNT max_code_size = fork.max_code_size() # Init code returns (max_code_size + 1) bytes, triggering the @@ -1963,9 +1915,9 @@ def test_create_code_deposit_oog_refunds_state_gas( # discrimination window so SSTORE regular fits but state gas # spillover fails. pre_sstore_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) - pre_sstore_regular = pre_sstore_code.gas_cost(fork) - new_account_state_gas + pre_sstore_regular = pre_sstore_code.regular_cost(fork) probe_code = Op.SSTORE(0, 1) - probe_regular = probe_code.gas_cost(fork) - sstore_state_gas + probe_regular = probe_code.regular_cost(fork) target_gas_left = probe_regular + sstore_state_gas // 2 forwarded_gas = target_gas_left * 64 + pre_sstore_regular caller = pre.deploy_contract( @@ -1973,7 +1925,7 @@ def test_create_code_deposit_oog_refunds_state_gas( ) tx = Transaction( to=caller, - state_gas_reservoir=new_account_state_gas, + state_gas_reservoir=create_call.state_cost(fork), sender=pre.fund_eoa(), ) @@ -2065,7 +2017,7 @@ def test_create_account_charge_reduces_child_gas( The target is a pre-existing balance-only leaf, the EIP-8037 success-refund path that the old conditional charge skipped. """ - new_account = fork.gas_costs().NEW_ACCOUNT + new_account = create_opcode(account_new=True).state_cost(fork) memory_gas = fork.memory_expansion_gas_calculator() # Factory `gas_left` at the NEW_ACCOUNT charge. Three times @@ -2128,12 +2080,9 @@ def test_create_account_charge_reduces_child_gas( pre.fund_address(create_address, amount=1) # Regular gas the factory spends before the NEW_ACCOUNT charge: the - # initcode setup MSTORE plus the create opcode regular portion - # (`gas_cost` folds NEW_ACCOUNT into the create op, so strip it). + # initcode setup MSTORE plus the create opcode's regular portion. setup = Op.MSTORE(0, mstore_value) - pre_charge_regular = ( - setup.gas_cost(fork) + create_call.gas_cost(fork) - new_account - ) + pre_charge_regular = setup.gas_cost(fork) + create_call.regular_cost(fork) forwarded_gas = gas_at_charge + pre_charge_regular caller = pre.deploy_contract( code=Op.CALL(gas=forwarded_gas, address=factory) @@ -2187,7 +2136,6 @@ def test_failed_create_tx_refills_top_frame_new_account( * HALT (INVALID) refills the spilled ``NEW_ACCOUNT`` to ``gas_left`` and then burns all of it, so the sender pays the full ``gas_limit``. """ - gas_costs = fork.gas_costs() intrinsic_calc = fork.transaction_intrinsic_cost_calculator() intrinsic_regular = intrinsic_calc( @@ -2199,7 +2147,7 @@ def test_failed_create_tx_refills_top_frame_new_account( # regular execution so the initcode runs to completion. gas_limit = ( intrinsic_regular - + gas_costs.NEW_ACCOUNT + + fork.transaction_top_frame_state_gas(contract_creation=True) + init_code.regular_cost(fork) + 1000 ) @@ -2381,9 +2329,7 @@ def test_create_onto_alive_refunds_to_gas_left( pre.fund_address(target, amount=1) gas_limit = ( - fork.transaction_intrinsic_cost_calculator()() - + create.regular_cost(fork) - + fork.gas_costs().NEW_ACCOUNT + fork.transaction_intrinsic_cost_calculator()() + create.gas_cost(fork) ) tx = Transaction(to=contract, gas_limit=gas_limit, sender=pre.fund_eoa()) @@ -2475,9 +2421,6 @@ def test_oversized_initcode_opcode_no_state_gas( initcode = Initcode(deploy_code=Op.STOP, initcode_length=size) initcode_bytes = bytes(initcode) - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - create_call = ( create_opcode( value=0, @@ -2509,7 +2452,7 @@ def test_oversized_initcode_opcode_no_state_gas( sender=pre.fund_eoa(), to=factory, data=initcode_bytes, - state_gas_reservoir=create_state_gas, + state_gas_reservoir=create_call.state_cost(fork), ) post: dict = {factory: Account(storage=storage)} @@ -2541,7 +2484,6 @@ def test_selfdestruct_in_create_tx_initcode( created contract's ``NEW_ACCOUNT`` plus the fresh beneficiary's ``NEW_ACCOUNT`` charged by the SELFDESTRUCT. """ - gas_costs = fork.gas_costs() create_state_gas = fork.create_state_gas(code_size=0) beneficiary = 0xDEAD @@ -2557,10 +2499,10 @@ def test_selfdestruct_in_create_tx_initcode( # State: the created contract's top-frame NEW_ACCOUNT plus the fresh # beneficiary's NEW_ACCOUNT from the SELFDESTRUCT. - expected_state = create_state_gas + gas_costs.NEW_ACCOUNT + expected_state = create_state_gas + initcode.state_cost(fork) initcode_gas = initcode.gas_cost(fork) - gas_limit = intrinsic_regular + gas_costs.NEW_ACCOUNT + initcode_gas + 1000 + gas_limit = intrinsic_regular + create_state_gas + initcode_gas + 1000 tx = Transaction( sender=sender, @@ -2609,17 +2551,15 @@ def test_inner_create_succeeds_code_deposit_state_gas( gas. On success the block state gas is the outer ``NEW_ACCOUNT`` plus the inner account creation and code deposit. """ - 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.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) + ) + Op.RETURN(31, 1, code_deposit_size=len(deploy_code)) inner_bytes = bytes(inner_initcode) + inner_code_deposit = inner_initcode.state_cost(fork) setup = Op.MSTORE( 0, @@ -2629,6 +2569,8 @@ def test_inner_create_succeeds_code_deposit_state_gas( inner_create = Op.POP(Op.CREATE2(0, 0, len(inner_bytes), 0)) else: inner_create = Op.POP(Op.CREATE(0, 0, len(inner_bytes))) + # Inner account creation plus the inner contract's code deposit. + inner_state_gas = inner_create.state_cost(fork) + inner_code_deposit if outer_outcome == "succeeds": termination = Op.RETURN(0, 0) @@ -2654,7 +2596,7 @@ def test_inner_create_succeeds_code_deposit_state_gas( # the inner code deposit. gas_limit = ( intrinsic_total - + gas_costs.NEW_ACCOUNT + + outer_state_gas + initcode_gas + inner_code_deposit + 1000 @@ -2710,9 +2652,6 @@ def test_nested_create_fail_parent_revert_state_gas( Verify factory nonce is rolled back when the factory reverts after a failed inner CREATE, and preserved when the factory returns. """ - gas_costs = fork.gas_costs() - create_state_gas = gas_costs.NEW_ACCOUNT - if child_failure == "revert": init_code = Op.REVERT(0, 0) else: @@ -2723,6 +2662,7 @@ def test_nested_create_fail_parent_revert_state_gas( if create_opcode == Op.CREATE2 else create_opcode(value=0, offset=0, size=len(init_code)) ) + create_state_gas = create_call.state_cost(fork) factory = pre.deploy_contract( code=( @@ -2832,7 +2772,6 @@ def test_inner_create_fail_refunds_in_creation_tx( 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)) @@ -2865,10 +2804,11 @@ def test_inner_create_fail_refunds_in_creation_tx( initcode_gas = initcode.gas_cost(fork) per_inner_slack = 2_000 + new_account = create_opcode(account_new=True).state_cost(fork) gas_limit = ( intrinsic_total + initcode_gas - + num_inner_ops * (gas_costs.NEW_ACCOUNT + per_inner_slack) + + num_inner_ops * (new_account + per_inner_slack) ) create_address = compute_create_address(address=sender, nonce=0) @@ -2992,7 +2932,6 @@ def test_create_account_creation_charge( makes state gas dominate, so gas_used drops by exactly NEW_ACCOUNT when refunded. """ - new_account = fork.gas_costs().NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) mstore_value, size = init_code_at_high_bytes(Op.STOP) create_call = ( @@ -3000,6 +2939,7 @@ def test_create_account_creation_charge( if create_opcode == Op.CREATE2 else create_opcode(value=0, offset=0, size=size) ) + new_account = create_call.state_cost(fork) storage = Storage() factory = pre.deploy_contract( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py index 4f31f16b85d..406550445af 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_delegation_pointer.py @@ -121,21 +121,21 @@ def test_delegation_pointer_new_account_state_gas( via a delegation pointer, the new-account state gas is charged identically to a direct call. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - target = pre.nonexistent_account() parent_storage = Storage() + call = Op.CALL( + gas=100_000, + address=target, + value=1, + value_transfer=True, + account_new=True, + ) contract = pre.deploy_contract( - code=( - Op.SSTORE( - parent_storage.store_next(1), - Op.CALL(gas=100_000, address=target, value=1), - ) - ), + code=Op.SSTORE(parent_storage.store_next(1), call), balance=1, ) + new_account_state_gas = call.state_cost(fork) # EOA delegates to the contract delegator = pre.fund_eoa(delegation=contract, amount=1) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py index 04a109b5cd8..02b3bab730f 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py @@ -52,28 +52,21 @@ def test_exact_coinbase_fee_simple_sstore( Motivated by BAL devnet-3 ethrex/besu coinbase balance mismatch where clients diverged on cumulative `receipt_gas_used`. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - # Gas breakdown for tx 1 (SSTORE zero-to-nonzero, no calldata): - # PUSH1(1) + PUSH1(0) + SSTORE(cold, zero-to-nonzero) + STOP - intrinsic_regular = gas_costs.TX_BASE - if fork.is_eip_enabled(2780): - # EIP-2780 surfaces an explicit recipient-access charge for - # non-self, non-create transactions on top of ``TX_BASE``. - intrinsic_regular += gas_costs.COLD_ACCOUNT_ACCESS - evm_regular = ( - 2 * gas_costs.VERY_LOW # PUSH1 + PUSH1 - + gas_costs.COLD_STORAGE_WRITE # SSTORE cold zero-to-nonzero - ) - tx1_gas_used = intrinsic_regular + evm_regular + sstore_state_gas - expected_coinbase = tx1_gas_used - # Tx 1: single SSTORE zero-to-nonzero sstore_storage = Storage() - sstore_contract = pre.deploy_contract( - code=(Op.SSTORE(sstore_storage.store_next(1), 1)), + sstore_code = Op.SSTORE(sstore_storage.store_next(1), 1) + sstore_contract = pre.deploy_contract(code=sstore_code) + + # tx 1 gas used: the intrinsic (TX_BASE plus the EIP-2780 + # recipient-access charge) plus the SSTORE code's own regular and + # state cost. + tx1_gas_used = ( + fork.transaction_intrinsic_cost_calculator()() + + sstore_code.gas_cost(fork) ) + expected_coinbase = tx1_gas_used # Tx 2: reporter reads BALANCE(COINBASE) into slot 0 reporter = pre.deploy_contract( diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py index d834129c84a..b9b1f8c5074 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_ordering.py @@ -43,11 +43,7 @@ def _single_sstore_probe_gas(fork: Fork) -> int: The probe bytecode is Op.SSTORE(0, 1): two pushes + SSTORE. """ - gas_costs = fork.gas_costs() - sstore_regular = gas_costs.COLD_STORAGE_WRITE - sstore_state = Op.SSTORE(new_value=1).state_cost(fork) - push_gas = 2 * gas_costs.VERY_LOW - return push_gas + sstore_regular + sstore_state - 1 + return Op.SSTORE(0, 1).gas_cost(fork) - 1 @pytest.mark.valid_from("EIP8037") @@ -69,7 +65,6 @@ def test_sstore_oog_reservoir_inflation_detection( With wrong ordering (state gas first): reservoir is inflated, probe succeeds. """ - gas_costs = fork.gas_costs() initcode = Initcode(deploy_code=Op.STOP) initcode_len = len(initcode) @@ -105,14 +100,13 @@ def test_sstore_oog_reservoir_inflation_detection( # Compute probe gas: enough for 4 SSTOREs' regular gas + pushes, # but after 4th regular charge, gas_left < the state gas spill. - sstore_regular = gas_costs.COLD_STORAGE_WRITE sstore_state = Op.SSTORE(new_value=1).state_cost(fork) - push_per_sstore = 2 * gas_costs.VERY_LOW + sstore_regular = Op.SSTORE(0, 1).regular_cost(fork) create_state_gas = fork.create_state_gas( code_size=len(initcode.deploy_code) ) spill = 4 * sstore_state - create_state_gas - probe_gas = 4 * (push_per_sstore + sstore_regular) + spill // 2 + probe_gas = 4 * sstore_regular + spill // 2 caller_storage = Storage() caller = pre.deploy_contract( @@ -165,9 +159,6 @@ def test_call_oog_reservoir_inflation_detection( A single-SSTORE probe detects the inflation: with correct reservoir (0) it OOGs; with inflated reservoir it succeeds. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - dead_address = 0xDEAD child_code = Op.CALL( gas=0, @@ -177,10 +168,12 @@ def test_call_oog_reservoir_inflation_detection( args_size=0, ret_offset=0, ret_size=0, + value_transfer=True, + account_new=True, ) - pushes_gas = 7 * gas_costs.VERY_LOW - call_regular_gas = gas_costs.COLD_ACCOUNT_ACCESS + gas_costs.CALL_VALUE - child_gas = pushes_gas + call_regular_gas + new_account_state_gas - 1 + # One gas short of the CALL's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -221,18 +214,11 @@ def test_selfdestruct_oog_reservoir_inflation_detection( Single-SSTORE probe detects the inflation. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - dead_beneficiary = 0xBEEF - child_code = Op.SELFDESTRUCT(dead_beneficiary) - pushes_gas = gas_costs.VERY_LOW - selfdestruct_regular_gas = ( - gas_costs.OPCODE_SELFDESTRUCT_BASE + gas_costs.COLD_ACCOUNT_ACCESS - ) - child_gas = ( - pushes_gas + selfdestruct_regular_gas + new_account_state_gas - 1 - ) + child_code = Op.SELFDESTRUCT(dead_beneficiary, account_new=True) + # One gas short of the SELFDESTRUCT's full cost (regular plus the + # NEW_ACCOUNT state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code, balance=1) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -280,39 +266,32 @@ def test_create_oog_reservoir_inflation_detection( (empty initcode) and `oog_on_init_code_word_cost` (32-byte initcode). """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - if oog_step == "create_base": initcode_size = 0 - setup_gas = 0 - init_code_word_cost = 0 else: initcode_size = WORD_SIZE - setup_gas = ( - Op.MSTORE.popped_stack_items * gas_costs.VERY_LOW - + gas_costs.OPCODE_MSTORE_BASE - + gas_costs.MEMORY_PER_WORD - ) - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD if create_opcode == Op.CREATE: - create_op = create_opcode(value=0, offset=0, size=initcode_size) + create_op = create_opcode( + value=0, offset=0, size=initcode_size, init_code_size=initcode_size + ) else: create_op = create_opcode( - value=0, offset=0, size=initcode_size, salt=0 + value=0, + offset=0, + size=initcode_size, + salt=0, + init_code_size=initcode_size, ) - pushes_gas = create_opcode.popped_stack_items * gas_costs.VERY_LOW if oog_step == "create_base": child_code = create_op else: - child_code = Op.MSTORE(0, 0) + create_op + child_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op - create_regular_gas = gas_costs.OPCODE_CREATE_BASE + init_code_word_cost - child_gas = ( - setup_gas + pushes_gas + create_regular_gas + new_account_state_gas - 1 - ) + # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + child_gas = child_code.gas_cost(fork) - 1 child = pre.deploy_contract(child_code) probe = pre.deploy_contract(Op.SSTORE(0, 1)) @@ -358,40 +337,33 @@ def test_create_oog_full_burn_no_state_credit( Verify a CREATE OOG inside a non-creation tx burns the whole tx gas_limit — no state-gas leftover is credited at tx-end. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - if oog_step == "create_base": initcode_size = 0 - setup_gas = 0 - init_code_word_cost = 0 else: initcode_size = WORD_SIZE - setup_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.OPCODE_MSTORE_BASE - + gas_costs.MEMORY_PER_WORD - ) - init_code_word_cost = gas_costs.CODE_INIT_PER_WORD if create_opcode == Op.CREATE: - create_op = create_opcode(value=0, offset=0, size=initcode_size) + create_op = create_opcode( + value=0, offset=0, size=initcode_size, init_code_size=initcode_size + ) else: create_op = create_opcode( - value=0, offset=0, size=initcode_size, salt=0 + value=0, + offset=0, + size=initcode_size, + salt=0, + init_code_size=initcode_size, ) - pushes_gas = create_opcode.popped_stack_items * gas_costs.VERY_LOW if oog_step == "create_base": factory_code = create_op else: - factory_code = Op.MSTORE(0, 0) + create_op + factory_code = Op.MSTORE(0, 0, new_memory_size=WORD_SIZE) + create_op factory = pre.deploy_contract(factory_code) - create_regular_gas = gas_costs.OPCODE_CREATE_BASE + init_code_word_cost - body_gas = ( - setup_gas + pushes_gas + create_regular_gas + new_account_state_gas - 1 - ) + # One gas short of the CREATE's full cost (regular plus the NEW_ACCOUNT + # state charge), so it OOGs on the account-creation charge. + body_gas = factory_code.gas_cost(fork) - 1 intrinsic_calc = fork.transaction_intrinsic_cost_calculator() tx_gas_limit = intrinsic_calc() + body_gas diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py index d52ac0d60b1..99af67b5a2c 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_pricing.py @@ -556,22 +556,21 @@ def test_call_new_account_state_gas_scales_with_cpsb( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None env = Environment(gas_limit=block_gas_limit) - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - empty = pre.fund_eoa(0) + call = Op.CALL( + gas=100_000, + address=empty, + value=1, + value_transfer=True, + account_new=True, + ) storage = Storage() contract = pre.deploy_contract( - code=( - Op.SSTORE( - storage.store_next(1, "call_success"), - Op.CALL(gas=100_000, address=empty, value=1), - ) - ), + code=Op.SSTORE(storage.store_next(1, "call_success"), call), balance=1, ) - tx_gas = min(gas_limit_cap + new_account_state_gas, block_gas_limit) + tx_gas = min(gas_limit_cap + call.state_cost(fork), block_gas_limit) tx = Transaction( to=contract, gas_limit=tx_gas, @@ -599,8 +598,7 @@ def test_selfdestruct_new_beneficiary_scales_with_cpsb( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None env = Environment(gas_limit=block_gas_limit) - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = pre.fund_eoa(0) storage = Storage() 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 445cb1d885c..5ac3ba9226c 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 @@ -174,15 +174,13 @@ def test_insufficient_gas_for_sstore_state_cost( gas, but not enough to also cover the SSTORE state gas. The SSTORE should OOG, leaving storage slot 0 unchanged at zero. """ - gas_costs = fork.gas_costs() - contract = pre.deploy_contract( - code=Op.SSTORE(0, 1), - ) + contract_code = Op.SSTORE(0, 1) + contract = pre.deploy_contract(code=contract_code) # Enough for intrinsic + warm SSTORE regular gas, but not the # state gas cost for zero-to-nonzero transition intrinsic_cost = fork.transaction_intrinsic_cost_calculator() - gas_limit = intrinsic_cost() + gas_costs.COLD_STORAGE_WRITE + gas_limit = intrinsic_cost() + contract_code.regular_cost(fork) tx = Transaction( to=contract, @@ -690,12 +688,13 @@ def test_create_tx_reservoir( beyond TX_MAX_GAS_LIMIT feeds the reservoir. When False, all state gas comes from gas_left (reservoir is zero). """ - gas_costs = fork.gas_costs() gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None init_code = Op.STOP - create_state_gas = gas_costs.NEW_ACCOUNT + create_state_gas = fork.transaction_top_frame_state_gas( + contract_creation=True + ) if gas_above_cap: gas_limit = gas_limit_cap + create_state_gas @@ -1247,7 +1246,7 @@ def test_nested_failure_resets_to_tx_reservoir( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT + new_account_state_gas = Op.CREATE(account_new=True).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() body_state_total = sum(b.state_cost(fork) for b in frame_bodies) 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 ff0fec86530..f02eaca1336 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 @@ -50,7 +50,7 @@ def test_selfdestruct_new_beneficiary_state_gas( spilled into `gas_left` (in-cap tx): the block bills NEW_ACCOUNT in the state dimension and the beneficiary is created. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = 0xDEAD contract = pre.deploy_contract( @@ -184,8 +184,7 @@ def test_selfdestruct_new_beneficiary_header_gas_used( beneficiary, charging GAS_NEW_ACCOUNT state gas. The block must be accepted with correct 2D gas accounting in the header. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) beneficiary = pre.fund_eoa(amount=0) @@ -232,7 +231,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( transfer remains billed. """ beneficiary = 0xDEAD - inner_code = Op.SELFDESTRUCT(beneficiary) + inner_code = Op.SELFDESTRUCT(beneficiary, account_new=True) inner = pre.deploy_contract(code=inner_code, balance=1) caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.REVERT(0, 0) caller = pre.deploy_contract(code=caller_code) @@ -240,8 +239,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( expected_regular = ( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) - + inner_code.gas_cost(fork) - + fork.gas_costs().ACCOUNT_WRITE + + inner_code.regular_cost(fork) ) tx = Transaction(to=caller, sender=pre.fund_eoa()) @@ -271,8 +269,6 @@ def test_create_selfdestruct_no_refund_account_and_storage( num_slots: int, ) -> None: """Verify same tx CREATE+SELFDESTRUCT does not refund state gas.""" - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() init_code = Bytecode() @@ -299,7 +295,9 @@ def test_create_selfdestruct_no_refund_account_and_storage( factory_code = mstore + Op.POP(create_call) factory = pre.deploy_contract(code=factory_code) - total_state_gas = new_account_state_gas + num_slots * sstore_state_gas + total_state_gas = factory_code.state_cost(fork) + init_code.state_cost( + fork + ) regular_used = ( intrinsic_gas + factory_code.gas_cost(fork) @@ -344,8 +342,6 @@ def test_create_selfdestruct_no_refund_code_deposit_state_gas( state gas. """ assert code_size >= 2 - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - code_deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) if beneficiary_type == "self": selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) @@ -382,7 +378,7 @@ def test_create_selfdestruct_no_refund_code_deposit_state_gas( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = new_account_state_gas + code_deposit_state_gas + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) tx = Transaction( to=factory, data=bytes(initcode), @@ -407,9 +403,6 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( Verify block header gas reflects the full account plus code-deposit state-gas charge on a same-tx CREATE+SELFDESTRUCT. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) sd_len = len(bytes(selfdestruct)) code_size = 256 @@ -417,7 +410,6 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( 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, @@ -439,7 +431,7 @@ def test_create_selfdestruct_code_deposit_no_refund_header_check( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = new_account_state_gas + code_deposit_state_gas + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) tx = Transaction( to=factory, data=bytes(initcode), @@ -472,7 +464,6 @@ def test_create_selfdestruct_sstore_restoration_refund( Verify SSTORE restoration still refunds its slot state gas when the surrounding contract SELFDESTRUCTs. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() @@ -500,6 +491,7 @@ def test_create_selfdestruct_sstore_restoration_refund( factory_code = mstore + Op.POP(create_call) factory = pre.deploy_contract(code=factory_code) + new_account_state_gas = factory_code.state_cost(fork) state_used = new_account_state_gas regular_used = ( intrinsic_gas @@ -595,8 +587,6 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( Verify SELFDESTRUCT in a nested DELEGATECALL/CALLCODE frame below a same-tx-created contract does not refund state gas. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() # Bottom of the chain does the SELFDESTRUCT; intermediate helpers @@ -627,9 +617,6 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( + Op.STOP ) deployed = bytes(deployed_code) - code_deposit_state_gas = fork.code_deposit_state_gas( - code_size=len(deployed) - ) initcode = Initcode(deploy_code=deployed) initcode_len = len(initcode) @@ -678,18 +665,14 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( factory = pre.deploy_contract(code=factory_code) created_address = compute_create_address(address=factory, nonce=1) - total_state_gas = ( - new_account_state_gas + code_deposit_state_gas + 2 * sstore_state_gas - ) + total_state_gas = factory_code.state_cost(fork) + initcode.state_cost(fork) regular_used = ( intrinsic_gas + factory_code.gas_cost(fork) + initcode.gas_cost(fork) + deployed_code.gas_cost(fork) + chain_regular_gas - - new_account_state_gas - - code_deposit_state_gas - - 2 * sstore_state_gas + - total_state_gas ) expected_gas_used = max(regular_used, total_state_gas) @@ -768,9 +751,8 @@ def test_create_tx_selfdestruct_initcode_state_gas( ) -> None: """ Verify a creation tx whose initcode SELFDESTRUCTs the new contract - still pays the intrinsic NEW_ACCOUNT state gas. + still pays the top-frame NEW_ACCOUNT state gas. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT intrinsic_calc = fork.transaction_intrinsic_cost_calculator() sender = pre.fund_eoa(amount=10**18) @@ -783,30 +765,32 @@ def test_create_tx_selfdestruct_initcode_state_gas( else: beneficiary = pre.fund_eoa(amount=0) + creates_new_beneficiary = beneficiary_kind == "empty" and tx_value > 0 + # `current_target` is added to `accessed_addresses` at message # entry, so SELFDESTRUCT to self skips the cold-access surcharge. if beneficiary_kind == "self": - init_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)( - beneficiary - ) + init_code = Op.SELFDESTRUCT.with_metadata( + address_warm=True, account_new=creates_new_beneficiary + )(beneficiary) else: - init_code = Op.SELFDESTRUCT(beneficiary) - intrinsic_total = intrinsic_calc( + init_code = Op.SELFDESTRUCT.with_metadata( + account_new=creates_new_beneficiary + )(beneficiary) + intrinsic_regular = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) - intrinsic_regular = intrinsic_total - new_account_state_gas - creates_new_beneficiary = beneficiary_kind == "empty" and tx_value > 0 - expected_state = new_account_state_gas + ( - new_account_state_gas if creates_new_beneficiary else 0 - ) + expected_state = fork.transaction_top_frame_state_gas( + contract_creation=True + ) + init_code.state_cost(fork) expected_regular = intrinsic_regular + init_code.regular_cost(fork) expected_gas_used = max(expected_regular, expected_state) tx = Transaction( to=None, data=init_code, - gas_limit=intrinsic_total + 100_000 + expected_state, + gas_limit=intrinsic_regular + 100_000 + expected_state, sender=sender, value=tx_value, ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py index f0df65f9569..c537b42088b 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_set_code.py @@ -2092,7 +2092,6 @@ def test_same_tx_clear_then_reset_pre_delegated( intrinsic_regular, top_frame_regular, top_frame_state = _auth_gas( fork, authorization_list ) - assert top_frame_regular == fork.gas_costs().ACCOUNT_WRITE assert top_frame_state == 0 cumulative_gas_used, header_gas_used = _receipt_and_header( intrinsic_regular, top_frame_regular, top_frame_state diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py index 653a986f8cc..71dcef99b11 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py @@ -436,8 +436,7 @@ def test_sstore_stipend_check_excludes_reservoir( With below_stipend: SSTORE fails (gas_left too low, reservoir ignored). With at_stipend: SSTORE has full regular gas and proceeds. """ - gas_costs = fork.gas_costs() - stipend = gas_costs.CALL_STIPEND + 1 + stipend = fork.call_value_stipend() + 1 sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: Op.SSTORE(0, 1) = 2 pushes + SSTORE opcode. @@ -446,12 +445,12 @@ def test_sstore_stipend_check_excludes_reservoir( # Full regular gas for the child (pushes + SSTORE regular cost). # State gas comes from the reservoir so it doesn't affect gas_left. - child_full_regular = child_code.gas_cost(fork) - sstore_state_gas + child_full_regular = child_code.regular_cost(fork) # below_stipend: give 1 less than stipend after pushes, fails check. # at_stipend: give full regular gas, passes check and completes. if gas_above_stipend < 0: - push_gas = 2 * gas_costs.VERY_LOW + push_gas = 2 * Op.PUSH1(0).regular_cost(fork) child_gas = push_gas + stipend - 1 else: child_gas = child_full_regular @@ -812,14 +811,8 @@ def test_sstore_restoration_charge_in_ancestor( refund must propagate up the chain to the ancestor that charged the 0 to x. A probe SSTORE sized to OOG by 1 detects any loss. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + sstore_state_gas - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 # Innermost frame does x to 0; each hop above delegates down. delegate_target = pre.deploy_contract( @@ -885,15 +878,9 @@ def test_sstore_restoration_sub_frame_revert( to OOG by 1 then fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 child_code = Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.REVERT(0, 0) child = pre.deploy_contract(code=child_code) @@ -940,16 +927,10 @@ def test_sstore_restoration_ancestor_revert( sized to OOG by 1 fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 set_op = Op.SSTORE.with_metadata( key_warm=False, @@ -1039,17 +1020,11 @@ def test_sstore_restoration_charge_in_ancestor_intermediate_revert( amount must reach the caller via `incorporate_child_on_error`. A probe SSTORE sized to OOG by 1 detects loss. """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() # Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1, # so it OOGs by 1 when the reservoir is 0 and succeeds otherwise. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + sstore_state_gas - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 inner_code = ( Op.SSTORE.with_metadata( @@ -1137,15 +1112,9 @@ def test_sstore_restoration_create_init_revert( fails, since its fixed forwarded gas cannot reach the `gas_left` refund. """ - gas_costs = fork.gas_costs() # Probe SSTORE(0, 1): 2 pushes + cold write + state gas - 1. OOGs by # 1 when the reservoir is 0, as forwarded gas misses gas_left. - probe_gas = ( - 2 * gas_costs.VERY_LOW - + gas_costs.COLD_STORAGE_WRITE - + Op.SSTORE(new_value=1).state_cost(fork) - - 1 - ) + probe_gas = Op.SSTORE(0, 1).gas_cost(fork) - 1 init_code = Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.REVERT(0, 0) probe = pre.deploy_contract(code=Op.SSTORE(0, 1)) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py index 1337c77b230..d214e83bc21 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py @@ -41,25 +41,6 @@ pytestmark = pytest.mark.valid_from("Amsterdam") -def _access_list_floor_token_gas( - access_list: List[AccessList], fork: Fork -) -> int: - """ - Return the EIP-7981 calldata-floor-token gas the Amsterdam intrinsic - calculator charges for an access list. - - Every byte of each address (20) and storage key (32) is four floor - tokens, each priced at ``TX_DATA_TOKEN_FLOOR``. Subtracting this from - the measured intrinsic delta isolates the pure EIP-8038 per-entry - surcharge. - """ - total_bytes = 0 - for access in access_list: - total_bytes += len(access.address) - total_bytes += 32 * len(access.storage_keys) - return total_bytes * 4 * fork.gas_costs().TX_DATA_TOKEN_FLOOR - - def _make_access_list( n_addr: int, n_keys_each: int, *, duplicate: bool = False ) -> List[AccessList]: @@ -101,25 +82,7 @@ def test_access_list_intrinsic_surcharge( A simple value-less transaction then exercises the access list end to end. """ - gas_costs = fork.gas_costs() - intrinsic = fork.transaction_intrinsic_cost_calculator() - access_list = _make_access_list(n_addr, n_keys_each, duplicate=duplicate) - n_keys = n_addr * n_keys_each - - base = intrinsic(return_cost_deducted_prior_execution=True) - with_al = intrinsic( - access_list=access_list, - return_cost_deducted_prior_execution=True, - ) - surcharge = ( - with_al - base - _access_list_floor_token_gas(access_list, fork) - ) - expected = ( - n_addr * gas_costs.TX_ACCESS_LIST_ADDRESS - + n_keys * gas_costs.TX_ACCESS_LIST_STORAGE_KEY - ) - assert surcharge == expected contract = pre.deploy_contract(code=Op.STOP) tx = Transaction( @@ -149,8 +112,6 @@ def test_access_list_duplicate_address_key_intrinsic_and_warmth( runtime the slot is nonetheless warm on its first ``SLOAD`` (``WARM_SLOAD``), since warmth is set-membership, not a counter. """ - gas_costs = fork.gas_costs() - intrinsic = fork.transaction_intrinsic_cost_calculator() slot = 0x42 # First runtime SLOAD of the listed slot stores the warm access cost. @@ -175,20 +136,6 @@ def test_access_list_duplicate_address_key_intrinsic_and_warmth( AccessList(address=contract, storage_keys=[slot]), ] - base = intrinsic(return_cost_deducted_prior_execution=True) - with_al = intrinsic( - access_list=access_list, - return_cost_deducted_prior_execution=True, - ) - surcharge = ( - with_al - base - _access_list_floor_token_gas(access_list, fork) - ) - expected_surcharge = ( - 2 * gas_costs.TX_ACCESS_LIST_ADDRESS - + 2 * gas_costs.TX_ACCESS_LIST_STORAGE_KEY - ) - assert surcharge == expected_surcharge - expected_gas = Op.SLOAD(key_warm=True).gas_cost(fork) tx = Transaction( to=contract, @@ -218,8 +165,7 @@ def test_access_list_warms_storage_slot( overwrite of a non-zero original to a new non-zero value pays ``WARM_SLOAD + STORAGE_WRITE``. """ - gas_costs = fork.gas_costs() - very_low = gas_costs.VERY_LOW + very_low = Op.PUSH1(0).regular_cost(fork) slot = 0x42 if op == "SLOAD": diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py index 37e543f4376..c58fc42934d 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py @@ -89,8 +89,6 @@ def test_call_access_gas( EIP-8038 charges ``COLD_ACCOUNT_ACCESS`` (3,000) cold and ``WARM_ACCESS`` (100) warm for all four call opcodes. """ - gas_costs = fork.gas_costs() - target = pre.deploy_contract(Op.STOP) measured_code = call_opcode(gas=0, address=target) @@ -99,11 +97,8 @@ def test_call_access_gas( pre, fork, measured_code, call_opcode(address_warm=False) ) - expected_gas = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # Cross-check the framework opcode model agrees with the formula. - assert expected_gas == cost_metadata.gas_cost(fork) + # The opcode's own cost is the expected access gas. + expected_gas = cost_metadata.gas_cost(fork) access_list = ( [AccessList(address=target, storage_keys=[])] if warm else None @@ -143,24 +138,14 @@ def test_call_value_alive_target_gas( the caller is ``access + ACCOUNT_WRITE`` while the *charged* schedule is ``access + CALL_VALUE``. Both are asserted. """ - gas_costs = fork.gas_costs() transfers_value = call_opcode in (Op.CALL, Op.CALLCODE) - # Verify the EIP-8038 decomposition of the value-transfer charge. - assert gas_costs.CALL_VALUE == gas_costs.ACCOUNT_WRITE + ( - gas_costs.CALL_STIPEND - ) # The measured-vs-charged duality below hinges on the callee being a - # pure `STOP`: it executes no opcodes, so the forwarded `CALL_STIPEND` - # is wholly unused and returned. Pin that the callee is exactly the - # single zero byte with no gas cost, and that the returned stipend is - # precisely `CALL_VALUE - ACCOUNT_WRITE`. + # pure `STOP`: it executes no opcodes, so the forwarded value-call + # stipend is wholly unused and returned. callee = Op.STOP assert bytes(callee) == b"\x00" assert callee.gas_cost(fork) == 0 - assert gas_costs.CALL_VALUE - gas_costs.ACCOUNT_WRITE == ( - gas_costs.CALL_STIPEND - ) # Alive target with balance so no account creation occurs. target = pre.deploy_contract(callee, balance=1) @@ -191,22 +176,14 @@ def test_call_value_alive_target_gas( pre, fork, measured_code, own_cold, balance=1 ) - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # Charged schedule: access + CALL_VALUE (verified via the opcode - # model). CALL gas is wholly regular under EIP-8038 (no state map). - charged_gas = access_cost + ( - gas_costs.CALL_VALUE if transfers_value else 0 - ) - assert charged_gas == cost_metadata.gas_cost(fork) + # CALL gas is wholly regular under EIP-8038 (no state map). assert cost_metadata.state_cost(fork) == 0 - # Consumed gas: the STOP callee returns the forwarded CALL_STIPEND, - # so the caller's measured consumption is access + ACCOUNT_WRITE for - # value transfers, and just access otherwise. - measured_gas = access_cost + ( - gas_costs.ACCOUNT_WRITE if transfers_value else 0 + # Consumed gas: the STOP callee returns the forwarded stipend, so the + # caller's measured consumption is the opcode's charged cost minus the + # stipend for value transfers, and just the access cost otherwise. + measured_gas = cost_metadata.gas_cost(fork) - ( + fork.call_value_stipend() if transfers_value else 0 ) access_list = ( @@ -237,7 +214,6 @@ def test_callcode_value_to_nonexistent_no_new_account( created. The block ``gas_used`` therefore equals the regular tx cost with ``CALL_VALUE`` but with no 183,600 state-gas component. """ - gas_costs = fork.gas_costs() intrinsic = fork.transaction_intrinsic_cost_calculator()() target = 0xDEAD # non-existent @@ -260,24 +236,18 @@ def test_callcode_value_to_nonexistent_no_new_account( caller_code = Op.POP(callcode) + Op.STOP caller = pre.deploy_contract(code=caller_code, balance=1) - # CALLCODE-to-nonexistent regular charge: access + CALL_VALUE, no - # NEW_ACCOUNT (asserted via the metadata-only opcode model). + # CALLCODE carries no state-gas (NEW_ACCOUNT) component: the value + # stays in the caller's own context, so no beneficiary is created. callcode_meta = Op.CALLCODE(address_warm=False, value_transfer=True) - assert callcode_meta.gas_cost(fork) == gas_costs.COLD_ACCOUNT_ACCESS + ( - gas_costs.CALL_VALUE - ) - # CALLCODE carries no state-gas (NEW_ACCOUNT) component. assert callcode_meta.state_cost(fork) == 0 # Whole tx is regular gas; no NEW_ACCOUNT state component appears. - # The CALLCODE forwards CALL_STIPEND to the callee, which (running in - # the caller's own context with empty code) leaves it unused and - # returns it, so consumed gas is the charge minus the stipend. + # The CALLCODE forwards the value-call stipend to the callee, which + # (running in the caller's own context with empty code) leaves it + # unused and returns it, so consumed gas is the charge minus stipend. expected_gas_used = ( - intrinsic + caller_code.gas_cost(fork) - gas_costs.CALL_STIPEND + intrinsic + caller_code.gas_cost(fork) - fork.call_value_stipend() ) - # Guard the no-state assertion: NEW_ACCOUNT would dominate if charged. - assert expected_gas_used < gas_costs.NEW_ACCOUNT tx = Transaction( to=caller, @@ -305,16 +275,13 @@ def test_call_value_to_new_account_seam( dimension. The block header reflects ``max(regular, state)``, which is dominated by the state charge. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT intrinsic = fork.transaction_intrinsic_cost_calculator()() # Fresh, value-receiving target (state-empty, will be created). target = pre.fund_eoa(amount=0) - # Metadata-bearing CALL so `caller_code.gas_cost(fork)` folds the - # value transfer and account-creation charges; we then split off the - # NEW_ACCOUNT state component for the 2D header accounting. + # Metadata-bearing CALL so its cost splits into the regular + # (access + value transfer) and state (NEW_ACCOUNT) dimensions. call = Op.CALL.with_metadata( address_warm=False, value_transfer=True, account_new=True )( @@ -329,23 +296,15 @@ def test_call_value_to_new_account_seam( caller_code = Op.POP(call) + Op.STOP caller = pre.deploy_contract(code=caller_code, balance=1) - # Regular dimension: access + value (NOT new account, which is the - # state dimension). Asserted via the metadata-only opcode model. - call_meta = Op.CALL( - address_warm=False, value_transfer=True, account_new=True - ) - call_regular = call_meta.gas_cost(fork) - new_account_state_gas - assert call_regular == gas_costs.COLD_ACCOUNT_ACCESS + gas_costs.CALL_VALUE - assert call_regular == 13_300 - - # block_gas_used = max(block_regular, block_state). The CALL opcode - # has no state-gas map, so its NEW_ACCOUNT charge spills as regular - # gas in the bytecode total; strip it back out to isolate the - # regular axis and re-add NEW_ACCOUNT explicitly on the state axis. - tx_regular = intrinsic + caller_code.gas_cost(fork) - new_account_state_gas - tx_state = new_account_state_gas + new_account_state_gas = call.state_cost(fork) + + # block_gas_used = max(block_regular, block_state). The CALL's + # NEW_ACCOUNT lands on the state axis; the regular axis is the + # access plus value-transfer cost. + tx_regular = intrinsic + caller_code.regular_cost(fork) + tx_state = caller_code.state_cost(fork) expected_gas_used = max(tx_regular, tx_state) - # State must dominate here, proving the 183,600 hit the state axis. + # State must dominate here, proving NEW_ACCOUNT hit the state axis. assert expected_gas_used == new_account_state_gas tx = Transaction( @@ -390,8 +349,6 @@ def test_call_to_delegated_target_double_access( ``STATICCALL`` carry no value but still pay the delegation surcharge. """ - gas_costs = fork.gas_costs() - # Final code-bearing account that the delegation points at. delegate = pre.deploy_contract(Op.STOP) # EOA delegated (EIP-7702) to `delegate`. @@ -407,16 +364,8 @@ def test_call_to_delegated_target_double_access( pre, fork, measured_code, call_opcode(address_warm=False) ) - target_cost = ( - gas_costs.WARM_ACCESS if target_warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - delegate_cost = ( - gas_costs.WARM_ACCESS - if delegate_warm - else gas_costs.COLD_ACCOUNT_ACCESS - ) - expected_gas = target_cost + delegate_cost - assert expected_gas == cost_metadata.gas_cost(fork) + # The opcode's own cost folds the target and delegate accesses. + expected_gas = cost_metadata.gas_cost(fork) # Warm the target and/or the delegate leaf via the access list. access_entries = [] @@ -493,8 +442,6 @@ def test_call_self_is_warm( The current target is in the accessed-addresses set on message entry, so a call to ``ADDRESS`` pays only ``WARM_ACCESS`` (100). """ - gas_costs = fork.gas_costs() - # `Op.ADDRESS` is the call's address argument, embedded inside the # runnable call; the self address is in the accessed set on entry, so # the call is warm. The overhead subtracts the call's own cold cost, @@ -505,7 +452,6 @@ def test_call_self_is_warm( ) expected_gas = call_opcode(address_warm=True).gas_cost(fork) - assert expected_gas == gas_costs.WARM_ACCESS tx = Transaction(to=measure_address, sender=pre.fund_eoa()) @@ -539,15 +485,15 @@ def test_call_forwarded_gas_63_64( ``gas_left`` already net of the post-8038 cold access cost (not before it, and not double-charging it). """ - gas_costs = fork.gas_costs() sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) # Child: a single cold zero-to-nonzero SSTORE as proof of execution. # Its regular need is the two operand pushes plus the cold storage # write (the state portion is funded separately via the reservoir, # which is passed to the child in full with no 63/64 rule). - child = pre.deploy_contract(Op.SSTORE(0, 1)) - child_regular = 2 * gas_costs.VERY_LOW + gas_costs.COLD_STORAGE_WRITE + child_code = Op.SSTORE(0, 1) + child = pre.deploy_contract(child_code) + child_regular = child_code.regular_cost(fork) # Smallest budget whose 63/64 floor still reaches `child_regular`. forward_budget = child_regular * 64 // 63 @@ -557,24 +503,21 @@ def test_call_forwarded_gas_63_64( # Wrapper: cold zero-value CALL requesting max gas (so the forwarded # amount is bound by `gas_left`, not by the request). ret_size=0 # avoids any memory-expansion term. - wrapper = pre.deploy_contract( - Op.CALL( - gas=0xFFFFFFFF, - address=child, - value=0, - args_offset=0, - args_size=0, - ret_offset=0, - ret_size=0, - ) + wrapper_call = Op.CALL( + gas=0xFFFFFFFF, + address=child, + value=0, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, ) + wrapper = pre.deploy_contract(wrapper_call) - # At the wrapper's CALL the access charge (`extra_gas`) is deducted - # first, leaving exactly `forward_budget` as `gas_left` for the 63/64 - # floor. The seven CALL operand pushes precede it. - wrapper_pushes = 7 * gas_costs.VERY_LOW - extra_gas = gas_costs.COLD_ACCOUNT_ACCESS # cold call, value 0 - wrapper_gas = wrapper_pushes + extra_gas + forward_budget + # At the wrapper's CALL the cold access charge is deducted first + # (folded with the operand pushes into its regular cost), leaving + # exactly `forward_budget` as `gas_left` for the 63/64 floor. + wrapper_gas = wrapper_call.regular_cost(fork) + forward_budget # Outer caller hands the wrapper exactly `wrapper_gas`. caller = pre.deploy_contract( @@ -610,9 +553,7 @@ def test_account_warmth_reverts_on_subcall_revert( rolled back on revert (mirrors the ``SLOAD`` warmth-revert case for the account dimension). """ - gas_costs = fork.gas_costs() cold_gas = Op.BALANCE(address_warm=False).gas_cost(fork) - assert cold_gas == gas_costs.COLD_ACCOUNT_ACCESS # Address whose warmth we probe; left out of the access list so its # first runtime touch is cold. @@ -664,8 +605,6 @@ def test_call_to_double_delegated_target_single_hop( second hop, so ``final``'s leaf is not charged. Both the framework opcode model and a runtime ``CodeGasMeasure`` confirm the value. """ - gas_costs = fork.gas_costs() - # A -> B -> C delegation chain. `mid` is an EOA whose code is the # 7702 delegation designator pointing at `final`; `target` delegates # to `mid` in turn. @@ -680,8 +619,8 @@ def test_call_to_double_delegated_target_single_hop( delegated_address=True, delegated_address_warm=False, ) - expected_gas = 2 * gas_costs.COLD_ACCOUNT_ACCESS - assert expected_gas == cost_metadata.gas_cost(fork) + # Cold target leaf plus cold delegation leaf; no state gas. + expected_gas = cost_metadata.gas_cost(fork) assert cost_metadata.state_cost(fork) == 0 measured_code = Op.CALL(gas=0, address=target) @@ -711,8 +650,6 @@ def test_call_precompile_is_warm( every transaction, so a call to one pays only ``WARM_ACCESS`` (100). The identity precompile (address 4) is used as the target. """ - gas_costs = fork.gas_costs() - identity_precompile = Address(4) measured_code = call_opcode(gas=0, address=identity_precompile) @@ -721,7 +658,6 @@ def test_call_precompile_is_warm( ) expected_gas = call_opcode(address_warm=True).gas_cost(fork) - assert expected_gas == gas_costs.WARM_ACCESS tx = Transaction(to=measure_address, sender=pre.fund_eoa()) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py index d76057ecf1e..f91e6091f87 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py @@ -74,15 +74,6 @@ def test_create_regular_gas( account-creation state gas is excluded by subtracting ``create_state_gas(0)``. """ - gas_costs = fork.gas_costs() - # The EIP-8038 CREATE regular base equals ACCOUNT_WRITE + - # COLD_STORAGE_ACCESS = 11,000. - assert gas_costs.OPCODE_CREATE_BASE == 11_000 - assert ( - gas_costs.OPCODE_CREATE_BASE - == gas_costs.ACCOUNT_WRITE + gas_costs.COLD_STORAGE_ACCESS - ) - # Isolate the regular dimension: opcode total minus its account # creation state gas (the only state component carried by the CREATE # opcode itself; code deposit is charged on RETURN inside initcode). @@ -93,17 +84,6 @@ def test_create_regular_gas( # Equivalent isolation via the regular_cost helper. assert regular_gas == create_meta.regular_cost(fork) - init_code_words = (init_code_size + 31) // 32 - expected_regular = ( - gas_costs.OPCODE_CREATE_BASE - + gas_costs.CODE_INIT_PER_WORD * init_code_words - ) - if create_opcode == Op.CREATE2: - expected_regular += ( - gas_costs.OPCODE_KECCAK256_PER_WORD * init_code_words - ) - assert regular_gas == expected_regular - # Runtime confirmation via CodeGasMeasure: a factory whose CREATE # deploys empty code, so no code-deposit state gas is charged and the # only state component is the account-creation gas funded from the @@ -125,7 +105,8 @@ def test_create_regular_gas( if create_opcode == Op.CREATE2 else Op.CREATE(value=0, offset=0, size=init_code_size) ) - arg_pushes = (4 if create_opcode == Op.CREATE2 else 3) * gas_costs.VERY_LOW + push_cost = Op.PUSH1(0).regular_cost(fork) + arg_pushes = (4 if create_opcode == Op.CREATE2 else 3) * push_cost memory_setup = ( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE, new_memory_size=init_code_size) @@ -177,36 +158,24 @@ def test_create2_keccak_word_delta( regular cost shared with ``CREATE``. Both opcodes carry the identical EIP-8038 ``CREATE_ACCESS`` base and EIP-3860 word cost. - The regular-gas delta is asserted via the opcode model - (``create2_regular - create_regular`` equals the keccak word - surcharge). At runtime a factory then measures a single ``CREATE2`` - with ``CodeGasMeasure`` and stores its absolute regular cost: the - surcharge is established by the model assertion, and the runtime leg - confirms the absolute ``CREATE2`` regular cost. + A factory measures a single ``CREATE2`` with ``CodeGasMeasure`` and + stores its absolute regular cost, confirming the opcode's own + ``regular_cost`` (which folds the keccak word surcharge) against the + runtime charge. """ - gas_costs = fork.gas_costs() - init_code_words = (init_code_size + 31) // 32 - keccak_surcharge = gas_costs.OPCODE_KECCAK256_PER_WORD * init_code_words - - create_regular = Op.CREATE(init_code_size=init_code_size).regular_cost( - fork - ) create2_regular = Op.CREATE2(init_code_size=init_code_size).regular_cost( fork ) - assert create2_regular - create_regular == keccak_surcharge - - # Runtime confirmation. Init code is all-zero bytes (`STOP`), so the - # child frame halts immediately (zero gas) depositing empty code; the - # CREATE2 charges no code-deposit state gas and no child execution gas - # is folded into the measurement. The single CREATE2 regular cost is - # measured via CodeGasMeasure with a reservoir sized for its account - # creation state gas, keeping the GAS-measured `gas_left` free of - # state-gas spill. The opcode-model assertion above is the - # load-bearing keccak-delta check; this confirms the absolute value. + + # Init code is all-zero bytes (`STOP`), so the child frame halts + # immediately (zero gas) depositing empty code; the CREATE2 charges no + # code-deposit state gas and no child execution gas is folded into the + # measurement. The single CREATE2 regular cost is measured via + # CodeGasMeasure with a reservoir sized for its account creation state + # gas, keeping the GAS-measured `gas_left` free of state-gas spill. padded = b"\x00" * init_code_size - push4 = 4 * gas_costs.VERY_LOW + push4 = 4 * Op.PUSH1(0).regular_cost(fork) storage = Storage() measure_create2 = CodeGasMeasure( code=Op.CREATE2(value=0, offset=0, size=init_code_size, salt=0), @@ -303,7 +272,9 @@ def exact_execution_gas( flat regular per-byte deposit cost. The single call is therefore correct in either regime. """ - execution = exact_intrinsic_gas + fork.gas_costs().NEW_ACCOUNT + execution = exact_intrinsic_gas + fork.transaction_top_frame_state_gas( + contract_creation=True + ) execution += initcode.execution_gas(fork) execution += initcode.deployment_gas(fork) return execution @@ -377,7 +348,9 @@ def test_create_tx_gas_boundary( elif succeeds: # Fresh target: top-frame NEW_ACCOUNT plus the per-byte code # deposit are the state-gas axis; the rest is regular. - state_used = fork.gas_costs().NEW_ACCOUNT + state_used = fork.transaction_top_frame_state_gas( + contract_creation=True + ) state_used += fork.code_deposit_state_gas( code_size=len(initcode.deploy_code) ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py index 210879bf6f3..667cedbb638 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py @@ -180,7 +180,7 @@ def test_selfdestruct_funds_new_account( tx = Transaction( to=suicidal, gas_limit=1_000_000, - state_gas_reservoir=fork.gas_costs().NEW_ACCOUNT, + state_gas_reservoir=Op.SELFDESTRUCT(account_new=True).state_cost(fork), sender=pre.fund_eoa(), ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py index 40376c8af55..6108be154d3 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py @@ -67,18 +67,6 @@ def test_access_list_no_fallback( by ``num_addresses * addr_delta + num_keys * key_delta``; with the sender funded to the wei, that fallback must not slip through. """ - new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() - addr_delta = ( - new_costs.TX_ACCESS_LIST_ADDRESS - old_costs.TX_ACCESS_LIST_ADDRESS - ) - key_delta = ( - new_costs.TX_ACCESS_LIST_STORAGE_KEY - - old_costs.TX_ACCESS_LIST_STORAGE_KEY - ) - fallback_delta = num_addresses * addr_delta + num_keys * key_delta - assert fallback_delta > 0 - # All storage keys live on the first listed address; the remaining # addresses carry no keys. storage_keys = list(range(num_keys)) @@ -94,10 +82,8 @@ def test_access_list_no_fallback( access_list=access_list, return_cost_deducted_prior_execution=True, ) - # One gas below the spec-correct intrinsic: a fallback client using - # the old constants needs only `intrinsic - fallback_delta`. + # One gas below the spec-correct intrinsic. gas_limit = intrinsic - 1 - assert intrinsic - fallback_delta <= gas_limit < intrinsic sender = pre.fund_eoa(amount=gas_limit * GAS_PRICE) tx = Transaction( @@ -137,14 +123,6 @@ def test_authorization_no_fallback( ``num_auths * auth_delta``; the exact-balance sender leaves no slack for that fallback. """ - new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() - auth_delta = ( - new_costs.AUTH_PER_EMPTY_ACCOUNT - old_costs.AUTH_PER_EMPTY_ACCOUNT - ) - fallback_delta = num_auths * auth_delta - assert fallback_delta > 0 - target = pre.deploy_contract(code=b"") authorization_list = [ AuthorizationTuple( @@ -160,7 +138,6 @@ def test_authorization_no_fallback( return_cost_deducted_prior_execution=True, ) gas_limit = intrinsic - 1 - assert intrinsic - fallback_delta <= gas_limit < intrinsic # Set-code (type-4) txs require EIP-1559 fee fields. With # max_fee == max_priority and value 0, the upfront debit the @@ -197,18 +174,10 @@ def test_cold_account_access_no_fallback( per-access delta, and with the sender funded to the wei that fallback must not execute. """ - new_costs = fork.gas_costs() - old_costs = fork.parent_or_fail().gas_costs() - fallback_delta = ( - new_costs.COLD_ACCOUNT_ACCESS - old_costs.COLD_ACCOUNT_ACCESS - ) - assert fallback_delta > 0 - intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True, ) gas_limit = intrinsic - 1 - assert intrinsic - fallback_delta <= gas_limit < intrinsic sender = pre.fund_eoa(amount=gas_limit * GAS_PRICE) tx = Transaction( diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py index 90f446398ce..5470eeb0436 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py @@ -97,7 +97,7 @@ def test_ext_code_opcode_gas( more than ``BALANCE``/``EXTCODEHASH`` at equal warmth (the second, code-reading database access). """ - gas_costs = fork.gas_costs() + del code_read_surcharge # encoded in `cost_metadata` target = pre.deploy_contract(Op.STOP) @@ -117,13 +117,10 @@ def test_ext_code_opcode_gas( ) measure_address = pre.deploy_contract(code=code_gas_measure) - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - surcharge = gas_costs.WARM_ACCESS if code_read_surcharge else 0 - expected_gas = access_cost + surcharge - # Cross-check the framework opcode model agrees with the formula. - assert expected_gas == cost_metadata(warm).gas_cost(fork) + # The opcode's own cost is the expected measured gas: it folds the + # access cost and, for EXTCODESIZE/EXTCODECOPY, the code-read + # surcharge. + expected_gas = cost_metadata(warm).gas_cost(fork) # Warm the target via the access list when required; the cold case # leaves it absent so its first runtime access is cold. @@ -163,8 +160,6 @@ def test_extcodecopy_nonzero_composes_additively( flat add-on that does not interact with the copy or memory terms, so the measured gas must equal the sum of all four components. """ - gas_costs = fork.gas_costs() - # Target carries enough code to satisfy the copy; STOP padding keeps # it a deployable contract with a non-empty code hash. target = pre.deploy_contract(Op.STOP * copy_size) @@ -192,21 +187,6 @@ def test_extcodecopy_nonzero_composes_additively( ) expected_gas = oracle.gas_cost(fork) - # Additive decomposition the surcharge must satisfy. - words = (copy_size + 31) // 32 - access_cost = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - memory_expansion = fork.memory_expansion_gas_calculator()( - new_bytes=copy_size, previous_bytes=0 - ) - assert expected_gas == ( - access_cost - + gas_costs.WARM_ACCESS # EIP-8038 code-read surcharge - + gas_costs.OPCODE_COPY_PER_WORD * words - + memory_expansion - ) - code_gas_measure = CodeGasMeasure( code=measured_code, overhead_cost=measured_code.gas_cost(fork) - oracle.gas_cost(fork), @@ -243,16 +223,12 @@ def test_extcodehash_empty_account( or ``WARM_ACCESS`` (warm) regardless of the target being empty. The returned hash of an empty/non-existent account is ``0``. """ - gas_costs = fork.gas_costs() - # A non-existent (empty) target: never deployed, no balance, no code. empty_addr = Address(0xDEAD) - expected_gas = ( - gas_costs.WARM_ACCESS if warm else gas_costs.COLD_ACCOUNT_ACCESS - ) - # No code-read surcharge for EXTCODEHASH; the opcode model must agree. - assert expected_gas == Op.EXTCODEHASH(address_warm=warm).gas_cost(fork) + # EXTCODEHASH reads only the account leaf (no code-read surcharge), so + # its bare cost is the plain account access. + expected_gas = Op.EXTCODEHASH(address_warm=warm).gas_cost(fork) # Measure the access cost, then store the returned hash so the # empty-account 0 result is asserted alongside the pricing. The diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py index a1de15f4108..cc7f3d21802 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py @@ -5,8 +5,9 @@ "Same operation, different gas" across the Amsterdam boundary. A block at ``timestamp=14_999`` runs under the pre-fork (parent) schedule; a block at ``timestamp=15_000`` runs under the EIP-8038 schedule. Every -before/after magnitude is derived from -``fork.fork_at(timestamp=...).gas_costs()`` — nothing is hardcoded. +before/after magnitude is derived from the opcode's own cost at each +fork (``bytecode.gas_cost`` / ``regular_cost`` / ``refund``) — nothing +is hardcoded. Two proof styles are used: @@ -14,11 +15,12 @@ access and the ``EXT*`` code-read surcharge) are measured exactly with ``CodeGasMeasure`` in each regime and asserted against the derived cost. -* Constant repricings that the runtime opcode model cannot isolate - without state-gas confounders (``CALL_VALUE``, ``CREATE`` base, - ``SELFDESTRUCT`` account-write) are asserted at the constant level - from the derived schedules while the operation is still exercised in - both blocks to prove it runs in each regime. +* Repricings that the runtime opcode model cannot isolate without + state-gas confounders (``CALL`` with value, ``CREATE``, + ``SELFDESTRUCT`` to a fresh beneficiary, ``SSTORE`` first change) are + exercised in both blocks to prove the operation still runs in each + regime, with the ``SSTORE`` regular/state split and clear refund + compared across forks via the bytecode's own cost methods. * The authorization intrinsic rise is proven behaviourally: a tx whose ``gas_limit`` equals the old auth intrinsic is valid before the fork and rejected with ``INTRINSIC_GAS_TOO_LOW`` after. @@ -126,8 +128,10 @@ def test_cold_account_access_at_transition( before = fork.fork_at(timestamp=BEFORE_TS) after = fork.fork_at(timestamp=AFTER_TS) - cost_before = before.gas_costs().COLD_ACCOUNT_ACCESS - cost_after = after.gas_costs().COLD_ACCOUNT_ACCESS + # BALANCE's bare cost equals COLD_ACCOUNT_ACCESS in each regime. + cold_balance = Op.BALANCE.with_metadata(address_warm=False) + cost_before = cold_balance.gas_cost(before) + cost_after = cold_balance.gas_cost(after) assert cost_after > cost_before target = pre.deploy_contract(code=Op.STOP) @@ -179,7 +183,6 @@ def test_ext_code_surcharge_at_transition( after ) - Op.BALANCE(address_warm=True).gas_cost(after) assert surcharge_before == 0 - assert surcharge_after == after.gas_costs().WARM_ACCESS assert surcharge_after > surcharge_before extcodesize_cost_before = Op.EXTCODESIZE(address_warm=False).gas_cost( @@ -221,13 +224,6 @@ def test_call_value_cost_at_transition( is exercised in both blocks to prove it still succeeds in each regime. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - call_value_before = before.gas_costs().CALL_VALUE - call_value_after = after.gas_costs().CALL_VALUE - assert call_value_after > call_value_before - callee_before = pre.deploy_contract(code=Op.STOP, balance=0) callee_after = pre.deploy_contract(code=Op.STOP, balance=0) @@ -271,17 +267,6 @@ def test_create_base_cost_at_transition( asserted from the derived schedules and a ``CREATE`` is exercised in both blocks to prove it still deploys. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - create_base_before = before.gas_costs().OPCODE_CREATE_BASE - create_base_after = after.gas_costs().OPCODE_CREATE_BASE - assert create_base_after != create_base_before - # Post-fork base is the harmonized ACCOUNT_WRITE + COLD_STORAGE_ACCESS. - assert create_base_after == ( - after.gas_costs().ACCOUNT_WRITE + after.gas_costs().COLD_STORAGE_ACCESS - ) - init_code = Op.STOP init_word = int.from_bytes(bytes(init_code), "big") << ( 256 - 8 * len(init_code) @@ -332,13 +317,6 @@ def test_selfdestruct_account_write_at_transition( ``SELFDESTRUCT`` to a fresh beneficiary is exercised in both blocks to prove it still runs. """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - account_write_before = before.gas_costs().ACCOUNT_WRITE - account_write_after = after.gas_costs().ACCOUNT_WRITE - assert account_write_after > account_write_before - # Fresh empty beneficiaries so the positive-balance-to-empty branch # that adds ACCOUNT_WRITE is taken in each regime. beneficiary_before = pre.fund_eoa(amount=0) @@ -406,20 +384,12 @@ def test_sstore_write_cost_at_transition( assert state_after > 0 assert total_after != total_before - # After the fork the regular portion is the EIP-8038 split: - # COLD_STORAGE_ACCESS plus the standalone STORAGE_WRITE (modeled as - # COLD_STORAGE_WRITE minus COLD_STORAGE_ACCESS). - after_costs = after.gas_costs() - storage_write_after = ( - after_costs.COLD_STORAGE_WRITE - after_costs.COLD_STORAGE_ACCESS - ) - assert regular_after == ( - after_costs.COLD_STORAGE_ACCESS + storage_write_after - ) - # The storage-clear refund also rises across the boundary. - refund_before = before.gas_costs().REFUND_STORAGE_CLEAR - refund_after = after_costs.REFUND_STORAGE_CLEAR + clear_sstore = Op.SSTORE.with_metadata( + original_value=1, current_value=1, new_value=0 + ) + refund_before = clear_sstore.refund(before) + refund_after = clear_sstore.refund(after) assert refund_after > refund_before # Exercise the zero-to-nonzero SSTORE in both regimes; the slot ends diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py index 2fb565c7a36..b1c560095ac 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py @@ -74,20 +74,9 @@ def _selfdestruct_regular(fork: Fork, *, warm: bool, account_new: bool) -> int: the ``GAS_NEW_ACCOUNT`` account-creation cost is the EIP-8037 state dimension and is excluded from ``regular_cost``. """ - gas_costs = fork.gas_costs() - regular = Op.SELFDESTRUCT( + return Op.SELFDESTRUCT( address_warm=warm, account_new=account_new ).regular_cost(fork) - # SELFDESTRUCT charges a cold-access surcharge only; a warm - # beneficiary adds nothing beyond the base (no WARM_ACCESS). - access = 0 if warm else gas_costs.COLD_ACCOUNT_ACCESS - expected = ( - gas_costs.OPCODE_SELFDESTRUCT_BASE - + access - + (gas_costs.ACCOUNT_WRITE if account_new else 0) - ) - assert regular == expected - return regular def _destructor_code( @@ -123,8 +112,7 @@ def test_selfdestruct_new_beneficiary_regular_gas( EIP-8037 suite asserts it); here it is funded from the reservoir and the value transfer to the new beneficiary confirms the path. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT + new_account_state_gas = Op.SELFDESTRUCT(account_new=True).state_cost(fork) regular = _selfdestruct_regular(fork, warm=warm, account_new=True) assert regular == (13_000 if warm else 16_000) @@ -391,12 +379,6 @@ def test_selfdestruct_self_or_precompile_beneficiary( transfer would otherwise create one and charge ``GAS_NEW_ACCOUNT`` on the state axis). """ - gas_costs = fork.gas_costs() - - regular = _selfdestruct_regular(fork, warm=True, account_new=False) - # SELFDESTRUCT has no warm-access surcharge: warm == base only. - assert regular == gas_costs.OPCODE_SELFDESTRUCT_BASE - if beneficiary_kind == "self": # Self is warm on entry; the PUSH is `ADDRESS` (BASE=2). A # non-zero balance is transferred to self (no creation). @@ -469,15 +451,7 @@ def test_selfdestruct_oog_boundary( gas short OOGs (CALL returns 0) before the value transfer, so the beneficiary is never created. """ - gas_costs = fork.gas_costs() - beneficiary = Address(0xDEAD) - regular = _selfdestruct_regular(fork, warm=False, account_new=True) - assert regular == ( - gas_costs.OPCODE_SELFDESTRUCT_BASE - + gas_costs.COLD_ACCOUNT_ACCESS - + gas_costs.ACCOUNT_WRITE - ) destructor_code = _destructor_code( beneficiary, warm=False, account_new=True @@ -564,9 +538,6 @@ def test_same_tx_created_selfdestruct_self_burn( # Self-beneficiary on a balance-bearing same-tx-created contract is # alive: account_new is false, so only the warm base is charged. - regular = _selfdestruct_regular(fork, warm=True, account_new=False) - assert regular == fork.gas_costs().OPCODE_SELFDESTRUCT_BASE - # Creation intrinsic is regular-only under EIP-2780; the pre-existing # target adds no top-frame NEW_ACCOUNT and the self-burn adds no state # gas, so net state gas is zero. The regular consumption exceeds the @@ -627,7 +598,6 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( created target is alive at message entry (EIP-8037), while the fresh beneficiary's ``NEW_ACCOUNT`` persists. """ - new_account_state_gas = fork.gas_costs().NEW_ACCOUNT intrinsic_calc = fork.transaction_intrinsic_cost_calculator() amount = 1 @@ -644,16 +614,17 @@ def test_same_tx_created_selfdestruct_to_fresh_beneficiary( init_code = Op.SELFDESTRUCT.with_metadata( address_warm=False, account_new=True )(beneficiary) + # The creation NEW_ACCOUNT is refunded (target alive at entry) and is + # not part of the intrinsic under EIP-2780; only the fresh + # beneficiary's NEW_ACCOUNT (the SELFDESTRUCT state cost) persists. + new_account_state_gas = init_code.state_cost(fork) regular = _selfdestruct_regular(fork, warm=False, account_new=True) assert regular == 16_000 - intrinsic_total = intrinsic_calc( + intrinsic_regular = intrinsic_calc( calldata=bytes(init_code), contract_creation=True ) - # The creation NEW_ACCOUNT is refunded (target alive at entry); only - # the fresh beneficiary's NEW_ACCOUNT remains as net state gas. - intrinsic_regular = intrinsic_total - new_account_state_gas expected_state = new_account_state_gas expected_regular = intrinsic_regular + init_code.regular_cost(fork) expected_gas_used = max(expected_regular, expected_state) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py index a3a50b782a5..93c7ea34d5c 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py @@ -11,10 +11,11 @@ cold/warm account-access costs that an authorized delegation incurs when later accessed by a ``CALL``. -The regular per-authorization intrinsic magnitude is -``fork.gas_costs().REGULAR_PER_AUTH_BASE_COST`` (``7816`` on Amsterdam: +The regular per-authorization intrinsic magnitude is the fixed +per-authorization base cost charged by the intrinsic (on Amsterdam, ``101 * 16`` calldata tokens plus the ``3000`` ecrecover, ``3000`` cold -and ``2 * 100`` warm accesses of the EIP-7702 base). The top-frame +and ``2 * 100`` warm accesses of the EIP-7702 base), isolated here as +the intrinsic delta of adding one authorization. The top-frame state charges are asserted by the sibling ``eip8037_state_creation_gas_cost_increase`` and ``eip2780_reduce_intrinsic_tx_gas`` suites; this suite does not @@ -58,11 +59,19 @@ def _regular_per_auth(fork: Fork) -> int: authorization. Under EIP-2780 the intrinsic charges only the state-independent - ``REGULAR_PER_AUTH_BASE_COST`` per authorization; the account-write - (``ACCOUNT_WRITE``) and delegation-write (``AUTH_BASE``) costs are - charged lazily at the top frame, not in the intrinsic. + per-authorization base cost; the account-write (``ACCOUNT_WRITE``) + and delegation-write (``AUTH_BASE``) costs are charged lazily at the + top frame, not in the intrinsic. Isolated as the intrinsic delta of + adding one authorization. """ - return fork.gas_costs().REGULAR_PER_AUTH_BASE_COST + calc = fork.transaction_intrinsic_cost_calculator() + return calc( + authorization_list_or_count=1, + return_cost_deducted_prior_execution=True, + ) - calc( + authorization_list_or_count=0, + return_cost_deducted_prior_execution=True, + ) def _regular_intrinsic( @@ -490,12 +499,13 @@ def test_auth_account_warming( ``COLD_ACCOUNT_ACCESS``. When the sponsor is the authority, the authority is already warm for the same reason. - All costs are taken from ``fork.gas_costs()`` so the repricing is - asserted against the live schedule rather than hardcoded constants. + All costs are derived from the fork's opcode schedule (not + hardcoded) so the repricing is asserted against the live values. """ - gas_costs = fork.gas_costs() - cold = gas_costs.COLD_ACCOUNT_ACCESS - warm = gas_costs.WARM_ACCESS + # Bare account-access costs, isolated via BALANCE (no code-read + # surcharge and no operand push). + cold = Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork) + warm = Op.BALANCE.with_metadata(address_warm=True).gas_cost(fork) delegation_target = pre.deploy_contract(code=Op.STOP) @@ -529,7 +539,7 @@ def test_auth_account_warming( # Measure the cost of a single CALL to the authority. The CALL # opcode leaves one stack item (success); the overhead is the PUSHes # for its arguments. - overhead_cost = gas_costs.VERY_LOW * len(Op.CALL.kwargs) + overhead_cost = Op.PUSH1(0).regular_cost(fork) * len(Op.CALL.kwargs) storage = Storage() callee_code = CodeGasMeasure( code=Op.CALL(gas=0, address=authority), @@ -573,7 +583,27 @@ def test_many_auths_block_limit( gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - per_auth_total = fork.gas_costs().AUTH_PER_EMPTY_ACCOUNT + contract = pre.deploy_contract(code=Op.STOP) + + # Per-authorization total for a fresh (empty) authority: the regular + # intrinsic base plus the top-frame account-write, account-creation + # and delegation-write charges, derived from the fork's calculators so + # it tracks the repricing. + probe_auth = AuthorizationTuple( + address=contract, + nonce=0, + signer=pre.fund_eoa(), + creates_account=True, + writes_delegation=True, + first_write=True, + ) + per_auth_total = ( + _regular_per_auth(fork) + + fork.transaction_top_frame_gas_calculator()( + authorizations=[probe_auth] + ) + + fork.transaction_top_frame_state_gas(authorizations=[probe_auth]) + ) base = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=0, ) @@ -581,7 +611,6 @@ def test_many_auths_block_limit( num_auths = (gas_limit_cap - base) // per_auth_total assert num_auths >= 2 - contract = pre.deploy_contract(code=Op.STOP) signers = [pre.fund_eoa() for _ in range(num_auths)] authorization_list = [ AuthorizationTuple(address=contract, nonce=0, signer=signer) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py index e6ba6c912fe..f3cdf3e1669 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py @@ -183,8 +183,8 @@ def test_sstore_cold_then_warm_same_slot( ) second = second_bare(data_slot, 3) - expected_first = first.regular_cost(fork) - 2 * fork.gas_costs().VERY_LOW - expected_second = second.regular_cost(fork) - 2 * fork.gas_costs().VERY_LOW + expected_first = first_bare.regular_cost(fork) + expected_second = second_bare.regular_cost(fork) # Each measured write stores its own runtime cost; the overhead # subtraction strips the two operand PUSHes so the stored value is the diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py index 7becbe2bb22..4fbe570673d 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py @@ -84,9 +84,6 @@ def test_sstore_clear_grants_refund( observed in ``cumulative_gas_used``. The non-zero original means no EIP-8037 state refund participates. """ - gas_costs = fork.gas_costs() - refund_clear = gas_costs.REFUND_STORAGE_CLEAR - clear = Op.SSTORE.with_metadata( key_warm=False, original_value=1, @@ -100,8 +97,8 @@ def test_sstore_clear_grants_refund( contract = pre.deploy_contract(code=code, storage={0: 1}) - # Sanity: the slot's refund counter accrues exactly one clear grant. - assert code.refund(fork) == refund_clear + # The slot's clear grants exactly one REFUND_STORAGE_CLEAR. + refund_clear = code.refund(fork) expected_cumulative = _cumulative_gas_used(code, fork) # The cap must not bind here, so the full grant is visible. intrinsic = fork.transaction_intrinsic_cost_calculator()( @@ -181,11 +178,6 @@ def test_sstore_restore_nonzero_refunds_write( burned so the quotient cap does not bind and the full refund is observable. """ - gas_costs = fork.gas_costs() - storage_write = ( - gas_costs.COLD_STORAGE_WRITE - gas_costs.COLD_STORAGE_ACCESS - ) - code = Op.SSTORE.with_metadata( key_warm=False, original_value=1, @@ -202,7 +194,8 @@ def test_sstore_restore_nonzero_refunds_write( contract = pre.deploy_contract(code=code, storage={0: 1}) - assert code.refund(fork) == storage_write + # Restoring the non-zero original refunds STORAGE_WRITE. + storage_write = code.refund(fork) expected_cumulative = _cumulative_gas_used(code, fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True @@ -241,9 +234,6 @@ def test_sstore_refund_quotient_cap( always below the accrued refund, so the applied refund is the cap and ``cumulative_gas_used`` reflects ``min(gas_used // 5, accrued)``. """ - gas_costs = fork.gas_costs() - accrued = num_clears * gas_costs.REFUND_STORAGE_CLEAR - code = Bytecode() for slot in range(num_clears): code += Op.SSTORE.with_metadata( @@ -258,7 +248,8 @@ def test_sstore_refund_quotient_cap( storage=dict.fromkeys(range(num_clears), 1), ) - assert code.refund(fork) == accrued + # num_clears distinct clears accrue num_clears * REFUND_STORAGE_CLEAR. + accrued = code.refund(fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True ) @@ -299,9 +290,7 @@ def test_sstore_refund_cap_exact_equality( *exactly*, the boundary between the cap binding and not binding. The full refund applies and ``cumulative_gas_used`` is ``gross - accrued``. """ - gas_costs = fork.gas_costs() quotient = fork.max_refund_quotient() - accrued = gas_costs.REFUND_STORAGE_CLEAR clear = Op.SSTORE.with_metadata( key_warm=False, @@ -309,6 +298,7 @@ def test_sstore_refund_cap_exact_equality( current_value=1, new_value=0, )(0, 0) + accrued = clear.refund(fork) intrinsic = fork.transaction_intrinsic_cost_calculator()( return_cost_deducted_prior_execution=True @@ -330,7 +320,6 @@ def test_sstore_refund_cap_exact_equality( code = clear + Op.JUMPDEST * num_jumpdest contract = pre.deploy_contract(code=code, storage={0: 1}) - assert code.refund(fork) == accrued gross = intrinsic + code.regular_cost(fork) + code.state_cost(fork) # Exact equality: the cap is neither under nor over the accrued refund. assert gross == target_gross diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py index fc79ee51299..5716c9dc277 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py @@ -46,28 +46,23 @@ def test_transient_storage_gas_unchanged( write repricing did not bleed into transient storage. """ gas_costs = fork.gas_costs() - very_low = gas_costs.VERY_LOW - - # Bare opcode costs: subtract the PUSH wrapper from each. - tload_bare = Op.TLOAD(0).gas_cost(fork) - 1 * very_low - tstore_bare = Op.TSTORE(0, 1).gas_cost(fork) - 2 * very_low - - assert tload_bare == gas_costs.OPCODE_TLOAD == 100 - assert tstore_bare == gas_costs.OPCODE_TSTORE == 100 - # Guard against over-eager repricing: transient write must not have - # been folded into the (repriced) persistent cold write cost. + # Guard against over-eager repricing: the transient write must not + # have been folded into the (repriced) persistent cold write cost. assert gas_costs.OPCODE_TSTORE != gas_costs.COLD_STORAGE_WRITE - # Measure TSTORE then TLOAD of the same transient slot in one frame. + # Measure TSTORE then TLOAD of the same transient slot in one frame, + # subtracting the PUSH wrapper so the stored value is the bare opcode + # cost. + push_cost = Op.PUSH1(0).regular_cost(fork) tstore_code = CodeGasMeasure( code=Op.TSTORE(0, 1), - overhead_cost=2 * very_low, + overhead_cost=2 * push_cost, extra_stack_items=0, sstore_key=0, ) tload_code = CodeGasMeasure( code=Op.TLOAD(0), - overhead_cost=1 * very_low, + overhead_cost=1 * push_cost, extra_stack_items=1, sstore_key=1, ) @@ -75,7 +70,9 @@ def test_transient_storage_gas_unchanged( tx = Transaction(to=contract, sender=pre.fund_eoa()) - # Slot 0: measured TSTORE cost. Slot 1: measured TLOAD cost. + # Slot 0: measured TSTORE cost. Slot 1: measured TLOAD cost. Both must + # equal the fork's declared transient-storage opcode costs, which + # EIP-8038 leaves unchanged. post = { contract: Account( storage={ From 60625f4bc0fa7436e0e3ad1cda5c7ec3abc1f49b Mon Sep 17 00:00:00 2001 From: marioevz Date: Thu, 16 Jul 2026 15:37:53 +0300 Subject: [PATCH 3/5] fix(claude): Update `write-test.md` skill --- .claude/commands/write-test.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.claude/commands/write-test.md b/.claude/commands/write-test.md index 3895d81e0ef..bf6e8eee42a 100644 --- a/.claude/commands/write-test.md +++ b/.claude/commands/write-test.md @@ -43,8 +43,20 @@ Conventions and patterns for writing consensus tests. Run this skill before writ ## Fork-Aware Logic - `fork >= Cancun` for conditional behavior based on fork -- `fork.gas_costs()` returns `GasCosts` dataclass with constants like `G_WARM_SLOAD`, `G_COLD_ACCOUNT_ACCESS`, `G_BASE`, etc. -- `fork.transaction_intrinsic_cost_calculator()` for computing tx intrinsic gas +- `fork.fork_at(timestamp=...)` gives the fork active before/after a transition boundary +- For gas amounts, see **Gas Cost Expectations** below — prefer framework cost constructs over reading `fork.gas_costs()` constants directly + +## Gas Cost Expectations + +Never hand-reconstruct a gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule duplicates the framework's own calculation and silently breaks when a future fork reprices. Instead: + +- **Read the cost off the bytecode under test.** Set the relevant opcode metadata (`account_new`, `value_transfer`, `address_warm`, `key_warm`/`original_value`/`current_value`/`new_value`, `init_code_size`, `code_deposit_size`, `new_memory_size`, ...) and use `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior — e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`; `fork.transaction_top_frame_state_gas(contract_creation=True)` for the created account's `NEW_ACCOUNT` (under EIP-2780 it is NOT part of the intrinsic — never subtract it from the intrinsic); `fork.transaction_data_floor_cost_calculator()`; `fork.call_value_stipend()`. +- **A single bare opcode/schedule cost** (e.g. an account-access constant) comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)`. +- **Fork-transition / cross-fork comparisons:** evaluate the same bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare `before` vs `after` costs — do not compare raw schedule constants. +- **Do not add "self-check" asserts** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork; they add no coverage over the runtime behavior the test already exercises and only break on repricing. +- **If the framework cannot express a cost, fix the framework** (wire the opcode into its gas/state map, add an accessor) rather than reconstructing it in the test. If the use case does not support the framework, the framework needs an update. +- **Exception:** a test whose *subject* is a specific schedule value (e.g. a regression that an opcode's cost is unchanged) may compare a runtime measurement (`CodeGasMeasure`) against `fork.gas_costs().OPCODE_*`. Even then, never hardcode the literal value. ## Transactions From 5e5c13beb8882bcc2d0c1f91e40e7ac252dd8ff2 Mon Sep 17 00:00:00 2001 From: marioevz Date: Thu, 16 Jul 2026 15:55:30 +0300 Subject: [PATCH 4/5] fix(docs): Update human docs --- docs/writing_tests/fork_methods.md | 9 +++++++-- docs/writing_tests/opcode_metadata.md | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/writing_tests/fork_methods.md b/docs/writing_tests/fork_methods.md index efb4c90d4e3..6f29d6f3a0c 100644 --- a/docs/writing_tests/fork_methods.md +++ b/docs/writing_tests/fork_methods.md @@ -38,11 +38,13 @@ def test_some_feature(fork): ```python def test_transaction_gas(fork, state_test): - gas_cost = fork.gas_costs().GAS_TX_BASE + # Derive the fork's intrinsic gas from the calculator rather than + # summing raw `gas_costs()` constants (see the Gas Parameters warning). + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() # Create a transaction with the correct gas parameters for this fork tx = Transaction( - gas_limit=gas_cost + 10000, + gas_limit=intrinsic_gas + 10000, # ... ) @@ -114,6 +116,9 @@ fork.memory_expansion_gas_calculator() # Returns a callable fork.transaction_intrinsic_cost_calculator() # Returns a callable ``` +!!! warning "Do not reconstruct expected gas from `gas_costs()` constants" + `fork.gas_costs()` exposes the raw schedule for framework internals. When a test needs an *expected* gas amount, derive it from a cost construct that tracks the live schedule (`bytecode.gas_cost(fork)` / `.regular_cost(fork)` / `.state_cost(fork)` / `.refund(fork)`, opcode metadata, the intrinsic/top-frame/data-floor calculators, `fork.call_value_stipend()`) rather than hand-summing constants — hand-built expectations silently break when a fork reprices. See [Opcode Metadata and Gas Calculations](opcode_metadata.md#do-not-hand-reconstruct-gas-from-constants). + ### Transaction Types Methods for determining valid transaction types: diff --git a/docs/writing_tests/opcode_metadata.md b/docs/writing_tests/opcode_metadata.md index e6530e42674..5149fb9ea0b 100644 --- a/docs/writing_tests/opcode_metadata.md +++ b/docs/writing_tests/opcode_metadata.md @@ -9,6 +9,21 @@ The execution testing package provides capabilities to calculate gas costs and r - Validating gas cost calculations for specific opcode scenarios - Future-proofing tests against breaking in upcoming forks that change gas rules +## Do Not Hand-Reconstruct Gas From Constants + +Never build an expected gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule by hand duplicates the framework's own calculation and silently breaks when a future fork reprices or restructures a cost. Always derive the expectation from a framework construct that tracks the live schedule: + +- **The bytecode/opcode under test:** set the relevant metadata (see below) and read `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior, e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`. +- **A single bare opcode/schedule cost** comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)` yields the cold account-access cost with no operand pushes. +- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`, `fork.transaction_top_frame_state_gas(contract_creation=True)` (the created account's new-account state gas — on recent forks it is charged at the top frame, *not* in the intrinsic, so never subtract it from the intrinsic), `fork.transaction_data_floor_cost_calculator()`, and `fork.call_value_stipend()`. +- **Cross-fork / fork-transition comparisons:** evaluate the *same* bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare the resulting costs — do not compare raw schedule constants. + +Additional rules: + +- **Do not add "self-check" assertions** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork. They add no coverage over the runtime behavior the test already exercises and only break on repricing. +- **If the framework cannot express a cost, extend the framework** (wire the opcode into its gas/state map, add an accessor) rather than working around it in the test. +- **Exception:** a test whose *subject* is a specific schedule value — for example a regression asserting that an opcode's cost is unchanged across a fork — may compare a runtime measurement (`CodeGasMeasure`) against the fork's declared `fork.gas_costs().OPCODE_*` value. Even then, never hardcode the literal number. + ## Opcode Metadata Many opcodes accept metadata parameters that affect their gas cost calculations. Metadata represents runtime state information that influences gas consumption. From 274374777f04b04ec4205a19da9240335098b397 Mon Sep 17 00:00:00 2001 From: marioevz Date: Wed, 22 Jul 2026 11:11:33 +0300 Subject: [PATCH 5/5] fix(tests): Review comments, repurpose stale test --- .../test_state_gas_create.py | 148 ++++++++---------- .../test_state_gas_multi_block.py | 5 +- 2 files changed, 70 insertions(+), 83 deletions(-) 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 b398f230f0f..c2dfeaf0810 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 @@ -17,7 +17,6 @@ Block, BlockchainTestFiller, Bytecode, - CodeGasMeasure, Fork, Header, Initcode, @@ -2906,10 +2905,10 @@ def test_create_collision_burned_gas_counted_in_block_regular( @pytest.mark.parametrize( - "target", + "account_new", [ - pytest.param("new", id="new_account"), - pytest.param("existing", id="existing_account"), + pytest.param(True, id="new_account"), + pytest.param(False, id="existing_account"), ], ) @pytest.mark.with_all_create_opcodes() @@ -2919,133 +2918,122 @@ def test_create_account_creation_charge( pre: Alloc, fork: Fork, create_opcode: Op, - target: str, + account_new: bool, ) -> None: """ - Verify NEW_ACCOUNT is charged for a new account and refunded for a - pre-existing balance-only leaf. + Verify NEW_ACCOUNT is charged only when the created account does not + already exist in the trie. Empty init code means zero code deposit, so NEW_ACCOUNT is the only create state cost. A fresh target is charged it; a pre-existing - balance-only target (balance, no code, zero nonce) refunds it on - success. The probe SSTORE both confirms the create succeeded and - makes state gas dominate, so gas_used drops by exactly NEW_ACCOUNT - when refunded. + balance-only target (balance, no code, zero nonce) is not. The probe + SSTORE both confirms the create succeeded and makes state gas dominate + the header, so gas_used differs by exactly NEW_ACCOUNT between the two + cases. """ - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) mstore_value, size = init_code_at_high_bytes(Op.STOP) - create_call = ( - create_opcode(value=0, offset=0, size=size, salt=0) - if create_opcode == Op.CREATE2 - else create_opcode(value=0, offset=0, size=size) + create_call = create_opcode( + value=0, offset=0, size=size, account_new=account_new ) - new_account = create_call.state_cost(fork) - storage = Storage() - factory = pre.deploy_contract( - code=Op.MSTORE(0, mstore_value) - + Op.SSTORE( - storage.store_next(1, "create_succeeds"), Op.GT(create_call, 0) - ) + factory_code = Op.MSTORE(0, mstore_value) + Op.SSTORE( + storage.store_next(1, "create_succeeds"), Op.GT(create_call, 0) ) + factory = pre.deploy_contract(code=factory_code) # Factory deployed via deploy_contract starts at nonce 1. - if create_opcode == Op.CREATE2: - create_address = compute_create2_address( - address=factory, salt=0, initcode=bytes(Op.STOP) - ) - else: - create_address = compute_create_address(address=factory, nonce=1) - if target == "existing": + create_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=bytes(Op.STOP), + opcode=create_opcode, + ) + if not account_new: pre.fund_address(create_address, amount=1) + # State gas dominates the header, so gas_used equals the factory's + # state cost: NEW_ACCOUNT plus the probe SSTORE for a fresh target, + # just the SSTORE for a pre-existing one. + state_cost = factory_code.state_cost(fork) tx = Transaction( to=factory, - state_gas_reservoir=new_account + sstore_state_gas, + state_gas_reservoir=state_cost, sender=pre.fund_eoa(), ) - # State gas dominates regular: a new account adds NEW_ACCOUNT on top - # of the probe SSTORE, a pre-existing target refunds it. - expected = sstore_state_gas + (new_account if target == "new" else 0) state_test( pre=pre, tx=tx, post={factory: Account(storage=storage)}, - blockchain_test_header_verify=Header(gas_used=expected), + blockchain_test_header_verify=Header(gas_used=state_cost), ) @pytest.mark.with_all_create_opcodes() +@pytest.mark.parametrize( + "sufficient_gas", + [ + pytest.param(True, id="sufficient_gas"), + pytest.param(False, id="insufficient_gas"), + ], +) @pytest.mark.valid_from("EIP8037") -def test_create_refund_credited_against_child_spill( +def test_no_account_charge_on_existing_account( state_test: StateTestFiller, pre: Alloc, fork: Fork, create_opcode: Op, + sufficient_gas: bool, ) -> None: """ - Verify the NEW_ACCOUNT refund routing is visible through GAS. + Verify the create opcode is not charged NEW_ACCOUNT when the target + account already exists in the trie. - The reservoir covers exactly the CREATE NEW_ACCOUNT charge, leaving - none for the child frame, whose initcode SSTOREs then spill more - than NEW_ACCOUNT of state gas from gas_left. The target is alive - (pre-funded), so NEW_ACCOUNT is refunded and credited LIFO against - the incorporated child spill, landing in the parent's gas_left - where GAS (which excludes the reservoir) observes it. + The factory is forwarded exactly the create's regular gas, with no + NEW_ACCOUNT included. Because the target is pre-funded (alive), that + budget is sufficient and the create succeeds, deploying empty code + (created nonce 1). With one gas less it runs out of gas at the + create's upfront charge, before the nonce bump, leaving the target + untouched (nonce 0). The empty reservoir keeps the state-gas + dimension from masking the boundary. """ - gas_costs = fork.gas_costs() - - initcode = Op.SSTORE(0, 1) + Op.SSTORE(1, 1) + Op.STOP - child_spill = initcode.state_cost(fork) - assert child_spill >= gas_costs.NEW_ACCOUNT - - mstore_value, initcode_size = init_code_at_high_bytes(initcode) - create_call = ( - create_opcode( - value=0, - offset=0, - size=initcode_size, - salt=0, - init_code_size=initcode_size, - ) - if create_opcode == Op.CREATE2 - else create_opcode( - value=0, - offset=0, - size=initcode_size, - init_code_size=initcode_size, - ) + factory_code = create_opcode( + value=0, + offset=0, + size=1, # Nothing in memory, equivalent to Op.STOP + # Gas accounting + init_code_size=1, + new_memory_size=1, + account_new=False, ) - factory = pre.deploy_contract( - code=Op.MSTORE(0, mstore_value) - + CodeGasMeasure(code=create_call, extra_stack_items=1), - ) + factory = pre.deploy_contract(code=factory_code) + created = compute_create_address( address=factory, nonce=1, salt=0, - initcode=initcode, + initcode=Op.STOP, opcode=create_opcode, ) pre.fund_address(created, amount=1) - expected_gas = ( - create_call.regular_cost(fork) - + initcode.regular_cost(fork) - + child_spill - - gas_costs.NEW_ACCOUNT # refund credited to gas_left - ) + call_gas = factory_code.gas_cost(fork) + if not sufficient_gas: + call_gas -= 1 + entry_code = Op.CALL(gas=call_gas, address=factory) + entry = pre.deploy_contract(code=entry_code) tx = Transaction( - to=factory, - state_gas_reservoir=gas_costs.NEW_ACCOUNT, + to=entry, + state_gas_reservoir=0, # To allow subcall to run OOG sender=pre.fund_eoa(), ) post = { - factory: Account(storage={0: expected_gas}), - created: Account(nonce=1, balance=1, storage={0: 1, 1: 1}), + created: Account( + nonce=1 if sufficient_gas else 0, balance=1, code=b"" + ), } state_test(pre=pre, post=post, tx=tx) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py index 02b3bab730f..fecc4871775 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_multi_block.py @@ -52,11 +52,10 @@ def test_exact_coinbase_fee_simple_sstore( Motivated by BAL devnet-3 ethrex/besu coinbase balance mismatch where clients diverged on cumulative `receipt_gas_used`. """ - sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) - # Tx 1: single SSTORE zero-to-nonzero sstore_storage = Storage() - sstore_code = Op.SSTORE(sstore_storage.store_next(1), 1) + sstore_code = Op.SSTORE(sstore_storage.store_next(1), 1, new_value=1) + sstore_state_gas = sstore_code.state_cost(fork) sstore_contract = pre.deploy_contract(code=sstore_code) # tx 1 gas used: the intrinsic (TX_BASE plus the EIP-2780