From da4670580c7cd414d14a5e537289ecaf5a78eba4 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 18 Jun 2026 18:41:25 +0200 Subject: [PATCH 01/14] feat(spec-specs, tests): add EIP-8038 state-access gas cost update (#2972) Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Mario Vega <11726710+marioevz@users.noreply.github.com> --- .../forks/forks/eips/amsterdam/eip_8037.py | 89 +---- .../forks/forks/eips/amsterdam/eip_8038.py | 188 +++++++++++ .../forks/forks/eips/cancun/eip_1153.py | 19 +- .../src/execution_testing/forks/gas_costs.py | 3 + src/ethereum/forks/amsterdam/transactions.py | 8 +- .../forks/amsterdam/vm/eoa_delegation.py | 18 +- src/ethereum/forks/amsterdam/vm/gas.py | 64 +++- .../amsterdam/vm/instructions/environment.py | 7 +- .../amsterdam/vm/instructions/storage.py | 23 +- .../forks/amsterdam/vm/instructions/system.py | 9 +- .../forks/amsterdam/vm/interpreter.py | 4 +- .../test_gas_accounting.py | 4 + .../test_refunds.py | 21 +- .../test_floor_boundary_exact_balance.py | 6 +- .../spec.py | 11 +- .../test_state_gas_call.py | 7 +- .../test_state_gas_create.py | 16 +- .../test_state_gas_selfdestruct.py | 26 +- .../test_state_gas_set_code.py | 34 +- .../test_state_gas_sstore.py | 33 +- .../test_tstorage_create_contexts.py | 91 ++++-- .../test_collision_selfdestruct.py | 14 +- .../stBadOpcode/test_measure_gas.py | 33 +- .../stBadOpcode/test_operation_diff_gas.py | 39 ++- .../test_create2_oo_gafter_init_code.py | 58 +++- .../stCreate2/test_create2_smart_init_code.py | 26 +- .../test_create_address_warm_after_fail.py | 35 +- .../test_create_oo_gafter_init_code.py | 58 +++- .../test_14_revert_after_nested_staticcall.py | 35 +- .../test_suicide_to_existing_contract.py | 13 +- .../test_suicide_to_not_existing_contract.py | 16 +- .../test_eip2929.py | 307 ++++++++++++------ .../test_eip2929_minus_ff.py | 17 +- .../test_gas_cost.py | 53 ++- .../test_gas_cost_berlin.py | 31 +- .../test_gas_cost_memory.py | 15 +- .../test_raw_ext_code_copy_gas.py | 23 +- .../test_raw_ext_code_copy_memory_gas.py | 23 +- .../test_raw_ext_code_size_gas.py | 23 +- .../test_call_one_v_call_suicide.py | 21 +- .../test_call_one_v_call_suicide2.py | 22 +- .../test_call_zero_v_call_suicide.py | 13 +- .../test_extcodesize_to_epmty_paris.py | 28 +- .../test_extcodesize_to_non_existent.py | 26 +- .../stEIP2930/test_address_opcodes.py | 193 ++++++++--- .../stEIP2930/test_coinbase_t01.py | 18 +- .../stEIP2930/test_coinbase_t2.py | 18 +- .../stEIP2930/test_manual_create.py | 35 +- .../stEIP2930/test_storage_costs.py | 126 ++++--- .../stEIP2930/test_varied_context.py | 279 +++++++++------- tests/ported_static/stMemoryTest/test_oog.py | 16 +- ...zero_value_call_to_non_non_zero_balance.py | 28 +- ..._value_callcode_to_non_non_zero_balance.py | 28 +- .../test_precomps_eip2929_cancun.py | 37 ++- .../stRefundTest/test_refund50_1.py | 24 +- ...st_refund_call_a_not_enough_gas_in_call.py | 28 +- .../test_refund_change_non_zero_storage.py | 23 +- .../stRefundTest/test_refund_ff.py | 24 +- .../test_refund_get_ether_back.py | 36 +- .../stRefundTest/test_refund_max.py | 24 +- .../stRefundTest/test_refund_no_oog_1.py | 42 ++- .../stRefundTest/test_refund_sstore.py | 39 ++- .../stRevertTest/test_revert_opcode_calls.py | 15 +- .../stSpecialTest/test_eoa_empty_paris.py | 33 +- .../test_contract_store_clears_success.py | 26 +- ...test_internal_call_store_clears_success.py | 32 +- ..._and_internal_call_store_clears_success.py | 32 +- tests/ported_static/vmTests/test_suicide.py | 23 +- tests/prague/eip7702_set_code_tx/test_gas.py | 17 +- .../eip7702_set_code_tx/test_set_code_txs.py | 10 +- 70 files changed, 2167 insertions(+), 649 deletions(-) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py index f1c2a4a4bda..bac17899e18 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py @@ -4,6 +4,13 @@ Harmonization, increase and separate metering of state creation gas costs to mitigate state growth and unblock scaling. +The companion EIP-8038 state-access repricing lives in its own `EIP8038` +mixin. Because the EIP mixins are ordered by number, `EIP8037` sits +immediately above `EIP8038` in the MRO, so `super().gas_costs()` here +returns the EIP-8038 schedule and this mixin folds its state-creation gas +into the shared `STORAGE_SET`, `TX_CREATE`, and `AUTH_PER_EMPTY_ACCOUNT` +totals on top of it. + https://eips.ethereum.org/EIPS/eip-8037 """ @@ -23,9 +30,6 @@ STATE_BYTES_PER_STORAGE_SET = 64 STATE_BYTES_PER_AUTH_BASE = 23 -PER_AUTH_BASE_COST = 7_500 -REGULAR_GAS_CREATE = 9_000 - SYSTEM_MAX_SSTORES_PER_CALL = 16 @@ -74,25 +78,23 @@ def create_state_gas(cls, *, code_size: int = 0) -> int: @classmethod def gas_costs(cls) -> GasCosts: """ - Return gas costs updated for two-dimensional gas metering, - with state gas folded into the relevant totals. + Return gas costs with the EIP-8037 state-creation gas folded + into the relevant totals, layered on top of the EIP-8038 + state-access repricing returned by `super().gas_costs()`. """ cpsb = cls.cost_per_state_byte() parent = super(EIP8037, cls).gas_costs() new_acct = STATE_BYTES_PER_NEW_ACCOUNT * cpsb + return replace( parent, - BLOCK_ACCESS_LIST_ITEM=2000, STORAGE_SET=( - parent.COLD_STORAGE_WRITE - - parent.COLD_STORAGE_ACCESS - + STATE_BYTES_PER_STORAGE_SET * cpsb + parent.STORAGE_SET + STATE_BYTES_PER_STORAGE_SET * cpsb ), NEW_ACCOUNT=new_acct, - OPCODE_CREATE_BASE=REGULAR_GAS_CREATE, - TX_CREATE=(REGULAR_GAS_CREATE + new_acct), + TX_CREATE=parent.TX_CREATE + new_acct, AUTH_PER_EMPTY_ACCOUNT=( - PER_AUTH_BASE_COST + parent.AUTH_PER_EMPTY_ACCOUNT + (STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) * cpsb ), @@ -250,36 +252,6 @@ def transaction_intrinsic_state_gas( ) return state_gas - @classmethod - def _calculate_sstore_gas( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - Calculate the regular SSTORE gas cost. The state portion is - returned separately by `_calculate_sstore_state_gas`. A cold - slot adds `COLD_STORAGE_ACCESS`, a write to an unchanged - original adds `COLD_STORAGE_WRITE` minus `COLD_STORAGE_ACCESS`, - and every other case adds `WARM_SLOAD`. - """ - metadata = opcode.metadata - - original_value = metadata["original_value"] - current_value = metadata["current_value"] - if current_value is None: - current_value = original_value - new_value = metadata["new_value"] - - gas_cost = 0 if metadata["key_warm"] else gas_costs.COLD_STORAGE_ACCESS - - if original_value == current_value and current_value != new_value: - gas_cost += ( - gas_costs.COLD_STORAGE_WRITE - gas_costs.COLD_STORAGE_ACCESS - ) - else: - gas_cost += gas_costs.WARM_SLOAD - - return gas_cost - @classmethod def _calculate_sstore_state_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts @@ -307,39 +279,6 @@ def _calculate_sstore_state_gas( return STATE_BYTES_PER_STORAGE_SET * cpsb return 0 - @classmethod - def _calculate_sstore_refund( - cls, opcode: OpcodeBase, gas_costs: GasCosts - ) -> int: - """ - Calculate the regular SSTORE gas refund. The state portion is - returned separately by `_calculate_sstore_state_refund`. - """ - metadata = opcode.metadata - - original_value = metadata["original_value"] - current_value = metadata["current_value"] - if current_value is None: - current_value = original_value - new_value = metadata["new_value"] - - refund = 0 - if current_value != new_value: - if original_value != 0 and current_value != 0 and new_value == 0: - refund += gas_costs.REFUND_STORAGE_CLEAR - - if original_value != 0 and current_value == 0: - refund -= gas_costs.REFUND_STORAGE_CLEAR - - if original_value == new_value: - refund += ( - gas_costs.COLD_STORAGE_WRITE - - gas_costs.COLD_STORAGE_ACCESS - - gas_costs.WARM_SLOAD - ) - - return refund - @classmethod def _calculate_sstore_state_refund( cls, opcode: OpcodeBase, gas_costs: GasCosts diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py new file mode 100644 index 00000000000..26c05c5a0ea --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8038.py @@ -0,0 +1,188 @@ +""" +EIP-8038: State Access Gas Cost Increase. + +Harmonization and increase of state-access gas costs, repricing warm and +cold account and storage access, account writes, and the related access +list and authorization costs. + +This mixin ships alongside EIP-8037 in Amsterdam. It carries the +state-access repricing only; the EIP-8037 state-creation gas is folded in +on top by the (lower-numbered, therefore shallower) `EIP8037` mixin, which +reads these values via `super().gas_costs()` and adds its state-byte +portions to the shared `STORAGE_SET`, `TX_CREATE`, and +`AUTH_PER_EMPTY_ACCOUNT` totals. + +https://eips.ethereum.org/EIPS/eip-8038 +""" + +from dataclasses import replace +from typing import Callable, Dict + +from execution_testing.vm import ( + OpcodeBase, + Opcodes, +) + +from ....base_fork import BaseFork +from ....gas_costs import GasCosts + + +class EIP8038(BaseFork): + """EIP-8038 class.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """ + Return the EIP-8038 state-access gas repricing, layered on top + of the parent fork's schedule. EIP-8037 then folds its + state-creation gas into the relevant totals via + `super().gas_costs()`. + """ + parent = super(EIP8038, cls).gas_costs() + + warm_access = 100 + cold_account_access = 3_000 + cold_storage_access = 3_000 + storage_write = 10_000 + # The framework models the SSTORE write via the compound + # COLD_STORAGE_WRITE (access + write), so preserve the invariant + # COLD_STORAGE_WRITE - COLD_STORAGE_ACCESS == STORAGE_WRITE. + cold_storage_write = cold_storage_access + storage_write + # Surcharge for the first write to an account leaf, introduced as a + # standalone parameter by this repricing. + account_write = 8_000 + create_access = 11_000 + # ecRecover stays PRECOMPILE_ECRECOVER (3000) until EIP-7904 lands. + regular_per_auth_base_cost = ( + 1_616 + 3_000 + cold_account_access + 2 * warm_access + ) + + return replace( + parent, + WARM_ACCESS=warm_access, + WARM_SLOAD=warm_access, + COLD_ACCOUNT_ACCESS=cold_account_access, + COLD_STORAGE_ACCESS=cold_storage_access, + COLD_STORAGE_WRITE=cold_storage_write, + ACCOUNT_WRITE=account_write, + CALL_VALUE=account_write + 2_300, # ACCOUNT_WRITE + CALL_STIPEND + REFUND_STORAGE_CLEAR=12_480, + TX_ACCESS_LIST_ADDRESS=3_000, + TX_ACCESS_LIST_STORAGE_KEY=3_000, + BLOCK_ACCESS_LIST_ITEM=2000, + STORAGE_SET=storage_write, + OPCODE_CREATE_BASE=create_access, + TX_CREATE=create_access, + AUTH_PER_EMPTY_ACCOUNT=account_write + regular_per_auth_base_cost, + ) + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """ + Return the opcode gas map with the EIP-8038 `EXT*` update: + `EXTCODESIZE` and `EXTCODECOPY` charge an extra `WARM_ACCESS` + for the second database read (the code). + """ + gas_costs = cls.gas_costs() + opcode_gas_map = dict(super(EIP8038, cls).opcode_gas_map()) + + def with_extra_warm_access( + inner: int | Callable[[OpcodeBase], int], + ) -> Callable[[OpcodeBase], int]: + def fn(opcode: OpcodeBase) -> int: + inner_gas = inner(opcode) if callable(inner) else inner + return inner_gas + gas_costs.WARM_ACCESS + + return fn + + for opcode in (Opcodes.EXTCODESIZE, Opcodes.EXTCODECOPY): + opcode_gas_map[opcode] = with_extra_warm_access( + opcode_gas_map[opcode] + ) + return opcode_gas_map + + @classmethod + def _calculate_selfdestruct_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate the regular SELFDESTRUCT gas cost. EIP-8038 adds + `ACCOUNT_WRITE` when a positive balance is sent to an empty + account, on top of the inherited cost (where `NEW_ACCOUNT` + holds the EIP-8037 state-gas portion). + """ + gas_cost = super(EIP8038, cls)._calculate_selfdestruct_gas( + opcode, gas_costs + ) + if opcode.metadata["account_new"]: + gas_cost += gas_costs.ACCOUNT_WRITE + return gas_cost + + @classmethod + def _calculate_sstore_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate the regular SSTORE gas cost. The state portion is + returned separately by `_calculate_sstore_state_gas`. Under + EIP-8038 the access cost (`COLD_STORAGE_ACCESS` when cold, else + `WARM_SLOAD`) is always charged, and a first-time change to the + slot additionally charges the write cost `STORAGE_WRITE` + (modeled as `COLD_STORAGE_WRITE` minus `COLD_STORAGE_ACCESS`). + """ + metadata = opcode.metadata + + original_value = metadata["original_value"] + current_value = metadata["current_value"] + if current_value is None: + current_value = original_value + new_value = metadata["new_value"] + + gas_cost = ( + gas_costs.WARM_SLOAD + if metadata["key_warm"] + else gas_costs.COLD_STORAGE_ACCESS + ) + + if original_value == current_value and current_value != new_value: + gas_cost += ( + gas_costs.COLD_STORAGE_WRITE - gas_costs.COLD_STORAGE_ACCESS + ) + + return gas_cost + + @classmethod + def _calculate_sstore_refund( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate the regular SSTORE gas refund. The state portion is + returned separately by `_calculate_sstore_state_refund`. + """ + metadata = opcode.metadata + + original_value = metadata["original_value"] + current_value = metadata["current_value"] + if current_value is None: + current_value = original_value + new_value = metadata["new_value"] + + refund = 0 + if current_value != new_value: + if original_value != 0 and current_value != 0 and new_value == 0: + refund += gas_costs.REFUND_STORAGE_CLEAR + + if original_value != 0 and current_value == 0: + refund -= gas_costs.REFUND_STORAGE_CLEAR + + if original_value == new_value: + # Refund the STORAGE_WRITE charged on the first-time + # change earlier in the transaction. + refund += ( + gas_costs.COLD_STORAGE_WRITE + - gas_costs.COLD_STORAGE_ACCESS + ) + + return refund diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py index c98a22bc3f4..68133d5e8cd 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_1153.py @@ -7,16 +7,31 @@ https://eips.ethereum.org/EIPS/eip-1153 """ +from dataclasses import replace from typing import Callable, Dict, List from execution_testing.vm import OpcodeBase, Opcodes from ....base_fork import BaseFork +from ....gas_costs import GasCosts class EIP1153(BaseFork): """EIP-1153 class.""" + @classmethod + def gas_costs(cls) -> GasCosts: + """ + Set dedicated TLOAD and TSTORE gas costs. Transient storage is + in-memory only; its cost matches a warm storage access at + introduction but is independent of state-access pricing. + """ + return replace( + super(EIP1153, cls).gas_costs(), + OPCODE_TLOAD=100, + OPCODE_TSTORE=100, + ) + @classmethod def opcode_gas_map( cls, @@ -26,8 +41,8 @@ def opcode_gas_map( base_map = super(EIP1153, cls).opcode_gas_map() return { **base_map, - Opcodes.TLOAD: gas_costs.WARM_SLOAD, - Opcodes.TSTORE: gas_costs.WARM_SLOAD, + Opcodes.TLOAD: gas_costs.OPCODE_TLOAD, + Opcodes.TSTORE: gas_costs.OPCODE_TSTORE, } @classmethod diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index fcb8148cca0..ae190d5678a 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -36,6 +36,7 @@ class GasCosts: CALL_VALUE: int CALL_STIPEND: int NEW_ACCOUNT: int + ACCOUNT_WRITE: int = 0 # Contract Creation CODE_DEPOSIT_PER_BYTE: int @@ -146,3 +147,5 @@ class GasCosts: OPCODE_BLOBHASH: int = 0 OPCODE_MCOPY_BASE: int = 0 OPCODE_CLZ: int = 0 + OPCODE_TLOAD: int = 0 + OPCODE_TSTORE: int = 0 diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index b05711d28b4..026114d896e 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -670,7 +670,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: create_state_gas = Uint(0) if tx.to == Bytes0(b""): create_state_gas = StateGasCosts.NEW_ACCOUNT - create_regular_gas = GasCosts.REGULAR_GAS_CREATE + init_code_cost( + create_regular_gas = GasCosts.CREATE_ACCESS + init_code_cost( ulen(tx.data) ) @@ -693,9 +693,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: auth_regular_gas = Uint(0) auth_state_gas = Uint(0) if isinstance(tx, SetCodeTransaction): - auth_regular_gas = GasCosts.PER_AUTH_BASE_COST * ulen( - tx.authorizations - ) + auth_regular_gas = ( + GasCosts.ACCOUNT_WRITE + GasCosts.REGULAR_PER_AUTH_BASE_COST + ) * ulen(tx.authorizations) auth_state_gas = ( StateGasCosts.NEW_ACCOUNT + StateGasCosts.AUTH_BASE ) * ulen(tx.authorizations) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 8237225c713..aa35e153a14 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -192,14 +192,17 @@ def validate_authorization( return (authority, authority_account) -def set_delegation(message: Message) -> Uint: +def set_delegation(message: Message) -> Tuple[Uint, Uint]: """ Set the delegation code for the authorities in the message. Refills `StateGasCosts.NEW_ACCOUNT` when the authority's account leaf already exists, and `StateGasCosts.AUTH_BASE` when its code - slot already holds a delegation indicator. The total is returned - so block accounting can subtract it from `tx_state_gas`. + slot already holds a delegation indicator. When the authority leaf + already exists, the worst-case `GasCosts.ACCOUNT_WRITE` charged in + the intrinsic cost is also refunded to the regular-gas refund + counter. The totals are returned so block accounting can subtract + the state refill from `tx_state_gas` and apply the regular refund. Parameters ---------- @@ -210,10 +213,14 @@ def set_delegation(message: Message) -> Uint: ------- state_refund : `Uint` Total state gas refunded across all processed authorizations. + regular_refund : `Uint` + Total regular gas (`ACCOUNT_WRITE`) refunded for authorities + whose account leaf already existed. """ tx_state = message.tx_env.state state_refund = Uint(0) + regular_refund = Uint(0) for auth in message.tx_env.authorizations: match validate_authorization(message, auth): case None: @@ -225,6 +232,9 @@ def set_delegation(message: Message) -> Uint: refund = StateGasCosts.NEW_ACCOUNT message.state_gas_reservoir += refund state_refund += refund + # The new-account ACCOUNT_WRITE charged at intrinsic time is + # not needed: refund it to the regular refund counter. + regular_refund += GasCosts.ACCOUNT_WRITE # No new delegation indicator bytes are written: either the # authority already has one (overwrite in place / clear) or @@ -253,4 +263,4 @@ def set_delegation(message: Message) -> Uint: get_account(tx_state, message.code_address).code_hash, ) - return state_refund + return state_refund, regular_refund diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 156d7db6ec8..06ee00eb5c8 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -67,23 +67,21 @@ class GasCosts: # Access WARM_ACCESS: Final[Uint] = Uint(100) - COLD_ACCOUNT_ACCESS: Final[Uint] = Uint(2600) - COLD_STORAGE_ACCESS: Final[Uint] = Uint(2100) + COLD_ACCOUNT_ACCESS: Final[Uint] = Uint(3000) + COLD_STORAGE_ACCESS: Final[Uint] = Uint(3000) # Storage - COLD_STORAGE_WRITE: Final[Uint] = Uint(5000) + STORAGE_WRITE: Final[Uint] = Uint(10000) # Call - CALL_VALUE: Final[Uint] = Uint(9000) + CALL_VALUE: Final[Uint] = Uint(10300) # ACCOUNT_WRITE + CALL_STIPEND CALL_STIPEND: Final[Uint] = Uint(2300) + ACCOUNT_WRITE: Final[Uint] = Uint(8000) # Contract Creation CODE_DEPOSIT_PER_BYTE: Final[Uint] = Uint(200) CODE_INIT_PER_WORD: Final[Uint] = Uint(2) - REGULAR_GAS_CREATE: Final[Uint] = Uint(9000) - - # Authorization - PER_AUTH_BASE_COST: Final[Uint] = Uint(7500) + CREATE_ACCESS: Final[Uint] = ACCOUNT_WRITE + COLD_STORAGE_ACCESS # Utility ZERO: Final[Uint] = Uint(0) @@ -91,7 +89,9 @@ class GasCosts: FAST_STEP: Final[Uint] = Uint(5) # Refunds - REFUND_STORAGE_CLEAR: Final[int] = 4800 + REFUND_STORAGE_CLEAR: Final[int] = int( + (STORAGE_WRITE + COLD_STORAGE_ACCESS) * Uint(4800) // Uint(5000) + ) # Precompiles PRECOMPILE_ECRECOVER: Final[Uint] = Uint(3000) @@ -132,8 +132,31 @@ class GasCosts: TX_CREATE: Final[Uint] = Uint(32000) TX_DATA_TOKEN_STANDARD: Final[Uint] = Uint(4) TX_DATA_TOKEN_FLOOR: Final[Uint] = Uint(16) - TX_ACCESS_LIST_ADDRESS: Final[Uint] = Uint(2400) - TX_ACCESS_LIST_STORAGE_KEY: Final[Uint] = Uint(1900) + TX_ACCESS_LIST_ADDRESS: Final[Uint] = COLD_ACCOUNT_ACCESS + TX_ACCESS_LIST_STORAGE_KEY: Final[Uint] = COLD_STORAGE_ACCESS + + # Authorization + AUTH_TUPLE_BYTES: Final[Uint] = Uint(101) + """ + Calldata bytes charged for one [EIP-7702] authorization tuple. + + Counts the tuple's fields: the chain id, authority address, nonce, + signature parity, and the two signature scalars. Charged at the + calldata floor rate (`TX_DATA_TOKEN_FLOOR`). + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + REGULAR_PER_AUTH_BASE_COST: Final[Uint] = ( + AUTH_TUPLE_BYTES * TX_DATA_TOKEN_FLOOR + + PRECOMPILE_ECRECOVER + + COLD_ACCOUNT_ACCESS + + Uint(2) * WARM_ACCESS + ) + """ + Regular gas charged per EIP-7702 authorization, in addition to + `ACCOUNT_WRITE`. + """ # Block LIMIT_ADJUSTMENT_FACTOR: Final[Uint] = Uint(1024) @@ -199,6 +222,25 @@ class GasCosts: OPCODE_DUPN: Final[Uint] = VERY_LOW OPCODE_SWAPN: Final[Uint] = VERY_LOW OPCODE_EXCHANGE: Final[Uint] = VERY_LOW + OPCODE_TLOAD: Final[Uint] = Uint(100) + """ + Cost of the opcode that reads from transient storage. + + Transient storage is in-memory only; its cost is independent of + state-access pricing ([EIP-7971] proposes dedicated values). + + [EIP-7971]: https://eips.ethereum.org/EIPS/eip-7971 + """ + + OPCODE_TSTORE: Final[Uint] = Uint(100) + """ + Cost of the opcode that writes to transient storage. + + Transient storage is in-memory only; its cost is independent of + state-access pricing ([EIP-7971] proposes dedicated values). + + [EIP-7971]: https://eips.ethereum.org/EIPS/eip-7971 + """ # Dynamic Opcode Components OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py index 431cd3ba4c6..8a7e9ec1486 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/environment.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -341,10 +341,12 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GasCosts.WARM_ACCESS) + access_gas_cost = GasCosts.WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + access_gas_cost += GasCosts.WARM_ACCESS # Code reading cost (EIP-8038) + charge_gas(evm, access_gas_cost) # OPERATION tx_state = evm.message.tx_env.state @@ -386,6 +388,7 @@ def extcodecopy(evm: Evm) -> None: else: evm.accessed_addresses.add(address) access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + access_gas_cost += GasCosts.WARM_ACCESS # Code reading cost (EIP-8038) total_gas_cost = access_gas_cost + copy_gas_cost + extend_memory.cost diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 4e864b8ec71..91aec91163d 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -93,17 +93,17 @@ def sstore(evm: Evm) -> None: gas_cost = Uint(0) state_gas = StateGas(Uint(0)) + # Access cost: cold or warm, always charged. if (evm.message.current_target, key) not in evm.accessed_storage_keys: evm.accessed_storage_keys.add((evm.message.current_target, key)) gas_cost += GasCosts.COLD_STORAGE_ACCESS - - if original_value == current_value and current_value != new_value: - # charge regular cost for the operation, even when we - # already charge state gas for state creation - gas_cost += GasCosts.COLD_STORAGE_WRITE - GasCosts.COLD_STORAGE_ACCESS else: gas_cost += GasCosts.WARM_ACCESS + # Write cost: charged on the first change to the slot this transaction. + if original_value == current_value and current_value != new_value: + gas_cost += GasCosts.STORAGE_WRITE + # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -115,12 +115,9 @@ def sstore(evm: Evm) -> None: evm.refund_counter -= GasCosts.REFUND_STORAGE_CLEAR if original_value == new_value: - # Storage slot being restored to its original value - evm.refund_counter += int( - GasCosts.COLD_STORAGE_WRITE - - GasCosts.COLD_STORAGE_ACCESS - - GasCosts.WARM_ACCESS - ) + # Slot restored to its original value: refund the STORAGE_WRITE + # charged on the first-time change earlier this transaction. + evm.refund_counter += int(GasCosts.STORAGE_WRITE) if original_value == current_value and current_value != new_value: if original_value == 0: @@ -157,7 +154,7 @@ def tload(evm: Evm) -> None: key = pop(evm.stack).to_be_bytes32() # GAS - charge_gas(evm, GasCosts.WARM_ACCESS) + charge_gas(evm, GasCosts.OPCODE_TLOAD) # OPERATION value = get_transient_storage( @@ -187,7 +184,7 @@ def tstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - charge_gas(evm, GasCosts.WARM_ACCESS) + charge_gas(evm, GasCosts.OPCODE_TSTORE) set_transient_storage( evm.message.tx_env.state, evm.message.current_target, diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 185bc277fd5..a41e41a719f 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -194,7 +194,7 @@ def create(evm: Evm) -> None: init_code_gas = init_code_cost(Uint(memory_size)) charge_gas( evm, - GasCosts.REGULAR_GAS_CREATE + extend_memory.cost + init_code_gas, + GasCosts.CREATE_ACCESS + extend_memory.cost + init_code_gas, ) # OPERATION @@ -248,7 +248,7 @@ def create2(evm: Evm) -> None: init_code_gas = init_code_cost(Uint(memory_size)) charge_gas( evm, - GasCosts.REGULAR_GAS_CREATE + GasCosts.CREATE_ACCESS + GasCosts.OPCODE_KECCAK256_PER_WORD * call_data_words + extend_memory.cost + init_code_gas, @@ -668,16 +668,19 @@ def selfdestruct(evm: Evm) -> None: evm.accessed_addresses.add(beneficiary) state_gas = StateGas(Uint(0)) + account_write_gas = Uint(0) if ( not is_account_alive(tx_state, beneficiary) and get_account(tx_state, evm.message.current_target).balance != 0 ): state_gas = StateGasCosts.NEW_ACCOUNT + # EIP-8038: positive balance sent to an empty account. + account_write_gas = GasCosts.ACCOUNT_WRITE # Charge regular gas before state gas so that a regular-gas OOG # does not consume state gas that would inflate the parent's # reservoir on frame failure. - charge_gas(evm, gas_cost) + charge_gas(evm, gas_cost + account_write_gas) charge_state_gas(evm, state_gas) originator = evm.message.current_target diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 5df1dccc8a0..fda390023d1 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -154,7 +154,9 @@ def process_message_call(message: Message) -> MessageCallOutput: ) else: if message.tx_env.authorizations != (): - state_refund += set_delegation(message) + auth_state_refund, auth_regular_refund = set_delegation(message) + state_refund += auth_state_refund + refund_counter += U256(auth_regular_refund) delegated_address = get_delegated_code_address(message.code) if delegated_address is not None: diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py index cc9fcee86ea..b521689d295 100644 --- a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py @@ -105,6 +105,10 @@ def build_refund_tx( auth_state_refund = ( gsc.REFUND_AUTH_PER_EXISTING_ACCOUNT * refunds_count ) + # The worst-case `ACCOUNT_WRITE` charged at intrinsic + # time is refunded via the refund counter for existing + # authorities, even if the transaction reverts. + refund_counter += gsc.ACCOUNT_WRITE * refunds_count case _: raise ValueError( f"Unknown refund type: {refund_type} (Test needs update)" diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_refunds.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_refunds.py index 7463c6b66ef..e21d8c5a77e 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_refunds.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_refunds.py @@ -97,11 +97,15 @@ def max_refund(fork: Fork, refund_type: RefundTypes) -> int: if refund_type == RefundTypes.STORAGE_CLEAR else 0 ) - if ( - not fork.is_eip_enabled(8037) - and refund_type == RefundTypes.AUTHORIZATION_EXISTING_AUTHORITY - ): - max_refund += gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT + if refund_type == RefundTypes.AUTHORIZATION_EXISTING_AUTHORITY: + if fork.is_eip_enabled(8037): + # The worst-case `ACCOUNT_WRITE` charged at intrinsic time + # is refunded via the refund counter when the authority's + # account leaf already exists; the state-gas portion is + # refilled separately and is not subject to the cap. + max_refund += gas_costs.ACCOUNT_WRITE + else: + max_refund += gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT return max_refund @@ -160,10 +164,11 @@ def intrinsic_gas_data_floor_minimum_delta() -> int: would always be the below the execution gas cost even after the refund is applied. - This value has been set as of Amsterdam and should be adjusted if the gas - costs change. + This value has been set as of Amsterdam (with the provisional + state-access repricing) and should be adjusted if the gas costs + change. """ - return 250 + return 11_000 @pytest.fixture diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py index 10c15ec10a3..69fcff2d79a 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py @@ -31,7 +31,11 @@ @pytest.mark.parametrize( "nonzero_bytes", [ - pytest.param(1000, id="1000_nonzero_bytes"), + # Must be large enough that the floor midpoint chosen below + # stays above the access-list intrinsic cost (asserted in the + # test body): each nonzero byte adds 64 gas to the floor but + # only 16 to the intrinsic cost. + pytest.param(1700, id="1700_nonzero_bytes"), pytest.param(2000, id="2000_nonzero_bytes"), ], ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py index c5fbeb36071..e807c34ebd4 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/spec.py @@ -47,7 +47,10 @@ class Spec: STATE_BYTES_PER_STORAGE_SET = 64 STATE_BYTES_PER_AUTH_BASE = 23 - # Regular gas constants (EIP-8037 replaces old combined costs) - REGULAR_GAS_CREATE = 9000 - PER_AUTH_BASE_COST = 7500 - GAS_COLD_STORAGE_WRITE = 5000 + # Regular gas constants. EIP-8037 separated state from regular gas; + # EIP-8038 then repriced them. + REGULAR_GAS_CREATE = 11000 + # Total regular intrinsic per EIP-7702 authorization: + # ACCOUNT_WRITE (8000) + REGULAR_PER_AUTH_BASE_COST (7816). + PER_AUTH_BASE_COST = 15816 + GAS_COLD_STORAGE_WRITE = 13000 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 a30fdb28391..62f3a91eeca 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 @@ -92,8 +92,9 @@ def test_delegatecall_child_spill_not_double_charged( """ Test DELEGATECALL child state gas paid from `gas_left` is not recharged. - With gas below the Amsterdam tx gas cap, the top-level frame starts with - no state gas reservoir and the child pays for SSTOREs by spilling from + With the gas limit pinned to the Amsterdam tx gas cap and no requested + reservoir (`state_gas_reservoir=0`), the top-level frame starts with no + state gas reservoir and the child pays for SSTOREs by spilling from `gas_left`. The parent frame must not charge the same state growth again at frame end. """ @@ -115,7 +116,7 @@ def test_delegatecall_child_spill_not_double_charged( tx = Transaction( to=caller, - gas_limit=700_000, + state_gas_reservoir=0, sender=pre.fund_eoa(), ) 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 8cea2d84600..3a4fd08acd0 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 @@ -657,8 +657,8 @@ def test_code_deposit_oog_preserves_parent_reservoir( init_code = Op.RETURN(0, deploy_size) # Limited regular gas forwarded to the factory. After CREATE - # takes 63/64, the factory retains ~15 K for its SSTOREs. - child_gas = 1_000_000 + # takes 63/64, the factory retains ~23 K for its SSTOREs. + child_gas = 1_500_000 factory_storage = Storage() factory = pre.deploy_contract( @@ -790,8 +790,9 @@ def test_parent_state_gas_after_child_failure( # 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 (no-op) + 2 PUSHes + SSTORE - # (zero-to-nonzero regular) + # 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 @@ -801,7 +802,6 @@ def test_parent_state_gas_after_child_failure( factory_post_create_regular = ( gas_costs.VERY_LOW + gas_costs.COLD_STORAGE_ACCESS - + gas_costs.WARM_ACCESS + gas_costs.VERY_LOW * 2 + gas_costs.COLD_STORAGE_WRITE ) @@ -2465,7 +2465,9 @@ def test_selfdestruct_in_create_tx_initcode( create_state_gas = fork.create_state_gas(code_size=0) beneficiary = 0xDEAD - initcode = Op.SELFDESTRUCT(beneficiary) + # `account_new` folds the beneficiary's `ACCOUNT_WRITE` regular + # cost and account-creation state gas into `gas_cost`. + initcode = Op.SELFDESTRUCT(beneficiary, account_new=True) sender = pre.fund_eoa() intrinsic_calc = fork.transaction_intrinsic_cost_calculator() @@ -2476,7 +2478,7 @@ def test_selfdestruct_in_create_tx_initcode( expected_state = create_state_gas + gas_costs.NEW_ACCOUNT initcode_gas = initcode.gas_cost(fork) - gas_limit = intrinsic_total + initcode_gas + gas_costs.NEW_ACCOUNT + 1000 + gas_limit = intrinsic_total + initcode_gas + 1000 tx = Transaction( sender=sender, 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 82c465cd67a..df255111a23 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 @@ -714,34 +714,30 @@ def test_selfdestruct_via_delegatecall_chain_no_refund( @pytest.mark.valid_from("EIP8037") -def test_selfdestruct_new_beneficiary_no_regular_account_creation_cost( +def test_selfdestruct_new_beneficiary_account_write_cost( state_test: StateTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - Verify SELFDESTRUCT to a new beneficiary does not charge a - regular account-creation cost on top of state gas. + Verify SELFDESTRUCT to a new beneficiary charges `ACCOUNT_WRITE` + regular gas plus the account-creation state gas, and not the + legacy combined regular account-creation cost. """ - gas_costs = fork.gas_costs() - new_account_state_gas = gas_costs.NEW_ACCOUNT - beneficiary = pre.fund_eoa(amount=0) - victim_code = Op.SELFDESTRUCT(beneficiary) + victim_code = Op.SELFDESTRUCT(beneficiary, account_new=True) victim = pre.deploy_contract(code=victim_code, balance=1) - # Tight budget: slack is less than the old pre-Amsterdam regular - # account-creation cost, so any extra regular draw would OOG. + # Tight budget: slack is less than the legacy 25,000 regular + # account-creation cost minus `ACCOUNT_WRITE`, so any regular draw + # beyond `ACCOUNT_WRITE` would OOG. The opcode metadata folds the + # `ACCOUNT_WRITE` regular cost and the account-creation state gas + # into `gas_cost`. intrinsic = fork.transaction_intrinsic_cost_calculator()() tx = Transaction( to=victim, - gas_limit=( - intrinsic - + victim_code.gas_cost(fork) - + new_account_state_gas - + 20_000 - ), + gas_limit=(intrinsic + victim_code.gas_cost(fork) + 4_000), sender=pre.fund_eoa(), ) 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 bf18b2ba4a0..d95a44acbdb 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 @@ -386,6 +386,10 @@ def test_auth_refund_block_gas_accounting( `RESET_DELEGATION_ADDRESS`; same full refill, since the refill keys off the *pre-state* code slot, not what we're writing. + When the authority's account leaf already exists, the worst-case + `ACCOUNT_WRITE` charged at intrinsic time is additionally refunded + via the regular refund counter, subject to the refund cap. + Verified via header `gas_used`, receipt `cumulative_gas_used`, and the authority post-state (catches a silently-skipped auth). """ @@ -397,6 +401,7 @@ def test_auth_refund_block_gas_accounting( ) intrinsic_regular = total_intrinsic - intrinsic_state_gas new_account_refund = fork.gas_costs().REFUND_AUTH_PER_EXISTING_ACCOUNT + account_write = fork.gas_costs().ACCOUNT_WRITE # Per-auth intrinsic state gas covers NEW_ACCOUNT + AUTH_BASE; the # AUTH_BASE portion is what's left after stripping NEW_ACCOUNT. auth_base_refund = intrinsic_state_gas - new_account_refund @@ -411,17 +416,20 @@ def test_auth_refund_block_gas_accounting( signer = pre.fund_eoa(amount=0) pre_nonce = 0 auth_refund = auth_base_refund if authorize_to_null else 0 + refund_counter = 0 elif signer_pre_state == "existing_leaf": signer = pre.fund_eoa() pre_nonce = 0 auth_refund = new_account_refund + ( auth_base_refund if authorize_to_null else 0 ) + refund_counter = account_write elif signer_pre_state == "existing_delegation": # `fund_eoa(delegation=...)` sets the authority's nonce to 1. signer = pre.fund_eoa(delegation=contract_old) pre_nonce = 1 auth_refund = new_account_refund + auth_base_refund + refund_counter = account_write else: raise ValueError(f"unknown signer_pre_state: {signer_pre_state!r}") @@ -450,7 +458,14 @@ def test_auth_refund_block_gas_accounting( intrinsic_regular, intrinsic_state_gas - auth_refund, ) - receipt_cumulative_gas_used = total_intrinsic - auth_refund + # The state refill is not subject to the refund cap; the regular + # `ACCOUNT_WRITE` refund is. + gas_used_before_refund = total_intrinsic - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + receipt_cumulative_gas_used = gas_used_before_refund - regular_refund tx = Transaction( to=contract_new, @@ -1409,9 +1424,12 @@ def test_auth_sender_billing_after_failure( on top-level failure. For existing accounts, set_delegation refunds new-account state - gas to the reservoir. On REVERT, the restored reservoir reduces - the sender's bill via the billing formula. The sender pays less - than in the new-account case by exactly the refund amount. + gas to the reservoir and the worst-case `ACCOUNT_WRITE` to the + regular refund counter; both survive the top-level REVERT since + delegations are applied before execution. On REVERT, the restored + reservoir and the capped regular refund reduce the sender's bill + via the billing formula. The sender pays less than in the + new-account case. """ auth_intrinsic_state = fork.transaction_intrinsic_state_gas( authorization_count=1, @@ -1431,7 +1449,13 @@ def test_auth_sender_billing_after_failure( revert_gas = (Op.REVERT(0, 0)).gas_cost(fork) auth_refund = new_account_refund if authority_exists else 0 - expected_cumulative = intrinsic_total + revert_gas - auth_refund + refund_counter = fork.gas_costs().ACCOUNT_WRITE if authority_exists else 0 + gas_used_before_refund = intrinsic_total + revert_gas - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + expected_cumulative = gas_used_before_refund - regular_refund expected_gas_used = max( intrinsic_regular + revert_gas, auth_intrinsic_state - auth_refund, 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 50e404d149d..11ff400a16a 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 @@ -968,29 +968,36 @@ def test_sstore_restoration_ancestor_revert( probe = pre.deploy_contract(code=probe_code) caller_storage = Storage() - caller_code = Op.POP(call_opcode(gas=Op.GAS, address=middle)) + Op.SSTORE( + # The probe OOGs and returns 0, so the caller's outer SSTORE is a + # cold no-op (0 to 0) on a fresh slot, charging only + # COLD_STORAGE_ACCESS rather than the cold set `regular_cost` + # assumes by default. + caller_code = Op.POP( + call_opcode(gas=Op.GAS, address=middle) + ) + Op.SSTORE.with_metadata( + key_warm=False, + original_value=0, + current_value=0, + new_value=0, + )( caller_storage.store_next(0, "probe_must_fail"), Op.CALL(gas=probe_gas, address=probe), ) caller = pre.deploy_contract(code=caller_code) - # Block state gas commits only the caller's outer SSTORE-set. The - # probe OOGs and inner's set+clear cancel before middle reverts. - # The probe's CALL burns its forwarded budget on the OOG, less the - # cold-call surcharge already in the caller's static regular cost. - # Header gas_used is max(regular, state). - probe_burned = ( - probe_gas - gas_costs.COLD_ACCOUNT_ACCESS - 2 * gas_costs.WARM_ACCESS - ) - expected_regular = ( + # No SSTORE-set persists (inner's set+clear cancel, middle reverts, + # the probe OOGs and reverts, and the caller's outer SSTORE is a + # no-op), so block state gas is zero and header gas_used (the max of + # regular and state) is just the regular total. The probe burns its + # full forwarded budget on the OOG; its CALL's cold-access surcharge + # is already counted in the caller's regular cost. + expected_gas_used = ( intrinsic_cost + caller_code.regular_cost(fork) + middle_code.regular_cost(fork) + inner_code.regular_cost(fork) - + probe_burned + + probe_gas ) - expected_state = Op.SSTORE(new_value=1).state_cost(fork) - expected_gas_used = max(expected_regular, expected_state) # gas_limit at the cap means the caller's reservoir starts at 0. tx = Transaction( diff --git a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py index bd69ce23086..ed426e0be87 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py @@ -6,7 +6,6 @@ import pytest from execution_testing import ( - AccessList, Account, Address, Alloc, @@ -280,24 +279,33 @@ def test_tstore_rollback_on_failed_create( https://github.com/ethereum/execution-specs/issues/917 Initcode does TLOAD(1) to compute a return size, then does - TSTORE(1, 0x6000), then returns data of the computed size. - When TLOAD(1) is 0, the return size is 0x600a (exceeds max code - size 0x6000), so creation fails. + TSTORE(1, max_code_size), then returns data of the computed size. + When TLOAD(1) is 0, the return size exceeds the max code size, so + creation fails. The caller invokes CREATE/CREATE2 twice with the same initcode. If TSTORE from the first (failed) creation is properly rolled - back, the second creation also sees TLOAD(1)==0 and fails the - same way. If not rolled back, TLOAD(1)==0x6000 and the second - creation succeeds. + back, the second CREATE2 also sees TLOAD(1)==0 and fails the same + way, so nothing is deployed. If it were not rolled back, the + second CREATE2 would see TLOAD(1)==max_code_size, return a small + valid contract and succeed; the post-state therefore asserts the + target address is non-existent. + + The create results are not recorded with SSTORE: a failed create + burns its full 63/64 gas forward, and under the EIP-8037 + transaction gas-limit cap two of them in sequence leave too little + regular gas to write the result. Asserting account non-existence + checks the same rollback property without that write. """ # Initcode: - # return_size = 0x600a - TLOAD(1) - # TSTORE(1, 0x6000) + # return_size = (max_code_size + 0x0A) - TLOAD(1) + # TSTORE(1, max_code_size) # RETURN(offset=0, size=return_size) # - # TLOAD(1)==0: return_size = 0x600a > max code size -> fail - # TLOAD(1)==0x6000: return_size = 0x0a <= max code size -> succeed + # TLOAD(1)==0: return_size > max code size -> fail + # TLOAD(1)==max_code_size: return_size = 0x0A <= max -> succeed max_code_size = fork.max_code_size() + salt = 0 initcode = ( Op.TLOAD(1) @@ -310,36 +318,47 @@ def test_tstore_rollback_on_failed_create( initcode_bytes = bytes(initcode) initcode_len = len(initcode_bytes) + create_call = ( + create_opcode(0, 0, initcode_len, salt) + if create_opcode == Op.CREATE2 + else create_opcode(0, 0, initcode_len) + ) caller_code = ( Om.MSTORE(initcode_bytes, 0) - + Op.SSTORE( - 0, - create_opcode(0, 0, initcode_len, 0) - if create_opcode == Op.CREATE2 - else create_opcode(0, 0, initcode_len), - ) - + Op.SSTORE( - 1, - create_opcode(0, 0, initcode_len, 0) - if create_opcode == Op.CREATE2 - else create_opcode(0, 0, initcode_len), - ) + + create_call + + Op.POP + + create_call + + Op.POP ) - caller_address = pre.deploy_contract(caller_code, storage={0: 1, 1: 1}) + caller_address = pre.deploy_contract(caller_code) + + # CREATE2 targets one deterministic address (salt + initcode) for + # both attempts; CREATE targets nonce-derived addresses (the + # deployed caller starts at nonce 1). + if create_opcode == Op.CREATE2: + created_addresses = [ + compute_create_address( + address=caller_address, + salt=salt, + initcode=initcode, + opcode=Op.CREATE2, + ) + ] + else: + created_addresses = [ + compute_create_address( + address=caller_address, nonce=nonce, opcode=Op.CREATE + ) + for nonce in (1, 2) + ] sender = pre.fund_eoa() - tx = Transaction( - sender=sender, - to=caller_address, - access_list=[ - AccessList(address=caller_address, storage_keys=[0, 1]), - ], - ) + tx = Transaction(sender=sender, to=caller_address) - post = { - # Both creations fail because TSTORE is rolled back; - # initial storage {0: 1, 1: 1} is overwritten to zeros - caller_address: Account(storage={0: 0, 1: 0}), - } + # Both creations fail because TSTORE is rolled back, so nothing is + # deployed; the caller nonce still advances once per attempt. + post = {caller_address: Account(nonce=3)} + for created_address in created_addresses: + post[created_address] = Account.NONEXISTENT # type: ignore state_test(pre=pre, post=post, tx=tx) diff --git a/tests/paris/eip7610_create_collision/test_collision_selfdestruct.py b/tests/paris/eip7610_create_collision/test_collision_selfdestruct.py index a0f58bbbf18..4bb20365f3d 100644 --- a/tests/paris/eip7610_create_collision/test_collision_selfdestruct.py +++ b/tests/paris/eip7610_create_collision/test_collision_selfdestruct.py @@ -74,14 +74,12 @@ def test_selfdestruct_after_create2_collision( + Op.SSTORE( storage.store_next(1, "create2_call_success"), Op.CALL( - # Forwarded budget covers deployer's CREATE2 (charged - # then refunded on collision under EIP-8037) plus its - # SSTORE; both 0 pre-EIP-8037 and scale with cpsb. - gas=( - 500_000 - + fork.gas_costs().NEW_ACCOUNT - + Op.SSTORE(new_value=1).state_cost(fork) - ), + # The colliding CREATE2 consumes 63/64 of the deployer's + # gas (the account-creation state gas is charged then + # refunded on collision under EIP-8037); size the budget + # so the surviving 1/64 still covers the deployer's cold + # SSTORE of the CREATE2 result. + gas=500_000 + 64 * fork.gas_costs().COLD_STORAGE_WRITE, address=deployer, args_size=Op.CALLDATASIZE, ), diff --git a/tests/ported_static/stBadOpcode/test_measure_gas.py b/tests/ported_static/stBadOpcode/test_measure_gas.py index b262b96b127..f100b2b1ea8 100644 --- a/tests/ported_static/stBadOpcode/test_measure_gas.py +++ b/tests/ported_static/stBadOpcode/test_measure_gas.py @@ -3,6 +3,15 @@ Ported from: state_tests/stBadOpcode/measureGasFiller.yml + +@manually-enhanced: Do not overwrite. A binary search measures the gas +an opcode needs to succeed. Only the EXTCODE case shifts: it runs a +warm `EXTCODESIZE` plus a warm `EXTCODECOPY` (the target is warmed by +earlier search iterations), and EIP-8038 adds a flat +100 to each warm +extcode access. The stored threshold therefore grows by the sum of the +two opcodes' warm `(Amsterdam - Cancun)` cost deltas, derived from the +fork's own gas model so it is exactly 0 before EIP-8038; do not +hardcode the Amsterdam number. """ import pytest @@ -363,6 +372,26 @@ def test_measure_gas( address=Address(0x0000000000000000000000000000000000C0DEF2), # noqa: E501 ) + # The EXTCODE search measures a warm `EXTCODESIZE` plus a warm + # `EXTCODECOPY` (the target is warmed by earlier search iterations). + # EIP-8038 adds a flat surcharge to each warm extcode access, so the + # threshold grows by the two opcodes' combined warm cost delta versus + # Cancun. Derived from the fork gas model so it is 0 before EIP-8038. + # The EXTCODECOPY metadata mirrors the measured access: a 0x20-byte + # copy into already-expanded memory, so only the account-access + # component varies across forks. + warm_extcode_delta = ( + Op.EXTCODESIZE.with_metadata(address_warm=True).gas_cost(fork) - 100 + ) + ( + Op.EXTCODECOPY.with_metadata( + address_warm=True, + data_size=0x20, + new_memory_size=0x120, + old_memory_size=0x120, + ).gas_cost(fork) + - 103 + ) + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, @@ -397,7 +426,9 @@ def test_measure_gas( { "indexes": {"data": [10], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 221})}, + "result": { + contract_12: Account(storage={0: 221 + warm_extcode_delta}) + }, }, { "indexes": {"data": [9], "gas": -1, "value": -1}, diff --git a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py index 82a3e5c4cbd..be3bf8c0559 100644 --- a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py +++ b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py @@ -3,6 +3,16 @@ Ported from: state_tests/stBadOpcode/operationDiffGasFiller.yml + +@manually-enhanced: Do not overwrite. A search measures the gas an +opcode needs to succeed. Two access classes shift under EIP-8038: the +CALL-family probes (`CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL`) make +one cold account access to the callee, repricing by +`COLD_ACCOUNT_ACCESS - 2600`; the EXTCODE probe runs a cold +`EXTCODESIZE` plus a warm `EXTCODECOPY`, each carrying the extra +extcode surcharge. Every delta is derived from the fork's own gas +model, so it is exactly 0 before EIP-8038 and tracks future parameter +changes; do not hardcode the Amsterdam numbers. """ import pytest @@ -358,6 +368,27 @@ def test_operation_diff_gas( address=Address(0x0000000000000000000000000000000000C0DEF2), # noqa: E501 ) + # The CALL-family probes make one cold account access to the callee; + # EIP-8038 reprices it by `COLD_ACCOUNT_ACCESS - 2600`. The EXTCODE + # probe runs a cold `EXTCODESIZE` plus a warm `EXTCODECOPY`, each + # carrying the extcode surcharge. Both deltas come from the fork gas + # model, so they are 0 before EIP-8038. The EXTCODECOPY metadata + # mirrors the measured access (a 0x20-byte copy into already-expanded + # memory) so only the account-access component varies across forks. + gas_costs = fork.gas_costs() + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + extcode_probe_delta = ( + Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 + ) + ( + Op.EXTCODECOPY.with_metadata( + address_warm=True, + data_size=0x20, + new_memory_size=0x120, + old_memory_size=0x120, + ).gas_cost(fork) + - 103 + ) + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, @@ -372,7 +403,9 @@ def test_operation_diff_gas( { "indexes": {"data": [2, 3, 4, 5], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 2700})}, + "result": { + contract_12: Account(storage={0: 2700 + cold_account_delta}) + }, }, { "indexes": {"data": [8, 6, 7], "gas": -1, "value": -1}, @@ -382,7 +415,9 @@ def test_operation_diff_gas( { "indexes": {"data": [10], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 2800})}, + "result": { + contract_12: Account(storage={0: 2800 + extcode_probe_delta}) + }, }, { "indexes": {"data": [9], "gas": -1, "value": -1}, diff --git a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py index fd230fb046f..05a64e6befd 100644 --- a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py +++ b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py @@ -3,9 +3,15 @@ Ported from: state_tests/stCreate2/Create2OOGafterInitCodeFiller.json -@manually-enhanced: Do not overwrite. tx_gas[1] is tuned to barely -succeed CREATE2 on Cancun; on Amsterdam EIP-8037 the NEW_ACCOUNT -state-gas spills, so lift the budget by Fork.oog_budget_lift. +@manually-enhanced: Do not overwrite. The init code RETURNs a 5-byte +deployed contract; g0 must run out before the deposit (account stays +NONEXISTENT) and g1 must just clear it (account created). On Cancun +the deploy gap is the 1000-gas regular code deposit and the test's +two budgets straddle it. EIP-8037/8038 move account creation into a +spilling state-gas charge AND drop OPCODE_CREATE_BASE, so the budget +that reaches the same RETURN point changes by a fork-derived amount. +The lift restores the straddle: it is exactly 0 pre-EIP-8037 and +tracks the parameters. See `_oog_lift` below for the derivation. """ import pytest @@ -113,19 +119,39 @@ def test_create2_oo_gafter_init_code( tx_data = [ Bytes(""), ] - # Lift both entries on Amsterdam so the test still exercises its - # named scenario. With only tx_gas[1] lifted, g=0 OoG'd at CREATE2 - # dispatch (NEW_ACCOUNT state-gas spill) before init code ever ran — - # the assertion still passes (`NONEXISTENT` either way) but the - # failure mode is "dispatch-time OoG" instead of "OoG after init - # code". A simple `fork.oog_budget_lift(creates_before_oog=1)` (183600) - # is *too* generous and pushes g=0 past the deploy threshold; the - # Cancun 1000-gas gap between g=0 and g=1 collapses on Amsterdam - # because once dispatch is cleared, the 5-byte init code is cheap - # enough to always complete. The value below is the middle of the - # empirically-safe range (166499, 167000) where g=0 still OoGs at - # dispatch *and* g=1 just clears the deploy threshold (~221.5k). - _oog_lift = 166_750 if fork.is_eip_enabled(8037) else 0 + # The init code RETURNs a 5-byte deployed contract, so the CREATE2 + # frame is charged a code deposit after the init RETURN. On Cancun + # that deposit is 1000 (200 * 5 regular) and the two budgets below + # straddle it: g0 reaches RETURN just under 1000 gas (deploy fails, + # account NONEXISTENT) and g1 just over (deploy succeeds). The + # 1000-gas gap between the budgets is exactly this Cancun deploy + # threshold. + # + # EIP-8037/8038 change the CREATE2 dispatch in two ways that the + # budget must absorb before the init code RETURNs: the new + # `create_state_gas()` spills into regular gas (empty reservoir), + # and `OPCODE_CREATE_BASE` drops from its Cancun value of 32000. + # Their sum is the net extra the dispatch consumes from the budget. + # The deposit step then changes too: its regular `CODE_DEPOSIT_PER_BYTE + # * 5` portion is now covered by the state-gas reservoir credited at + # dispatch, while `code_deposit_state_gas(code_size=5)` spills and + # must come from the forwarded gas instead. The lift restores the + # Cancun straddle by funding the net dispatch consumption plus the + # deposit's state spill, minus the regular deposit the budgets + # already carried in their 1000-gas gap. Every term is 0 + # pre-EIP-8037, so the original Cancun behavior is preserved. + gas_costs = fork.gas_costs() + _cancun_create_base = 32000 + _deploy_size = 5 + _oog_lift = 0 + if fork.is_eip_enabled(8037): + _oog_lift = ( + fork.oog_budget_lift( + creates_before_oog=1, deploy_code_size=_deploy_size + ) + + (gas_costs.OPCODE_CREATE_BASE - _cancun_create_base) + - gas_costs.CODE_DEPOSIT_PER_BYTE * _deploy_size + ) tx_gas = [54000 + _oog_lift, 55000 + _oog_lift] tx = Transaction( diff --git a/tests/ported_static/stCreate2/test_create2_smart_init_code.py b/tests/ported_static/stCreate2/test_create2_smart_init_code.py index 09b73134629..f63fd21214e 100644 --- a/tests/ported_static/stCreate2/test_create2_smart_init_code.py +++ b/tests/ported_static/stCreate2/test_create2_smart_init_code.py @@ -4,9 +4,16 @@ Ported from: state_tests/stCreate2/create2SmartInitCodeFiller.json -@manually-enhanced: Do not overwrite. tx_gas was raised from 400 000 to -1 000 000 so the CREATE2 path can afford its EIP-8037 NEW_ACCOUNT state -gas on Amsterdam (post-state expectations are unchanged on all forks). +@manually-enhanced: Do not overwrite. The d0 call chain performs two +value-bearing CREATE2s plus a SELFDESTRUCT to a non-alive beneficiary +and two fresh SSTORE-sets before it finishes; with an empty state-gas +reservoir every one of those state-gas charges spills into regular gas +on EIP-8037, overrunning the original 400 000 budget. Lift the budget +by exactly that spilled state gas via `fork.oog_budget_lift` (three +`create_state_gas()` charges -- two CREATE2 dispatches and the +SELFDESTRUCT account creation -- plus two fresh SSTORE-set state +costs), which is 0 pre-EIP-8037. Post-state expectations are unchanged +on all forks. """ import pytest @@ -173,11 +180,14 @@ def test_create2_smart_init_code( Hash(contract_0, left_padding=True), Hash(contract_1, left_padding=True), ] - # EIP-8037 NEW_ACCOUNT + per-byte state-gas spill into the regular - # budget on Amsterdam; pre-EIP-8037 forks keep the original 400 000. - outer_tx_gas = 400_000 - if fork.is_eip_enabled(8037): - outer_tx_gas = 1_000_000 + # The d0 chain spills three `create_state_gas()` charges (two + # CREATE2 dispatches and the SELFDESTRUCT to a non-alive + # beneficiary) and two fresh SSTORE-set state costs into regular + # gas when the reservoir is empty. Lift the original budget by + # exactly that spilled state gas; 0 pre-EIP-8037. + outer_tx_gas = 400_000 + fork.oog_budget_lift( + creates_before_oog=3, sstores_before_oog=2 + ) tx_gas = [outer_tx_gas] tx = Transaction( diff --git a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py index 80c2ba100bc..358473d4fb0 100644 --- a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py +++ b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py @@ -9,6 +9,14 @@ Ported from: state_tests/stCreateTest/CreateAddressWarmAfterFailFiller.yml + +@manually-enhanced: Do not overwrite. The post-state records the +measured cost of accessing the create address after a failed CREATE, +which is a cold account access. EIP-8038 reprices a cold account +access from 2 600 to 3 000, so each such measurement gains 400 at +Amsterdam. Derive that delta from the fork's gas model so it is +exactly 0 pre-EIP-8037 and tracks parameter changes; do not hardcode +the Amsterdam value. """ import pytest @@ -381,6 +389,11 @@ def test_create_address_warm_after_fail( address=Address(0x00000000000000000000000000000000000C0DEC), # noqa: E501 ) + # The create address access after a failed CREATE is cold here; + # EIP-8038 reprices a cold account access from 2 600 to 3 000. + # Derive the delta from the fork so it is 0 pre-EIP-8037. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + expect_entries_: list[dict] = [ { "indexes": {"data": [0, 2, 11, 4], "gas": -1, "value": [0]}, @@ -396,7 +409,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -447,7 +460,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -498,7 +511,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -549,7 +562,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -600,7 +613,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -649,9 +662,9 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828, + 12: 2828 + cold_account_delta, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=0, @@ -703,9 +716,9 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828, + 12: 2828 + cold_account_delta, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=0, @@ -753,7 +766,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, @@ -804,7 +817,7 @@ def test_create_address_warm_after_fail( 5: 1, 12: 328, 13: 316, - 14: 2828, + 14: 2828 + cold_account_delta, 15: 316, }, nonce=1, diff --git a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py index 76b1fd09653..6e5e05c2593 100644 --- a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py +++ b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py @@ -3,9 +3,15 @@ Ported from: state_tests/stCreateTest/CreateOOGafterInitCodeFiller.json -@manually-enhanced: Do not overwrite. tx_gas[1] is tuned to barely -succeed CREATE on Cancun; on Amsterdam EIP-8037 the NEW_ACCOUNT -state-gas spills, so lift the budget by Fork.oog_budget_lift. +@manually-enhanced: Do not overwrite. The init code RETURNs a 5-byte +deployed contract; g0 must run out before the deposit (account stays +NONEXISTENT) and g1 must just clear it (account created). On Cancun +the deploy gap is the 1000-gas regular code deposit and the test's +two budgets straddle it. EIP-8037/8038 move account creation into a +spilling state-gas charge AND drop OPCODE_CREATE_BASE, so the budget +that reaches the same RETURN point changes by a fork-derived amount. +The lift restores the straddle: it is exactly 0 pre-EIP-8037 and +tracks the parameters. See `_oog_lift` below for the derivation. """ import pytest @@ -109,19 +115,39 @@ def test_create_oo_gafter_init_code( tx_data = [ Bytes(""), ] - # Lift both entries on Amsterdam so the test still exercises its - # named scenario. With only tx_gas[1] lifted, g=0 OoG'd at CREATE - # dispatch (NEW_ACCOUNT state-gas spill) before init code ever ran — - # the assertion still passes (`NONEXISTENT` either way) but the - # failure mode is "dispatch-time OoG" instead of "OoG after init - # code". A simple `fork.oog_budget_lift(creates_before_oog=1)` (183600) - # is *too* generous and pushes g=0 past the deploy threshold; the - # Cancun 1000-gas gap between g=0 and g=1 collapses on Amsterdam - # because once dispatch is cleared, the 5-byte init code is cheap - # enough to always complete. The value below is the middle of the - # empirically-safe range (166499, 167000) where g=0 still OoGs at - # dispatch *and* g=1 just clears the deploy threshold (~221.5k). - _oog_lift = 166_750 if fork.is_eip_enabled(8037) else 0 + # The init code RETURNs a 5-byte deployed contract, so the CREATE + # frame is charged a code deposit after the init RETURN. On Cancun + # that deposit is 1000 (CODE_DEPOSIT_PER_BYTE * 5 regular) and the + # two budgets below straddle it: g0 reaches RETURN just under the + # threshold (deploy fails, account NONEXISTENT) and g1 just over + # (deploy succeeds). The 1000-gas gap between the budgets is exactly + # this Cancun deploy threshold. + # + # EIP-8037/8038 change the CREATE dispatch in two ways that the + # budget must absorb before the init code RETURNs: the new + # `create_state_gas()` spills into regular gas (empty reservoir), + # and `OPCODE_CREATE_BASE` drops from its Cancun value of 32000. + # Their sum is the net extra the dispatch consumes from the budget. + # The deposit step then changes too: its regular `CODE_DEPOSIT_PER_BYTE + # * 5` portion is now covered by the state-gas reservoir credited at + # dispatch, while `code_deposit_state_gas(code_size=5)` spills and + # must come from the forwarded gas instead. The lift restores the + # Cancun straddle by funding the net dispatch consumption plus the + # deposit's state spill, minus the regular deposit the budgets + # already carried in their 1000-gas gap. Every term is 0 + # pre-EIP-8037, so the original Cancun behavior is preserved. + gas_costs = fork.gas_costs() + _cancun_create_base = 32000 + _deploy_size = 5 + _oog_lift = 0 + if fork.is_eip_enabled(8037): + _oog_lift = ( + fork.oog_budget_lift( + creates_before_oog=1, deploy_code_size=_deploy_size + ) + + (gas_costs.OPCODE_CREATE_BASE - _cancun_create_base) + - gas_costs.CODE_DEPOSIT_PER_BYTE * _deploy_size + ) tx_gas = [54000 + _oog_lift, 55000 + _oog_lift] tx = Transaction( diff --git a/tests/ported_static/stEIP1153_transientStorage/test_14_revert_after_nested_staticcall.py b/tests/ported_static/stEIP1153_transientStorage/test_14_revert_after_nested_staticcall.py index 06ddaf015a0..739a986e972 100644 --- a/tests/ported_static/stEIP1153_transientStorage/test_14_revert_after_nested_staticcall.py +++ b/tests/ported_static/stEIP1153_transientStorage/test_14_revert_after_nested_staticcall.py @@ -3,6 +3,16 @@ Ported from: state_tests/Cancun/stEIP1153_transientStorage/14_revertAfterNestedStaticcallFiller.yml + +@manually-enhanced: Do not overwrite. The caller writes four fresh +storage slots and asserts the resulting values (slot 1's pre-marker +must be overwritten). EIP-8037/8038 spill each fresh SSTORE's +state-gas charge back into regular gas (the reservoir is empty), +pushing total consumption past the original 400 000 transaction +budget; the final SSTORE then OOGs and reverts the whole call, +leaving slot 1 at its marker. Bump the gas limit by the summed +fork-derived SSTORE increases so the success path stays funded; the +bump is exactly 0 before EIP-8037. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +42,30 @@ def test_14_revert_after_nested_staticcall( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Transient storage can't be manipulated from nested staticcall.""" + + # EIP-8037/8038 spill each fresh SSTORE's state-gas charge back into + # regular gas. The caller writes three fresh slots (0, 2, 3: each a + # cold zero -> nonzero set) and clears slot 1's cold marker; sum the + # per-slot increases so the original budget stays sufficient. Each + # term is exactly 0 before EIP-8037. + def _sstore_delta(cancun_cost: int, **metadata: int) -> int: + op = Op.SSTORE.with_metadata(**metadata) + return op.gas_cost(fork) - cancun_cost + + cold_set_delta = _sstore_delta( + 22100, key_warm=False, original_value=0, current_value=0, new_value=10 + ) + cold_clear_delta = _sstore_delta( + 5000, + key_warm=False, + original_value=65535, + current_value=65535, + new_value=0, + ) + gas_limit_bump = 3 * cold_set_delta + cold_clear_delta coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0x3635C9ADC5DEA00000) @@ -136,7 +169,7 @@ def test_14_revert_after_nested_staticcall( sender=sender, to=target, data=Bytes("f5f40590"), - gas_limit=400000, + gas_limit=400000 + gas_limit_bump, max_fee_per_gas=2000, max_priority_fee_per_gas=0, access_list=[], diff --git a/tests/ported_static/stEIP150Specific/test_suicide_to_existing_contract.py b/tests/ported_static/stEIP150Specific/test_suicide_to_existing_contract.py index f8cea302098..15bacfb9c84 100644 --- a/tests/ported_static/stEIP150Specific/test_suicide_to_existing_contract.py +++ b/tests/ported_static/stEIP150Specific/test_suicide_to_existing_contract.py @@ -3,6 +3,13 @@ Ported from: state_tests/stEIP150Specific/SuicideToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-0 CALL to a cold contract that then +SELFDESTRUCTs back to its (warm, alive) caller. EIP-8038 reprices the +cold account access of that CALL; the beneficiary is warm so the +SELFDESTRUCT is unchanged. The delta is therefore the fork's +`COLD_ACCOUNT_ACCESS - 2600`, exactly 0 before EIP-8038. """ import pytest @@ -15,6 +22,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +37,11 @@ def test_suicide_to_existing_contract( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_suicide_to_existing_contract.""" + # EIP-8038 cold account access reprice; 0 before EIP-8038. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -90,7 +101,7 @@ def test_suicide_to_existing_contract( balance=0, nonce=0, ), - target: Account(storage={1: 7637}), + target: Account(storage={1: 7637 + cold_account_delta}), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_suicide_to_not_existing_contract.py b/tests/ported_static/stEIP150Specific/test_suicide_to_not_existing_contract.py index 5a3ff77c96f..6dfee984b84 100644 --- a/tests/ported_static/stEIP150Specific/test_suicide_to_not_existing_contract.py +++ b/tests/ported_static/stEIP150Specific/test_suicide_to_not_existing_contract.py @@ -3,6 +3,14 @@ Ported from: state_tests/stEIP150Specific/SuicideToNotExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-0 CALL to a cold contract that then +SELFDESTRUCTs (with a zero balance) to a cold, non-alive beneficiary. +EIP-8038 reprices both the CALL's cold account access and the +SELFDESTRUCT beneficiary's cold access; no value is sent so there is +no new-account write. The delta is therefore twice the fork's +`COLD_ACCOUNT_ACCESS - 2600`, exactly 0 before EIP-8038. """ import pytest @@ -15,6 +23,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +38,13 @@ def test_suicide_to_not_existing_contract( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_suicide_to_not_existing_contract.""" + # EIP-8038 cold account access reprice; 0 before EIP-8038. Charged + # twice: once for the CALL target, once for the cold SELFDESTRUCT + # beneficiary. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -88,7 +102,7 @@ def test_suicide_to_not_existing_contract( balance=0, nonce=0, ), - target: Account(storage={1: 10237}), + target: Account(storage={1: 10237 + 2 * cold_account_delta}), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929.py index c62a6e1859a..8cc16c0dc46 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929.py @@ -3,6 +3,25 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/eip2929Filler.yml + +@manually-enhanced: Do not overwrite. Each parametrization runs three +operations (`oper1, oper2, oper3` from the calldata) on the same +measurement contract and stores each one's `Op.GAS` cost in slots 0, +1, 2. EIP-8038 reprices state access, so the cost of every measured +operation shifts by the (Amsterdam - Cancun) repricing of whatever +cold/warm account or storage access it performs. The access pattern, +and hence the delta, depends on what the two preceding operations +already warmed, so the deltas are computed by a small simulator +(`_slot_deltas`) that walks the operation triple while tracking the +warm state of the contract-0 account and storage slot 0x100. Each +component is built only from the fork's own gas model +(`COLD_ACCOUNT_ACCESS`, `COLD_STORAGE_ACCESS`, the EIP-8038 extra +`WARM_ACCESS` for code reads, and `Op.SSTORE` metadata costs), so +every delta is exactly 0 pre-EIP-8037 and tracks future parameter +changes. The `far*` operations call contract-1 (which does +`BALANCE(contract-0)`) or contract-2 (which does `SLOAD(0x100)`), so +they contribute the inner access's delta. Do not hardcode the +Amsterdam numbers. """ import pytest @@ -836,113 +855,201 @@ def test_eip2929( nonce=0, ) + # EIP-8038 access-repricing component deltas (each 0 pre-EIP-8037). + gas_costs = fork.gas_costs() + eip_active = fork.is_eip_enabled(8037) + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + cold_storage_delta = gas_costs.COLD_STORAGE_ACCESS - 2100 + # EIP-8038 charges an extra warm access for an EXTCODE* code read, + # on every access (cold adds it on top of the cold account cost, + # warm pays it as a second warm access). + extra_code_read = gas_costs.WARM_ACCESS if eip_active else 0 + cold_code_read_delta = cold_account_delta + extra_code_read + warm_code_read_delta = extra_code_read + + def _sstore_delta(cancun_cost: int, **metadata: int) -> int: + return Op.SSTORE.with_metadata(**metadata).gas_cost(fork) - cancun_cost + + # SSTORE 24743 -> 5 (existing nonzero slot changed to a new nonzero + # value): cold first write vs warm subsequent write. + cold_sstore_write_delta = _sstore_delta( + 5000, key_warm=False, original_value=1, current_value=1, new_value=2 + ) + warm_sstore_write_delta = _sstore_delta( + 2900, key_warm=True, original_value=1, current_value=1, new_value=2 + ) + + # Operation opcodes (from the calldata oper words). + op_nop, op_sload, op_sstore = 0x0, 0x1, 0x2 + op_balance, op_extsize, op_extcopy, op_exthash = 0xB, 0xC, 0xD, 0xE + op_call0, op_callcode0, op_deleg0, op_static0 = 0x15, 0x16, 0x17, 0x18 + op_call1, op_callcode2, op_deleg2 = 0x1F, 0x20, 0x21 + account_c0_ops = { + op_balance, + op_exthash, + op_call0, + op_callcode0, + op_deleg0, + op_static0, + } + code_read_ops = {op_extsize, op_extcopy} + inner_sload_ops = {op_sload, op_callcode2, op_deleg2} + # oper triples per data index, matching tx_data below. + oper_triples = { + 0: (op_nop, op_nop, op_nop), + 1: (op_sload, op_sload, op_sload), + 2: (op_sstore, op_sstore, op_sstore), + 3: (op_balance, op_balance, op_balance), + 4: (op_extsize, op_extsize, op_extsize), + 5: (op_extcopy, op_extcopy, op_extcopy), + 6: (op_exthash, op_exthash, op_exthash), + 7: (op_call0, op_call0, op_call0), + 8: (op_callcode0, op_callcode0, op_callcode0), + 9: (op_deleg0, op_deleg0, op_deleg0), + 10: (op_static0, op_static0, op_static0), + 11: (op_call1, op_call1, op_call1), + 12: (op_callcode2, op_callcode2, op_callcode2), + 13: (op_deleg2, op_deleg2, op_deleg2), + 14: (op_sload, op_sstore, op_sload), + 15: (op_sload, op_callcode2, op_deleg2), + 16: (op_sload, op_sstore, op_deleg2), + 17: (op_callcode2, op_sload, op_deleg2), + 18: (op_deleg2, op_sload, op_sstore), + 19: (op_balance, op_extsize, op_exthash), + 20: (op_balance, op_exthash, op_extsize), + 21: (op_extsize, op_balance, op_exthash), + 22: (op_extsize, op_exthash, op_balance), + 23: (op_exthash, op_extsize, op_balance), + 24: (op_exthash, op_balance, op_extsize), + 25: (op_call0, op_callcode0, op_call0), + 26: (op_callcode0, op_callcode0, op_call0), + 27: (op_deleg0, op_static0, op_deleg0), + 28: (op_deleg0, op_static0, op_static0), + 29: (op_balance, op_call0, op_callcode0), + 30: (op_extsize, op_call0, op_callcode0), + 31: (op_exthash, op_call0, op_callcode0), + 32: (op_balance, op_callcode0, op_call0), + 33: (op_extsize, op_callcode0, op_call0), + 34: (op_exthash, op_callcode0, op_call0), + 35: (op_balance, op_extsize, op_call1), + 36: (op_balance, op_call1, op_exthash), + 37: (op_call1, op_exthash, op_balance), + } + + def _slot_deltas(index: int) -> tuple[int, int, int]: + """ + Return the (slot0, slot1, slot2) EIP-8038 deltas for an index. + + Walk the operation triple, tracking the warm state of the + contract-0 account and storage slot 0x100 (pre-value 24743), + and accumulate the (Amsterdam - Cancun) repricing each measured + operation incurs. The `far*` calls reach a pre-warmed contract + whose body performs the inner access, so they contribute that + inner access's delta. + """ + c0_warm = False + slot_warm = False + slot_value = 24743 + out = [] + for op in oper_triples[index]: + delta = 0 + if op in inner_sload_ops: + # Direct SLOAD(0x100) or a far call whose body SLOADs it. + if not slot_warm: + delta = cold_storage_delta + slot_warm = True + elif op == op_sstore: + if slot_value != 5: + delta = ( + cold_sstore_write_delta + if not slot_warm + else warm_sstore_write_delta + ) + slot_value = 5 + slot_warm = True + elif op in code_read_ops: + delta = ( + cold_code_read_delta + if not c0_warm + else warm_code_read_delta + ) + c0_warm = True + elif op in account_c0_ops or op == op_call1: + # Account access to contract-0 (CALL1's body BALANCEs it). + if not c0_warm: + delta = cold_account_delta + c0_warm = True + out.append(delta) + return out[0], out[1], out[2] + + def _expect(index: int, base: tuple[int, int, int]) -> dict: + """Storage dict with each slot bumped by its EIP-8038 delta.""" + deltas = _slot_deltas(index) + return {i: base[i] + deltas[i] for i in range(3)} + + # Cancun-era base value of each measured slot, per data index. The + # per-index EIP-8038 delta is added by `_expect`, so each entry is a + # single data index (grouped entries with identical Cancun bases can + # still need different deltas once the access pattern differs). + base_values: dict[int, tuple[int, int, int]] = { + 1: (2090, 90, 90), + 2: (4991, 91, 91), + 3: (2590, 90, 90), + 4: (2590, 90, 90), + 5: (2597, 97, 97), + 6: (2590, 90, 90), + 7: (2608, 108, 108), + 8: (2608, 108, 108), + 9: (2605, 105, 105), + 10: (2605, 105, 105), + 11: (2711, 211, 211), + 12: (2211, 211, 211), + 13: (2208, 208, 208), + 14: (2090, 2891, 90), + 15: (2090, 211, 208), + 16: (2090, 2891, 208), + 17: (2211, 90, 208), + 18: (2208, 90, 2891), + 19: (2590, 90, 90), + 20: (2590, 90, 90), + 21: (2590, 90, 90), + 22: (2590, 90, 90), + 23: (2590, 90, 90), + 24: (2590, 90, 90), + 25: (2608, 108, 108), + 26: (2608, 108, 108), + 27: (2605, 105, 105), + 28: (2605, 105, 105), + 29: (2590, 108, 108), + 30: (2590, 108, 108), + 31: (2590, 108, 108), + 32: (2590, 108, 108), + 33: (2590, 108, 108), + 34: (2590, 108, 108), + 35: (2590, 90, 211), + 36: (2590, 211, 90), + 37: (2711, 90, 90), + } + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_3: Account(storage={0: 0})}, }, - { - "indexes": {"data": [1], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2090, 1: 90, 2: 90})}, - }, - { - "indexes": {"data": [2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 4991, 1: 91, 2: 91})}, - }, - { - "indexes": {"data": [14], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2090, 1: 2891, 2: 90})}, - }, - { - "indexes": { - "data": [3, 4, 6, 19, 20, 21, 22, 23, 24], - "gas": -1, - "value": -1, - }, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2590, 1: 90, 2: 90})}, - }, - { - "indexes": {"data": [5], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2597, 1: 97, 2: 97})}, - }, - { - "indexes": {"data": [8, 25, 26, 7], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2608, 1: 108, 2: 108})}, - }, - { - "indexes": {"data": [9, 10, 27, 28], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2605, 1: 105, 2: 105})}, - }, - { - "indexes": { - "data": [32, 33, 34, 29, 30, 31], - "gas": -1, - "value": -1, - }, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2590, 1: 108, 2: 108})}, - }, - { - "indexes": {"data": [11], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2711, 1: 211, 2: 211})}, - }, - { - "indexes": {"data": [35], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2590, 1: 90, 2: 211})}, - }, - { - "indexes": {"data": [36], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2590, 1: 211, 2: 90})}, - }, - { - "indexes": {"data": [37], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2711, 1: 90, 2: 90})}, - }, - { - "indexes": {"data": [12], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2211, 1: 211, 2: 211})}, - }, - { - "indexes": {"data": [13], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2208, 1: 208, 2: 208})}, - }, - { - "indexes": {"data": [15], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2090, 1: 211, 2: 208})}, - }, - { - "indexes": {"data": [16], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_3: Account(storage={0: 2090, 1: 2891, 2: 208}) - }, - }, - { - "indexes": {"data": [17], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2211, 1: 90, 2: 208})}, - }, - { - "indexes": {"data": [18], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 2208, 1: 90, 2: 2891})}, - }, ] + for index in sorted(base_values): + expect_entries_.append( + { + "indexes": {"data": [index], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + contract_3: Account( + storage=_expect(index, base_values[index]) + ) + }, + } + ) post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929_minus_ff.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929_minus_ff.py index 0e9ecf78d46..fbba23d97e0 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929_minus_ff.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_eip2929_minus_ff.py @@ -3,6 +3,15 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/eip2929-ffFiller.yml + +@manually-enhanced: Do not overwrite. The first expect-entry (the +`simple`/NOP case) measures a `CALL` into a contract that +`SELFDESTRUCT`s with an as-yet-untouched (cold) beneficiary. EIP-8038 +reprices the cold account access (`COLD_ACCOUNT_ACCESS`, 2600 -> 3000), +so that cost shifts by `COLD_ACCOUNT_ACCESS - 2600`, derived from the +fork's own constant so it is exactly 0 pre-EIP-8038. The other entry +pre-warms the beneficiary, so its cost is unchanged. Do not hardcode +the Amsterdam number. """ import pytest @@ -99,6 +108,8 @@ def test_eip2929_minus_ff( v: int, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" + # EIP-8038 cold account repricing (2600 -> 3000); 0 on earlier forks. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x000000000000000000000000000000000000DE57) contract_1 = Address(0x000000000000000000000000000000000000CA11) @@ -315,7 +326,11 @@ def test_eip2929_minus_ff( { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_2: Account(storage={0: 7726, 1: 105})}, + "result": { + contract_2: Account( + storage={0: 7726 + cold_account_delta, 1: 105} + ) + }, }, { "indexes": { diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py index 98b4497269a..d539c14f735 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py @@ -3,6 +3,17 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostFiller.yml + +@manually-enhanced: Do not overwrite. This crafts a one-opcode +contract, CALLs it, and stores the opcode's measured gas via `Op.GAS`. +EIP-8038 reprices state access, so four opcodes shift: `BALANCE` and +`SELFDESTRUCT` (cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), +`EXTCODESIZE` (cold account plus the extra `WARM_ACCESS` charged for +the opcode's second read of the code, +500), and `SSTORE` to a cold +fresh slot (`COLD_STORAGE_ACCESS` 2100 -> 3000, +900). `BALANCE` and +`EXTCODESIZE` share a Cancun baseline but need different deltas, so +their expect-entries are split. Every delta is derived from the fork's +own constants and is exactly 0 pre-EIP-8038; do not hardcode it. """ import pytest @@ -714,6 +725,14 @@ def test_gas_cost( v: int, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" + gas_costs = fork.gas_costs() + # EIP-8038 access repricing; each term is 0 on earlier forks. + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + cold_storage_delta = gas_costs.COLD_STORAGE_ACCESS - 2100 + # EXTCODESIZE also gains an extra warm access for its code read. + code_read_delta = cold_account_delta + ( + gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = EOA( key=0x40AC0FC28C27E961EE46EC43355A094DE205856EDBD4654CF2577C2608D4EC1E @@ -1070,10 +1089,15 @@ def test_gas_cost( }, }, { + # SSTORE to a cold fresh slot: cold storage repricing. "indexes": {"data": [39], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - addr: Account(storage=_storage_with_any({0: 700}, [1])) + addr: Account( + storage=_storage_with_any( + {0: 700 + cold_storage_delta}, [1] + ) + ) }, }, { @@ -1084,17 +1108,38 @@ def test_gas_cost( }, }, { + # SELFDESTRUCT to a cold (zero) beneficiary: cold account. "indexes": {"data": [45], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - addr: Account(storage=_storage_with_any({0: 2000}, [1])) + addr: Account( + storage=_storage_with_any( + {0: 2000 + cold_account_delta}, [1] + ) + ) + }, + }, + { + # BALANCE on a cold (zero) address: cold account. + "indexes": {"data": [23], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + addr: Account( + storage=_storage_with_any( + {0: 1300 + cold_account_delta}, [1] + ) + ) }, }, { - "indexes": {"data": [31, 23], "gas": -1, "value": -1}, + # EXTCODESIZE on a cold (zero) address: cold account plus the + # extra warm access for the opcode's code read. + "indexes": {"data": [31], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - addr: Account(storage=_storage_with_any({0: 1300}, [1])) + addr: Account( + storage=_storage_with_any({0: 1300 + code_read_delta}, [1]) + ) }, }, ] diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py index c122aab7d30..5a92e62ca28 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py @@ -3,6 +3,18 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostBerlinFiller.yml + +@manually-enhanced: Do not overwrite. This crafts a one-opcode +contract, CALLs it, and stores the opcode's measured gas minus the +data's hardcoded Cancun-era expected cost (so the net is normally 0). +EIP-8038 reprices state access, so four opcodes now exceed their old +expected cost by a fork-derived delta: `BALANCE` and `SELFDESTRUCT` +(cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), `EXTCODESIZE` +(cold account plus the extra `WARM_ACCESS` for the opcode's code read, ++500), and `SLOAD` (cold storage, `COLD_STORAGE_ACCESS` 2100 -> 3000, ++900). The stored net for those four data indices becomes that delta; +every other index stays 0. Each delta is derived from the fork's own +constants and is exactly 0 pre-EIP-8038; do not hardcode it. """ import pytest @@ -710,6 +722,23 @@ def test_gas_cost_berlin( v: int, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" + gas_costs = fork.gas_costs() + # EIP-8038 access repricing; each term is 0 on earlier forks. + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + cold_storage_delta = gas_costs.COLD_STORAGE_ACCESS - 2100 + # EXTCODESIZE also gains an extra warm access for its code read. + code_read_delta = cold_account_delta + ( + gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 + ) + # Each measured opcode subtracts its Cancun-era expected cost, so the + # net is the (Amsterdam - Cancun) repricing of the one state access + # it performs (cold address 0 / cold fresh slot), keyed by data index. + measured_delta = { + 23: cold_account_delta, # BALANCE + 31: code_read_delta, # EXTCODESIZE + 39: cold_storage_delta, # SLOAD + 45: cold_account_delta, # SELFDESTRUCT + }.get(d, 0) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xBA1A9CE0BA1A9CE) @@ -969,6 +998,6 @@ def test_gas_cost_berlin( value=tx_value[v], ) - post = {addr: Account(storage=_storage_with_any({0: 0}, [1]))} + post = {addr: Account(storage=_storage_with_any({0: measured_delta}, [1]))} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_memory.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_memory.py index aad4a36436a..ba5c6ac4b67 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_memory.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_memory.py @@ -3,6 +3,15 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostMemoryFiller.yml + +@manually-enhanced: Do not overwrite. The second expect-entry (data +36-48) stores the regular gas of a measured window that includes one +extra cold `CALL` to a previously untouched contract relative to its +baseline. EIP-8038 reprices `COLD_ACCOUNT_ACCESS` (2600 -> 3000), so +that net cost shifts by `COLD_ACCOUNT_ACCESS - 2600`, derived from the +fork's own constant so it is exactly 0 pre-EIP-8038. The first entry +measures a difference of two equal-cost operations and is unchanged. +Do not hardcode the Amsterdam number. """ import pytest @@ -495,6 +504,8 @@ def test_gas_cost_memory( v: int, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" + # EIP-8038 cold account repricing (2600 -> 3000); 0 on earlier forks. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x000000000000000000000000000000000000BA5E) contract_1 = Address(0x000000000000000000000000000000000010BA5E) @@ -846,7 +857,9 @@ def test_gas_cost_memory( "value": -1, }, "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 1900})}, + "result": { + contract_3: Account(storage={0: 1900 + cold_account_delta}) + }, }, ] diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_gas.py index c75b732ee35..4a23ef5d079 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_gas.py @@ -3,6 +3,17 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/RawExtCodeCopyGasFiller.json + +@manually-enhanced: Do not overwrite. This measures the regular gas +that a single cold `EXTCODECOPY` consumes via `Op.GAS`. EIP-8038 +reprices the cold account access (`COLD_ACCOUNT_ACCESS`, 2600 -> 3000) +and charges an extra `WARM_ACCESS` for the opcode's second read (the +code). The stored cost therefore shifts by +`(COLD_ACCOUNT_ACCESS - 2600) + WARM_ACCESS`. The cold term comes from +the fork's own constant; the extra warm term is gated on the +`is_eip_enabled(8037)` flag (the registered flag that activates the +repricing at Amsterdam), so the delta is exactly 0 on earlier forks. +Do not hardcode it. """ import pytest @@ -15,6 +26,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +41,17 @@ def test_raw_ext_code_copy_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_raw_ext_code_copy_gas.""" + gas_costs = fork.gas_costs() + # EIP-8038: cold account repricing plus the extra warm access charged + # for the opcode's second read (the code). Both terms are 0 before + # EIP-8038, so the stored cost is unchanged on earlier forks. + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + code_read_delta = cold_account_delta + ( + gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -68,6 +89,6 @@ def test_raw_ext_code_copy_gas( gas_limit=600000, ) - post = {target: Account(storage={1: 2629})} + post = {target: Account(storage={1: 2629 + code_read_delta})} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_memory_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_memory_gas.py index a03431f4f6c..d16eef9ae8f 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_memory_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_copy_memory_gas.py @@ -3,6 +3,17 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/RawExtCodeCopyMemoryGasFiller.json + +@manually-enhanced: Do not overwrite. This measures the regular gas +that a single cold `EXTCODECOPY` (with memory expansion) consumes via +`Op.GAS`. EIP-8038 reprices the cold account access +(`COLD_ACCOUNT_ACCESS`, 2600 -> 3000) and charges an extra +`WARM_ACCESS` for the opcode's second read (the code). The stored cost +therefore shifts by `(COLD_ACCOUNT_ACCESS - 2600) + WARM_ACCESS`. The +cold term comes from the fork's own constant; the extra warm term is +gated on the `is_eip_enabled(8037)` flag (the registered flag that +activates the repricing at Amsterdam), so the delta is exactly 0 on +earlier forks. Do not hardcode it. """ import pytest @@ -15,6 +26,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +43,17 @@ def test_raw_ext_code_copy_memory_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_raw_ext_code_copy_memory_gas.""" + gas_costs = fork.gas_costs() + # EIP-8038: cold account repricing plus the extra warm access charged + # for the opcode's second read (the code). Both terms are 0 before + # EIP-8038, so the stored cost is unchanged on earlier forks. + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + code_read_delta = cold_account_delta + ( + gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -72,6 +93,6 @@ def test_raw_ext_code_copy_memory_gas( gas_limit=600000, ) - post = {target: Account(storage={1: 4948})} + post = {target: Account(storage={1: 4948 + code_read_delta})} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_size_gas.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_size_gas.py index 4da7798cbdc..cc1641db59f 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_size_gas.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_raw_ext_code_size_gas.py @@ -3,6 +3,17 @@ Ported from: state_tests/stEIP150singleCodeGasPrices/RawExtCodeSizeGasFiller.json + +@manually-enhanced: Do not overwrite. This measures the regular gas +that a single cold `EXTCODESIZE` consumes via `Op.GAS`. EIP-8038 +reprices the cold account access (`COLD_ACCOUNT_ACCESS`, 2600 -> 3000) +and charges an extra `WARM_ACCESS` for the opcode's second read (the +code). The stored cost therefore shifts by +`(COLD_ACCOUNT_ACCESS - 2600) + WARM_ACCESS`. The cold term comes from +the fork's own constant; the extra warm term is gated on the +`is_eip_enabled(8037)` flag (the registered flag that activates the +repricing at Amsterdam), so the delta is exactly 0 on earlier forks. +Do not hardcode it. """ import pytest @@ -15,6 +26,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +41,17 @@ def test_raw_ext_code_size_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_raw_ext_code_size_gas.""" + gas_costs = fork.gas_costs() + # EIP-8038: cold account repricing plus the extra warm access charged + # for the opcode's second read (the code). Both terms are 0 before + # EIP-8038, so the stored cost is unchanged on earlier forks. + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + code_read_delta = cold_account_delta + ( + gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -68,6 +89,6 @@ def test_raw_ext_code_size_gas( gas_limit=600000, ) - post = {target: Account(storage={1: 2616})} + post = {target: Account(storage={1: 2616 + code_read_delta})} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide.py b/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide.py index d99dcdf7185..b9be146f25c 100644 --- a/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide.py +++ b/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide.py @@ -3,6 +3,15 @@ Ported from: state_tests/stEIP158Specific/CALL_OneVCallSuicideFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a CALL with value to a not-yet-accessed contract that +SELFDESTRUCTs to the (alive) caller. EIP-8038 reprices the cold account +access (2600 -> 3000) and the CALL value transfer (9000 -> 10300); the +beneficiary stays alive so there is no new-account write. The delta is +`(COLD_ACCOUNT_ACCESS - 2600) + (CALL_VALUE - 9000)`, exactly 0 before +EIP-8037 and tracks parameter changes; do not hardcode the Amsterdam +value. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +39,14 @@ def test_call_one_v_call_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_one_v_call_suicide.""" + # EIP-8038 deltas, each 0 before EIP-8037. The CALL pays a cold + # account access plus a value transfer; the beneficiary stays alive. + gas_costs = fork.gas_costs() + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + call_value_delta = gas_costs.CALL_VALUE - 9000 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -84,7 +100,10 @@ def test_call_one_v_call_suicide( post = { addr: Account(storage={}, balance=0), - target: Account(storage={100: 14337}, balance=100), + target: Account( + storage={100: 14337 + cold_account_delta + call_value_delta}, + balance=100, + ), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide2.py b/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide2.py index 2c222ca68f4..ad7c1dcc8ed 100644 --- a/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide2.py +++ b/tests/ported_static/stEIP158Specific/test_call_one_v_call_suicide2.py @@ -3,6 +3,15 @@ Ported from: state_tests/stEIP158Specific/CALL_OneVCallSuicide2Filler.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-1 CALL to a cold contract that then +SELFDESTRUCTs (with a zero balance) to a cold, alive beneficiary. +EIP-8038 reprices the CALL's cold account access and value transfer, +plus the SELFDESTRUCT beneficiary's cold access; the beneficiary is +alive so there is no new-account write. The delta is therefore +`2 * (COLD_ACCOUNT_ACCESS - 2600) + (CALL_VALUE - 9000)`, exactly 0 +before EIP-8038. """ import pytest @@ -16,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -30,8 +40,15 @@ def test_call_one_v_call_suicide2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_one_v_call_suicide2.""" + # EIP-8038 deltas, each 0 before EIP-8038. The CALL pays the cold + # account reprice and the value-transfer reprice; the cold + # SELFDESTRUCT beneficiary pays a second cold account reprice. + gas_costs = fork.gas_costs() + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + call_value_delta = gas_costs.CALL_VALUE - 9000 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) addr_2 = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) sender = EOA( @@ -90,7 +107,10 @@ def test_call_one_v_call_suicide2( post = { addr: Account(storage={}, balance=0), - target: Account(storage={100: 16937}, balance=99), + target: Account( + storage={100: 16937 + 2 * cold_account_delta + call_value_delta}, + balance=99, + ), addr_2: Account(balance=1), } diff --git a/tests/ported_static/stEIP158Specific/test_call_zero_v_call_suicide.py b/tests/ported_static/stEIP158Specific/test_call_zero_v_call_suicide.py index 901902b9a5b..6dbef1bc4d7 100644 --- a/tests/ported_static/stEIP158Specific/test_call_zero_v_call_suicide.py +++ b/tests/ported_static/stEIP158Specific/test_call_zero_v_call_suicide.py @@ -3,6 +3,13 @@ Ported from: state_tests/stEIP158Specific/CALL_ZeroVCallSuicideFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-0 CALL to a cold contract that then +SELFDESTRUCTs back to its (warm, alive) caller. EIP-8038 reprices the +cold account access of that CALL; the beneficiary is warm so the +SELFDESTRUCT is unchanged. The delta is therefore the fork's +`COLD_ACCOUNT_ACCESS - 2600`, exactly 0 before EIP-8038. """ import pytest @@ -15,6 +22,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +37,11 @@ def test_call_zero_v_call_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_zero_v_call_suicide.""" + # EIP-8038 cold account access reprice; 0 before EIP-8038. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -83,7 +94,7 @@ def test_call_zero_v_call_suicide( post = { addr: Account(balance=0), - target: Account(storage={100: 7637}), + target: Account(storage={100: 7637 + cold_account_delta}), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP158Specific/test_extcodesize_to_epmty_paris.py b/tests/ported_static/stEIP158Specific/test_extcodesize_to_epmty_paris.py index 35e01d26d30..8437cc2a5fb 100644 --- a/tests/ported_static/stEIP158Specific/test_extcodesize_to_epmty_paris.py +++ b/tests/ported_static/stEIP158Specific/test_extcodesize_to_epmty_paris.py @@ -3,6 +3,15 @@ Ported from: state_tests/stEIP158Specific/EXTCODESIZE_toEpmtyParisFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of an EXTCODESIZE on a cold (empty, code-less) EOA plus +the SSTORE that clears a populated slot to its (zero) result. +EIP-8038 reprices the cold account access and adds a second +WARM_ACCESS for the code read (EXTCODESIZE delta), and spills the +cold SSTORE-clear's state-gas into regular gas (the reservoir is +empty). Both deltas are derived from the fork's own opcode gas model, +so each is exactly 0 before EIP-8038. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +39,22 @@ def test_extcodesize_to_epmty_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_extcodesize_to_epmty_paris.""" + # EIP-8038 deltas, each 0 before EIP-8038. EXTCODESIZE gains the + # cold account reprice plus a second WARM_ACCESS for the code read; + # the cold SSTORE-clear (nonzero -> 0) spills its state-gas back + # into regular gas. + extcodesize_delta = ( + Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 + ) + cold_clear_sstore_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + - 5000 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -64,7 +88,9 @@ def test_extcodesize_to_epmty_paris( post = { addr: Account(storage={}, code=b"", balance=10, nonce=0), - target: Account(storage={100: 7617}), + target: Account( + storage={100: 7617 + extcodesize_delta + cold_clear_sstore_delta} + ), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP158Specific/test_extcodesize_to_non_existent.py b/tests/ported_static/stEIP158Specific/test_extcodesize_to_non_existent.py index 66b98387b28..5f597f0b42e 100644 --- a/tests/ported_static/stEIP158Specific/test_extcodesize_to_non_existent.py +++ b/tests/ported_static/stEIP158Specific/test_extcodesize_to_non_existent.py @@ -3,6 +3,14 @@ Ported from: state_tests/stEIP158Specific/EXTCODESIZE_toNonExistentFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of an EXTCODESIZE on a cold, non-existent address plus the +SSTORE that stores its (zero) result. EIP-8038 reprices the cold +account access and adds a second WARM_ACCESS for the code read +(EXTCODESIZE delta), and reprices the cold value-unchanged SSTORE. +Both deltas are derived from the fork's own opcode gas model, so each +is exactly 0 before EIP-8038. """ import pytest @@ -16,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -30,8 +39,21 @@ def test_extcodesize_to_non_existent( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_extcodesize_to_non_existent.""" + # EIP-8038 deltas, each 0 before EIP-8038. EXTCODESIZE gains the + # cold account reprice plus a second WARM_ACCESS for the code read; + # the cold value-unchanged SSTORE gains its own reprice. + extcodesize_delta = ( + Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 + ) + cold_noop_sstore_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=0, current_value=0, new_value=0 + ).gas_cost(fork) + - 2200 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) sender = EOA( @@ -75,7 +97,9 @@ def test_extcodesize_to_non_existent( Address( 0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B ): Account.NONEXISTENT, - contract_0: Account(storage={100: 4817}), + contract_0: Account( + storage={100: 4817 + extcodesize_delta + cold_noop_sstore_delta} + ), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP2930/test_address_opcodes.py b/tests/ported_static/stEIP2930/test_address_opcodes.py index 2a55b6043b5..38279bf7408 100644 --- a/tests/ported_static/stEIP2930/test_address_opcodes.py +++ b/tests/ported_static/stEIP2930/test_address_opcodes.py @@ -3,6 +3,18 @@ Ported from: state_tests/stEIP2930/addressOpcodesFiller.yml + +@manually-enhanced: Do not overwrite. The contract measures, via +`Op.GAS`, the regular gas of each account-touching opcode (`BALANCE`, +`EXTCODESIZE`, `EXTCODEHASH`, `EXTCODECOPY`) on both a first (cold or +pre-warmed) and a second (warm) access. EIP-8038 reprices these: cold +`BALANCE`/`EXTCODEHASH` by +`COLD_ACCOUNT_ACCESS - 2600`, while +`EXTCODESIZE`/`EXTCODECOPY` carry an extra flat surcharge on both their +warm and cold forms. The single Cancun-era literals are therefore split +per opcode and per access, each adjusted by that opcode's own warm or +cold `(Amsterdam - Cancun)` cost delta taken from the fork gas model, so +every value is exactly 0 before EIP-8038 and tracks future parameter +changes; do not hardcode the Amsterdam numbers. """ import pytest @@ -21,7 +33,7 @@ from execution_testing.specs.static_state.expect_section import ( resolve_expect_post, ) -from execution_testing.vm import Op +from execution_testing.vm import Op, Opcode REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" @@ -545,65 +557,154 @@ def test_address_opcodes( address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) + # Per-opcode warm and cold cost deltas versus Cancun, derived from + # the fork gas model so each is exactly 0 before EIP-8038. The + # EXTCODECOPY metadata mirrors the measured access (a 0x20-byte copy + # into already-expanded memory) so only the account-access component + # varies across forks. `BALANCE`/`EXTCODEHASH` warm forms are + # unchanged; `EXTCODESIZE`/`EXTCODECOPY` gain a flat warm surcharge. + extcodecopy_meta = dict( + data_size=0x20, new_memory_size=0x120, old_memory_size=0x120 + ) + + def _account_delta(op: Opcode, warm: bool, base: int, **meta: int) -> int: + cost = op.with_metadata(address_warm=warm, **meta).gas_cost(fork) + return cost - base + + balance_warm_d = _account_delta(Op.BALANCE, True, 100) + balance_cold_d = _account_delta(Op.BALANCE, False, 2600) + extcodesize_warm_d = _account_delta(Op.EXTCODESIZE, True, 100) + extcodesize_cold_d = _account_delta(Op.EXTCODESIZE, False, 2600) + extcodehash_warm_d = _account_delta(Op.EXTCODEHASH, True, 100) + extcodehash_cold_d = _account_delta(Op.EXTCODEHASH, False, 2600) + extcodecopy_warm_d = _account_delta( + Op.EXTCODECOPY, True, 103, **extcodecopy_meta + ) + extcodecopy_cold_d = _account_delta( + Op.EXTCODECOPY, False, 2603, **extcodecopy_meta + ) + + # Slot 0 holds the first access (pre-warmed in the valid cases, cold + # in the invalid cases); slot 1 holds the always-warm second access. expect_entries_: list[dict] = [ + # valid (pre-warmed first access): both slots measure a warm + # access; only EXTCODESIZE/EXTCODECOPY shift. { "indexes": { - "data": [ - 0, - 1, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - ], + "data": [0, 1, 4, 5, 6, 7, 8, 9, 10, 11], + "gas": -1, + "value": -1, + }, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 97 + balance_warm_d, + 1: 97 + balance_warm_d, + } + ) + }, + }, + { + "indexes": { + "data": [12, 13, 16, 17, 18, 19, 20, 21, 22, 23], + "gas": -1, + "value": -1, + }, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 97 + extcodesize_warm_d, + 1: 97 + extcodesize_warm_d, + } + ) + }, + }, + { + "indexes": { + "data": [24, 25, 28, 29, 30, 31, 32, 33, 34, 35], "gas": -1, "value": -1, }, "network": [">=Cancun"], - "result": {contract_0: Account(storage={0: 97, 1: 97})}, + "result": { + contract_0: Account( + storage={ + 0: 97 + extcodehash_warm_d, + 1: 97 + extcodehash_warm_d, + } + ) + }, }, { "indexes": { - "data": [2, 3, 14, 15, 26, 27, 38, 39], + "data": [36, 37, 40, 41, 42, 43, 44, 45, 46, 47], "gas": -1, "value": -1, }, "network": [">=Cancun"], - "result": {contract_0: Account(storage={0: 2597, 1: 97, 2: 0})}, + "result": { + contract_0: Account( + storage={ + 0: 97 + extcodecopy_warm_d, + 1: 97 + extcodecopy_warm_d, + } + ) + }, + }, + # invalid (cold first access): slot 0 cold, slot 1 warm. + { + "indexes": {"data": [2, 3], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 2597 + balance_cold_d, + 1: 97 + balance_warm_d, + 2: 0, + } + ) + }, + }, + { + "indexes": {"data": [14, 15], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 2597 + extcodesize_cold_d, + 1: 97 + extcodesize_warm_d, + 2: 0, + } + ) + }, + }, + { + "indexes": {"data": [26, 27], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 2597 + extcodehash_cold_d, + 1: 97 + extcodehash_warm_d, + 2: 0, + } + ) + }, + }, + { + "indexes": {"data": [38, 39], "gas": -1, "value": -1}, + "network": [">=Cancun"], + "result": { + contract_0: Account( + storage={ + 0: 2597 + extcodecopy_cold_d, + 1: 97 + extcodecopy_warm_d, + 2: 0, + } + ) + }, }, ] diff --git a/tests/ported_static/stEIP2930/test_coinbase_t01.py b/tests/ported_static/stEIP2930/test_coinbase_t01.py index b6c08afc361..754f52c4628 100644 --- a/tests/ported_static/stEIP2930/test_coinbase_t01.py +++ b/tests/ported_static/stEIP2930/test_coinbase_t01.py @@ -3,6 +3,14 @@ Ported from: state_tests/stEIP2930/coinbaseT01Filler.yml + +@manually-enhanced: Do not overwrite. The target contract measures, via +`Op.GAS`, the regular gas of a `CALL` that transfers value to the warm, +already-existing coinbase. EIP-8038 reprices the value-transfer +component (`CALL_VALUE` 9 000 -> 10 300), so the measurement grows by +`gas_costs.CALL_VALUE - 9000`. That delta is derived from the fork's +own gas model, so it is exactly 0 before EIP-8038 and tracks future +parameter changes; do not hardcode the Amsterdam number. """ import pytest @@ -111,16 +119,22 @@ def test_coinbase_t01( nonce=1, ) + # EIP-8038 reprices the value-transfer component of `CALL`; with the + # coinbase warm and already in state, the measured gas grows by the + # `CALL_VALUE` reprice alone. Derived from the fork gas model so it + # is 0 before EIP-8038. + call_value_delta = fork.gas_costs().CALL_VALUE - 9000 + expect_entries_: list[dict] = [ { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {target: Account(storage={0: 6800})}, + "result": {target: Account(storage={0: 6800 + call_value_delta})}, }, { "indexes": {"data": [0, 2], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {target: Account(storage={0: 6800})}, + "result": {target: Account(storage={0: 6800 + call_value_delta})}, }, ] diff --git a/tests/ported_static/stEIP2930/test_coinbase_t2.py b/tests/ported_static/stEIP2930/test_coinbase_t2.py index ea96482ef8b..5e8924e0b17 100644 --- a/tests/ported_static/stEIP2930/test_coinbase_t2.py +++ b/tests/ported_static/stEIP2930/test_coinbase_t2.py @@ -3,6 +3,14 @@ Ported from: state_tests/stEIP2930/coinbaseT2Filler.yml + +@manually-enhanced: Do not overwrite. The target contract measures, via +`Op.GAS`, the regular gas of a `CALL` that transfers value to the warm, +already-existing coinbase. EIP-8038 reprices the value-transfer +component (`CALL_VALUE` 9 000 -> 10 300), so the measurement grows by +`gas_costs.CALL_VALUE - 9000`. That delta is derived from the fork's +own gas model, so it is exactly 0 before EIP-8038 and tracks future +parameter changes; do not hardcode the Amsterdam number. """ import pytest @@ -105,16 +113,22 @@ def test_coinbase_t2( nonce=1, ) + # EIP-8038 reprices the value-transfer component of `CALL`; with the + # coinbase warm and already in state, the measured gas grows by the + # `CALL_VALUE` reprice alone. Derived from the fork gas model so it + # is 0 before EIP-8038. + call_value_delta = fork.gas_costs().CALL_VALUE - 9000 + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {target: Account(storage={0: 6800})}, + "result": {target: Account(storage={0: 6800 + call_value_delta})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {target: Account(storage={0: 6800})}, + "result": {target: Account(storage={0: 6800 + call_value_delta})}, }, ] diff --git a/tests/ported_static/stEIP2930/test_manual_create.py b/tests/ported_static/stEIP2930/test_manual_create.py index 5ca118c9646..6bf26e1228a 100644 --- a/tests/ported_static/stEIP2930/test_manual_create.py +++ b/tests/ported_static/stEIP2930/test_manual_create.py @@ -6,12 +6,13 @@ @manually-enhanced: Do not overwrite. The three parametrizations of this test measure regular gas around a fresh SSTORE-set inside a -CREATE-deployed contract. EIP-8037 splits the Cancun-era SSTORE-set -base into a smaller regular portion plus 37 568 state-gas; with an -empty reservoir the full state-gas spills into regular gas and -`Op.GAS` reads +20 468 = 37 568 - 17 100 compared to Cancun. Bake -that delta into both `[">=Cancun"]` expect entries fork-conditionally -via `Op.SSTORE(new_value=1).state_cost(fork) - 17100`. +CREATE-deployed contract. EIP-8037 moves the bulk of the SSTORE-set +cost into a per-storage state-gas charge; with an empty reservoir it +spills back into regular gas, which `Op.GAS` observes. Derive the +warm and cold fresh-set deltas from the fork's own gas model so each +is exactly 0 pre-EIP-8037 and tracks parameter changes; bake the +warm delta into the declared-key entry and the cold delta into the +undeclared-key entries. """ import pytest @@ -90,12 +91,18 @@ def test_manual_create( pre[sender] = Account(balance=0x1000000000000000000, nonce=1) - # EIP-8037 SSTORE-set spillover: +20 468 regular gas per fresh set - # when the reservoir is empty. - sstore_set_delta = ( - (Op.SSTORE(new_value=1).state_cost(fork) - 17100) - if fork.is_eip_enabled(8037) - else 0 + # EIP-8037 SSTORE-set spill into regular gas (empty reservoir). + # Derive the warm and cold fresh-set deltas from the fork's own + # gas model so each is exactly 0 pre-EIP-8037. + def _sstore_delta(cancun_cost: int, **metadata: int) -> int: + op = Op.SSTORE.with_metadata(**metadata) + return op.gas_cost(fork) - cancun_cost + + warm_set_delta = _sstore_delta( + 20000, key_warm=True, current_value=0, new_value=2 + ) + cold_set_delta = _sstore_delta( + 22100, key_warm=False, current_value=0, new_value=2 ) expect_entries_: list[dict] = [ @@ -104,7 +111,7 @@ def test_manual_create( "network": [">=Cancun"], "result": { compute_create_address(address=sender, nonce=1): Account( - storage={0: 20008 + sstore_set_delta, 1: 106} + storage={0: 20008 + warm_set_delta, 1: 106} ), }, }, @@ -113,7 +120,7 @@ def test_manual_create( "network": [">=Cancun"], "result": { compute_create_address(address=sender, nonce=1): Account( - storage={0: 22108 + sstore_set_delta, 1: 106} + storage={0: 22108 + cold_set_delta, 1: 106} ), }, }, diff --git a/tests/ported_static/stEIP2930/test_storage_costs.py b/tests/ported_static/stEIP2930/test_storage_costs.py index 03bb03dd189..5e588d94abe 100644 --- a/tests/ported_static/stEIP2930/test_storage_costs.py +++ b/tests/ported_static/stEIP2930/test_storage_costs.py @@ -4,19 +4,19 @@ Ported from: state_tests/stEIP2930/storageCostsFiller.yml -@manually-enhanced: Do not overwrite. The SSTORE gas measurements in -this test were authored against the Cancun-era SSTORE-set base cost -of 20 000 (per EIP-2200). EIP-8037 splits that cost into a smaller -regular portion (~2 900) plus a per-storage state-gas charge of -`STATE_BYTES_PER_STORAGE_SET (32) * COST_PER_STATE_BYTE (1174) = -37 568`. When the state-gas reservoir is empty — as it is here, since -the tests don't pre-allocate state-gas budget — the full state-gas -spills back into regular gas, so `Op.GAS` observes -`+37 568 - 17 100 = +20 468` regular gas per fresh SSTORE-set -compared to Cancun. Bake that fork-conditional delta into the -expected post-state values for the 10 parametrizations whose measured -SSTORE writes triggered the spill; the remaining entries (SLOAD-only, -no-op SSTOREs) are unaffected. +@manually-enhanced: Do not overwrite. This test measures the regular +gas consumed by storage accesses via `Op.GAS`. EIP-8037 moves the +bulk of storage-write cost into a per-storage state-gas charge; with +an empty state-gas reservoir (these tests pre-allocate none) the full +state gas spills back into regular gas, so each measurement shifts by +its `(Amsterdam - Cancun)` cost delta. Six access classes shift: warm +and cold fresh SSTORE-sets (state-gas spill dominates), warm and cold +SSTORE writes to existing slots (clear/reset: the storage-write +component), cold value-unchanged SSTOREs, and cold SLOADs (the +`COLD_STORAGE_ACCESS` repricing). Warm reads and no-op SSTOREs are +unchanged. Each delta below is derived from the fork's own opcode gas +model, so it is exactly 0 pre-EIP-8037 and tracks future parameter +changes; do not hardcode the Amsterdam numbers. """ import pytest @@ -661,113 +661,150 @@ def test_storage_costs( address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) - # EIP-8037 splits the SSTORE-set base cost (Cancun: 20 000 regular) - # into a smaller regular portion plus per-storage state-gas. When - # the state-gas reservoir is empty for these tests, the full state - # gas spills into regular gas, so Op.GAS sees +20 468 per fresh - # SSTORE-set compared to Cancun (=37 568 state-gas - 17 100 base - # regular drop). Apply that delta to the 10 measurements that - # trigger a fresh-set spill; the SLOAD-only and no-op SSTORE - # entries below are unchanged. - sstore_set_delta = ( - (Op.SSTORE(new_value=1).state_cost(fork) - 17100) - if fork.is_eip_enabled(8037) - else 0 + # EIP-8037 moves the bulk of storage-write cost into a per-storage + # state-gas charge. These tests pre-allocate no state-gas reservoir, + # so the full state gas spills back into regular gas and `Op.GAS` + # observes each measured SSTORE/SLOAD at its combined regular + state + # cost. Every measured access therefore shifts by its + # (Amsterdam - Cancun) delta; derive each delta from the fork's own + # opcode gas model so it is exactly 0 pre-EIP-8037 and tracks future + # parameter changes. The subtracted Cancun-era pure costs are frozen + # historical values. + def _sstore_delta(cancun_cost: int, **metadata: int) -> int: + op = Op.SSTORE.with_metadata(**metadata) + return op.gas_cost(fork) - cancun_cost + + d_warm_set = _sstore_delta( + 20000, key_warm=True, original_value=0, current_value=0, new_value=2 + ) + d_cold_set = _sstore_delta( + 22100, key_warm=False, original_value=0, current_value=0, new_value=2 + ) + d_warm_write = _sstore_delta( + 2900, key_warm=True, original_value=1, current_value=1, new_value=2 + ) + d_cold_write = _sstore_delta( + 5000, key_warm=False, original_value=1, current_value=1, new_value=2 + ) + d_cold_noop = _sstore_delta( + 2200, key_warm=False, original_value=1, current_value=1, new_value=1 ) + d_cold_read = fork.gas_costs().COLD_STORAGE_ACCESS - 2100 expect_entries_: list[dict] = [ + # declaredKeyWrite: warm fresh SSTORE-set. { "indexes": {"data": [0, 35], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_0: Account( - storage={0: 2, 1: 20003 + sstore_set_delta} - ) + contract_0: Account(storage={0: 2, 1: 20003 + d_warm_set}) }, }, + # undeclaredKeyWrite: cold fresh SSTORE-set. { "indexes": {"data": [6, 12, 18], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_0: Account( - storage={0: 2, 1: 22103 + sstore_set_delta} - ) + contract_0: Account(storage={0: 2, 1: 22103 + d_cold_set}) }, }, + # declaredKeyUpdate: warm SSTORE-reset (nonzero -> nonzero). { "indexes": {"data": [3], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 48879, 1: 2903})}, + "result": { + contract_3: Account(storage={0: 48879, 1: 2903 + d_warm_write}) + }, }, + # undeclaredKeyUpdate: cold SSTORE-reset (nonzero -> nonzero). { "indexes": {"data": [9, 15, 21], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_3: Account(storage={0: 48879, 1: 5003})}, + "result": { + contract_3: Account(storage={0: 48879, 1: 5003 + d_cold_write}) + }, }, + # declaredKeyNOP: warm value-unchanged SSTORE (no write). { "indexes": {"data": [4], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_4: Account(storage={0: 24743, 1: 103})}, }, + # undeclaredKeyNOP: cold value-unchanged SSTORE. { "indexes": {"data": [10, 16, 22], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_4: Account(storage={0: 24743, 1: 2203})}, + "result": { + contract_4: Account(storage={0: 24743, 1: 2203 + d_cold_noop}) + }, }, + # declaredKeyNOP0: warm value-unchanged SSTORE (no write). { "indexes": {"data": [5], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_5: Account(storage={1: 103})}, }, + # undeclaredKeyNOP0: cold value-unchanged SSTORE. { "indexes": {"data": [11, 17, 23], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_5: Account(storage={1: 2203})}, + "result": {contract_5: Account(storage={1: 2203 + d_cold_noop})}, }, + # declaredKeyDel: warm SSTORE-clear (nonzero -> 0). { "indexes": {"data": [2], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_2: Account(storage={0: 0, 1: 2903})}, + "result": { + contract_2: Account(storage={0: 0, 1: 2903 + d_warm_write}) + }, }, + # undeclaredKeyDel: cold SSTORE-clear (nonzero -> 0). { "indexes": {"data": [8, 14, 20], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_2: Account(storage={0: 0, 1: 5003})}, + "result": { + contract_2: Account(storage={0: 0, 1: 5003 + d_cold_write}) + }, }, + # declaredKeyRead: warm SLOAD. { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_1: Account(storage={1: 100})}, }, + # undeclaredKeyRead: cold SLOAD. { "indexes": {"data": [7, 13, 19], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_1: Account(storage={1: 2100})}, + "result": {contract_1: Account(storage={1: 2100 + d_cold_read})}, }, + # postSSTORE write: key already warm/dirty, no fresh-set spill. { "indexes": {"data": [24, 25], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_6: Account(storage={0: 2, 1: 103})}, }, + # postSSTORE read: key already warm. { "indexes": {"data": [26, 27], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_7: Account(storage={0: 24743, 1: 100})}, }, + # postSLOAD write: SLOAD warms the key, then warm fresh SSTORE-set. { "indexes": {"data": [28, 29], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_8: Account( - storage={0: 2, 1: 20000 + sstore_set_delta} - ) + contract_8: Account(storage={0: 2, 1: 20000 + d_warm_set}) }, }, + # postSLOAD read: key already warm. { "indexes": {"data": [30, 31], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": {contract_9: Account(storage={1: 97})}, }, + # declaredTo: warm SLOAD (slot 1) + warm fresh SSTORE-set (slot 2). { "indexes": {"data": [32], "gas": -1, "value": -1}, "network": [">=Cancun"], @@ -776,12 +813,13 @@ def test_storage_costs( storage={ 0: 2, 1: 100, - 2: 20000 + sstore_set_delta, + 2: 20000 + d_warm_set, 24743: 57005, } ) }, }, + # undeclaredTo: cold SLOAD (slot 1) + cold fresh SSTORE-set (slot 2). { "indexes": {"data": [33, 34], "gas": -1, "value": -1}, "network": [">=Cancun"], @@ -789,8 +827,8 @@ def test_storage_costs( contract_10: Account( storage={ 0: 2, - 1: 2100, - 2: 22100 + sstore_set_delta, + 1: 2100 + d_cold_read, + 2: 22100 + d_cold_set, 24743: 57005, } ), diff --git a/tests/ported_static/stEIP2930/test_varied_context.py b/tests/ported_static/stEIP2930/test_varied_context.py index 3c78c50a4c9..705de655138 100644 --- a/tests/ported_static/stEIP2930/test_varied_context.py +++ b/tests/ported_static/stEIP2930/test_varied_context.py @@ -4,21 +4,21 @@ Ported from: state_tests/stEIP2930/variedContextFiller.yml -@manually-enhanced: Do not overwrite. 28 parametrizations of this -test measure gas consumption around SSTORE/CALL/SELFDESTRUCT in -various access-list contexts. EIP-8037 splits the Cancun-era base -costs (SSTORE-set 20 000, CALL-new-account 25 000, SELFDESTRUCT-new- -beneficiary 25 000) into smaller regular portions plus per-storage -or per-new-account state-gas charges. When the reservoir is empty — -the case here, since no state-gas budget is pre-allocated — the -full state-gas spills back into regular gas and Op.GAS reads three -distinct deltas: - +20 468 per fresh SSTORE-set - +106 488 per NEW_ACCOUNT (CALL with value or SELFDESTRUCT) - +126 956 = both, for SELFDESTRUCT-with-write paths -Each affected post-state literal is bumped by the appropriate -delta fork-conditionally; pre-EIP-8037 forks use the original -values. +@manually-enhanced: Do not overwrite. This test measures gas +consumption around SSTORE/CALL/SELFDESTRUCT in various access-list +contexts via `Op.GAS`. EIP-8037/8038 reprice several components; +with an empty reservoir (the case here) the state-gas portion +spills back into regular gas, so each measurement shifts by its +`(Amsterdam - Cancun)` delta. Every delta below is derived from the +fork's own opcode gas model, so it is exactly 0 pre-EIP-8037 and +tracks parameter changes: warm/cold fresh SSTORE-sets, the +NEW_ACCOUNT spill for CALL-with-value and SELFDESTRUCT-to-non-alive +(the latter also gaining `ACCOUNT_WRITE` and the cold reprice), and +the cold account/storage access reprices. The `*ValidGas` +parametrizations instead forward a fixed in-bytecode gas budget that +EIP-8038 made insufficient; those budgets are bumped by the inner +SSTORE-write increase so the success path stays funded while the +under-funded cold path still runs out of gas. """ import pytest @@ -1152,9 +1152,24 @@ def test_varied_context( # { ; WRITE_INVALID_OOG WRITE_VALID_NO_OOG # (call 0x0B65 0xF114 0 0 0 0 0x20) # } + # EIP-8038 raises the inner SSTORE-write cost. Bump the "valid" gas + # these callers forward by exactly that increase so their success + # path stays funded at Amsterdam (preserving the original Cancun + # margin) while the under-funded cold "invalid" path still OOGs. + # The contract_13 inner SSTORE is a warm reset; contract_15's is a + # cold reset. Both bumps are 0 pre-EIP-8037. + _warm_reset = Op.SSTORE.with_metadata( + key_warm=True, original_value=1, current_value=1, new_value=2 + ) + _cold_reset = Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=2 + ) + valid_write_gas = 0xB65 + (_warm_reset.gas_cost(fork) - 2900) + valid_read_gas = 0x1800 + (_cold_reset.gas_cost(fork) - 5000) + contract_13 = pre.deploy_contract( # noqa: F841 code=Op.CALL( - gas=0xB65, + gas=valid_write_gas, address=0xF114, value=0x0, args_offset=0x0, @@ -1173,7 +1188,7 @@ def test_varied_context( # } contract_15 = pre.deploy_contract( # noqa: F841 code=Op.CALL( - gas=0x1800, + gas=valid_read_gas, address=0xF115, value=0x0, args_offset=0x0, @@ -1337,24 +1352,41 @@ def test_varied_context( address=Address(0x0000000000000000000000000000000000001016), # noqa: E501 ) - # EIP-8037 splits SSTORE-set, NEW_ACCOUNT call value transfer, and - # SELFDESTRUCT new-beneficiary base costs into state-gas portions. - # With an empty reservoir (the case here), the full state-gas - # spills into regular gas, which Op.GAS observes. - # sstore-set spill: +37 568 - 17 100 = +20 468 per fresh set - # new-account spill: +131 488 - 25 000 = +106 488 per CALL - # with value to a non-alive account, and - # per SELFDESTRUCT to non-alive beneficiary - # suicide-write spill: +126 956 = both deltas combined - sstore_set_delta = ( - (Op.SSTORE(new_value=1).state_cost(fork) - 17100) - if fork.is_eip_enabled(8037) - else 0 + # EIP-8037/8038 reprice several access components. With an empty + # reservoir (the case here) the state-gas portion spills back into + # regular gas, which `Op.GAS` observes. Derive each delta from the + # fork's own gas model so it is exactly 0 pre-EIP-8037 and tracks + # parameter changes. + gas_costs = fork.gas_costs() + + def _sstore_delta(cancun_cost: int, **metadata: int) -> int: + op = Op.SSTORE.with_metadata(**metadata) + return op.gas_cost(fork) - cancun_cost + + # Fresh SSTORE-set (state-gas spill dominates), warm vs cold key. + warm_set_delta = _sstore_delta( + 20000, key_warm=True, current_value=0, new_value=2 + ) + cold_set_delta = _sstore_delta( + 22100, key_warm=False, current_value=0, new_value=2 ) + # CALL value transfer to a non-alive account: the 25 000 NEW_ACCOUNT + # base becomes a spilling state-gas charge. new_account_delta = ( (fork.create_state_gas() - 25000) if fork.is_eip_enabled(8037) else 0 ) - suicide_write_delta = sstore_set_delta + new_account_delta + # Cold account access reprice (0 pre-Amsterdam). + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + # Cold storage access (SLOAD) reprice (0 pre-Amsterdam). + cold_storage_delta = gas_costs.COLD_STORAGE_ACCESS - 2100 + # SELFDESTRUCT to a non-alive cold beneficiary: new-account spill, + # the new ACCOUNT_WRITE charge (0 pre-Amsterdam), and the cold + # reprice. + suicide_new_delta = ( + new_account_delta + gas_costs.ACCOUNT_WRITE + cold_account_delta + ) + # callWriteSuicide measures a warm SSTORE-set then that SELFDESTRUCT. + suicide_write_delta = warm_set_delta + suicide_new_delta expect_entries_: list[dict] = [ { @@ -1362,7 +1394,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_0: Account( - storage={0: 2, 1: (20003 + sstore_set_delta), 2: 107} + storage={0: 2, 1: (20003 + warm_set_delta), 2: 107} ) }, }, @@ -1371,7 +1403,11 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_0: Account( - storage={0: 2, 1: (22103 + sstore_set_delta), 2: 2107} + storage={ + 0: 2, + 1: (22103 + cold_set_delta), + 2: 2107 + cold_storage_delta, + } ) }, }, @@ -1380,7 +1416,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_2: Account( - storage={0: 2, 1: (20003 + sstore_set_delta), 2: 107} + storage={0: 2, 1: (20003 + warm_set_delta), 2: 107} ) }, }, @@ -1389,7 +1425,11 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_2: Account( - storage={0: 2, 1: (22103 + sstore_set_delta), 2: 2107} + storage={ + 0: 2, + 1: (22103 + cold_set_delta), + 2: 2107 + cold_storage_delta, + } ) }, }, @@ -1400,8 +1440,8 @@ def test_varied_context( contract_3: Account( storage={ 0: 2, - 1: (22103 + sstore_set_delta), - 2: 2107, + 1: (22103 + cold_set_delta), + 2: 2107 + cold_storage_delta, 24743: 57005, } ) @@ -1414,7 +1454,7 @@ def test_varied_context( contract_3: Account( storage={ 0: 2, - 1: (20003 + sstore_set_delta), + 1: (20003 + warm_set_delta), 2: 107, 24743: 57005, } @@ -1424,7 +1464,9 @@ def test_varied_context( { "indexes": {"data": [6], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_4: Account(storage={0: 2107})}, + "result": { + contract_4: Account(storage={0: 2107 + cold_storage_delta}) + }, }, { "indexes": {"data": [7], "gas": -1, "value": -1}, @@ -1436,7 +1478,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_26: Account( - storage={0: (20003 + sstore_set_delta), 1: 100} + storage={0: (20003 + warm_set_delta), 1: 100} ) }, }, @@ -1445,7 +1487,10 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_26: Account( - storage={0: (22103 + sstore_set_delta), 1: 2100} + storage={ + 0: (22103 + cold_set_delta), + 1: 2100 + cold_storage_delta, + } ) }, }, @@ -1460,21 +1505,37 @@ def test_varied_context( "indexes": {"data": [11], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_7: Account(storage={0: (24601 + suicide_write_delta)}) + contract_7: Account( + storage={ + 0: ( + 24601 + + cold_set_delta + + suicide_new_delta + + cold_account_delta + ) + } + ) }, }, { "indexes": {"data": [12], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_9: Account(storage={0: 100 + new_account_delta}) + contract_9: Account(storage={0: 100 + suicide_new_delta}) }, }, { "indexes": {"data": [13], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - contract_9: Account(storage={0: 4600 + new_account_delta}) + contract_9: Account( + storage={ + 0: 4600 + + suicide_new_delta + + cold_account_delta + + cold_storage_delta + } + ) }, }, { @@ -1524,7 +1585,7 @@ def test_varied_context( 268: 103, 269: 103, 270: 103, - 271: (20003 + sstore_set_delta), + 271: (20003 + warm_set_delta), 512: 100, 513: 100, 514: 100, @@ -1541,22 +1602,22 @@ def test_varied_context( 525: 100, 526: 100, 527: 100, - 768: (20003 + sstore_set_delta), - 769: (20003 + sstore_set_delta), - 770: (20003 + sstore_set_delta), - 771: (20003 + sstore_set_delta), - 772: (20003 + sstore_set_delta), - 773: (20003 + sstore_set_delta), - 774: (20003 + sstore_set_delta), - 775: (20003 + sstore_set_delta), - 776: (20003 + sstore_set_delta), - 777: (20003 + sstore_set_delta), - 778: (20003 + sstore_set_delta), - 779: (20003 + sstore_set_delta), - 780: (20003 + sstore_set_delta), - 781: (20003 + sstore_set_delta), - 782: (20003 + sstore_set_delta), - 783: (20003 + sstore_set_delta), + 768: (20003 + warm_set_delta), + 769: (20003 + warm_set_delta), + 770: (20003 + warm_set_delta), + 771: (20003 + warm_set_delta), + 772: (20003 + warm_set_delta), + 773: (20003 + warm_set_delta), + 774: (20003 + warm_set_delta), + 775: (20003 + warm_set_delta), + 776: (20003 + warm_set_delta), + 777: (20003 + warm_set_delta), + 778: (20003 + warm_set_delta), + 779: (20003 + warm_set_delta), + 780: (20003 + warm_set_delta), + 781: (20003 + warm_set_delta), + 782: (20003 + warm_set_delta), + 783: (20003 + warm_set_delta), 1024: 100, 1025: 100, 1026: 100, @@ -1617,7 +1678,7 @@ def test_varied_context( 268: 103, 269: 103, 270: 103, - 271: (22103 + sstore_set_delta), + 271: (22103 + cold_set_delta), 512: 100, 513: 100, 514: 100, @@ -1633,39 +1694,39 @@ def test_varied_context( 524: 100, 525: 100, 526: 100, - 527: 2100, - 768: (22103 + sstore_set_delta), - 769: (22103 + sstore_set_delta), - 770: (22103 + sstore_set_delta), - 771: (22103 + sstore_set_delta), - 772: (22103 + sstore_set_delta), - 773: (22103 + sstore_set_delta), - 774: (22103 + sstore_set_delta), - 775: (22103 + sstore_set_delta), - 776: (22103 + sstore_set_delta), - 777: (22103 + sstore_set_delta), - 778: (22103 + sstore_set_delta), - 779: (22103 + sstore_set_delta), - 780: (22103 + sstore_set_delta), - 781: (22103 + sstore_set_delta), - 782: (22103 + sstore_set_delta), - 783: (22103 + sstore_set_delta), - 1024: 2100, - 1025: 2100, - 1026: 2100, - 1027: 2100, - 1028: 2100, - 1029: 2100, - 1030: 2100, - 1031: 2100, - 1032: 2100, - 1033: 2100, - 1034: 2100, - 1035: 2100, - 1036: 2100, - 1037: 2100, - 1038: 2100, - 1039: 2100, + 527: 2100 + cold_storage_delta, + 768: (22103 + cold_set_delta), + 769: (22103 + cold_set_delta), + 770: (22103 + cold_set_delta), + 771: (22103 + cold_set_delta), + 772: (22103 + cold_set_delta), + 773: (22103 + cold_set_delta), + 774: (22103 + cold_set_delta), + 775: (22103 + cold_set_delta), + 776: (22103 + cold_set_delta), + 777: (22103 + cold_set_delta), + 778: (22103 + cold_set_delta), + 779: (22103 + cold_set_delta), + 780: (22103 + cold_set_delta), + 781: (22103 + cold_set_delta), + 782: (22103 + cold_set_delta), + 783: (22103 + cold_set_delta), + 1024: 2100 + cold_storage_delta, + 1025: 2100 + cold_storage_delta, + 1026: 2100 + cold_storage_delta, + 1027: 2100 + cold_storage_delta, + 1028: 2100 + cold_storage_delta, + 1029: 2100 + cold_storage_delta, + 1030: 2100 + cold_storage_delta, + 1031: 2100 + cold_storage_delta, + 1032: 2100 + cold_storage_delta, + 1033: 2100 + cold_storage_delta, + 1034: 2100 + cold_storage_delta, + 1035: 2100 + cold_storage_delta, + 1036: 2100 + cold_storage_delta, + 1037: 2100 + cold_storage_delta, + 1038: 2100 + cold_storage_delta, + 1039: 2100 + cold_storage_delta, 24743: 57005, 48879: 2, 61440: 48879, @@ -1693,7 +1754,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_18, nonce=0): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta)} + storage={0: 65535, 1: (20017 + warm_set_delta)} ), }, }, @@ -1702,7 +1763,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_18, nonce=0): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta)} + storage={0: 65535, 1: (22117 + cold_set_delta)} ), }, }, @@ -1711,7 +1772,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0xD82F21135ED7D7D833A9F2A0F1CF6C3DA214B8E3): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta)} + storage={0: 65535, 1: (20017 + warm_set_delta)} ), }, }, @@ -1720,7 +1781,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0xD82F21135ED7D7D833A9F2A0F1CF6C3DA214B8E3): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta)} + storage={0: 65535, 1: (22117 + cold_set_delta)} ), }, }, @@ -1729,7 +1790,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_20, nonce=0): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta)} + storage={0: 65535, 1: (20017 + warm_set_delta)} ), }, }, @@ -1738,7 +1799,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_20, nonce=0): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta)} + storage={0: 65535, 1: (22117 + cold_set_delta)} ), }, }, @@ -1747,7 +1808,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0x530508498D2AA75D8E591612809FEC3D37A45615): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta)} + storage={0: 65535, 1: (20017 + warm_set_delta)} ), }, }, @@ -1756,7 +1817,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0x530508498D2AA75D8E591612809FEC3D37A45615): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta)} + storage={0: 65535, 1: (22117 + cold_set_delta)} ), }, }, @@ -1765,7 +1826,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_22, nonce=0): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta), 2: 117} + storage={0: 65535, 1: (20017 + warm_set_delta), 2: 117} ), }, }, @@ -1774,7 +1835,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { compute_create_address(address=contract_22, nonce=0): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta), 2: 117} + storage={0: 65535, 1: (22117 + cold_set_delta), 2: 117} ), }, }, @@ -1783,7 +1844,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0x83FBDAE70258AC0FA837B701CC63CEDF48D4B6BF): Account( - storage={0: 65535, 1: (20017 + sstore_set_delta), 2: 117} + storage={0: 65535, 1: (20017 + warm_set_delta), 2: 117} ), }, }, @@ -1792,7 +1853,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { Address(0x83FBDAE70258AC0FA837B701CC63CEDF48D4B6BF): Account( - storage={0: 65535, 1: (22117 + sstore_set_delta), 2: 117} + storage={0: 65535, 1: (22117 + cold_set_delta), 2: 117} ), }, }, @@ -1801,7 +1862,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_25: Account( - storage={0: 24743, 1: (20017 + sstore_set_delta), 2: 117} + storage={0: 24743, 1: (20017 + warm_set_delta), 2: 117} ) }, }, @@ -1810,7 +1871,7 @@ def test_varied_context( "network": [">=Cancun"], "result": { contract_25: Account( - storage={0: 24743, 1: (22117 + sstore_set_delta), 2: 117} + storage={0: 24743, 1: (22117 + cold_set_delta), 2: 117} ) }, }, diff --git a/tests/ported_static/stMemoryTest/test_oog.py b/tests/ported_static/stMemoryTest/test_oog.py index 5c2f1cc4f01..cd47deb6687 100644 --- a/tests/ported_static/stMemoryTest/test_oog.py +++ b/tests/ported_static/stMemoryTest/test_oog.py @@ -3,6 +3,15 @@ Ported from: state_tests/stMemoryTest/oogFiller.yml + +@manually-enhanced: Do not overwrite. Each parametrization forwards a +fixed in-bytecode gas budget to an inner operation and asserts whether +it succeeds. The `0x3E` (RETURNDATACOPY) success case routes through a +nested value-0 CALL to a cold contract; EIP-8038's cold account access +reprice consumes the budget's slack and OOGs the copy. Bump only that +budget by the fork-derived `COLD_ACCOUNT_ACCESS - 2600` so the success +path stays funded; the value is exactly 0 before EIP-8038 and all +other budgets are untouched. """ import pytest @@ -297,6 +306,11 @@ def test_oog( v: int, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" + # EIP-8038 cold account access reprice; 0 before EIP-8038. The + # `0x3E` RETURNDATACOPY success case forwards just enough gas for a + # nested CALL to a cold contract plus the copy; the reprice eats the + # slack, so add it back to that one budget. + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x0000000000000000000000000000000000010020) contract_1 = Address(0x0000000000000000000000000000000000010037) @@ -753,7 +767,7 @@ def test_oog( Bytes("1a8451e6") + Hash(0x3C) + Hash(0xFFFF), Bytes("1a8451e6") + Hash(0x3C) + Hash(0x2BC), Bytes("1a8451e6") + Hash(0x3E) + Hash(0xFFFF), - Bytes("1a8451e6") + Hash(0x3E) + Hash(0xC02), + Bytes("1a8451e6") + Hash(0x3E) + Hash(0xC02 + cold_account_delta), Bytes("1a8451e6") + Hash(0x3E) + Hash(0x7D0), Bytes("1a8451e6") + Hash(0x3E) + Hash(0xC01), Bytes("1a8451e6") + Hash(0x51) + Hash(0xFFFF), diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_non_non_zero_balance.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_non_non_zero_balance.py index 462c93ae2ae..5df9cd42733 100644 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_non_non_zero_balance.py +++ b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_call_to_non_non_zero_balance.py @@ -3,6 +3,15 @@ Ported from: state_tests/stNonZeroCallsTest/NonZeroValue_CALL_ToNonNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-1 CALL to a cold, alive EOA plus the SSTORE +storing the (success) result. EIP-8038 reprices the CALL's cold +account access and value transfer, and reprices the cold +value-unchanged SSTORE. The delta is therefore +`(COLD_ACCOUNT_ACCESS - 2600) + (CALL_VALUE - 9000)` plus the cold +SSTORE reprice, each derived from the fork and exactly 0 before +EIP-8038. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +41,24 @@ def test_non_zero_value_call_to_non_non_zero_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_non_zero_value_call_to_non_non_zero_balance.""" + # EIP-8038 deltas, each 0 before EIP-8038. The CALL pays the cold + # account reprice and the value-transfer reprice; the cold + # value-unchanged SSTORE gains its own reprice. + gas_costs = fork.gas_costs() + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + call_value_delta = gas_costs.CALL_VALUE - 9000 + cold_noop_sstore_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=0, current_value=0, new_value=0 + ).gas_cost(fork) + - 2200 + ) + call_measure_delta = ( + cold_account_delta + call_value_delta + cold_noop_sstore_delta + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -76,7 +102,7 @@ def test_non_zero_value_call_to_non_non_zero_balance( post = { addr: Account(balance=100), - target: Account(storage={100: 11535}), + target: Account(storage={100: 11535 + call_measure_delta}), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_non_non_zero_balance.py b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_non_non_zero_balance.py index 20ecf378e6b..a55087af42b 100644 --- a/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_non_non_zero_balance.py +++ b/tests/ported_static/stNonZeroCallsTest/test_non_zero_value_callcode_to_non_non_zero_balance.py @@ -3,6 +3,15 @@ Ported from: state_tests/stNonZeroCallsTest/NonZeroValue_CALLCODE_ToNonNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. The measured slot captures the +regular gas of a value-1 CALLCODE to a cold, alive EOA plus the SSTORE +storing the (success) result. EIP-8038 reprices the CALLCODE's cold +account access and value transfer, and reprices the cold +value-unchanged SSTORE. The delta is therefore +`(COLD_ACCOUNT_ACCESS - 2600) + (CALL_VALUE - 9000)` plus the cold +SSTORE reprice, each derived from the fork and exactly 0 before +EIP-8038. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +41,24 @@ def test_non_zero_value_callcode_to_non_non_zero_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_non_zero_value_callcode_to_non_non_zero_balance.""" + # EIP-8038 deltas, each 0 before EIP-8038. The CALLCODE pays the + # cold account reprice and the value-transfer reprice; the cold + # value-unchanged SSTORE gains its own reprice. + gas_costs = fork.gas_costs() + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + call_value_delta = gas_costs.CALL_VALUE - 9000 + cold_noop_sstore_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=0, current_value=0, new_value=0 + ).gas_cost(fork) + - 2200 + ) + call_measure_delta = ( + cold_account_delta + call_value_delta + cold_noop_sstore_delta + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xE8D4A51000) @@ -76,7 +102,7 @@ def test_non_zero_value_callcode_to_non_non_zero_balance( post = { addr: Account(balance=100), - target: Account(storage={100: 11535}), + target: Account(storage={100: 11535 + call_measure_delta}), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stPreCompiledContracts/test_precomps_eip2929_cancun.py b/tests/ported_static/stPreCompiledContracts/test_precomps_eip2929_cancun.py index 27dbb54b3af..cbd90e4a2a0 100644 --- a/tests/ported_static/stPreCompiledContracts/test_precomps_eip2929_cancun.py +++ b/tests/ported_static/stPreCompiledContracts/test_precomps_eip2929_cancun.py @@ -8,12 +8,14 @@ test measure the regular gas consumed by a CALL with value to an inactive precompile address. EIP-8037 replaces the Cancun-era CALL_NEW_ACCOUNT cost of 25 000 with a per-new-account state-gas -charge of `STATE_BYTES_PER_NEW_ACCOUNT (112) * COST_PER_STATE_BYTE -(1174) = 131 488`. With an empty reservoir (the case here), the -full state-gas spills back into regular gas, so `Op.GAS` reads -+106 488 (= 131 488 - 25 000) compared to Cancun. Bake that delta -into the two affected `[">=Cancun"]` expect-entries fork-condition- -ally; the third entry is gated to `["Cancun"]` only and unchanged. +charge that, with an empty reservoir (the case here), spills back +into regular gas; EIP-8038 also reprices the cold account access +from 2 600 to 3 000. `Op.GAS` therefore reads +`fork.create_state_gas() - 25 000 + COLD_ACCOUNT_ACCESS - 2 600` +extra regular gas compared to Cancun. Derive that delta from the +fork so it is 0 pre-EIP-8037 and tracks parameter changes; bake it +into the two affected `[">=Cancun"]` expect-entries. The third +entry is gated to `["Cancun"]` only and unchanged. """ import pytest @@ -3591,12 +3593,18 @@ def test_precomps_eip2929_cancun( nonce=1, ) - # EIP-8037 replaces the 25 000 CALL_NEW_ACCOUNT base cost with a - # 131 488 state-gas charge. With an empty reservoir the full - # state-gas spills into regular gas, so Op.GAS reads +106 488. + # These measurements isolate repriced components applied per + # expect-entry below: EIP-8037 replaces the 25 000 CALL_NEW_ACCOUNT + # base cost with a state-gas charge that, with an empty reservoir, + # spills back into regular gas; EIP-8038 separately reprices a cold + # account access from 2 600 to 3 000. Derive both from the fork so + # they are 0 pre-EIP-8037 and track parameter changes. `new` + # entries shift by the account delta, `no` entries by the cold + # delta, and `all` entries by both. new_account_delta = ( (fork.create_state_gas() - 25000) if fork.is_eip_enabled(8037) else 0 ) + cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 expect_entries_: list[dict] = [ { @@ -4060,7 +4068,9 @@ def test_precomps_eip2929_cancun( "value": -1, }, "network": [">=Cancun"], - "result": {target: Account(storage={0: 0, 1: 2500})}, + "result": { + target: Account(storage={0: 0, 1: 2500 + cold_account_delta}) + }, }, { "indexes": { @@ -4248,7 +4258,12 @@ def test_precomps_eip2929_cancun( }, "network": [">=Cancun"], "result": { - target: Account(storage={0: 0, 1: 27500 + new_account_delta}) + target: Account( + storage={ + 0: 0, + 1: 27500 + new_account_delta + cold_account_delta, + } + ) }, }, { diff --git a/tests/ported_static/stRefundTest/test_refund50_1.py b/tests/ported_static/stRefundTest/test_refund50_1.py index 1135b777d09..0a631f155bb 100644 --- a/tests/ported_static/stRefundTest/test_refund50_1.py +++ b/tests/ported_static/stRefundTest/test_refund50_1.py @@ -3,6 +3,16 @@ Ported from: state_tests/stRefundTest/refund50_1Filler.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract clears five cold storage slots; EIP-8038 raises each cold +SSTORE-clear charge from 5000 to 13000. The EIP-3529 refund cap +(`gas_used // 5`) binds at both forks (the clear refunds far exceed a +fifth of gas used), so the extra charge raises `gas_used` by exactly +four fifths of itself. Derive the per-clear charge delta from the fork +gas model (0 pre-EIP-8037) and subtract `gas_price * 5 * delta * 4 // 5` +from the Cancun balance; do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +40,7 @@ def test_refund50_1( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund50_1.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -65,10 +77,20 @@ def test_refund50_1( gas_limit=100000, ) + # EIP-8038 raises each cold SSTORE-clear charge; with the EIP-3529 + # refund cap binding, gas_used rises by 4/5 of the extra charge. + cold_clear_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + - 5000 + ) + extra_gas_used = 5 * cold_clear_delta * 4 // 5 + post = { target: Account(storage={}), coinbase: Account(balance=0), - sender: Account(balance=0x92F810, nonce=1), + sender: Account(balance=0x92F810 - 10 * extra_gas_used, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_call_a_not_enough_gas_in_call.py b/tests/ported_static/stRefundTest/test_refund_call_a_not_enough_gas_in_call.py index def38bc9f7b..eaf1f5a8c97 100644 --- a/tests/ported_static/stRefundTest/test_refund_call_a_not_enough_gas_in_call.py +++ b/tests/ported_static/stRefundTest/test_refund_call_a_not_enough_gas_in_call.py @@ -3,6 +3,16 @@ Ported from: state_tests/stRefundTest/refund_CallA_notEnoughGasInCallFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The inner +CALL is starved of gas so its SSTORE clear always reverts (no refund +survives); the only surviving repricing is in the outer frame, where +EIP-8038 raises the cold account-access charged by the CALL (2600 -> +3000) and the cold no-op SSTORE of slot 0 (2200 -> 3000). Derive both +deltas from the fork gas model (0 pre-EIP-8037) and subtract +`gas_price * (call_access_delta + outer_sstore_delta)` from the Cancun +balance; do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +40,7 @@ def test_refund_call_a_not_enough_gas_in_call( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_call_a_not_enough_gas_in_call.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -81,10 +93,24 @@ def test_refund_call_a_not_enough_gas_in_call( value=10, ) + # The inner SSTORE clear always reverts (gas-starved), so its refund + # never survives. Only the outer frame reprices under EIP-8038: the + # cold account access charged by the CALL and the cold no-op SSTORE + # of slot 0 (original == current == new == 0). + gas_costs = fork.gas_costs() + call_access_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + outer_sstore_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=0, current_value=0, new_value=0 + ).gas_cost(fork) + - 2200 + ) + gas_used_delta = call_access_delta + outer_sstore_delta + post = { target: Account(storage={1: 1}, balance=0xDE0B6B3A764000A), coinbase: Account(balance=0), - sender: Account(balance=0xA8DF4, nonce=1), + sender: Account(balance=0xA8DF4 - 10 * gas_used_delta, nonce=1), addr: Account(storage={1: 1}), } diff --git a/tests/ported_static/stRefundTest/test_refund_change_non_zero_storage.py b/tests/ported_static/stRefundTest/test_refund_change_non_zero_storage.py index b82dcefae41..800ea8835ea 100644 --- a/tests/ported_static/stRefundTest/test_refund_change_non_zero_storage.py +++ b/tests/ported_static/stRefundTest/test_refund_change_non_zero_storage.py @@ -3,6 +3,16 @@ Ported from: state_tests/stRefundTest/refund_changeNonZeroStorageFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract resets one warm-after-cold storage slot from a non-zero value +to another non-zero value (1 -> 23); EIP-8038 raises this cold +SSTORE-reset charge from 5000 to 13000. There is no storage-clear +refund, so `gas_used` rises by exactly the charge delta. Derive that +delta from the fork gas model (0 pre-EIP-8037) and subtract +`gas_price * delta` from the Cancun balance; do not hardcode the +Amsterdam value. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +40,7 @@ def test_refund_change_non_zero_storage( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_change_non_zero_storage.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -61,10 +73,19 @@ def test_refund_change_non_zero_storage( value=10, ) + # EIP-8038 raises the cold SSTORE-reset charge (non-zero to non-zero); + # with no storage-clear refund, gas_used rises by the full delta. + cold_reset_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=23 + ).gas_cost(fork) + - 5000 + ) + post = { target: Account(storage={1: 23}, balance=0xDE0B6B3A764000A), coinbase: Account(balance=0), - sender: Account(balance=0x3C2F689A, nonce=1), + sender: Account(balance=0x3C2F689A - 10 * cold_reset_delta, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_ff.py b/tests/ported_static/stRefundTest/test_refund_ff.py index 06c1e9435f2..3de243ac410 100644 --- a/tests/ported_static/stRefundTest/test_refund_ff.py +++ b/tests/ported_static/stRefundTest/test_refund_ff.py @@ -3,6 +3,16 @@ Ported from: state_tests/stRefundTest/refundFFFiller.yml + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract self-destructs and sends its (zero) balance to a cold, already +existing beneficiary; EIP-8038 raises the cold account-access surcharge +from 2600 to 3000. No positive balance is moved, so no `ACCOUNT_WRITE` +applies and there is no refund, so `gas_used` rises by exactly the +SELFDESTRUCT charge delta. Derive that delta from the fork gas model +(0 pre-EIP-8037) and subtract `gas_price * delta` from the Cancun +balance; do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +40,7 @@ def test_refund_ff( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -64,6 +76,16 @@ def test_refund_ff( access_list=[], ) - post = {sender: Account(balance=0xE8D4A51000)} + # EIP-8038 raises the cold account-access surcharge applied by + # SELFDESTRUCT; with no balance transfer and no refund, gas_used + # rises by exactly this charge delta. + selfdestruct_delta = ( + Op.SELFDESTRUCT.with_metadata( + address_warm=False, account_new=False + ).gas_cost(fork) + - 7600 + ) + + post = {sender: Account(balance=0xE8D4A51000 - 1000 * selfdestruct_delta)} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_get_ether_back.py b/tests/ported_static/stRefundTest/test_refund_get_ether_back.py index 81633765711..88385a01af7 100644 --- a/tests/ported_static/stRefundTest/test_refund_get_ether_back.py +++ b/tests/ported_static/stRefundTest/test_refund_get_ether_back.py @@ -3,6 +3,18 @@ Ported from: state_tests/stRefundTest/refund_getEtherBackFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract clears one cold storage slot (1 -> 0); EIP-8038 raises the cold +SSTORE-clear charge from 5000 to 13000 and the storage-clear refund from +4800 to 12480. The EIP-3529 refund cap (`gas_used // 5`) does not bind at +Cancun but does at Amsterdam, so the shift is modeled from the fork gas +model: reconstruct the cap-bounded `gas_used` from the fork-invariant +non-SSTORE gross gas plus the fork SSTORE charge minus the capped refund, +and subtract the same expression evaluated with the pre-repricing Cancun +charges (so the adjustment is exactly 0 pre-EIP-8037). Do not hardcode +the Amsterdam value. """ import pytest @@ -15,6 +27,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +42,7 @@ def test_refund_get_ether_back( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_get_ether_back.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -61,10 +75,30 @@ def test_refund_get_ether_back( value=10, ) + # Gas used = gross gas minus the capped storage-clear refund. The + # non-SSTORE gross gas is fork-invariant: the intrinsic transaction + # cost plus the two PUSH1s that feed the single SSTORE (STOP is free). + gas_costs = fork.gas_costs() + base_gross = gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + + def clear_gas_used(sstore_charge: int, clear_refund: int) -> int: + gross = base_gross + sstore_charge + return gross - min(clear_refund, gross // 5) + + sstore_charge = Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + # Cancun charges 5000 for the clear and refunds 4800; subtracting the + # same model evaluated at those constants makes this exactly 0 before + # the EIP-8037/8038 repricing. + gas_used_delta = clear_gas_used( + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR + ) - clear_gas_used(5000, 4800) + post = { target: Account(storage={}, balance=0xDE0B6B3A764000A), coinbase: Account(balance=0), - sender: Account(balance=0x3CF4376A, nonce=1), + sender: Account(balance=0x3CF4376A - 10 * gas_used_delta, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_max.py b/tests/ported_static/stRefundTest/test_refund_max.py index 4924bc77fa2..0270feee167 100644 --- a/tests/ported_static/stRefundTest/test_refund_max.py +++ b/tests/ported_static/stRefundTest/test_refund_max.py @@ -3,6 +3,16 @@ Ported from: state_tests/stRefundTest/refundMaxFiller.yml + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract clears eight cold storage slots; EIP-8038 raises each cold +SSTORE-clear charge from 5000 to 13000. The EIP-3529 refund cap +(`gas_used // 5`) binds at both forks (the clear refunds far exceed a +fifth of gas used), so the extra charge raises `gas_used` by exactly +four fifths of itself. Derive the per-clear charge delta from the fork +gas model (0 pre-EIP-8037) and subtract `gas_price * 8 * delta * 4 // 5` +from the Cancun balance; do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +25,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +40,7 @@ def test_refund_max( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -95,6 +107,16 @@ def test_refund_max( access_list=[], ) - post = {sender: Account(balance=0xE8D55F7E90)} + # EIP-8038 raises each cold SSTORE-clear charge; with the EIP-3529 + # refund cap binding, gas_used rises by 4/5 of the extra charge. + cold_clear_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + - 5000 + ) + extra_gas_used = 8 * cold_clear_delta * 4 // 5 + + post = {sender: Account(balance=0xE8D55F7E90 - 1000 * extra_gas_used)} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_no_oog_1.py b/tests/ported_static/stRefundTest/test_refund_no_oog_1.py index 79eb43dc804..764d3b6dc5d 100644 --- a/tests/ported_static/stRefundTest/test_refund_no_oog_1.py +++ b/tests/ported_static/stRefundTest/test_refund_no_oog_1.py @@ -3,6 +3,17 @@ Ported from: state_tests/stRefundTest/refund_NoOOG_1Filler.json + +@manually-enhanced: Do not overwrite. The transaction supplies exactly +enough gas to clear one cold storage slot (1 -> 0) and no more (the "no +out-of-gas" boundary). EIP-8038 raises the cold SSTORE-clear charge from +5000 to 13000, so the gas limit must rise by that charge delta to keep +the slot clearing instead of running out of gas. The asserted sender +balance equals its start minus `gas_used * gas_price`, and `gas_used` +is the gross gas minus the storage-clear refund (capped by EIP-3529 only +at Amsterdam). Both the gas limit bump and the balance shift are derived +from the fork gas model and are exactly 0 pre-EIP-8037; do not hardcode +the Amsterdam values. """ import pytest @@ -15,6 +26,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +41,7 @@ def test_refund_no_oog_1( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_no_oog_1.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -53,17 +66,42 @@ def test_refund_no_oog_1( nonce=0, ) + # EIP-8038 raises the cold SSTORE-clear charge; bump the gas limit by + # the charge delta so the clear still lands exactly at the limit + # (the "no out-of-gas" boundary) instead of running out of gas. + sstore_charge = Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + cold_clear_delta = sstore_charge - 5000 + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=26006, + gas_limit=26006 + cold_clear_delta, ) + # Gas used = gross gas minus the capped storage-clear refund. The + # non-SSTORE gross gas is fork-invariant: the intrinsic transaction + # cost plus the two PUSH1s that feed the single SSTORE (STOP is free). + gas_costs = fork.gas_costs() + base_gross = gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + + def clear_gas_used(charge: int, clear_refund: int) -> int: + gross = base_gross + charge + return gross - min(clear_refund, gross // 5) + + # Cancun charges 5000 for the clear and refunds 4800; subtracting the + # same model evaluated at those constants makes this exactly 0 before + # the EIP-8037/8038 repricing. + gas_used_delta = clear_gas_used( + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR + ) - clear_gas_used(5000, 4800) + post = { target: Account(storage={}), coinbase: Account(balance=0), - sender: Account(balance=0x9D0314, nonce=1), + sender: Account(balance=0x9D0314 - 10 * gas_used_delta, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_sstore.py b/tests/ported_static/stRefundTest/test_refund_sstore.py index 9a8cd6f7ddc..c93b742e994 100644 --- a/tests/ported_static/stRefundTest/test_refund_sstore.py +++ b/tests/ported_static/stRefundTest/test_refund_sstore.py @@ -3,6 +3,18 @@ Ported from: state_tests/stRefundTest/refundSSTOREFiller.yml + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which equals its start minus `gas_used * gas_price`. The +contract clears one cold storage slot (non-zero -> 0); EIP-8038 raises +the cold SSTORE-clear charge from 5000 to 13000 and the storage-clear +refund from 4800 to 12480. The EIP-3529 refund cap (`gas_used // 5`) does +not bind at Cancun but does at Amsterdam, so the shift is modeled from +the fork gas model: reconstruct the cap-bounded `gas_used` from the +fork-invariant non-SSTORE gross gas plus the fork SSTORE charge minus the +capped refund, and subtract the same expression evaluated with the +pre-repricing Cancun charges (so the adjustment is exactly 0 +pre-EIP-8037). Do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +27,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +42,7 @@ def test_refund_sstore( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Ori Pomerantz qbzzt1@gmail.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -65,6 +79,29 @@ def test_refund_sstore( access_list=[], ) - post = {sender: Account(balance=0xE8D4EE4E00)} + # Gas used = gross gas minus the capped storage-clear refund. The + # non-SSTORE gross gas is fork-invariant: the intrinsic transaction + # cost (including the single zero data byte) plus the PUSH1 and DUP1 + # that feed the SSTORE (STOP is free). + gas_costs = fork.gas_costs() + base_gross = ( + gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + gas_costs.TX_DATA_PER_ZERO + ) + + def clear_gas_used(sstore_charge: int, clear_refund: int) -> int: + gross = base_gross + sstore_charge + return gross - min(clear_refund, gross // 5) + + sstore_charge = Op.SSTORE.with_metadata( + key_warm=False, original_value=24743, current_value=24743, new_value=0 + ).gas_cost(fork) + # Cancun charges 5000 for the clear and refunds 4800; subtracting the + # same model evaluated at those constants makes this exactly 0 before + # the EIP-8037/8038 repricing. + gas_used_delta = clear_gas_used( + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR + ) - clear_gas_used(5000, 4800) + + post = {sender: Account(balance=0xE8D4EE4E00 - 1000 * gas_used_delta)} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_opcode_calls.py b/tests/ported_static/stRevertTest/test_revert_opcode_calls.py index 1089825aeca..e8dd0e77288 100644 --- a/tests/ported_static/stRevertTest/test_revert_opcode_calls.py +++ b/tests/ported_static/stRevertTest/test_revert_opcode_calls.py @@ -5,7 +5,13 @@ state_tests/stRevertTest/RevertOpcodeCallsFiller.json @manually-enhanced: Do not overwrite. Gas bumped fork-conditionally to cover EIP-8037 state-gas spill into regular gas; pre-EIP-8037 -behavior unchanged. +behavior unchanged. The d3 call chain ends in a fresh SSTORE-set in +the outermost (transaction) frame; with an empty state-gas reservoir +that set's state gas spills into regular gas, so the success path +(g=0) runs out at the final `SSTORE` unless the outer budget absorbs +the spill. Lift `tx_gas[0]` by one fresh-set SSTORE state cost via +`fork.oog_budget_lift`, which is exactly 0 pre-EIP-8037 and tracks +the parameter. g=1 (the OoG case) keeps the original budget. """ @@ -334,7 +340,12 @@ def test_revert_opcode_calls( Hash(addr_3, left_padding=True), Hash(addr_4, left_padding=True), ] - tx_gas = [460000, 83622] + # The g=0 success path bottoms out on a fresh SSTORE-set in the + # transaction frame whose EIP-8037 state gas spills (empty + # reservoir). Lift the outer budget by that spilled state cost so + # the chain still completes on Amsterdam; 0 pre-EIP-8037. + g0_lift = fork.oog_budget_lift(sstores_before_oog=1) + tx_gas = [460000 + g0_lift, 83622] tx = Transaction( sender=sender, diff --git a/tests/ported_static/stSpecialTest/test_eoa_empty_paris.py b/tests/ported_static/stSpecialTest/test_eoa_empty_paris.py index 5c93c366823..b6e28c96397 100644 --- a/tests/ported_static/stSpecialTest/test_eoa_empty_paris.py +++ b/tests/ported_static/stSpecialTest/test_eoa_empty_paris.py @@ -3,6 +3,14 @@ Ported from: state_tests/stSpecialTest/eoaEmptyParisFiller.yml + +@manually-enhanced: Do not overwrite. Two measured slots shift under +EIP-8038. Slot 0xF1 times a CALL that forwards `value` to the (warm) +origin EOA: when `value` is nonzero it gains the value-transfer +reprice `CALL_VALUE - 9000`; the value-0 cases are unchanged. Slot +0xFF times a value-0 CALL to a cold contract and gains the cold +account reprice `COLD_ACCOUNT_ACCESS - 2600`. Both deltas come from +the fork's own gas model, so each is exactly 0 before EIP-8038. """ import pytest @@ -97,6 +105,13 @@ def test_eoa_empty_paris( v: int, ) -> None: """Test_eoa_empty_paris.""" + # EIP-8038 deltas, each 0 before EIP-8038. Slot 0xF1's value-bearing + # CALL to the warm origin gains the value-transfer reprice; slot + # 0xFF's value-0 CALL to a cold contract gains the cold account + # reprice. + gas_costs = fork.gas_costs() + call_value_delta = gas_costs.CALL_VALUE - 9000 + cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x000000000000000000000000000000000000BAD1) contract_1 = Address(0x000000000000000000000000000000000000BAD2) @@ -244,7 +259,7 @@ def test_eoa_empty_paris( 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 241: 118, - 255: 7626, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 @@ -265,8 +280,8 @@ def test_eoa_empty_paris( 49: 0, 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 - 241: 6818, - 255: 7626, + 241: 6818 + call_value_delta, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 @@ -296,7 +311,7 @@ def test_eoa_empty_paris( 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 241: 118, - 255: 7626, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 @@ -317,8 +332,8 @@ def test_eoa_empty_paris( 49: 100, 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 - 241: 6818, - 255: 7626, + 241: 6818 + call_value_delta, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 @@ -340,7 +355,7 @@ def test_eoa_empty_paris( 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 241: 118, - 255: 7626, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 @@ -361,8 +376,8 @@ def test_eoa_empty_paris( 49: 0, 59: 0, 63: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 - 241: 6818, - 255: 7626, + 241: 6818 + call_value_delta, + 255: 7626 + cold_account_delta, 319: 0, 47825: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 47826: 0xC5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470, # noqa: E501 diff --git a/tests/ported_static/stTransactionTest/test_contract_store_clears_success.py b/tests/ported_static/stTransactionTest/test_contract_store_clears_success.py index 90f1ca0d067..a61f620fb02 100644 --- a/tests/ported_static/stTransactionTest/test_contract_store_clears_success.py +++ b/tests/ported_static/stTransactionTest/test_contract_store_clears_success.py @@ -3,6 +3,18 @@ Ported from: state_tests/stTransactionTest/ContractStoreClearsSuccessFiller.json + +@manually-enhanced: Do not overwrite. The contract clears 10 cold +storage slots (each 12 -> 0) and the transaction sends value alongside, +so the asserted post is the cleared storage plus the received value. +EIP-8038 raises the cold SSTORE-clear charge from 5000 to 13000, so the +10 clears no longer fit in the original gas limit and the contract runs +out of gas before clearing the storage or keeping the transfer. Bump the +gas limit by the per-clear charge delta times the 10 clears so every +clear still lands at Amsterdam. The delta is derived from the fork gas +model and is exactly 0 pre-EIP-8037; do not hardcode the Amsterdam +value. The post asserts only the target account (cleared storage and the +received value), which holds at every fork once the gas fits. """ import pytest @@ -15,6 +27,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +42,7 @@ def test_contract_store_clears_success( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_contract_store_clears_success.""" coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) @@ -72,11 +86,21 @@ def test_contract_store_clears_success( nonce=0, ) + # EIP-8038 raises the cold SSTORE-clear charge; bump the gas limit by + # the per-clear charge delta times the 10 clears so all of them still + # land instead of running out of gas before clearing the storage. + cold_clear_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + - 5000 + ) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=130000, + gas_limit=130000 + 10 * cold_clear_delta, value=10, ) diff --git a/tests/ported_static/stTransactionTest/test_internal_call_store_clears_success.py b/tests/ported_static/stTransactionTest/test_internal_call_store_clears_success.py index bdd47e999a2..e699a350a8d 100644 --- a/tests/ported_static/stTransactionTest/test_internal_call_store_clears_success.py +++ b/tests/ported_static/stTransactionTest/test_internal_call_store_clears_success.py @@ -3,6 +3,20 @@ Ported from: state_tests/stTransactionTest/InternalCallStoreClearsSuccessFiller.json + +@manually-enhanced: Do not overwrite. The `target` contract forwards a +fixed `CALL` gas budget (0x186A0) to `addr`, which clears 10 cold +storage slots (12 -> 0). EIP-8038 raises the cold SSTORE-clear charge +from 5000 to 13000, so the 10 clears jump from 50000 to 130000 gas and +no longer fit in the forwarded budget or the transaction gas limit: +`addr` runs out of gas, its slots stay set, and the inner value +transfer rolls back, defeating the "store clears success" intent. Both +the inner `CALL` gas argument and the transaction gas limit are raised +by `10 * cold_clear_delta` so all 10 clears still succeed. The per-clear +delta is derived from the fork gas model and is exactly 0 pre-EIP-8037; +do not hardcode the Amsterdam values. The asserted balances are +fork-invariant once the clears land, and the post does not assert the +sender balance, so no balance adjustment is needed. """ import pytest @@ -15,6 +29,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,6 +46,7 @@ def test_internal_call_store_clears_success( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_internal_call_store_clears_success.""" coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) @@ -45,6 +61,18 @@ def test_internal_call_store_clears_success( gas_limit=1000000, ) + # EIP-8038 raises the cold SSTORE-clear charge; bump the forwarded + # CALL gas and the transaction gas limit by the per-clear delta times + # the 10 clears so every clear still lands instead of running out of + # gas. The delta is 0 before the EIP-8037/8038 repricing. + cold_clear_delta = ( + Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + - 5000 + ) + clears_gas_bump = 10 * cold_clear_delta + # Source: lll # {(SSTORE 0 0)(SSTORE 1 0)(SSTORE 2 0)(SSTORE 3 0)(SSTORE 4 0)(SSTORE 5 0)(SSTORE 6 0)(SSTORE 7 0)(SSTORE 8 0)(SSTORE 9 0)} # noqa: E501 addr = pre.deploy_contract( # noqa: F841 @@ -77,7 +105,7 @@ def test_internal_call_store_clears_success( # { (CALL 100000 1 0 0 0 0) } # noqa: E501 target = pre.deploy_contract( # noqa: F841 code=Op.CALL( - gas=0x186A0, + gas=0x186A0 + clears_gas_bump, address=addr, value=0x1, args_offset=0x0, @@ -94,7 +122,7 @@ def test_internal_call_store_clears_success( sender=sender, to=target, data=Bytes(""), - gas_limit=160000, + gas_limit=160000 + clears_gas_bump, value=10, ) diff --git a/tests/ported_static/stTransactionTest/test_store_clears_and_internal_call_store_clears_success.py b/tests/ported_static/stTransactionTest/test_store_clears_and_internal_call_store_clears_success.py index 3af9a80d962..163c044ce66 100644 --- a/tests/ported_static/stTransactionTest/test_store_clears_and_internal_call_store_clears_success.py +++ b/tests/ported_static/stTransactionTest/test_store_clears_and_internal_call_store_clears_success.py @@ -3,6 +3,19 @@ Ported from: state_tests/stTransactionTest/StoreClearsAndInternalCallStoreClearsSuccessFiller.json + +@manually-enhanced: Do not overwrite. The outer contract `target` clears 4 +cold storage slots then `CALL`s the inner contract `addr`, which clears 10 +cold storage slots; the value transfer and clears must all succeed. +EIP-8037/8038 raise the cold SSTORE-clear charge from 5000 to 13000 at +Amsterdam, so both gas budgets must rise by that charge delta or the inner +frame runs out of gas (clearing only 4 of its 10 slots) and the value +transfer rolls back. The inner `CALL` only forwards a fixed gas amount, so +its budget is bumped by the 10 inner clears; the transaction gas limit is +bumped by all 14 clears (10 inner plus 4 outer) so the outer frame can both +pay its own clears and forward the larger amount. Both bumps are derived +from the fork gas model and are exactly 0 pre-EIP-8037; do not hardcode the +Amsterdam values. """ import pytest @@ -15,6 +28,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,11 +45,19 @@ def test_store_clears_and_internal_call_store_clears_success( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_store_clears_and_internal_call_store_clears_success.""" coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) sender = pre.fund_eoa(amount=0x1DCD6500) + # EIP-8037/8038 raise the cold SSTORE-clear charge; derive the per-clear + # delta (0 pre-EIP-8037) so both gas budgets keep every clear landing. + sstore_charge = Op.SSTORE.with_metadata( + key_warm=False, original_value=1, current_value=1, new_value=0 + ).gas_cost(fork) + cold_clear_delta = sstore_charge - 5000 + env = Environment( fee_recipient=coinbase, number=1, @@ -81,7 +103,10 @@ def test_store_clears_and_internal_call_store_clears_success( + Op.SSTORE(key=0x2, value=0x0) + Op.SSTORE(key=0x3, value=0x0) + Op.CALL( - gas=0xC350, + # The inner frame clears 10 cold slots; forward its extra + # charge so all 10 clears land at Amsterdam (delta is 0 + # pre-EIP-8037). + gas=0xC350 + 10 * cold_clear_delta, address=addr, value=0x1, args_offset=0x0, @@ -99,7 +124,10 @@ def test_store_clears_and_internal_call_store_clears_success( sender=sender, to=target, data=Bytes(""), - gas_limit=200000, + # The whole transaction clears 14 cold slots (4 in the outer frame, + # 10 in the inner frame); bump the limit by all of them so the outer + # frame can pay its own clears and forward the larger inner budget. + gas_limit=200000 + 14 * cold_clear_delta, value=10, ) diff --git a/tests/ported_static/vmTests/test_suicide.py b/tests/ported_static/vmTests/test_suicide.py index 02c1db148af..9cc37ebab8a 100644 --- a/tests/ported_static/vmTests/test_suicide.py +++ b/tests/ported_static/vmTests/test_suicide.py @@ -3,6 +3,18 @@ Ported from: state_tests/VMTests/vmTests/suicideFiller.yml + +@manually-enhanced: Do not overwrite. For the `caller` case the post-state +asserts the sender balance, which equals its start minus +`gas_used * gas_price`. The transaction calls a contract that CALLs a +cold, existing account (slot 0x1000) before it self-destructs; EIP-8038 +raises the cold account-access surcharge on that CALL from 2600 to 3000. +The SELFDESTRUCT itself is to a warm, non-empty beneficiary (the caller), +so its charge is unchanged, and there is no refund. Derive the +account-access delta from the fork gas model (0 pre-EIP-8037) and +subtract `gas_price * delta` from the Cancun balance; do not hardcode the +Amsterdam value. The `random` and `myself` cases assert only +non-gas-dependent balances and need no adjustment. """ import pytest @@ -133,12 +145,21 @@ def test_suicide( address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) + # The CALL into the self-destructing contract touches a cold, already + # existing account; EIP-8038 raises that cold account-access surcharge + # from 2600 to 3000. The SELFDESTRUCT beneficiary is warm and + # non-empty, so its charge is unchanged. The sender pays this extra + # gas at the base fee (no priority fee), so its balance drops by the + # account-access delta times the gas price. + cold_account_access_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + caller_balance = 0x5AF31075D9DE - 10 * cold_account_access_delta + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - sender: Account(balance=0x5AF31075D9DE), + sender: Account(balance=caller_balance), contract_3: Account(balance=0xFF100000000000), }, }, diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 431b4166add..4acb1f4f083 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -973,6 +973,7 @@ def test_gas_cost( def test_account_warming( state_test: StateTestFiller, pre: Alloc, + fork: Fork, authorization_list_with_properties: List[AuthorizationWithProperties], authorization_list: List[AuthorizationTuple], access_list: List[AccessList], @@ -988,8 +989,9 @@ def test_account_warming( # check. overhead_cost = 3 * len(Op.CALL.kwargs) - cold_account_cost = 2600 - warm_account_cost = 100 + gas_costs = fork.gas_costs() + cold_account_cost = gas_costs.COLD_ACCOUNT_ACCESS + warm_account_cost = gas_costs.WARM_ACCESS access_list_addresses = { access_list.address for access_list in access_list @@ -1190,6 +1192,7 @@ def test_intrinsic_gas_cost( def test_self_set_code_cost( state_test: StateTestFiller, pre: Alloc, + fork: Fork, pre_authorized: bool, ) -> None: """Test set to code account access cost when it delegates to itself.""" @@ -1200,6 +1203,10 @@ def test_self_set_code_cost( slot_call_cost = 1 + gas_costs = fork.gas_costs() + cold_account_cost = gas_costs.COLD_ACCOUNT_ACCESS + warm_account_cost = gas_costs.WARM_ACCESS + overhead_cost = 3 * len(Op.CALL.kwargs) callee_code = CodeGasMeasure( @@ -1211,7 +1218,11 @@ def test_self_set_code_cost( callee_address = pre.deploy_contract(callee_code) callee_storage = Storage() - callee_storage[slot_call_cost] = 200 if not pre_authorized else 2700 + callee_storage[slot_call_cost] = ( + 2 * warm_account_cost + if not pre_authorized + else cold_account_cost + warm_account_cost + ) tx = Transaction( to=callee_address, diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index a083c9035e0..e31a5323378 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -1470,6 +1470,7 @@ def test_ext_code_on_self_set_code( def test_set_code_address_and_authority_warm_state( state_test: StateTestFiller, pre: Alloc, + fork: Fork, set_code_address_first: bool, ) -> None: """ @@ -1512,13 +1513,18 @@ def test_set_code_address_and_authority_warm_state( callee_code += Op.SSTORE(slot_call_success, 1) + Op.STOP callee_address = pre.deploy_contract(callee_code) + gas_costs = fork.gas_costs() + cold_account_cost = gas_costs.COLD_ACCOUNT_ACCESS + warm_account_cost = gas_costs.WARM_ACCESS callee_storage = Storage() callee_storage[slot_call_success] = 1 callee_storage[slot_set_code_to_warm_state] = ( - 2_600 if set_code_address_first else 100 + cold_account_cost if set_code_address_first else warm_account_cost ) callee_storage[slot_authority_warm_state] = ( - 200 if set_code_address_first else 2_700 + 2 * warm_account_cost + if set_code_address_first + else warm_account_cost + cold_account_cost ) tx = Transaction( From 4d21c5b3d92844c1033efba4cf55a8c83302a59d Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 18 Jun 2026 22:59:30 +0100 Subject: [PATCH 02/14] feat(spec-specs, tests): Implement EIP-2780 (#3017) --- .../testing/src/execution_testing/__init__.py | 2 + .../src/execution_testing/forks/base_fork.py | 99 ++++ .../forks/forks/eips/amsterdam/eip_2780.py | 161 ++++++ .../forks/forks/eips/amsterdam/eip_7981.py | 5 + .../forks/forks/eips/berlin/eip_2930.py | 4 + .../forks/forks/eips/homestead/eip_2.py | 4 + .../forks/forks/eips/prague/eip_7623.py | 5 + .../forks/forks/eips/prague/eip_7702.py | 5 + .../execution_testing/forks/forks/forks.py | 4 + .../src/execution_testing/forks/gas_costs.py | 14 + .../src/execution_testing/recipient_type.py | 14 + .../tools/utility/generators.py | 18 +- src/ethereum/forks/amsterdam/fork.py | 37 +- src/ethereum/forks/amsterdam/transactions.py | 51 +- src/ethereum/forks/amsterdam/vm/__init__.py | 2 + src/ethereum/forks/amsterdam/vm/gas.py | 4 +- .../forks/amsterdam/vm/interpreter.py | 52 +- .../__init__.py | 1 + .../helpers.py | 43 ++ .../eip2780_reduce_intrinsic_tx_gas/spec.py | 17 + .../test_intrinsic_gas_boundary.py | 69 +++ .../test_top_frame_charges.py | 201 +++++++ .../test_value_moving_transactions.py | 363 ++++++++++++ .../test_value_moving_with_tx_delegation.py | 339 +++++++++++ .../test_warmth_invariants.py | 543 ++++++++++++++++++ .../test_eip_mainnet.py | 15 +- .../test_block_access_lists.py | 97 +++- .../test_block_access_lists_eip4895.py | 15 +- .../test_block_access_lists_invalid.py | 23 +- .../test_additional_coverage.py | 5 + .../test_floor_boundary_exact_balance.py | 2 +- .../test_block_2d_gas_accounting.py | 6 +- .../test_state_gas_create.py | 2 +- .../test_state_gas_multi_block.py | 4 + tests/berlin/eip2930_access_list/test_acl.py | 1 + tests/cancun/eip4844_blobs/conftest.py | 11 +- tests/cancun/eip4844_blobs/test_blob_txs.py | 118 +++- .../test_create2_oo_gafter_init_code.py | 8 + .../test_create_oo_gafter_init_code.py | 8 + ...e_consume_more_gas_then_transaction_has.py | 22 +- .../test_call_goes_oog_on_second_level.py | 20 +- .../stEIP1559/test_low_gas_limit.py | 15 +- .../stEIP2930/test_transaction_costs.py | 11 + ...ransaction_has_with_mem_expanding_calls.py | 20 +- ...te_call_on_eip_with_mem_expanding_calls.py | 22 +- .../stRefundTest/test_refund50_1.py | 9 +- .../stRefundTest/test_refund_ff.py | 7 +- .../test_refund_get_ether_back.py | 29 +- .../stRefundTest/test_refund_max.py | 9 +- .../test_refund_multimple_suicide.py | 17 +- .../stRefundTest/test_refund_no_oog_1.py | 36 +- .../test_refund_single_suicide.py | 17 +- .../stRefundTest/test_refund_sstore.py | 35 +- .../stTransactionTest/test_high_gas_limit.py | 20 +- .../test_transaction_sending_to_zero.py | 21 +- ...est_transaction_to_addressh160minus_one.py | 30 +- .../test_transaction_to_itself.py | 28 +- .../stZeroCallsTest/test_zero_value_call.py | 19 +- .../test_zero_value_call_to_empty_paris.py | 19 +- ...est_zero_value_call_to_non_zero_balance.py | 19 +- ...ero_value_call_to_one_storage_key_paris.py | 20 +- .../test_zero_value_callcode.py | 20 +- ...test_zero_value_callcode_to_empty_paris.py | 20 +- ...zero_value_callcode_to_non_zero_balance.py | 22 +- ...value_callcode_to_one_storage_key_paris.py | 21 +- .../test_zero_value_delegatecall.py | 21 +- ..._zero_value_delegatecall_to_empty_paris.py | 21 +- ..._value_delegatecall_to_non_zero_balance.py | 21 +- ...e_delegatecall_to_one_storage_key_paris.py | 21 +- tests/ported_static/vmTests/test_suicide.py | 11 +- .../eip7702_set_code_tx/test_set_code_txs.py | 37 +- .../eip4895_withdrawals/test_withdrawals.py | 10 +- 72 files changed, 2874 insertions(+), 168 deletions(-) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py create mode 100644 packages/testing/src/execution_testing/recipient_type.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/__init__.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_warmth_invariants.py diff --git a/packages/testing/src/execution_testing/__init__.py b/packages/testing/src/execution_testing/__init__.py index 6b079cd3d5d..87e87352186 100644 --- a/packages/testing/src/execution_testing/__init__.py +++ b/packages/testing/src/execution_testing/__init__.py @@ -31,6 +31,7 @@ ) from .fixtures import BaseFixture, FixtureCollector from .forks import Fork, GasCosts, RefundTypes, TransitionFork +from .recipient_type import RecipientType from .specs import ( BaseTest, BenchmarkTest, @@ -195,6 +196,7 @@ "OpcodeCallArg", "Opcodes", "ParameterSet", + "RecipientType", "ReferenceSpec", "ReferenceSpecTypes", "RefundTypes", diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 6449ced32d4..7d2bafc517d 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -32,6 +32,7 @@ Opcodes, ) +from ..recipient_type import RecipientType from .gas_costs import GasCosts @@ -116,6 +117,8 @@ def __call__( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: """ Return the intrinsic gas cost of a transaction given its properties. @@ -135,6 +138,14 @@ def __call__( that is deducted from the gas limit before the transaction starts execution. + sends_value: Whether the transaction transfers a non-zero value. + Forks that itemize the value-transfer charge in + intrinsic gas use this flag; ignored by older forks. + recipient_type: Category of the transaction recipient. Forks + that vary intrinsic gas by recipient kind + (e.g. no access cost for precompiles, no value + charge for self-transfers) use this; ignored + by older forks. Returns: Gas cost of a transaction @@ -142,6 +153,49 @@ def __call__( pass +class TopFrameGasCalculator(Protocol): + """ + A protocol to calculate the additional regular gas charged at the + top-level transaction frame, after intrinsic gas is deducted but + before EVM execution begins. + + Returns only the regular-gas portion of the post-intrinsic + state-aware preparation (e.g. the delegated-recipient access + charge). The state-gas portion is exposed separately by + ``BaseFork.transaction_top_frame_state_gas`` so tests can model the + two-dimensional reservoir explicitly or sum the two via + ``oog_budget_lift`` when targeting the spillover boundary. + + Returns 0 for forks that do not perform any such preparation. + """ + + def __call__( + self, + *, + contract_creation: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + """ + Return the regular gas consumed by top-frame preparation for a + transaction at this fork. + + Args: + contract_creation: Whether the transaction creates a contract. + Top-frame charges are zero for creates; + equivalent charges are paid via intrinsic + gas. + sends_value: Whether the transaction transfers a non-zero + value. + recipient_type: Category of the transaction recipient. + Drives the conditional charges. + + Returns: Regular gas added by top-frame preparation. + + """ + pass + + class BlobGasPriceCalculator(Protocol): """ A protocol to calculate the blob gas price given the excess blob gas at a @@ -705,6 +759,51 @@ def transaction_intrinsic_state_gas( del contract_creation, authorization_count return 0 + @classmethod + def transaction_top_frame_gas_calculator( + cls, + ) -> TopFrameGasCalculator: + """ + Return a callable that calculates the additional regular gas + charged at the top-level transaction frame, after intrinsic + gas is deducted but before EVM execution begins. + + Defaults to returning 0 for forks that do not perform such + post-intrinsic preparation. + """ + + def fn( + *, + contract_creation: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + del contract_creation, sends_value, recipient_type + return 0 + + return fn + + @classmethod + def transaction_top_frame_state_gas( + cls, + *, + contract_creation: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + """ + Return the state gas charged at the top-level transaction + frame, after intrinsic gas is deducted but before EVM execution + begins. Companion to ``transaction_top_frame_gas_calculator``; + tests targeting the spillover boundary feed this through + ``oog_budget_lift`` to get the equivalent regular-gas budget. + + Defaults to 0 for forks that do not perform such + post-intrinsic preparation. + """ + del contract_creation, sends_value, recipient_type + return 0 + @classmethod def system_call_gas_limit(cls) -> int: """ diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py new file mode 100644 index 00000000000..3746a921b42 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py @@ -0,0 +1,161 @@ +""" +EIP-2780: Resource-based intrinsic transaction gas. + +Decompose the intrinsic transaction gas into explicit recipient-access +and value-transfer primitives so that the cost paid before execution +reflects the actual work the transaction will perform. + +https://eips.ethereum.org/EIPS/eip-2780 +""" + +from dataclasses import replace +from typing import List, Sized + +from execution_testing.base_types import AccessList +from execution_testing.base_types.conversions import BytesConvertible + +from .....recipient_type import RecipientType +from ....base_fork import ( + BaseFork, + TopFrameGasCalculator, + TransactionIntrinsicCostCalculator, +) +from ....gas_costs import GasCosts + + +class EIP2780(BaseFork): + """EIP-2780 class.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """ + Lower ``TX_BASE`` to 12_000 to reflect the removal of the + bundled recipient access and account-write charges, and add + the transfer-log and value-transfer constants. + """ + parent = super(EIP2780, cls).gas_costs() + return replace( + parent, + TX_BASE=12_000, + TRANSFER_LOG_COST=1_756, + TX_VALUE_COST=4_244, + ) + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + Decompose intrinsic gas into explicit recipient and + value-transfer primitives. + + Non-create, non-self targets pay ``COLD_ACCOUNT_ACCESS`` + unconditionally; access lists do not warm transaction-level + accounts. Value-bearing transactions pay + ``TRANSFER_LOG_COST`` plus ``TX_VALUE_COST``; self-transfers + suppress the value-transfer charge entirely. + """ + super_fn = super(EIP2780, cls).transaction_intrinsic_cost_calculator() + gas_costs = cls.gas_costs() + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + authorization_list_or_count=authorization_list_or_count, + return_cost_deducted_prior_execution=True, + ) + + is_self_transfer = recipient_type == RecipientType.SELF + + if contract_creation: + if sends_value: + intrinsic_cost += gas_costs.TRANSFER_LOG_COST + elif not is_self_transfer: + intrinsic_cost += gas_costs.COLD_ACCOUNT_ACCESS + if sends_value: + intrinsic_cost += ( + gas_costs.TRANSFER_LOG_COST + gas_costs.TX_VALUE_COST + ) + + if return_cost_deducted_prior_execution: + return intrinsic_cost + + transaction_data_floor_cost_calculator = ( + cls.transaction_data_floor_cost_calculator() + ) + transaction_floor_data_cost = ( + transaction_data_floor_cost_calculator( + data=calldata, access_list=access_list + ) + ) + return max(intrinsic_cost, transaction_floor_data_cost) + + return fn + + @classmethod + def transaction_top_frame_gas_calculator( + cls, + ) -> TopFrameGasCalculator: + """ + Return the additional regular gas charged at the top-level + transaction frame, after intrinsic gas is deducted but before + the EVM dispatches. + + Charges ``COLD_ACCOUNT_ACCESS`` when the recipient is an + existing delegated account. The empty-recipient + ``NEW_ACCOUNT`` charge is state gas, returned separately by + ``transaction_top_frame_state_gas``. + """ + gas_costs = cls.gas_costs() + + def fn( + *, + contract_creation: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + del sends_value + if contract_creation: + return 0 + + if recipient_type == RecipientType.DELEGATION_7702: + return gas_costs.COLD_ACCOUNT_ACCESS + return 0 + + return fn + + @classmethod + def transaction_top_frame_state_gas( + cls, + *, + contract_creation: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + """ + Return the state gas charged at the top-level transaction + frame. Charges ``NEW_ACCOUNT`` when value is transferred to an + empty recipient; zero otherwise. Precompile recipients are + exempt: they are protocol-inherent rather than created by a + value transfer, so they never trigger ``NEW_ACCOUNT`` even + when state-empty. + """ + gas_costs = cls.gas_costs() + if contract_creation: + return 0 + if recipient_type == RecipientType.PRECOMPILE: + return 0 + if sends_value and recipient_type == RecipientType.EMPTY_ACCOUNT: + return gas_costs.NEW_ACCOUNT + return 0 diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7981.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7981.py index b2f927b507c..4aef4807938 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7981.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7981.py @@ -11,6 +11,7 @@ from execution_testing.base_types import AccessList from execution_testing.base_types.conversions import BytesConvertible +from .....recipient_type import RecipientType from ....base_fork import ( BaseFork, TransactionDataFloorCostCalculator, @@ -85,7 +86,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: + del sends_value, recipient_type + intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py b/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py index 7dede0ace15..675df0b7f10 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/berlin/eip_2930.py @@ -12,6 +12,7 @@ from execution_testing.base_types import AccessList from execution_testing.base_types.conversions import BytesConvertible +from .....recipient_type import RecipientType from ....base_fork import BaseFork, TransactionIntrinsicCostCalculator @@ -45,8 +46,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: del return_cost_deducted_prior_execution + del sends_value, recipient_type intrinsic_cost: int = super_fn( calldata=calldata, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py index 811094f0a98..2f4f8385d3f 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/homestead/eip_2.py @@ -9,6 +9,7 @@ from execution_testing.base_types import AccessList from execution_testing.base_types.conversions import BytesConvertible +from .....recipient_type import RecipientType from ....base_fork import BaseFork, TransactionIntrinsicCostCalculator @@ -33,8 +34,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: del return_cost_deducted_prior_execution + del sends_value, recipient_type intrinsic_cost: int = super_fn( calldata=calldata, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py index d52c5601a16..6b82b788379 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py @@ -12,6 +12,7 @@ from execution_testing.base_types import AccessList, Bytes from execution_testing.base_types.conversions import BytesConvertible +from .....recipient_type import RecipientType from ....base_fork import ( BaseFork, CalldataGasCalculator, @@ -95,7 +96,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: + del sends_value, recipient_type + intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py index 424c4ab2dab..7ce6a0876bf 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py @@ -13,6 +13,7 @@ from execution_testing.base_types.conversions import BytesConvertible from execution_testing.vm import OpcodeBase +from .....recipient_type import RecipientType from ....base_fork import ( BaseFork, RefundTypes, @@ -74,7 +75,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: + del sends_value, recipient_type + intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 04fc4838045..168a4b19e2b 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -21,6 +21,7 @@ Opcodes, ) +from ...recipient_type import RecipientType from ..base_fork import ( BaseFeeChangeCalculator, BaseFeePerGasCalculator, @@ -872,8 +873,11 @@ def fn( access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, ) -> int: del return_cost_deducted_prior_execution + del sends_value, recipient_type assert access_list is None, ( f"Access list is not supported in {cls.name()}" diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index ae190d5678a..0899e2e1402 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -36,7 +36,21 @@ class GasCosts: CALL_VALUE: int CALL_STIPEND: int NEW_ACCOUNT: int + # Surcharge for the first write to an account leaf; 0 before the + # state-access repricing introduces it as a standalone parameter. ACCOUNT_WRITE: int = 0 + # Access cost for a contract-creation slot; 0 before the + # state-access repricing introduces it as a standalone parameter. + CREATE_ACCESS: int = 0 + # Per-transfer log cost charged when a value-transferring + # transaction emits the synthetic transfer log; 0 before the + # intrinsic gas decomposition introduces it. + TRANSFER_LOG_COST: int = 0 + # Per-transfer value cost charged on top of the transfer log when a + # non-create transaction transfers a non-zero value to an account + # other than the sender; 0 before the intrinsic gas decomposition + # introduces it. + TX_VALUE_COST: int = 0 # Contract Creation CODE_DEPOSIT_PER_BYTE: int diff --git a/packages/testing/src/execution_testing/recipient_type.py b/packages/testing/src/execution_testing/recipient_type.py new file mode 100644 index 00000000000..f1bfe8770bb --- /dev/null +++ b/packages/testing/src/execution_testing/recipient_type.py @@ -0,0 +1,14 @@ +"""Recipient type enumeration for transaction gas calculations.""" + +from enum import Enum, auto + + +class RecipientType(Enum): + """The type of recipient for a transaction.""" + + SELF = auto() + EOA = auto() + CONTRACT = auto() + DELEGATION_7702 = auto() + PRECOMPILE = auto() + EMPTY_ACCOUNT = auto() diff --git a/packages/testing/src/execution_testing/tools/utility/generators.py b/packages/testing/src/execution_testing/tools/utility/generators.py index 028a114a6c4..53dc2cdc090 100644 --- a/packages/testing/src/execution_testing/tools/utility/generators.py +++ b/packages/testing/src/execution_testing/tools/utility/generators.py @@ -12,6 +12,7 @@ from execution_testing.exceptions import BlockException from execution_testing.forks import Berlin, Fork, TransitionFork from execution_testing.forks.base_fork import BaseFork +from execution_testing.recipient_type import RecipientType from execution_testing.specs import BlockchainTestFiller, StateTestFiller from execution_testing.specs.blockchain import Block from execution_testing.test_types import Alloc, Transaction @@ -406,10 +407,25 @@ def wrapper( # Simple test transaction to verify the block failed to modify the # state. value_receiver = pre.fund_eoa(amount=0) + # value_receiver is empty, so a value transfer to it pays + # the value-transfer intrinsic surcharges plus the + # top-frame ``NEW_ACCOUNT`` state-gas charge introduced by + # EIP-2780. Derive ``gas_limit`` from the fork so the tx + # is provisioned correctly across all forks. + test_tx_intrinsic_calc = ( + fork.transaction_intrinsic_cost_calculator() + ) + test_tx_gas_limit = test_tx_intrinsic_calc( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) test_tx = Transaction( to=value_receiver, value=1, - gas_limit=100_000, + gas_limit=test_tx_gas_limit, sender=pre.fund_eoa(), ) post = Alloc() diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 245161e2e1c..7172a39f3f6 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -503,8 +503,9 @@ def check_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, tx: Transaction, + sender: Address, tx_state: TransactionState, -) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], U64]: +) -> Tuple[Uint, Tuple[VersionedHash, ...], U64]: """ Check if the transaction is includable in the block. @@ -516,13 +517,13 @@ def check_transaction( The block output for the current block. tx : The transaction. + sender : + The recovered sender address of the transaction. tx_state : The transaction state tracker. Returns ------- - sender_address : - The sender of the transaction. effective_gas_price : The price to charge for gas when the transaction is executed. blob_versioned_hashes : @@ -584,15 +585,7 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - tx_chain_id = chain_id(tx) - if tx_chain_id is not None and tx_chain_id != block_env.chain_id: - raise WrongChainIdError( - expected=block_env.chain_id, - actual=tx_chain_id, - ) - - sender_address = recover_sender(tx) - sender_account = get_account(tx_state, sender_address) + sender_account = get_account(tx_state, sender) if isinstance(tx, FeeMarketCapableTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: @@ -665,7 +658,6 @@ def check_transaction( raise InvalidSenderError("not EOA") return ( - sender_address, effective_gas_price, blob_versioned_hashes, tx_blob_gas_used, @@ -800,6 +792,8 @@ def process_unchecked_system_transaction( tx_env = vm.TransactionEnvironment( origin=SYSTEM_ADDRESS, + recipient=target_address, + value=U256(0), gas_price=block_env.base_fee_per_gas, gas=SYSTEM_TRANSACTION_GAS, state_gas_reservoir=( @@ -1030,12 +1024,22 @@ def process_transaction( encode_transaction(tx), ) - intrinsic = validate_transaction(tx) + # `chain_id` also validates a legacy signature's `v` (raising + # `InvalidSignatureError` when `v` is neither 27/28 nor >= 35), so it + # must be called before `recover_sender`. + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender = recover_sender(tx) + intrinsic = validate_transaction(tx, sender) intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) ( - sender, effective_gas_price, blob_versioned_hashes, tx_blob_gas_used, @@ -1043,6 +1047,7 @@ def process_transaction( block_env=block_env, block_output=block_output, tx=tx, + sender=sender, tx_state=tx_state, ) @@ -1084,6 +1089,8 @@ def process_transaction( tx_env = vm.TransactionEnvironment( origin=sender, + recipient=tx.to, + value=tx.value, gas_price=effective_gas_price, gas=gas, state_gas_reservoir=state_gas_reservoir, diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index 026114d896e..136d0d91e72 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -577,7 +577,7 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> IntrinsicGasCost: +def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: """ Verifies a transaction. @@ -609,7 +609,7 @@ def validate_transaction(tx: Transaction) -> IntrinsicGasCost: """ from .vm.interpreter import MAX_INIT_CODE_SIZE - intrinsic = calculate_intrinsic_cost(tx) + intrinsic = calculate_intrinsic_cost(tx, sender) intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) if intrinsic_gas > tx.gas: raise InsufficientTransactionGasError("Insufficient intrinsic gas") @@ -631,7 +631,9 @@ def validate_transaction(tx: Transaction) -> IntrinsicGasCost: return intrinsic -def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: +def calculate_intrinsic_cost( + tx: Transaction, sender: Address +) -> IntrinsicGasCost: """ Calculates the gas that is charged before execution is started. @@ -645,12 +647,18 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: for all operations to be implemented. The intrinsic cost includes: - 1. Base cost (`TX_BASE`) - 2. Cost for data (zero and non-zero bytes) - 3. Cost for contract creation (if applicable) - 4. Cost for access list entries (if applicable) - 5. Cost for authorizations (if applicable) - + 1. Sender cost (`TX_BASE`). + 2. Recipient cost (`COLD_ACCOUNT_ACCESS` for a non-self-transfer + call, or `CREATE_ACCESS` plus `NEW_ACCOUNT` state gas for a + contract creation). + 3. Value cost (`TRANSFER_LOG_COST`, plus `TX_VALUE_COST` for a + non-self-transfer call) when ``tx.value > 0``. + 4. Calldata cost (zero and non-zero bytes). + 5. Access list entries (if applicable). + 6. Authorizations (if applicable). + + Self-transfers (``sender == tx.to``) skip the recipient and value + charges. This function takes a transaction and gas_limit as parameters and returns the intrinsic regular gas cost, intrinsic state gas cost, and the @@ -666,13 +674,24 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD - create_regular_gas = Uint(0) - create_state_gas = Uint(0) - if tx.to == Bytes0(b""): - create_state_gas = StateGasCosts.NEW_ACCOUNT - create_regular_gas = GasCosts.CREATE_ACCESS + init_code_cost( + is_create = tx.to == Bytes0(b"") + is_self_transfer = tx.to == sender + + recipient_regular_gas = Uint(0) + recipient_state_gas = Uint(0) + if is_create: + recipient_regular_gas = GasCosts.CREATE_ACCESS + init_code_cost( ulen(tx.data) ) + recipient_state_gas = StateGasCosts.NEW_ACCOUNT + if tx.value > U256(0): + recipient_regular_gas += GasCosts.TRANSFER_LOG_COST + elif not is_self_transfer: + recipient_regular_gas = GasCosts.COLD_ACCOUNT_ACCESS + if tx.value > U256(0): + recipient_regular_gas += ( + GasCosts.TRANSFER_LOG_COST + GasCosts.TX_VALUE_COST + ) access_list_cost = Uint(0) tokens_in_access_list = Uint(0) @@ -714,12 +733,12 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: intrinsic_regular_gas = ( GasCosts.TX_BASE + data_cost - + create_regular_gas + + recipient_regular_gas + access_list_cost + auth_regular_gas ) - intrinsic_state_gas = create_state_gas + auth_state_gas + intrinsic_state_gas = recipient_state_gas + auth_state_gas return IntrinsicGasCost( regular=RegularGas(intrinsic_regular_gas), diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 1f54d2b3c64..0b9dae40e86 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -120,6 +120,8 @@ class TransactionEnvironment: """ origin: Address + recipient: Bytes0 | Address + value: U256 gas_price: Uint gas: Uint state_gas_reservoir: Uint diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 06ee00eb5c8..83fe851d992 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -128,8 +128,10 @@ class GasCosts: BLOCK_ACCESS_LIST_ITEM: Final[Uint] = Uint(2000) # Transactions - TX_BASE: Final[Uint] = Uint(21000) + TX_BASE: Final[Uint] = Uint(12000) TX_CREATE: Final[Uint] = Uint(32000) + TX_VALUE_COST: Final[Uint] = Uint(4244) + TRANSFER_LOG_COST: Final[Uint] = Uint(1756) TX_DATA_TOKEN_STANDARD: Final[Uint] = Uint(4) TX_DATA_TOKEN_FLOOR: Final[Uint] = Uint(16) TX_ACCESS_LIST_ADDRESS: Final[Uint] = COLD_ACCOUNT_ACCESS diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index fda390023d1..a1c7265cc30 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -161,7 +161,6 @@ def process_message_call(message: Message) -> MessageCallOutput: delegated_address = get_delegated_code_address(message.code) if delegated_address is not None: message.disable_precompiles = True - message.accessed_addresses.add(delegated_address) message.code = get_code( tx_state, get_account(tx_state, delegated_address).code_hash, @@ -311,20 +310,47 @@ def process_message(message: Message) -> Evm: snapshot = copy_tx_state(tx_state) - if message.should_transfer_value and message.value != 0: - move_ether( - tx_state, - message.caller, - message.current_target, - message.value, - ) - if message.caller != message.current_target: - emit_transfer_log( - evm, message.caller, message.current_target, message.value - ) - # Execute message code and handle errors try: + # EIP-2780 top-frame charges: applied at the top of a + # transaction's call frame, after authorizations and before any + # opcode runs. Reads pre-value-transfer state so the + # EIP-161-empty check sees the recipient as it was at the start + # of the frame. Gated to non-create top-level frames; creates + # pay the equivalent NEW_ACCOUNT state gas. Precompile + # recipients are exempt from the NEW_ACCOUNT charge: they are + # protocol-inherent rather than created by a value transfer. + if message.depth == Uint(0) and message.target != Bytes0(b""): + recipient = message.current_target + recipient_is_precompile = recipient in PRE_COMPILED_CONTRACTS + if ( + message.value > U256(0) + and not recipient_is_precompile + and not is_account_alive(tx_state, recipient) + ): + charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) + recipient_code = get_code( + tx_state, get_account(tx_state, recipient).code_hash + ) + delegated_address = get_delegated_code_address(recipient_code) + if delegated_address is not None: + charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) + evm.accessed_addresses.add(delegated_address) + + if message.should_transfer_value and message.value != 0: + move_ether( + tx_state, + message.caller, + message.current_target, + message.value, + ) + if message.caller != message.current_target: + emit_transfer_log( + evm, + message.caller, + message.current_target, + message.value, + ) if evm.message.code_address in PRE_COMPILED_CONTRACTS: if not message.disable_precompiles: evm_trace(evm, PrecompileStart(evm.message.code_address)) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/__init__.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/__init__.py new file mode 100644 index 00000000000..85942b2be4c --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/__init__.py @@ -0,0 +1 @@ +"""Tests for [EIP-2780: Resource-based intrinsic transaction gas](https://eips.ethereum.org/EIPS/eip-2780).""" diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py new file mode 100644 index 00000000000..78350d79de9 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py @@ -0,0 +1,43 @@ +"""Shared helpers for EIP-2780 tests.""" + +from execution_testing import Address, Alloc, Op, RecipientType + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 + +EOA_INITIAL_BALANCE = 100 + +RECIPIENT_TYPES_NON_CREATE = [ + RecipientType.EOA, + RecipientType.CONTRACT, + RecipientType.EMPTY_ACCOUNT, + RecipientType.SELF, + RecipientType.DELEGATION_7702, +] + + +def setup_target( + pre: Alloc, recipient_type: RecipientType, sender: Address +) -> Address: + """ + Allocate a target account matching the given recipient type. + + ``EOA`` targets are pre-funded to ``EOA_INITIAL_BALANCE`` so that + post-state balance assertions distinguish a successful value + transfer from a no-op. + """ + match recipient_type: + case RecipientType.EOA: + return pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + case RecipientType.CONTRACT: + return pre.deploy_contract(code=Op.STOP) + case RecipientType.EMPTY_ACCOUNT: + return pre.fund_eoa(amount=0) + case RecipientType.SELF: + return sender + case RecipientType.DELEGATION_7702: + delegated_to = pre.deploy_contract(code=Op.STOP) + return pre.deploy_contract( + code=Spec7702.delegation_designation(delegated_to) + ) + case _: + raise ValueError(f"Unsupported recipient type {recipient_type}") diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py new file mode 100644 index 00000000000..869b3d72bf0 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py @@ -0,0 +1,17 @@ +"""Reference spec for [EIP-2780: Resource-based intrinsic transaction gas.](https://eips.ethereum.org/EIPS/eip-2780).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_2780 = ReferenceSpec( + git_path="EIPS/eip-2780.md", + version="4b612eec2ef70611bba3e0819d137dcfb9b6cd81", +) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py new file mode 100644 index 00000000000..d07370531ff --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py @@ -0,0 +1,69 @@ +""" +Gas-limit boundary tests for EIP-2780. + +Pin transactions one gas below the intrinsic charge layer to verify the +transaction is rejected by the pre-execution intrinsic gas check at +that boundary. Top-frame boundary OOGs are covered by the dedicated +top-frame charge tests in ``test_top_frame_charges.py``. +""" + +import pytest +from execution_testing import ( + Alloc, + Fork, + RecipientType, + StateTestFiller, + Transaction, + TransactionException, +) + +from .helpers import RECIPIENT_TYPES_NON_CREATE, setup_target +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.exception_test +@pytest.mark.parametrize("recipient_type", RECIPIENT_TYPES_NON_CREATE) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_gas_floor_boundary( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + recipient_type: RecipientType, + value: int, +) -> None: + """ + Reject when ``gas_limit = intrinsic_gas - 1``. + + The transaction never enters the EVM; it is rejected by the + pre-execution intrinsic gas check. + """ + sender = pre.fund_eoa(10**18) + target = setup_target(pre, recipient_type, sender) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=recipient_type, + return_cost_deducted_prior_execution=True, + ) + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=intrinsic_gas - 1, + gas_price=1_000_000_000, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, tx=tx, post={}) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py new file mode 100644 index 00000000000..dd791c5e78d --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -0,0 +1,201 @@ +""" +Dedicated tests for the EIP-2780 top-frame charge layer. + +The top-frame layer applies *after* intrinsic gas is deducted but +*before* the EVM dispatches at the transaction's outermost frame. Two +charges may fire there, depending on the recipient: + +- ``NEW_ACCOUNT`` (state gas) when the recipient is empty and the + transaction transfers value. +- ``COLD_ACCOUNT_ACCESS`` (regular gas) when the recipient holds an + EIP-7702 delegation. + +Each test parametrizes over the interesting outcomes for that charge: +running out of gas at the boundary, succeeding through the charge and +into the EVM, and (for the regular charge) succeeding through the +charge but reverting from the delegated code. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + Op, + RecipientType, + StateTestFiller, + Transaction, +) + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize("outcome", ["oog", "success"]) +def test_top_frame_state_charge( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + outcome: str, +) -> None: + """ + Recipient is empty and the transaction transfers a non-zero value, + so the top-frame fires the ``NEW_ACCOUNT`` state-gas charge. + + - ``oog``: gas limit is one short of covering the state charge. + The transaction passes the intrinsic check, enters + ``process_message``, and out-of-gases on + ``charge_state_gas(NEW_ACCOUNT)`` before any EVM bytecode runs. + The sender pays the full ``gas_limit`` and no value is + transferred. + - ``success``: gas limit covers the state charge. The value + transfer brings the recipient into existence and the recipient + ends the transaction holding the transferred balance. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = pre.fund_eoa(amount=0) + + value = 1 + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + assert top_frame_state_gas > 0, ( + "top-frame state gas must be non-zero for this scenario" + ) + + gas_price = 1_000_000_000 + if outcome == "oog": + gas_limit = intrinsic_gas + top_frame_state_gas - 1 + sender_final_balance = sender_initial_balance - gas_limit * gas_price + expected_target: Account | None = None + else: + total_gas_cost = intrinsic_gas + top_frame_state_gas + gas_limit = total_gas_cost + 1000 + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + expected_target = Account(balance=value) + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: expected_target, + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"]) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_regular_charge( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + outcome: str, + value: int, +) -> None: + """ + Recipient is an existing EIP-7702 delegation, so the top-frame + fires the ``COLD_ACCOUNT_ACCESS`` regular-gas charge regardless of + whether the transaction transfers value. + + - ``oog``: gas limit is one short of covering the regular charge + (plus the value-transfer charge when ``value > 0``). The + transaction OOGs at ``charge_gas(COLD_ACCOUNT_ACCESS)`` before + the delegated code runs. The sender pays the full ``gas_limit`` + and the recipient keeps its pre-tx state. + - ``success``: gas limit covers the regular charge; the delegated + code is a ``STOP`` and the transaction lands the value transfer. + - ``evm_reverts``: the delegated code reverts immediately. The + top-frame charge is consumed before dispatch and the two + ``PUSH`` opcodes that feed the ``REVERT`` are paid before the + revert; the value transfer is rolled back, the unused EVM + budget is returned, and the intrinsic and top-frame gas remain + paid. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + revert_code = Op.REVERT(0, 0) + if outcome == "evm_reverts": + delegated_to = pre.deploy_contract(code=revert_code) + else: + delegated_to = pre.deploy_contract(code=Op.STOP) + target_code = Spec7702.delegation_designation(delegated_to) + target = pre.deploy_contract(code=target_code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + assert top_frame_gas > 0, ( + "top-frame regular gas must be non-zero for this scenario" + ) + + gas_price = 1_000_000_000 + if outcome == "oog": + gas_limit = intrinsic_gas + top_frame_gas - 1 + sender_final_balance = sender_initial_balance - gas_limit * gas_price + target_balance = 0 + elif outcome == "success": + total_gas_cost = intrinsic_gas + top_frame_gas + gas_limit = total_gas_cost + 1000 + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + target_balance = value + else: + # Two ``PUSH`` opcodes feed ``REVERT`` before it halts. + revert_exec_gas = revert_code.gas_cost(fork) + gas_used = intrinsic_gas + top_frame_gas + revert_exec_gas + gas_limit = gas_used + 1000 + # Value transfer is rolled back, so the sender keeps the + # would-be transferred value. The intrinsic, top-frame, and + # pre-revert EVM gas stay paid. + sender_final_balance = sender_initial_balance - gas_used * gas_price + target_balance = 0 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=target_balance, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py new file mode 100644 index 00000000000..77f5fc28de6 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -0,0 +1,363 @@ +""" +Tests for EIP-2780 Reduce Transaction Intrinsic Cost. + +Test gas costs with EIP-2780 for value-moving transactions to: +- EOAs, +- contracts, +- empty accounts, +- the sender itself, +- delegated EOAs, +- newly created contracts, and +- precompiles. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Fork, + Initcode, + Op, + RecipientType, + StateTestFiller, + Transaction, + compute_create_address, +) + +from .helpers import ( + EOA_INITIAL_BALANCE, + RECIPIENT_TYPES_NON_CREATE, + setup_target, +) +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize("recipient_type", RECIPIENT_TYPES_NON_CREATE) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_value_moving_transactions( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + recipient_type: RecipientType, + value: int, +) -> None: + """ + Ensure value-moving transactions charge gas correctly across every + non-create recipient type. + + Self-transfers are carved out: the sender pays only the recipient + -access-free intrinsic and the value is moved to itself, so the + sender's post-tx balance reflects only gas. Pre-existing 7702 + delegations on the recipient surface as an extra top-frame + ``COLD_ACCOUNT_ACCESS``; empty recipients trigger the top-frame + ``NEW_ACCOUNT`` state charge when value is transferred. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = setup_target(pre, recipient_type, sender) + + target_initial_balance = ( + EOA_INITIAL_BALANCE if recipient_type == RecipientType.EOA else 0 + ) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=recipient_type, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=recipient_type, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=bool(value), + recipient_type=recipient_type, + ) + # Under the default zero state-gas reservoir, top-frame state gas + # spills entirely into regular gas. + total_gas_cost = intrinsic_gas + top_frame_gas + top_frame_state_gas + + tx_gas_limit = total_gas_cost + 1000 # add a small buffer + gas_price = 1_000_000_000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=tx_gas_limit, + gas_price=gas_price, + ) + + is_self_transfer = recipient_type == RecipientType.SELF + sender_value_delta = 0 if is_self_transfer else value + sender_final_balance = ( + sender_initial_balance + - sender_value_delta + - total_gas_cost * gas_price + ) + + post: dict[Address, Account | None] = { + sender: Account(nonce=1, balance=sender_final_balance), + } + if not is_self_transfer: + if recipient_type == RecipientType.EMPTY_ACCOUNT and value == 0: + post[target] = None + else: + post[target] = Account(balance=target_initial_balance + value) + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +@pytest.mark.parametrize( + "tx_reverts", + [ + pytest.param(False, id="success"), + pytest.param(True, id="init_reverts"), + ], +) +def test_value_contract_creation_tx( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + tx_reverts: bool, + value: int, +) -> None: + """ + Test value moving contract creation transactions. + + When the init code succeeds, the contract is deployed with the + transferred value and the receipt's ``gas_used`` equals the + intrinsic plus the execution gas. + + When the init code reverts, the deploy is rolled back: no code is + set, the value transfer is reversed, and the intrinsic + ``NEW_ACCOUNT`` state-gas charge is refilled to the reservoir. + Under the default zero state-gas reservoir, the refill cancels + the spilled-to-regular portion of the intrinsic exactly, so the + sender pays only the regular portion of the intrinsic plus the + few EVM gas units spent before the revert. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + code_to_deploy = Op.STOP + if tx_reverts: + # ``PUSH1 0 PUSH1 0 REVERT`` -- aborts immediately, so no + # code is deployed. + call_data = Op.REVERT(0, 0) + else: + call_data = Initcode(deploy_code=code_to_deploy) + execution_gas = call_data.gas_cost(fork) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=call_data, + contract_creation=True, + sends_value=bool(value), + return_cost_deducted_prior_execution=True, + ) + + if tx_reverts: + # The ``NEW_ACCOUNT`` state portion of the intrinsic is + # refilled to the reservoir on revert, so it does not appear + # on the receipt. + new_account_refund = fork.transaction_intrinsic_state_gas( + contract_creation=True, + ) + gas_used = intrinsic_gas + execution_gas - new_account_refund + # Value transfer rolled back. + sender_value_delta = 0 + expected_target = None + else: + gas_used = intrinsic_gas + execution_gas + sender_value_delta = value + expected_target = Account(code=code_to_deploy, balance=value) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + execution_gas + 1000 + + tx = Transaction( + sender=sender, + to=None, + value=value, + data=call_data, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + expected_target_address = compute_create_address(address=sender, nonce=0) + sender_final_balance = ( + sender_initial_balance - sender_value_delta - gas_used * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + expected_target_address: expected_target, + } + + state_test(pre=pre, tx=tx, post=post) + + +def _precompile_calldata(precompile: Address) -> bytes: + """Return minimal valid calldata for the given precompile address.""" + addr_int = int.from_bytes(precompile, "big") + + if addr_int == 0x0A: + # Valid point evaluation input from mainnet tx: + # https://etherscan.io/tx/0xcb3dc8f3b14f1cda0c16a619a112102a8ec70dce1b3f1b28272227cf8d5fbb0e + return ( + bytes.fromhex( + # versioned_hash (32) + "018156B94FE9735E573BAB36DAD05D60FEB720D424CCD20AAF719343C31E4246" + ) + + bytes.fromhex( + # z (32) + "019123BCB9D06356701F7BE08B4494625B87A7B02EDC566126FB81F6306E915F" + ) + + bytes.fromhex( + # y (32) + "6C2EB1E94C2532935B8465351BA1BD88EABE2B3FA1AADFF7D1CD816E8315BD38" + ) + + bytes.fromhex( + # kzg_commitment (48) + "A9546D41993E10DF2A7429B8490394EA9EE62807BAE6F326D1044A51581306F58D4B9DFD5931E044688855280FF3799E" + ) + + bytes.fromhex( + # kzg_proof (48) + "A2EA83D9391E0EE42E0C650ACC7A1F842A7D385189485DDB4FD54ADE3D9FD50D608167DCA6C776AAD4B8AD5C20691BFE" + ) + ) + + precompile_min_input = { + 0x01: 128, # ECRECOVER + 0x02: 0, # SHA256 (accepts empty) + 0x03: 0, # RIPEMD160 (accepts empty) + 0x04: 0, # IDENTITY (accepts empty) + 0x05: 96, # MODEXP + 0x06: 128, # BN256ADD + 0x07: 96, # BN256MUL + 0x08: 0, # BN256PAIRING (empty is valid) + 0x09: 213, # BLAKE2F + 0x0B: 256, # BLS12_G1_ADD + 0x0C: 160, # BLS12_G1_MSM + 0x0D: 512, # BLS12_G2_ADD + 0x0E: 288, # BLS12_G2_MSM + 0x0F: 384, # BLS12_PAIRING + 0x10: 64, # BLS12_MAP_FP_TO_G1 + 0x11: 128, # BLS12_MAP_FP2_TO_G2 + 0x100: 160, # P256VERIFY + } + + input_size = precompile_min_input.get(addr_int, 0) + return bytes([0x00] * input_size if input_size > 0 else []) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +@pytest.mark.parametrize( + "pre_funded", + [ + pytest.param(True, id="pre_funded"), + pytest.param(False, id="not_funded"), + ], +) +@pytest.mark.with_all_precompiles +def test_value_move_to_precompiles( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + precompile: Address, + pre_funded: bool, + value: int, +) -> None: + """ + Ensure value moving transactions to precompiles charge gas correctly. + + Precompile recipients pay the same ``COLD_ACCOUNT_ACCESS`` at + intrinsic time as any other non-self target -- access lists do + not warm transaction-level accounts. A value transfer to a + precompile additionally pays the transfer-log and value-transfer + charges. The top-frame ``NEW_ACCOUNT`` state charge is suppressed + for precompile recipients: they are protocol-inherent rather than + created by a value transfer. + + The ``pre_funded`` parameter exercises both pre-tx states of the + precompile address: the ``empty`` variant proves the carve-out is + explicit -- without it, a value transfer to an empty precompile + would trip the ``NEW_ACCOUNT`` charge that ``is_account_alive`` + would otherwise demand. The gas limit is pinned to + ``intrinsic_gas`` plus a buffer that covers the precompile + execution body, so a miscalculated intrinsic or a missing + carve-out would trip the gas budget rather than pass silently. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + pre_funded_amount = 0 + if pre_funded: + pre_funded_amount = 1 + pre.fund_address(precompile, amount=pre_funded_amount) + + tx_data = _precompile_calldata(precompile) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=bool(value), + recipient_type=RecipientType.PRECOMPILE, + return_cost_deducted_prior_execution=True, + ) + + # Precompile execution gas varies per precompile and input; pick + # a buffer large enough to cover the most expensive precompile in + # the matrix but still well under the ``NEW_ACCOUNT`` state + # charge, so a missing carve-out OOGs the ``empty`` variants. + precompile_execution_budget = 100_000 + tx_gas_limit = intrinsic_gas + precompile_execution_budget + gas_price = 1_000_000_000 + + tx = Transaction( + sender=sender, + to=precompile, + value=value, + data=tx_data, + gas_limit=tx_gas_limit, + gas_price=gas_price, + ) + + # Exact sender balance is not checked because precompile execution + # gas varies; verify value receipt and sender nonce instead. + final_precompile_balance = pre_funded_amount + value + expected_precompile: Account | None + if final_precompile_balance > 0: + expected_precompile = Account(balance=final_precompile_balance) + else: + expected_precompile = None + post = { + sender: Account(nonce=1), + precompile: expected_precompile, + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py new file mode 100644 index 00000000000..8722d39e1df --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_with_tx_delegation.py @@ -0,0 +1,339 @@ +""" +Tests for EIP-2780 x EIP-7702 interaction. + +When a type-4 transaction's authorization list installs a delegation on +``tx.to``, ``set_delegation`` runs before the top-frame check fires. +That ordering changes which top-frame charges apply: + +- ``COLD_ACCOUNT_ACCESS`` for the delegated recipient still fires; the + spec charges the access uniformly whenever the recipient holds a + delegation prefix at top-frame time, regardless of who installed it. +- ``NEW_ACCOUNT`` for a value transfer to an otherwise-empty recipient + is suppressed implicitly: ``set_delegation`` writes the delegation + code and increments the nonce, so ``is_account_alive`` returns + ``True`` by the time the top-frame check evaluates it. + +A complementary set of scenarios installs the delegation on the +*sender* (self-sponsored authorization). The authorization's nonce +must equal the sender's nonce *after* the transaction's nonce +increment. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + AuthorizationTuple, + Fork, + Op, + RecipientType, + StateTestFiller, + Transaction, +) + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_tx_installs_delegation_on_funded_recipient( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Scenario 1: ``tx.to`` is a funded EOA with no prior delegation. + The type-4 transaction's authorization installs delegation on + ``tx.to``. The top-frame ``COLD_ACCOUNT_ACCESS`` charge for the + now-delegated recipient still fires. + + The pre-existing authority account also produces a + ``REFUND_AUTH_PER_EXISTING_ACCOUNT`` state refund. + """ + gsc = fork.gas_costs() + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + target_initial_balance = 100 + target = pre.fund_eoa(amount=target_initial_balance) + delegated_to = pre.deploy_contract(code=Op.STOP) + + auth = AuthorizationTuple( + address=delegated_to, + nonce=0, + signer=target, + ) + + # Intrinsic sees the recipient in its pre-tx form (funded EOA); + # the delegation is materialized later by ``set_delegation`` and + # only surfaces at the top-frame check. + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.EOA, + authorization_list_or_count=[auth], + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + # The full intrinsic is deducted upfront. For each existing + # authority, ``set_delegation`` refunds ``NEW_ACCOUNT`` into the + # state gas reservoir (uncapped) and ``ACCOUNT_WRITE`` into the + # regular refund counter (capped at ``gas_used // 5`` by EIP-3529). + total_gas_cost = intrinsic_gas + top_frame_gas + state_refund = gsc.REFUND_AUTH_PER_EXISTING_ACCOUNT + gas_used_pre_regular_refund = total_gas_cost - state_refund + regular_refund = min(gsc.ACCOUNT_WRITE, gas_used_pre_regular_refund // 5) + gas_used = gas_used_pre_regular_refund - regular_refund + + tx_gas_limit = total_gas_cost + 1000 + gas_price = 1_000_000_000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + authorization_list=[auth], + gas_limit=tx_gas_limit, + max_fee_per_gas=gas_price, + max_priority_fee_per_gas=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - (gas_used * gas_price) + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account( + nonce=1, + balance=target_initial_balance + value, + code=Spec7702.delegation_designation(delegated_to), + ), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_tx_installs_delegation_on_empty_recipient( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Scenario 2: ``tx.to`` is a non-existent (empty) account. The type-4 + transaction's authorization installs delegation on ``tx.to``. + + ``set_delegation`` runs before the top-frame check and makes the + recipient alive, so the ``NEW_ACCOUNT`` state-gas charge that a + value transfer to an empty recipient would otherwise incur is + implicitly suppressed. The ``COLD_ACCOUNT_ACCESS`` charge for the + now-delegated recipient still fires. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + target = pre.fund_eoa(amount=0) + delegated_to = pre.deploy_contract(code=Op.STOP) + + auth = AuthorizationTuple( + address=delegated_to, + nonce=0, + signer=target, + ) + + # Intrinsic sees the recipient in its pre-tx form (empty); the + # delegation is materialized later by ``set_delegation`` and only + # surfaces at the top-frame check. + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.EMPTY_ACCOUNT, + authorization_list_or_count=[auth], + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + # Authority does not pre-exist, so no auth refund applies. + total_gas_cost = intrinsic_gas + top_frame_gas + + tx_gas_limit = total_gas_cost + 1000 + gas_price = 1_000_000_000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + authorization_list=[auth], + gas_limit=tx_gas_limit, + max_fee_per_gas=gas_price, + max_priority_fee_per_gas=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - (total_gas_cost * gas_price) + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account( + nonce=1, + balance=value, + code=Spec7702.delegation_designation(delegated_to), + ), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +@pytest.mark.parametrize( + "call_target", + [ + pytest.param("self", id="calls_self"), + pytest.param("other_eoa", id="calls_other"), + ], +) +def test_tx_installs_delegation_on_sender( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + call_target: str, + value: int, +) -> None: + """ + Self-sponsored type-4 transaction: the sender signs an + authorization installing delegation on itself, and the + authorization's nonce equals the sender's nonce *after* the + transaction-side increment (``1``). After ``set_delegation`` the + sender holds delegation code and its nonce reaches ``2``. + + Parametrized over the call target: + + - ``calls_self``: ``tx.to == sender``. The intrinsic self-transfer + carve-out suppresses the recipient access and value-transfer + charges; the top-frame fires ``COLD_ACCOUNT_ACCESS`` because + ``set_delegation`` has installed delegation code on the sender + by then. The transaction then dispatches into the sender's + delegated code. + - ``calls_other``: ``tx.to`` is a separate funded EOA. The + intrinsic charges include ``COLD_ACCOUNT_ACCESS`` for the + recipient (and the value-transfer charges when ``value > 0``). + The top-frame fires nothing because the recipient is a plain + EOA. The sender's delegation is installed and persists past the + transaction without ever being invoked. + """ + gsc = fork.gas_costs() + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + delegated_to = pre.deploy_contract(code=Op.STOP) + + auth = AuthorizationTuple( + address=delegated_to, + nonce=1, + signer=sender, + ) + + target_initial_balance = 0 + if call_target == "self": + target = sender + # Intrinsic carve-out fires (SELF); top-frame fires + # ``COLD_ACCOUNT_ACCESS`` because the sender is delegated by + # the time the check runs. + intrinsic_recipient_type = RecipientType.SELF + top_frame_recipient_type = RecipientType.DELEGATION_7702 + else: + target_initial_balance = 100 + target = pre.fund_eoa(amount=target_initial_balance) + # Recipient is a plain EOA, so no carve-out and no top-frame + # charge. + intrinsic_recipient_type = RecipientType.EOA + top_frame_recipient_type = RecipientType.EOA + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=intrinsic_recipient_type, + authorization_list_or_count=[auth], + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=top_frame_recipient_type, + ) + + # Sender is the existing authority, so ``set_delegation`` refunds + # ``NEW_ACCOUNT`` to the state-gas reservoir and ``ACCOUNT_WRITE`` + # to the regular refund counter (the latter capped at + # ``gas_used // 5`` by EIP-3529). + total_gas_cost = intrinsic_gas + top_frame_gas + state_refund = gsc.REFUND_AUTH_PER_EXISTING_ACCOUNT + gas_used_pre_regular_refund = total_gas_cost - state_refund + regular_refund = min(gsc.ACCOUNT_WRITE, gas_used_pre_regular_refund // 5) + gas_used = gas_used_pre_regular_refund - regular_refund + + tx_gas_limit = total_gas_cost + 1000 + gas_price = 1_000_000_000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + authorization_list=[auth], + gas_limit=tx_gas_limit, + max_fee_per_gas=gas_price, + max_priority_fee_per_gas=gas_price, + ) + + if call_target == "self": + # Value moves sender -> sender, net zero on balance. + sender_final_balance = sender_initial_balance - gas_used * gas_price + post = { + sender: Account( + nonce=2, + balance=sender_final_balance, + code=Spec7702.delegation_designation(delegated_to), + ), + } + else: + sender_final_balance = ( + sender_initial_balance - value - gas_used * gas_price + ) + post = { + sender: Account( + nonce=2, + balance=sender_final_balance, + code=Spec7702.delegation_designation(delegated_to), + ), + target: Account(balance=target_initial_balance + value), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_warmth_invariants.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_warmth_invariants.py new file mode 100644 index 00000000000..5c819beabd3 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_warmth_invariants.py @@ -0,0 +1,543 @@ +""" +EIP-2780 invariants for transaction-level account charges. + +The recipient and any EIP-7702 delegation target referenced at the +top-level transaction frame always pay the cold access rate, even when +the address is otherwise warm, identical to the sender, or refers to +itself: + +- The access list does not warm transaction-level accounts. Listing + ``tx.to`` (or a delegation target) pays the access-list cost but + does not waive the cold charge. +- The block coinbase is pre-warmed by the protocol before transaction + execution, but tx-level cold charges still fire when ``tx.to`` or a + delegation target happens to be the coinbase. +- Precompile addresses still pay the cold charge. +- Self-referential delegations (delegation target equal to the + sender, the recipient itself, or a precompile) all pay the cold + charge; the dispatched EVM frame then runs whatever code lives at + the target, including the degenerate cases of empty code (EOA, + precompile address) or a delegation prefix that itself decodes as + the ``INVALID`` opcode. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Environment, + Fork, + Op, + RecipientType, + StateTestFiller, + Transaction, +) + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_charges_recipient_in_access_list( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient is listed in the access list. The intrinsic charge still + includes ``COLD_ACCOUNT_ACCESS`` for the recipient on top of the + access-list cost itself. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + target_initial_balance = 100 + target = pre.fund_eoa(amount=target_initial_balance) + access_list = [AccessList(address=target, storage_keys=[])] + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + 1000 + + tx = Transaction( + ty=1, + sender=sender, + to=target, + value=value, + access_list=access_list, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - intrinsic_gas * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=target_initial_balance + value), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_charges_recipient_is_coinbase( + env: Environment, + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient is the block coinbase, which is implicitly warm before + transaction execution. The intrinsic charge still includes + ``COLD_ACCOUNT_ACCESS`` for the recipient. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = Address(env.fee_recipient) + # Pre-fund coinbase so it is alive at top-frame check time; this + # isolates the test to the intrinsic charge invariant and avoids + # the orthogonal ``NEW_ACCOUNT`` top-frame state charge that would + # otherwise fire for value transfer to an empty recipient. + pre.fund_address(target, amount=1) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + 1000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + # Coinbase also receives miner fees, so its post-tx balance is not + # asserted exactly; verifying the sender balance is sufficient to + # pin the intrinsic charge. + sender_final_balance = ( + sender_initial_balance - value - intrinsic_gas * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_charges_delegation_in_access_list( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient holds a pre-existing EIP-7702 delegation; the delegation + target is listed in the access list. The top-frame still charges + ``COLD_ACCOUNT_ACCESS`` for the delegation target on top of the + access-list cost itself. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + delegated_to = pre.deploy_contract(code=Op.STOP) + target_code = Spec7702.delegation_designation(delegated_to) + target = pre.deploy_contract(code=target_code) + access_list = [AccessList(address=delegated_to, storage_keys=[])] + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + + total_gas_cost = intrinsic_gas + top_frame_gas + gas_price = 1_000_000_000 + gas_limit = total_gas_cost + 1000 + + tx = Transaction( + ty=1, + sender=sender, + to=target, + value=value, + access_list=access_list, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=value, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_charges_delegation_is_coinbase( + env: Environment, + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient holds a pre-existing EIP-7702 delegation whose target is + the block coinbase. Coinbase is implicitly warm before execution; + the top-frame still charges ``COLD_ACCOUNT_ACCESS`` for the + delegation target. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + delegated_to = Address(env.fee_recipient) + target_code = Spec7702.delegation_designation(delegated_to) + target = pre.deploy_contract(code=target_code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + + total_gas_cost = intrinsic_gas + top_frame_gas + gas_price = 1_000_000_000 + gas_limit = total_gas_cost + 1000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + # Coinbase also receives miner fees, so its post-tx balance is not + # asserted exactly. + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=value, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_sender_is_coinbase( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Sender is the block coinbase. The intrinsic charge is unchanged + by sender identity, but the priority-fee payment loops back to + the sender, so the net gas cost reduces to ``gas_used * + base_fee_per_gas``. + + The coinbase override is wired via a custom ``Environment`` whose + ``fee_recipient`` matches the sender's address. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + target_initial_balance = 100 + target = pre.fund_eoa(amount=target_initial_balance) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + + base_fee = 7 + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + 1000 + # Sender pays the full gas fee upfront and is credited the + # priority fee back as the coinbase: net cost is + # ``gas_used * base_fee``. + sender_final_balance = ( + sender_initial_balance - value - intrinsic_gas * base_fee + ) + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=target_initial_balance + value), + } + + state_test( + pre=pre, + tx=tx, + post=post, + env=Environment(fee_recipient=sender, base_fee_per_gas=base_fee), + ) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_charges_delegation_is_sender( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient holds a pre-existing EIP-7702 delegation whose target is + the sender (``tx.origin``). The top-frame still charges + ``COLD_ACCOUNT_ACCESS`` for the delegation target; the dispatched + EVM frame finds the sender's empty EOA code and exits immediately. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + delegated_to = sender + target_code = Spec7702.delegation_designation(delegated_to) + target = pre.deploy_contract(code=target_code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + + total_gas_cost = intrinsic_gas + top_frame_gas + gas_price = 1_000_000_000 + gas_limit = total_gas_cost + 1000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=value, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_charges_delegation_is_recipient( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient holds a pre-existing EIP-7702 delegation pointing back + at itself. The top-frame charges ``COLD_ACCOUNT_ACCESS`` for the + delegation target (the recipient itself), and then the dispatched + EVM frame runs the recipient's code -- which *is* the delegation + prefix ``0xef 01 00 ``. The leading ``0xef`` decodes as the + ``INVALID`` opcode, consuming the remaining EVM budget. The + intrinsic and top-frame gas remain paid; the value transfer is + rolled back. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + # Pre-allocate an EOA that delegates to itself. The 1-wei balance + # keeps the account alive at top-frame check time so the + # ``NEW_ACCOUNT`` charge does not fire. + target = pre.fund_eoa(amount=1, delegation="Self") + target_code = Spec7702.delegation_designation(target) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + + # The dispatched frame burns the entire EVM budget on the + # ``INVALID`` opcode and the value transfer is rolled back, so the + # sender pays the full ``gas_limit``. + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + top_frame_gas + 50_000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = sender_initial_balance - gas_limit * gas_price + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + # Value transfer rolled back by the ``INVALID``; the pre-tx + # 1-wei balance is preserved. + target: Account(balance=1, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_top_frame_charges_delegation_is_precompile( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Recipient holds a pre-existing EIP-7702 delegation pointing at a + precompile address (``IDENTITY``, ``0x04``). The top-frame charges + ``COLD_ACCOUNT_ACCESS``; the dispatched EVM frame sets + ``disable_precompiles = True`` for delegated calls, so the + precompile body does not run. The code lookup at the precompile + address returns the empty byte string and the frame exits + immediately. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + delegated_to = Address(0x04) + target_code = Spec7702.delegation_designation(delegated_to) + target = pre.deploy_contract(code=target_code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + sends_value=bool(value), + recipient_type=RecipientType.DELEGATION_7702, + ) + + total_gas_cost = intrinsic_gas + top_frame_gas + gas_price = 1_000_000_000 + gas_limit = total_gas_cost + 1000 + + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - total_gas_cost * gas_price + ) + + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=value, code=target_code), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py b/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py index 73edc070e00..f14be0735dd 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_eip_mainnet.py @@ -9,6 +9,7 @@ Alloc, Fork, Op, + RecipientType, StateTestFiller, Transaction, TransactionReceipt, @@ -25,17 +26,29 @@ def test_simple_transfer_mainnet( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test that a simple ETH transfer emits a transfer log on mainnet.""" sender = pre.fund_eoa() recipient = pre.nonexistent_account() + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + gas_limit = intrinsic_gas + top_frame_state_gas + tx = Transaction( ty=0x02, sender=sender, to=recipient, value=1, - gas_limit=21_000, + gas_limit=gas_limit, expected_receipt=TransactionReceipt( logs=[transfer_log(sender, recipient, 1)] ), diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index bbf3877e673..e34f8cb13e2 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -28,6 +28,7 @@ Header, Initcode, Op, + RecipientType, StateTestFiller, Transaction, TransactionException, @@ -97,7 +98,14 @@ def test_bal_balance_changes( calldata=b"", contract_creation=False, access_list=[], + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + total_gas_cost = intrinsic_gas_cost + top_frame_state_gas # Hard-coded gas price allows to calculate the tx final price gas_price = 1_000_000_000 tx_value = 100 @@ -115,7 +123,7 @@ def test_bal_balance_changes( # Account for both the value sent and gas cost (gas_price * gas_used) alice_final_balance = ( - alice_initial_balance - tx_value - (intrinsic_gas_cost * gas_price) + alice_initial_balance - tx_value - (total_gas_cost * gas_price) ) block = Block( @@ -495,8 +503,15 @@ def test_bal_block_rewards( calldata=b"", contract_creation=False, access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, ) - tx_gas_limit = intrinsic_gas + 1000 # add a small buffer + expected_gas_used = intrinsic_gas + top_frame_state_gas + tx_gas_limit = expected_gas_used + 1000 # add a small buffer gas_price = 0xA tx_value = 100 extra_balance = 1000 @@ -516,7 +531,7 @@ def test_bal_block_rewards( # EIP-1559 fee calculation: # - Total gas cost - total_gas_cost = intrinsic_gas * gas_price + total_gas_cost = expected_gas_used * gas_price # - Tip portion genesis_env = Environment(base_fee_per_gas=0x7) @@ -525,7 +540,7 @@ def test_bal_block_rewards( parent_gas_used=0, parent_gas_limit=genesis_env.gas_limit, ) - tip_to_charlie = (gas_price - base_fee_per_gas) * intrinsic_gas + tip_to_charlie = (gas_price - base_fee_per_gas) * expected_gas_used alice_final_balance = alice_initial_balance - tx_value - total_gas_cost @@ -903,7 +918,9 @@ def test_bal_self_transfer( alice = pre.fund_eoa(amount=start_balance) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = intrinsic_gas_calculator( + recipient_type=RecipientType.SELF + ) tx = Transaction( sender=alice, @@ -947,7 +964,9 @@ def test_bal_zero_value_transfer( bob = pre.fund_eoa(amount=100) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = intrinsic_gas_calculator( + recipient_type=RecipientType.EOA + ) tx = Transaction( sender=alice, @@ -1538,8 +1557,14 @@ def test_bal_coinbase_zero_tip( calldata=b"", contract_creation=False, access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, ) - tx_gas_limit = intrinsic_gas + 1000 + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + tx_gas_limit = intrinsic_gas + top_frame_state_gas + 1000 # Calculate base fee genesis_env = Environment(base_fee_per_gas=0x7) @@ -1562,7 +1587,9 @@ def test_bal_coinbase_zero_tip( ) alice_final_balance = ( - alice_initial_balance - tx_value - (intrinsic_gas * base_fee_per_gas) + alice_initial_balance + - tx_value + - ((intrinsic_gas + top_frame_state_gas) * base_fee_per_gas) ) block = Block( @@ -2022,11 +2049,21 @@ def test_bal_multiple_balance_changes_same_account( charlie = pre.fund_eoa(amount=0) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - tx_intrinsic_gas = intrinsic_gas_calculator(calldata=b"", access_list=[]) + tx_intrinsic_gas = intrinsic_gas_calculator( + calldata=b"", + access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) # bob receives funds in tx0, then spends everything in tx1 gas_price = 10 - tx1_gas_cost = tx_intrinsic_gas * gas_price + expected_gas_used = tx_intrinsic_gas + top_frame_state_gas + tx1_gas_cost = expected_gas_used * gas_price spend_amount = 100 funding_amount = tx1_gas_cost + spend_amount @@ -2034,7 +2071,7 @@ def test_bal_multiple_balance_changes_same_account( sender=alice, to=bob, value=funding_amount, - gas_limit=tx_intrinsic_gas, + gas_limit=expected_gas_used, gas_price=gas_price, ) @@ -2042,7 +2079,7 @@ def test_bal_multiple_balance_changes_same_account( sender=bob, to=charlie, value=spend_amount, - gas_limit=tx_intrinsic_gas, + gas_limit=expected_gas_used, gas_price=gas_price, ) @@ -2949,6 +2986,8 @@ def test_bal_cross_tx_funding_chain( target = pre.deploy_contract(code=target_code) intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + # Last hop (eunice -> target) is a plain CONTRACT call with no + # value, so the default intrinsic applies. intrinsic_gas = intrinsic_calc() eunice_exact_gas = intrinsic_gas + target_code.gas_cost(fork) eunice_gas_limit = ( @@ -2957,7 +2996,21 @@ def test_bal_cross_tx_funding_chain( else eunice_exact_gas ) eunice_upfront = eunice_gas_limit * gas_price - transfer_cost = intrinsic_gas * gas_price + # Forwarding hops (alice -> bob, ..., dan -> eunice) transfer value + # to recipients that begin empty, so each pays the value-transfer + # intrinsic surcharges plus the top-frame ``NEW_ACCOUNT`` state + # charge that fires under EIP-2780. With the default zero + # state-gas reservoir the latter spills entirely into regular gas. + forwarding_intrinsic = intrinsic_calc( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + forwarding_top_frame_state = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + forwarding_gas = forwarding_intrinsic + forwarding_top_frame_state + transfer_cost = forwarding_gas * gas_price # Each sender (including alice) starts with or receives exactly what # the next forward + its own gas demands; everyone ends at zero in @@ -2990,28 +3043,28 @@ def test_bal_cross_tx_funding_chain( sender=alice, to=bob, value=alice_value, - gas_limit=intrinsic_gas, + gas_limit=forwarding_gas, gas_price=gas_price, ), Transaction( sender=bob, to=charlie, value=bob_value, - gas_limit=intrinsic_gas, + gas_limit=forwarding_gas, gas_price=gas_price, ), Transaction( sender=charlie, to=dan, value=charlie_value, - gas_limit=intrinsic_gas, + gas_limit=forwarding_gas, gas_price=gas_price, ), Transaction( sender=dan, to=eunice, value=dan_value, - gas_limit=intrinsic_gas, + gas_limit=forwarding_gas, gas_price=gas_price, ), Transaction( @@ -3628,7 +3681,11 @@ def test_bal_gas_limit_boundary( if with_tx: alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) + # Fund bob with 1 wei so the recipient is alive at top-frame + # check time; this avoids the EIP-2780 ``NEW_ACCOUNT`` state + # charge that would otherwise inflate the tx's gas needs past + # the BAL-sized ``block_gas_limit``. + bob = pre.fund_eoa(amount=1) # alice (sender) + bob (recipient) + coinbase (EIP-3651 warm). extra_items += 3 txs.append( @@ -3644,10 +3701,10 @@ def test_bal_gas_limit_boundary( ) expected_accounts[bob] = BalAccountExpectation( balance_changes=[ - BalBalanceChange(block_access_index=1, post_balance=1) + BalBalanceChange(block_access_index=1, post_balance=2) ], ) - post[bob] = Account(balance=1) + post[bob] = Account(balance=2) if with_cl_withdrawal: charlie = pre.fund_eoa(amount=0) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py index a78d67763dc..ce091534195 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py @@ -21,6 +21,7 @@ Header, Initcode, Op, + RecipientType, Transaction, Withdrawal, compute_create_address, @@ -730,7 +731,15 @@ def test_bal_withdrawal_to_coinbase( coinbase = pre.fund_eoa(amount=0) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas = intrinsic_gas_calculator() + intrinsic_gas = intrinsic_gas_calculator( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + total_intrinsic_gas = intrinsic_gas + top_frame_state_gas # Calculate tip to coinbase genesis_env = Environment(base_fee_per_gas=0x7) @@ -755,11 +764,11 @@ def test_bal_withdrawal_to_coinbase( sender=alice, to=bob, value=tx_value, - gas_limit=intrinsic_gas, + gas_limit=total_intrinsic_gas, **tx_kwargs, ) - tip_to_coinbase = priority_fee * intrinsic_gas + tip_to_coinbase = priority_fee * total_intrinsic_gas withdrawal_amount = 10 coinbase_final_balance = tip_to_coinbase + (withdrawal_amount * GWEI) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py index 33585b2a3cc..c788617a287 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py @@ -28,6 +28,7 @@ Header, Initcode, Op, + RecipientType, Storage, Transaction, Withdrawal, @@ -1464,14 +1465,21 @@ def test_bal_invalid_missing_coinbase( calldata=b"", contract_creation=False, access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + total_intrinsic_gas = intrinsic_gas + top_frame_state_gas gas_price = 0xA tx = Transaction( sender=alice, to=bob, value=100, - gas_limit=intrinsic_gas + 1000, + gas_limit=total_intrinsic_gas + 1000, gas_price=gas_price, ) @@ -1481,7 +1489,7 @@ def test_bal_invalid_missing_coinbase( parent_gas_used=0, parent_gas_limit=genesis_env.gas_limit, ) - tip = (gas_price - base_fee_per_gas) * intrinsic_gas + tip = (gas_price - base_fee_per_gas) * total_intrinsic_gas blockchain_test( pre=pre, @@ -1546,14 +1554,21 @@ def test_bal_invalid_coinbase_balance_value( calldata=b"", contract_creation=False, access_list=[], + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, ) + total_intrinsic_gas = intrinsic_gas + top_frame_state_gas gas_price = 0xA tx = Transaction( sender=alice, to=bob, value=100, - gas_limit=intrinsic_gas + 1000, + gas_limit=total_intrinsic_gas + 1000, gas_price=gas_price, ) @@ -1563,7 +1578,7 @@ def test_bal_invalid_coinbase_balance_value( parent_gas_used=0, parent_gas_limit=genesis_env.gas_limit, ) - tip = (gas_price - base_fee_per_gas) * intrinsic_gas + tip = (gas_price - base_fee_per_gas) * total_intrinsic_gas blockchain_test( pre=pre, diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py index c62550daa9d..11ef0937dd3 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_additional_coverage.py @@ -144,6 +144,11 @@ def test_token_calculation_verification( expected_intrinsic_cost = gas_costs.TX_BASE + ( expected_standard_tokens * gas_costs.TX_DATA_TOKEN_STANDARD ) + if fork.is_eip_enabled(2780): + # EIP-2780 surfaces an explicit recipient-access charge for + # non-self, non-create transactions; the ``to`` fixture + # defaults to a deployed contract, so the charge applies. + expected_intrinsic_cost += gas_costs.COLD_ACCOUNT_ACCESS assert intrinsic_cost_before_execution == expected_intrinsic_cost, ( f"Intrinsic cost mismatch for {description}: " f"{intrinsic_cost_before_execution} != {expected_intrinsic_cost} " diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py index b913e674f4d..4561cf54626 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/test_floor_boundary_exact_balance.py @@ -25,7 +25,7 @@ @pytest.mark.parametrize( "zero_bytes", [ - pytest.param(100, id="100_zero_bytes"), + pytest.param(200, id="200_zero_bytes"), pytest.param(1000, id="1000_zero_bytes"), ], ) 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 962e26fbdec..50d8547e2cb 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 @@ -625,7 +625,9 @@ def test_tx_inclusion_at_regular_gas_block_limit_small( """ gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + ) filler_tx_count = (fork.minimum_block_gas_limit() // intrinsic_gas) + 1 block_gas_limit = intrinsic_gas * (filler_tx_count + 1) @@ -636,6 +638,7 @@ def test_tx_inclusion_at_regular_gas_block_limit_small( Transaction( to=dest_contract, gas_limit=intrinsic_gas, + value=1, sender=filler_sender, ) for _ in range(filler_tx_count) @@ -647,6 +650,7 @@ def test_tx_inclusion_at_regular_gas_block_limit_small( excess_tx = Transaction( to=dest_contract, gas_limit=excess_tx_gas_limit, + value=1, sender=pre.fund_eoa(), error=error, ) 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 3a4fd08acd0..b9b5bde06f5 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 @@ -2472,7 +2472,7 @@ def test_selfdestruct_in_create_tx_initcode( sender = pre.fund_eoa() intrinsic_calc = fork.transaction_intrinsic_cost_calculator() intrinsic_total = intrinsic_calc( - calldata=bytes(initcode), contract_creation=True + calldata=bytes(initcode), contract_creation=True, sends_value=True ) expected_state = create_state_gas + gas_costs.NEW_ACCOUNT 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 37ad180f529..04a109b5cd8 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 @@ -58,6 +58,10 @@ def test_exact_coinbase_fee_simple_sstore( # 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 diff --git a/tests/berlin/eip2930_access_list/test_acl.py b/tests/berlin/eip2930_access_list/test_acl.py index 226f842bc6c..9e7299994e6 100644 --- a/tests/berlin/eip2930_access_list/test_acl.py +++ b/tests/berlin/eip2930_access_list/test_acl.py @@ -235,6 +235,7 @@ def test_transaction_intrinsic_gas_cost( calldata=tx_data, contract_creation=contract_creation, access_list=access_lists, + sends_value=True, ) if not enough_gas: tx_gas_limit -= 1 diff --git a/tests/cancun/eip4844_blobs/conftest.py b/tests/cancun/eip4844_blobs/conftest.py index 1504daa458f..6f93d162f88 100644 --- a/tests/cancun/eip4844_blobs/conftest.py +++ b/tests/cancun/eip4844_blobs/conftest.py @@ -9,6 +9,7 @@ Environment, Fork, Hash, + RecipientType, Transaction, TransitionFork, add_kzg_version, @@ -333,12 +334,20 @@ def non_zero_blob_gas_used_genesis_block( ] def create_blob_transaction(blob_range: Iterable[int]) -> Transaction: + intrinsic_gas = block_fork.transaction_intrinsic_cost_calculator()( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + top_frame_gas = block_fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) return Transaction( ty=Spec.BLOB_TX_TYPE, sender=sender, to=empty_account_destination, value=1, - gas_limit=21_000, + gas_limit=intrinsic_gas + top_frame_gas, max_fee_per_gas=tx_max_fee_per_gas, max_priority_fee_per_gas=0, max_fee_per_blob_gas=blob_gas_price_calculator( diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 50be25f9524..9b05cf607ef 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -33,6 +33,7 @@ Hash, Header, Op, + RecipientType, Removable, StateTestFiller, Storage, @@ -75,19 +76,93 @@ def destination_account( return pre.fund_eoa(destination_account_balance) +def _destination_recipient_type( + destination_account_code: Bytecode | None, + destination_account_balance: int, +) -> RecipientType: + if destination_account_code is not None: + return RecipientType.CONTRACT + if destination_account_balance == 0: + return RecipientType.EMPTY_ACCOUNT + return RecipientType.EOA + + @pytest.fixture def tx_gas( fork: Fork | TransitionFork, tx_calldata: bytes, tx_access_list: List[AccessList], + tx_value: int, + destination_account_code: Bytecode | None, + destination_account_balance: int, ) -> int: """Gas allocated to transactions sent during test.""" + post_transition_fork = fork.transitions_to() tx_intrinsic_cost_calculator = ( - fork.transitions_to().transaction_intrinsic_cost_calculator() + post_transition_fork.transaction_intrinsic_cost_calculator() + ) + recipient_type = _destination_recipient_type( + destination_account_code, destination_account_balance + ) + sends_value = tx_value > 0 + intrinsic = tx_intrinsic_cost_calculator( + calldata=tx_calldata, + access_list=tx_access_list, + recipient_type=recipient_type, + sends_value=sends_value, ) - return tx_intrinsic_cost_calculator( - calldata=tx_calldata, access_list=tx_access_list + top_frame_state = post_transition_fork.transaction_top_frame_state_gas( + recipient_type=recipient_type, + sends_value=sends_value, ) + return intrinsic + top_frame_state + + +@pytest.fixture +def tx_gas_per_tx( + fork: Fork | TransitionFork, + tx_gas: int, + tx_calldata: bytes, + tx_access_list: List[AccessList], + tx_value: int, + destination_account_code: Bytecode | None, + destination_account_balance: int, + blob_hashes_per_tx: List[List[bytes]], +) -> List[int]: + """ + Gas allocated to each transaction in the block. + + After the first value-sending tx to an initially-empty destination, + the recipient is no longer empty, so the EIP-2780 top-frame + ``NEW_ACCOUNT`` state-gas charge does not fire on subsequent txs. + """ + n_txs = len(blob_hashes_per_tx) + if n_txs <= 1: + return [tx_gas] * n_txs + + destination_starts_empty = ( + destination_account_code is None and destination_account_balance == 0 + ) + if destination_starts_empty and tx_value > 0: + post_transition_fork = fork.transitions_to() + intrinsic_calc = ( + post_transition_fork.transaction_intrinsic_cost_calculator() + ) + intrinsic = intrinsic_calc( + calldata=tx_calldata, + access_list=tx_access_list, + recipient_type=RecipientType.EOA, + sends_value=True, + ) + top_frame_state = post_transition_fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EOA, + sends_value=True, + ) + tx_gas_nonempty = intrinsic + top_frame_state + else: + tx_gas_nonempty = tx_gas + + return [tx_gas] + [tx_gas_nonempty] * (n_txs - 1) @pytest.fixture @@ -124,7 +199,7 @@ def blob_hashes_per_tx(blobs_per_tx: List[int]) -> List[List[Hash]]: @pytest.fixture def total_account_minimum_balance( # noqa: D103 blob_gas_per_blob: int, - tx_gas: int, + tx_gas_per_tx: List[int], tx_value: int, tx_max_fee_per_gas: int, tx_max_fee_per_blob_gas: int, @@ -135,15 +210,17 @@ def total_account_minimum_balance( # noqa: D103 transactions in the block of the test. """ minimum_cost = 0 - for tx_blob_count in [len(x) for x in blob_hashes_per_tx]: + for tx_i, tx_blob_count in enumerate(len(x) for x in blob_hashes_per_tx): blob_cost = tx_max_fee_per_blob_gas * blob_gas_per_blob * tx_blob_count - minimum_cost += (tx_gas * tx_max_fee_per_gas) + tx_value + blob_cost + minimum_cost += ( + (tx_gas_per_tx[tx_i] * tx_max_fee_per_gas) + tx_value + blob_cost + ) return minimum_cost @pytest.fixture def total_account_transactions_fee( # noqa: D103 - tx_gas: int, + tx_gas_per_tx: List[int], tx_value: int, blob_gas_price: int, block_base_fee_per_gas: int, @@ -156,7 +233,7 @@ def total_account_transactions_fee( # noqa: D103 Calculate actual fee for the blob transactions in the block of the test. """ total_cost = 0 - for tx_blob_count in [len(x) for x in blob_hashes_per_tx]: + for tx_i, tx_blob_count in enumerate(len(x) for x in blob_hashes_per_tx): blob_cost = blob_gas_price * blob_gas_per_blob * tx_blob_count block_producer_fee = ( tx_max_fee_per_gas - block_base_fee_per_gas @@ -164,7 +241,7 @@ def total_account_transactions_fee( # noqa: D103 else 0 ) total_cost += ( - (tx_gas * (block_base_fee_per_gas + block_producer_fee)) + tx_gas_per_tx[tx_i] * (block_base_fee_per_gas + block_producer_fee) + tx_value + blob_cost ) @@ -208,7 +285,7 @@ def sender(pre: Alloc, sender_initial_balance: int) -> Address: # noqa: D103 def txs( # noqa: D103 sender: EOA, destination_account: Optional[Address], - tx_gas: int, + tx_gas_per_tx: List[int], tx_value: int, tx_calldata: bytes, tx_max_fee_per_gas: int, @@ -225,7 +302,7 @@ def txs( # noqa: D103 sender=sender, to=destination_account, value=tx_value, - gas_limit=tx_gas, + gas_limit=tx_gas_per_tx[tx_i], data=tx_calldata, max_fee_per_gas=tx_max_fee_per_gas, max_priority_fee_per_gas=tx_max_priority_fee_per_gas, @@ -754,6 +831,7 @@ def test_sufficient_balance_blob_tx( @pytest.mark.valid_from("Cancun") def test_sufficient_balance_blob_tx_pre_fund_tx( blockchain_test: BlockchainTestFiller, + fork: Fork, total_account_minimum_balance: int, sender: EOA, env: Environment, @@ -773,15 +851,29 @@ def test_sufficient_balance_blob_tx_pre_fund_tx( - Transactions with max fee per blob gas lower or higher than the priority fee """ + recipient_type = ( + RecipientType.EOA if sender in pre else RecipientType.EMPTY_ACCOUNT + ) + sends_value = total_account_minimum_balance > 0 + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_calc( + recipient_type=recipient_type, + sends_value=sends_value, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + recipient_type=recipient_type, + sends_value=sends_value, + ) + pre_funding_gas_limit = intrinsic_gas + top_frame_state_gas pre_funding_sender = pre.fund_eoa( - amount=(21_000 * 100) + total_account_minimum_balance + amount=(pre_funding_gas_limit * 100) + total_account_minimum_balance ) txs = [ Transaction( sender=pre_funding_sender, to=sender, value=total_account_minimum_balance, - gas_limit=21_000, + gas_limit=pre_funding_gas_limit, ) ] + txs blockchain_test( diff --git a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py index 05a64e6befd..08c8f8e8445 100644 --- a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py +++ b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code.py @@ -152,6 +152,14 @@ def test_create2_oo_gafter_init_code( + (gas_costs.OPCODE_CREATE_BASE - _cancun_create_base) - gas_costs.CODE_DEPOSIT_PER_BYTE * _deploy_size ) + # EIP-2780 reshapes the tx intrinsic for non-self non-value txs: + # ``TX_BASE`` drops to 12_000 and an explicit + # ``COLD_ACCOUNT_ACCESS`` (3_000) recipient charge is added. The + # original test was built against Cancun's flat ``TX_BASE`` of + # 21_000, so shift the budget by the intrinsic delta to keep the + # straddle landing at the same RETURN point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + _oog_lift += intrinsic - 21_000 tx_gas = [54000 + _oog_lift, 55000 + _oog_lift] tx = Transaction( diff --git a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py index 6e5e05c2593..8fcff4f2bbd 100644 --- a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py +++ b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code.py @@ -148,6 +148,14 @@ def test_create_oo_gafter_init_code( + (gas_costs.OPCODE_CREATE_BASE - _cancun_create_base) - gas_costs.CODE_DEPOSIT_PER_BYTE * _deploy_size ) + # EIP-2780 reshapes the tx intrinsic for non-self non-value txs: + # ``TX_BASE`` drops to 12_000 and an explicit + # ``COLD_ACCOUNT_ACCESS`` (3_000) recipient charge is added. The + # original test was built against Cancun's flat ``TX_BASE`` of + # 21_000, so shift the budget by the intrinsic delta to keep the + # straddle landing at the same RETURN point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + _oog_lift += intrinsic - 21_000 tx_gas = [54000 + _oog_lift, 55000 + _oog_lift] tx = Transaction( diff --git a/tests/ported_static/stEIP150Specific/test_call_and_callcode_consume_more_gas_then_transaction_has.py b/tests/ported_static/stEIP150Specific/test_call_and_callcode_consume_more_gas_then_transaction_has.py index aa6ecf33493..1e588e4489e 100644 --- a/tests/ported_static/stEIP150Specific/test_call_and_callcode_consume_more_gas_then_transaction_has.py +++ b/tests/ported_static/stEIP150Specific/test_call_and_callcode_consume_more_gas_then_transaction_has.py @@ -3,6 +3,16 @@ Ported from: state_tests/stEIP150Specific/CallAndCallcodeConsumeMoreGasThenTransactionHasFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts +`storage[8] = 0x8D5B6` captured by `Op.GAS`, which depends on the exact +post-intrinsic execution budget. The original hardcoded `gas_limit` of +600_000 was built against Cancun's `TX_BASE` of 21_000; EIP-2780 lowers +the intrinsic for non-self non-value txs, so `gas_limit` is derived as +`600_000 + (intrinsic - 21_000)` from `transaction_intrinsic_cost_calculator` +to shift by the fork intrinsic delta and keep the Op.GAS assertion correct. +The `- 21_000` is the pre-EIP-2780 baseline intrinsic, so the adjustment +is exactly 0 pre-repricing. Do not hardcode the literal gas_limit. """ import pytest @@ -12,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +42,7 @@ def test_call_and_callcode_consume_more_gas_then_transaction_has( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_and_callcode_consume_more_gas_then_transaction_has.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -83,11 +95,19 @@ def test_call_and_callcode_consume_more_gas_then_transaction_has( nonce=0, ) + # The original test was built against Cancun's ``TX_BASE`` of + # 21_000. EIP-2780 lowers the intrinsic for non-self non-value + # txs, so shift ``gas_limit`` by the intrinsic delta to preserve + # the post-intrinsic execution budget the Op.GAS storage + # assertions depend on. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = {target: Account(storage={0: 18, 8: 0x8D5B6, 9: 1, 10: 1})} diff --git a/tests/ported_static/stEIP150Specific/test_call_goes_oog_on_second_level.py b/tests/ported_static/stEIP150Specific/test_call_goes_oog_on_second_level.py index ce3ec8f7f19..954c5e29ae7 100644 --- a/tests/ported_static/stEIP150Specific/test_call_goes_oog_on_second_level.py +++ b/tests/ported_static/stEIP150Specific/test_call_goes_oog_on_second_level.py @@ -3,6 +3,14 @@ Ported from: state_tests/stEIP150Specific/CallGoesOOGOnSecondLevelFiller.json + +@manually-enhanced: Do not overwrite. The `gas_limit` is derived from +the fork intrinsic calculator instead of the original hardcoded value. +The test fixes the post-intrinsic budget that the nested Op.GAS storage +assertions (8: 0x927BE, 8: 0x213FB6) depend on, so it shifts the base +2_200_000 budget by the intrinsic delta versus the pre-EIP-2780 Cancun +baseline of 21_000 (`intrinsic - 21_000`). This stays correct across +the EIP-2780 intrinsic decomposition. Do not hardcode the gas_limit. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,6 +38,7 @@ def test_call_goes_oog_on_second_level( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_goes_oog_on_second_level.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -93,11 +103,19 @@ def test_call_goes_oog_on_second_level( nonce=0, ) + # The original test was built against Cancun's ``TX_BASE`` of + # 21_000. EIP-2780 lowers the intrinsic for non-self non-value + # txs, so shift ``gas_limit`` by the intrinsic delta to preserve + # the post-intrinsic execution budget the Op.GAS storage + # assertions depend on. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 2_200_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=2200000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stEIP1559/test_low_gas_limit.py b/tests/ported_static/stEIP1559/test_low_gas_limit.py index d2baa131eae..9e42e627208 100644 --- a/tests/ported_static/stEIP1559/test_low_gas_limit.py +++ b/tests/ported_static/stEIP1559/test_low_gas_limit.py @@ -3,6 +3,13 @@ Ported from: state_tests/stEIP1559/lowGasLimitFiller.yml + +@manually-enhanced: Do not overwrite. The `-g3` case must sit just below +the fork intrinsic to trigger `INTRINSIC_GAS_TOO_LOW`. EIP-2780 decomposes +and lowers the intrinsic, so the original hardcoded `20000` is no longer +below it; instead derive `intrinsic - 1` from the fork's +`transaction_intrinsic_cost_calculator()` for the single zero-byte +calldata so the boundary stays correct across the repricing. """ import pytest @@ -129,7 +136,13 @@ def test_low_gas_limit( tx_data = [ Bytes("00"), ] - tx_gas = [90000, 50000, 25000, 20000] + # -g3 must sit below the fork's intrinsic to trigger + # ``INTRINSIC_GAS_TOO_LOW``. EIP-2780 lowers the intrinsic so the + # original ``20000`` is no longer below it; derive the boundary. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=Bytes("00"), + ) + tx_gas = [90000, 50000, 25000, intrinsic - 1] tx_access_lists: dict[int, list] = { 0: [], } diff --git a/tests/ported_static/stEIP2930/test_transaction_costs.py b/tests/ported_static/stEIP2930/test_transaction_costs.py index 3695bf9a542..fb03a48592e 100644 --- a/tests/ported_static/stEIP2930/test_transaction_costs.py +++ b/tests/ported_static/stEIP2930/test_transaction_costs.py @@ -3,6 +3,16 @@ Ported from: state_tests/stEIP2930/transactionCostsFiller.yml + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance after a STOP-only call. For Amsterdam+ it is derived from +`fork.transaction_intrinsic_cost_calculator()` (over calldata, +access_list, and sends_value) as `pre_balance - tx.value - +intrinsic_gas * gas_price`, instead of a hardcoded literal, so the +access-list-heavy cases stay correct across the EIP-2780 intrinsic +decomposition and EIP-7981/EIP-8038 access-list repricing. Pre-Amsterdam +forks (Cancun/Prague) keep their original hardcoded balances. No +21_000 baseline or SSTORE-clear constants are subtracted here. """ import pytest @@ -471,6 +481,7 @@ def test_transaction_costs( calldata=tx.data, contract_creation=tx.to is None, access_list=tx.access_list, + sends_value=bool(tx.value), ) post[sender] = Account( balance=( diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expanding_calls.py index ff44e5b435b..d9ae39a4bb4 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expanding_calls.py @@ -3,6 +3,14 @@ Ported from: state_tests/stMemExpandingEIP150Calls/CallAndCallcodeConsumeMoreGasThenTransactionHasWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the +remaining-gas snapshot stored by `Op.GAS` (slot 8 == 0x8D5B6), which +fixes the post-intrinsic execution budget. So `gas_limit` is derived +from the fork as `600_000 + (intrinsic - 21_000)`: it shifts the budget +by the intrinsic delta from the pre-EIP-2780 Cancun `TX_BASE` baseline +of 21_000, keeping the budget constant across the EIP-2780 intrinsic +decomposition and EIP-8038 access repricing. Do not hardcode 600_000. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +40,7 @@ def test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expanding_calls( # noqa: E501 state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_call_and_callcode_consume_more_gas_then_transaction_has_with_m...""" # noqa: E501 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -82,11 +92,19 @@ def test_call_and_callcode_consume_more_gas_then_transaction_has_with_mem_expand nonce=0, ) + # The original test was built against Cancun's ``TX_BASE`` of + # 21_000. EIP-2780 lowers the intrinsic for non-self non-value + # txs, so shift ``gas_limit`` by the intrinsic delta to preserve + # the post-intrinsic execution budget the Op.GAS storage + # assertion depends on. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_delegate_call_on_eip_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_delegate_call_on_eip_with_mem_expanding_calls.py index dec68916fab..7e3953c09c0 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_delegate_call_on_eip_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_delegate_call_on_eip_with_mem_expanding_calls.py @@ -3,6 +3,16 @@ Ported from: state_tests/stMemExpandingEIP150Calls/DelegateCallOnEIPWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the GAS +opcode value stored at target slot 8 (0x8D5B6), which depends on the +execution budget left after the intrinsic charge. The original test +hardcoded `gas_limit` against Cancun's `TX_BASE` of 21_000; EIP-2780 +lowers the intrinsic for non-self non-value txs, so `gas_limit` is +derived from the fork as `600_000 + (intrinsic - 21_000)`, subtracting +the pre-EIP-2780 baseline 21_000 so the budget is invariant across the +intrinsic decomposition and EIP-8038 access repricing. Do not hardcode +the literal gas_limit. """ import pytest @@ -12,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +42,7 @@ def test_delegate_call_on_eip_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_delegate_call_on_eip_with_mem_expanding_calls.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -69,11 +81,19 @@ def test_delegate_call_on_eip_with_mem_expanding_calls( nonce=0, ) + # The original test was built against Cancun's ``TX_BASE`` of + # 21_000. EIP-2780 lowers the intrinsic for non-self non-value + # txs, so shift ``gas_limit`` by the intrinsic delta to preserve + # the post-intrinsic execution budget the Op.GAS storage + # assertion depends on. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stRefundTest/test_refund50_1.py b/tests/ported_static/stRefundTest/test_refund50_1.py index 0a631f155bb..1dbb8c5ccb7 100644 --- a/tests/ported_static/stRefundTest/test_refund50_1.py +++ b/tests/ported_static/stRefundTest/test_refund50_1.py @@ -77,15 +77,18 @@ def test_refund50_1( gas_limit=100000, ) - # EIP-8038 raises each cold SSTORE-clear charge; with the EIP-3529 - # refund cap binding, gas_used rises by 4/5 of the extra charge. + # EIP-8038 raises each cold SSTORE-clear charge and EIP-2780 + # shifts the tx intrinsic. With the EIP-3529 refund cap binding, + # gas_used rises by 4/5 of the gross-gas delta. cold_clear_delta = ( Op.SSTORE.with_metadata( key_warm=False, original_value=1, current_value=1, new_value=0 ).gas_cost(fork) - 5000 ) - extra_gas_used = 5 * cold_clear_delta * 4 // 5 + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 + gross_delta = 5 * cold_clear_delta + intrinsic_delta + extra_gas_used = gross_delta * 4 // 5 post = { target: Account(storage={}), diff --git a/tests/ported_static/stRefundTest/test_refund_ff.py b/tests/ported_static/stRefundTest/test_refund_ff.py index 3de243ac410..e0535dc9ab8 100644 --- a/tests/ported_static/stRefundTest/test_refund_ff.py +++ b/tests/ported_static/stRefundTest/test_refund_ff.py @@ -85,7 +85,12 @@ def test_refund_ff( ).gas_cost(fork) - 7600 ) + # EIP-2780 lowers the intrinsic for non-self non-value txs; the + # delta is negative on Amsterdam, so it reduces ``gas_used`` and + # raises the sender balance correspondingly. + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 + gas_used_delta = selfdestruct_delta + intrinsic_delta - post = {sender: Account(balance=0xE8D4A51000 - 1000 * selfdestruct_delta)} + post = {sender: Account(balance=0xE8D4A51000 - 1000 * gas_used_delta)} state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_get_ether_back.py b/tests/ported_static/stRefundTest/test_refund_get_ether_back.py index 88385a01af7..2411e062abd 100644 --- a/tests/ported_static/stRefundTest/test_refund_get_ether_back.py +++ b/tests/ported_static/stRefundTest/test_refund_get_ether_back.py @@ -76,24 +76,35 @@ def test_refund_get_ether_back( ) # Gas used = gross gas minus the capped storage-clear refund. The - # non-SSTORE gross gas is fork-invariant: the intrinsic transaction - # cost plus the two PUSH1s that feed the single SSTORE (STOP is free). + # non-SSTORE gross gas comes from the fork's intrinsic calculator + # (covers TX_BASE and any EIP-2780 recipient/value surcharges) + # plus the two PUSH1s that feed the single SSTORE (STOP is free). gas_costs = fork.gas_costs() - base_gross = gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + # ``return_cost_deducted_prior_execution=True`` returns the + # upfront-deducted intrinsic only (Prague's calc would otherwise + # return ``max(intrinsic, EIP-7623 floor)``). + intrinsic = fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(tx.value), + return_cost_deducted_prior_execution=True, + ) + base_gross = intrinsic + 2 * gas_costs.VERY_LOW + cancun_base_gross = 21_000 + 2 * gas_costs.VERY_LOW - def clear_gas_used(sstore_charge: int, clear_refund: int) -> int: - gross = base_gross + sstore_charge + def clear_gas_used( + sstore_charge: int, clear_refund: int, gross_base: int + ) -> int: + gross = gross_base + sstore_charge return gross - min(clear_refund, gross // 5) sstore_charge = Op.SSTORE.with_metadata( key_warm=False, original_value=1, current_value=1, new_value=0 ).gas_cost(fork) # Cancun charges 5000 for the clear and refunds 4800; subtracting the - # same model evaluated at those constants makes this exactly 0 before - # the EIP-8037/8038 repricing. + # same model evaluated at those constants and the Cancun base makes + # this exactly 0 before the EIP-8037/8038 repricing. gas_used_delta = clear_gas_used( - sstore_charge, gas_costs.REFUND_STORAGE_CLEAR - ) - clear_gas_used(5000, 4800) + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR, base_gross + ) - clear_gas_used(5000, 4800, cancun_base_gross) post = { target: Account(storage={}, balance=0xDE0B6B3A764000A), diff --git a/tests/ported_static/stRefundTest/test_refund_max.py b/tests/ported_static/stRefundTest/test_refund_max.py index 0270feee167..ac549958112 100644 --- a/tests/ported_static/stRefundTest/test_refund_max.py +++ b/tests/ported_static/stRefundTest/test_refund_max.py @@ -107,15 +107,18 @@ def test_refund_max( access_list=[], ) - # EIP-8038 raises each cold SSTORE-clear charge; with the EIP-3529 - # refund cap binding, gas_used rises by 4/5 of the extra charge. + # EIP-8038 raises each cold SSTORE-clear charge and EIP-2780 + # shifts the tx intrinsic. With the EIP-3529 refund cap binding, + # gas_used rises by 4/5 of the gross-gas delta. cold_clear_delta = ( Op.SSTORE.with_metadata( key_warm=False, original_value=1, current_value=1, new_value=0 ).gas_cost(fork) - 5000 ) - extra_gas_used = 8 * cold_clear_delta * 4 // 5 + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 + gross_delta = 8 * cold_clear_delta + intrinsic_delta + extra_gas_used = gross_delta * 4 // 5 post = {sender: Account(balance=0xE8D55F7E90 - 1000 * extra_gas_used)} diff --git a/tests/ported_static/stRefundTest/test_refund_multimple_suicide.py b/tests/ported_static/stRefundTest/test_refund_multimple_suicide.py index 015e02e5d2a..97571fbd22c 100644 --- a/tests/ported_static/stRefundTest/test_refund_multimple_suicide.py +++ b/tests/ported_static/stRefundTest/test_refund_multimple_suicide.py @@ -3,6 +3,15 @@ Ported from: state_tests/stRefundTest/refund_multimpleSuicideFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which the original fixture hardcoded as 0x61EC43A. EIP-2780 +decomposes the intrinsic cost and lowers it for non-self, non-value +txs, so the balance is derived from the fork model instead: take +`fork.transaction_intrinsic_cost_calculator()()` minus the pre-EIP-2780 +baseline 21_000, then add `gas_price (10) * |delta|` back to the sender +(the delta is negative on Amsterdam). This keeps the adjustment exactly +0 pre-EIP-2780. Do not hardcode the Amsterdam value. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +39,7 @@ def test_refund_multimple_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_multimple_suicide.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -151,10 +162,14 @@ def test_refund_multimple_suicide( gas_limit=300000, ) + # EIP-2780 lowers the intrinsic for non-self non-value txs; the + # delta is negative on Amsterdam and raises the sender balance by + # ``gas_price * |delta|``. + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 post = { target: Account(balance=0xDE0B6B3A7640000), coinbase: Account(balance=0), - sender: Account(balance=0x61EC43A, nonce=1), + sender: Account(balance=0x61EC43A - 10 * intrinsic_delta, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_no_oog_1.py b/tests/ported_static/stRefundTest/test_refund_no_oog_1.py index 764d3b6dc5d..b74b450caf3 100644 --- a/tests/ported_static/stRefundTest/test_refund_no_oog_1.py +++ b/tests/ported_static/stRefundTest/test_refund_no_oog_1.py @@ -66,37 +66,47 @@ def test_refund_no_oog_1( nonce=0, ) - # EIP-8038 raises the cold SSTORE-clear charge; bump the gas limit by - # the charge delta so the clear still lands exactly at the limit - # (the "no out-of-gas" boundary) instead of running out of gas. + # EIP-8038 raises the cold SSTORE-clear charge and EIP-2780 shifts + # the tx intrinsic; bump the gas limit by both deltas so the clear + # still lands exactly at the limit (the "no out-of-gas" boundary) + # instead of running out of gas. sstore_charge = Op.SSTORE.with_metadata( key_warm=False, original_value=1, current_value=1, new_value=0 ).gas_cost(fork) cold_clear_delta = sstore_charge - 5000 + # ``return_cost_deducted_prior_execution=True`` returns the + # upfront-deducted intrinsic only (Prague's calc would otherwise + # return ``max(intrinsic, EIP-7623 floor)``). + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True, + ) + intrinsic_delta = intrinsic - 21_000 tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=26006 + cold_clear_delta, + gas_limit=26006 + cold_clear_delta + intrinsic_delta, ) # Gas used = gross gas minus the capped storage-clear refund. The - # non-SSTORE gross gas is fork-invariant: the intrinsic transaction - # cost plus the two PUSH1s that feed the single SSTORE (STOP is free). + # non-SSTORE gross gas comes from the fork's intrinsic calculator + # (covers TX_BASE and any EIP-2780 recipient surcharge) plus the + # two PUSH1s that feed the single SSTORE (STOP is free). gas_costs = fork.gas_costs() - base_gross = gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + base_gross = intrinsic + 2 * gas_costs.VERY_LOW + cancun_base_gross = 21_000 + 2 * gas_costs.VERY_LOW - def clear_gas_used(charge: int, clear_refund: int) -> int: - gross = base_gross + charge + def clear_gas_used(charge: int, clear_refund: int, gross_base: int) -> int: + gross = gross_base + charge return gross - min(clear_refund, gross // 5) # Cancun charges 5000 for the clear and refunds 4800; subtracting the - # same model evaluated at those constants makes this exactly 0 before - # the EIP-8037/8038 repricing. + # same model evaluated at those constants and the Cancun base makes + # this exactly 0 before the EIP-8037/8038 repricing. gas_used_delta = clear_gas_used( - sstore_charge, gas_costs.REFUND_STORAGE_CLEAR - ) - clear_gas_used(5000, 4800) + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR, base_gross + ) - clear_gas_used(5000, 4800, cancun_base_gross) post = { target: Account(storage={}), diff --git a/tests/ported_static/stRefundTest/test_refund_single_suicide.py b/tests/ported_static/stRefundTest/test_refund_single_suicide.py index 4cee829af0d..a1da9e3e582 100644 --- a/tests/ported_static/stRefundTest/test_refund_single_suicide.py +++ b/tests/ported_static/stRefundTest/test_refund_single_suicide.py @@ -3,6 +3,15 @@ Ported from: state_tests/stRefundTest/refund_singleSuicideFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance, which the original fixture hardcoded. EIP-2780 decomposes the +intrinsic and lowers it for this non-self, non-value tx, so the balance +is derived from the fork: ``intrinsic_delta`` subtracts the pre-EIP-2780 +baseline intrinsic 21_000 from the fork's intrinsic calculator (the +literal 21_000 is the old TX_BASE), making the delta 0 pre-EIP-2780 and +negative on Amsterdam. The sender balance is then adjusted by +``gas_price * intrinsic_delta`` (base fee 10). Do not hardcode it. """ import pytest @@ -15,6 +24,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,6 +39,7 @@ def test_refund_single_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_refund_single_suicide.""" coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) @@ -126,10 +137,14 @@ def test_refund_single_suicide( gas_limit=300000, ) + # EIP-2780 lowers the intrinsic for non-self non-value txs; the + # delta is negative on Amsterdam and raises the sender balance by + # ``gas_price * |delta|``. + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 post = { target: Account(balance=0xDE0B6B3A7640000), coinbase: Account(balance=0), - sender: Account(balance=0x1C5AF34, nonce=1), + sender: Account(balance=0x1C5AF34 - 10 * intrinsic_delta, nonce=1), } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_sstore.py b/tests/ported_static/stRefundTest/test_refund_sstore.py index c93b742e994..732f909022e 100644 --- a/tests/ported_static/stRefundTest/test_refund_sstore.py +++ b/tests/ported_static/stRefundTest/test_refund_sstore.py @@ -80,27 +80,40 @@ def test_refund_sstore( ) # Gas used = gross gas minus the capped storage-clear refund. The - # non-SSTORE gross gas is fork-invariant: the intrinsic transaction - # cost (including the single zero data byte) plus the PUSH1 and DUP1 - # that feed the SSTORE (STOP is free). + # non-SSTORE gross gas comes from the fork's intrinsic calculator + # (covers TX_BASE, calldata, and any EIP-2780 recipient surcharge) + # plus the PUSH1 and DUP1 that feed the SSTORE (STOP is free). gas_costs = fork.gas_costs() - base_gross = ( - gas_costs.TX_BASE + 2 * gas_costs.VERY_LOW + gas_costs.TX_DATA_PER_ZERO + # ``return_cost_deducted_prior_execution=True`` returns the + # upfront-deducted intrinsic only. Without it, Prague's + # ``intrinsic_calc`` returns ``max(intrinsic, EIP-7623 floor)`` — + # the floor only binds for data-heavy txs with little execution, + # which is not the case here. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=tx.data, + return_cost_deducted_prior_execution=True, ) + base_gross = intrinsic + 2 * gas_costs.VERY_LOW + # Cancun's intrinsic for this tx shape was 21_004 (TX_BASE + + # single zero-byte). Capture it as the baseline so the Cancun + # branch of ``gas_used_delta`` evaluates at the original base. + cancun_base_gross = 21_004 + 2 * gas_costs.VERY_LOW - def clear_gas_used(sstore_charge: int, clear_refund: int) -> int: - gross = base_gross + sstore_charge + def clear_gas_used( + sstore_charge: int, clear_refund: int, gross_base: int + ) -> int: + gross = gross_base + sstore_charge return gross - min(clear_refund, gross // 5) sstore_charge = Op.SSTORE.with_metadata( key_warm=False, original_value=24743, current_value=24743, new_value=0 ).gas_cost(fork) # Cancun charges 5000 for the clear and refunds 4800; subtracting the - # same model evaluated at those constants makes this exactly 0 before - # the EIP-8037/8038 repricing. + # same model evaluated at those constants and the Cancun base makes + # this exactly 0 before the EIP-8037/8038 repricing. gas_used_delta = clear_gas_used( - sstore_charge, gas_costs.REFUND_STORAGE_CLEAR - ) - clear_gas_used(5000, 4800) + sstore_charge, gas_costs.REFUND_STORAGE_CLEAR, base_gross + ) - clear_gas_used(5000, 4800, cancun_base_gross) post = {sender: Account(balance=0xE8D4EE4E00 - 1000 * gas_used_delta)} diff --git a/tests/ported_static/stTransactionTest/test_high_gas_limit.py b/tests/ported_static/stTransactionTest/test_high_gas_limit.py index db5ce28e428..60069018353 100644 --- a/tests/ported_static/stTransactionTest/test_high_gas_limit.py +++ b/tests/ported_static/stTransactionTest/test_high_gas_limit.py @@ -3,6 +3,13 @@ Ported from: state_tests/stTransactionTest/HighGasLimitFiller.json + +@manually-enhanced: Do not overwrite. The tx sends value to an empty +recipient, so EIP-2780 charges ``NEW_ACCOUNT`` state gas at the top +frame; with the default zero state-gas reservoir that charge spills into +regular gas. Instead of the original hardcoded ``gas_limit``, lift the +100000 base by ``fork.transaction_top_frame_state_gas`` so the budget +covers the spillover and stays exactly 0 on pre-EIP-2780 forks. """ import pytest @@ -13,6 +20,8 @@ Alloc, Bytes, Environment, + Fork, + RecipientType, StateTestFiller, Transaction, ) @@ -29,6 +38,7 @@ def test_high_gas_limit( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_high_gas_limit.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -49,11 +59,19 @@ def test_high_gas_limit( balance=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 ) + # EIP-2780 charges ``NEW_ACCOUNT`` state gas at the top frame when + # value is sent to an empty recipient; with the default zero + # state-gas reservoir that charge spills into regular gas, so lift + # ``gas_limit`` by exactly that amount (0 on pre-EIP-2780 forks). + top_frame_state_gas = fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) tx = Transaction( sender=sender, to=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), data=Bytes("3240349548983454"), - gas_limit=100000, + gas_limit=100000 + top_frame_state_gas, value=900, ) diff --git a/tests/ported_static/stTransactionTest/test_transaction_sending_to_zero.py b/tests/ported_static/stTransactionTest/test_transaction_sending_to_zero.py index 77749f1463e..1fbe1129c26 100644 --- a/tests/ported_static/stTransactionTest/test_transaction_sending_to_zero.py +++ b/tests/ported_static/stTransactionTest/test_transaction_sending_to_zero.py @@ -3,6 +3,14 @@ Ported from: state_tests/stTransactionTest/TransactionSendingToZeroFiller.json + +@manually-enhanced: Do not overwrite. The tx sends value 1 to the empty +zero address, so EIP-2780 charges NEW_ACCOUNT state gas at the top frame; +with the default zero reservoir that charge spills into regular gas. The +`gas_limit` is lifted by `fork.transaction_top_frame_state_gas` for an +EMPTY_ACCOUNT recipient with `sends_value=True` (0 on pre-EIP-2780 +forks), so the literal 25000 budget stays valid across the repricing. Do +not collapse the lift back to a hardcoded gas_limit. """ import pytest @@ -13,6 +21,8 @@ Alloc, Bytes, Environment, + Fork, + RecipientType, StateTestFiller, Transaction, ) @@ -29,6 +39,7 @@ def test_transaction_sending_to_zero( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_transaction_sending_to_zero.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -47,11 +58,19 @@ def test_transaction_sending_to_zero( pre[sender] = Account(balance=0x5F5E100) + # EIP-2780 charges ``NEW_ACCOUNT`` state gas at the top frame when + # value is sent to an empty recipient; with the default zero + # state-gas reservoir that charge spills into regular gas, so lift + # ``gas_limit`` by exactly that amount (0 on pre-EIP-2780 forks). + top_frame_state_gas = fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) tx = Transaction( sender=sender, to=Address(0x0000000000000000000000000000000000000000), data=Bytes(""), - gas_limit=25000, + gas_limit=25000 + top_frame_state_gas, value=1, ) diff --git a/tests/ported_static/stTransactionTest/test_transaction_to_addressh160minus_one.py b/tests/ported_static/stTransactionTest/test_transaction_to_addressh160minus_one.py index 0a27a5403dd..c8601268898 100644 --- a/tests/ported_static/stTransactionTest/test_transaction_to_addressh160minus_one.py +++ b/tests/ported_static/stTransactionTest/test_transaction_to_addressh160minus_one.py @@ -3,6 +3,14 @@ Ported from: state_tests/stTransactionTest/TransactionToAddressh160minusOneFiller.json + +@manually-enhanced: Do not overwrite. Sending value to the empty 0xff..ff +recipient triggers EIP-2780's NEW_ACCOUNT top-frame state-gas charge. +Both the tx and block ``gas_limit`` are lifted by +``fork.transaction_top_frame_state_gas(EMPTY_ACCOUNT, sends_value=True)`` +so the charge (which spills into regular gas via the zero reservoir) +fits the budget; this derived value is 0 on pre-EIP-2780 forks, keeping +the original hardcoded 22000/100000 limits intact there. """ import pytest @@ -13,6 +21,8 @@ Alloc, Bytes, Environment, + Fork, + RecipientType, StateTestFiller, Transaction, ) @@ -31,6 +41,7 @@ def test_transaction_to_addressh160minus_one( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_transaction_to_addressh160minus_one.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -38,22 +49,31 @@ def test_transaction_to_addressh160minus_one( key=0xF79127A3004ABDE26A4CBD80C428CB10F829FA11B54D36E7B326F4F4A5927ACF ) + pre[sender] = Account(balance=0x3B9ACA00) + + # EIP-2780 charges ``NEW_ACCOUNT`` state gas at the top frame when + # value is sent to an empty recipient; with the default zero + # state-gas reservoir that charge spills into regular gas, so lift + # ``gas_limit`` by exactly that amount (0 on pre-EIP-2780 forks). + # The block ``gas_limit`` must also accommodate the lifted tx. + top_frame_state_gas = fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + env = Environment( fee_recipient=coinbase, number=1, timestamp=1000, prev_randao=0x20000, base_fee_per_gas=10, - gas_limit=100000, + gas_limit=100000 + top_frame_state_gas, ) - - pre[sender] = Account(balance=0x3B9ACA00) - tx = Transaction( sender=sender, to=Address(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF), data=Bytes(""), - gas_limit=22000, + gas_limit=22000 + top_frame_state_gas, value=100, ) diff --git a/tests/ported_static/stTransactionTest/test_transaction_to_itself.py b/tests/ported_static/stTransactionTest/test_transaction_to_itself.py index 320a0073f09..28e0e14505a 100644 --- a/tests/ported_static/stTransactionTest/test_transaction_to_itself.py +++ b/tests/ported_static/stTransactionTest/test_transaction_to_itself.py @@ -3,6 +3,16 @@ Ported from: state_tests/stTransactionTest/TransactionToItselfFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the sender +balance after a self-transfer (to == sender). Instead of the original +hardcoded value, the balance shift is derived from the fork intrinsic +calculator: ``intrinsic(recipient_type=SELF, sends_value=True) - 21_000`` +is the delta versus the pre-EIP-2780 baseline intrinsic 21_000. EIP-2780 +carves out the recipient and value-transfer surcharges for self-sends, +dropping the intrinsic to ``TX_BASE`` (12_000 on Amsterdam), so the delta +is 0 at Cancun and negative afterward. The balance moves by +``gas_price * intrinsic_delta``. Do not hardcode the Amsterdam value. """ import pytest @@ -12,6 +22,8 @@ Alloc, Bytes, Environment, + Fork, + RecipientType, StateTestFiller, Transaction, ) @@ -27,6 +39,7 @@ def test_transaction_to_itself( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_transaction_to_itself.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -49,6 +62,19 @@ def test_transaction_to_itself( value=1, ) - post = {sender: Account(balance=0x3B9795B0, nonce=1)} + # EIP-2780 carves out self-transfers from the recipient and + # value-transfer surcharges, leaving only ``TX_BASE`` (12_000 on + # Amsterdam vs 21_000 on Cancun). Shift the sender balance by + # ``gas_price * delta``. + intrinsic_delta = ( + fork.transaction_intrinsic_cost_calculator()( + recipient_type=RecipientType.SELF, + sends_value=True, + ) + - 21_000 + ) + post = { + sender: Account(balance=0x3B9795B0 - 10 * intrinsic_delta, nonce=1) + } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_call.py b/tests/ported_static/stZeroCallsTest/test_zero_value_call.py index 7c0f01feb36..93a4faac1a7 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_call.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_call.py @@ -3,6 +3,14 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALLFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts storage slot +0 holds the `Op.GAS` value (0x8D5B6), which depends on the gas remaining +at a fixed execution point. EIP-2780 lowers the intrinsic for this non-self +non-value tx, so the gas budget is derived from the fork as +`600_000 + (intrinsic - 21_000)`: the fork intrinsic minus the +pre-EIP-2780 baseline 21_000 keeps the post-intrinsic budget fixed at +Cancun's value across forks. Do not hardcode the gas_limit. """ import pytest @@ -13,6 +21,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -30,6 +39,7 @@ def test_zero_value_call( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_call.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -70,11 +80,18 @@ def test_zero_value_call( address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=contract_0, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_empty_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_empty_paris.py index 9153b52e963..dea5687fb4b 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_empty_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_empty_paris.py @@ -3,6 +3,14 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALL_ToEmpty_ParisFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the +`Op.GAS` value stored at slot 0 (0x8D5B6), which depends on the gas +remaining at a fixed execution point. To keep that budget constant +across forks, `gas_limit` is derived as 600_000 plus the fork's intrinsic +cost minus the pre-EIP-2780 baseline intrinsic of 21_000 +(`intrinsic - 21_000`), since EIP-2780 lowers the intrinsic for non-self +non-value txs. Do not hardcode the gas_limit. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,6 +38,7 @@ def test_zero_value_call_to_empty_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_call_to_empty_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -65,11 +75,18 @@ def test_zero_value_call_to_empty_paris( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_non_zero_balance.py b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_non_zero_balance.py index 50c861952c0..4e65814717a 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_non_zero_balance.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_non_zero_balance.py @@ -3,6 +3,14 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALL_ToNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. Slot 0 stores `Op.GAS` and is +asserted at a fixed `0x8D5B6`, so the post-intrinsic execution budget +must stay constant across forks. The `gas_limit` is derived from the +fork intrinsic via `fork.transaction_intrinsic_cost_calculator()()` +minus the pre-EIP-2780 baseline `21_000`, leaving exactly 600_000 for +execution (the adjustment is 0 pre-EIP-2780). Do not hardcode the +gas_limit. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,6 +38,7 @@ def test_zero_value_call_to_non_zero_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_call_to_non_zero_balance.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -65,11 +75,18 @@ def test_zero_value_call_to_non_zero_balance( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_one_storage_key_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_one_storage_key_paris.py index a073612b73b..68d21a57d11 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_one_storage_key_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_call_to_one_storage_key_paris.py @@ -3,6 +3,15 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALL_ToOneStorageKey_ParisFiller.json + +@manually-enhanced: Do not overwrite. The contract stores `Op.GAS` into +slot 0, asserting `0x8D5B6` remaining at a fixed execution point, so the +tx gas budget must track the intrinsic across forks. `gas_limit` is +derived as `600_000 + (intrinsic - 21_000)`, where `intrinsic` comes from +`fork.transaction_intrinsic_cost_calculator()`; subtracting the +pre-EIP-2780 baseline intrinsic 21_000 keeps the post-intrinsic execution +budget fixed when EIP-2780 lowers the intrinsic for non-value, non-self +txs. Do not hardcode the literal gas limit. """ import pytest @@ -13,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -32,6 +42,7 @@ def test_zero_value_call_to_one_storage_key_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_call_to_one_storage_key_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -73,11 +84,18 @@ def test_zero_value_call_to_one_storage_key_paris( address=Address(0xF202BAE278AC09857F5A56991C7A4679632F5841), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode.py b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode.py index d4721fe68c5..a018699bea2 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode.py @@ -3,6 +3,15 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALLCODEFiller.json + +@manually-enhanced: Do not overwrite. The contract stores `Op.GAS` at +slot 0 (asserted as 0x8D5B6), so the post-state depends on the gas left +at a fixed execution point. The tx gas budget is derived from the fork +gas model instead of a hardcoded literal: `gas_limit = 600_000 + +(intrinsic - 21_000)` adds back whatever EIP-2780 shaved off the +intrinsic for this non-self non-value tx (subtracting the pre-EIP-2780 +baseline 21_000) so the 600_000 post-intrinsic execution budget, and +thus the stored GAS value, stays invariant across forks. """ import pytest @@ -13,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -30,6 +40,7 @@ def test_zero_value_callcode( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_callcode.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -70,11 +81,18 @@ def test_zero_value_callcode( address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=contract_0, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_empty_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_empty_paris.py index 8117720261d..935396c723e 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_empty_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_empty_paris.py @@ -3,6 +3,15 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALLCODE_ToEmpty_ParisFiller.json + +@manually-enhanced: Do not overwrite. The target stores `Op.GAS` into +slot 0 and the post-state asserts it as a fixed value (0x8D5B6), so the +remaining gas at that point must be fork-invariant. The `gas_limit` is +derived as `600_000 + (intrinsic - 21_000)` from the fork intrinsic +calculator instead of a hardcoded number: subtracting the pre-EIP-2780 +baseline intrinsic of 21_000 keeps a constant 600_000 post-intrinsic +execution budget when EIP-2780 lowers the intrinsic for non-self +non-value txs. Do not hardcode the gas_limit. """ import pytest @@ -12,6 +21,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +41,7 @@ def test_zero_value_callcode_to_empty_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_callcode_to_empty_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -67,11 +78,18 @@ def test_zero_value_callcode_to_empty_paris( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_non_zero_balance.py b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_non_zero_balance.py index 9b5ab5f0cae..f34ddf54aab 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_non_zero_balance.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_non_zero_balance.py @@ -3,6 +3,17 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALLCODE_ToNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. The post-state asserts the +`Op.GAS` value stored at slot 0 (0x8D5B6), which depends on the gas +remaining at a fixed execution point. To hold that point constant, the +`gas_limit` is derived from the fork gas model rather than hardcoded: +`gas_limit = 600_000 + (intrinsic - 21_000)`, where `intrinsic` comes +from `fork.transaction_intrinsic_cost_calculator()`. Subtracting the +pre-EIP-2780 baseline intrinsic 21_000 keeps the post-intrinsic +execution budget at 600_000 across the EIP-2780 intrinsic +decomposition (which lowers the intrinsic for non-self, non-value txs). +Do not hardcode the gas_limit. """ import pytest @@ -12,6 +23,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +43,7 @@ def test_zero_value_callcode_to_non_zero_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_callcode_to_non_zero_balance.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -67,11 +80,18 @@ def test_zero_value_callcode_to_non_zero_balance( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_one_storage_key_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_one_storage_key_paris.py index 6872e5b24d9..b11a46374e3 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_one_storage_key_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_callcode_to_one_storage_key_paris.py @@ -3,6 +3,16 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_CALLCODE_ToOneStorageKey_ParisFiller.json + +@manually-enhanced: Do not overwrite. The contract's first SSTORE records +`Op.GAS`, so the slot-0 post value (`0x8D5B6`) pins the remaining gas at a +fixed execution point. To keep that budget constant as the intrinsic +shifts, `gas_limit` is derived from the fork intrinsic calculator rather +than hardcoded: `600_000 + (intrinsic - 21_000)`, where `21_000` is the +pre-EIP-2780 baseline intrinsic. EIP-2780 lowers the intrinsic for this +non-self, zero-value tx, so the `- 21_000` term keeps the post-intrinsic +execution budget (and thus the `Op.GAS` assertion) correct across forks. +Do not replace the calculator-derived value with a literal. """ import pytest @@ -13,6 +23,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -32,6 +43,7 @@ def test_zero_value_callcode_to_one_storage_key_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_callcode_to_one_storage_key_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -73,11 +85,18 @@ def test_zero_value_callcode_to_one_storage_key_paris( address=Address(0xA93AE635B4FA4D618045C019AC32ED9ADC8F54EA), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall.py b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall.py index 3e630665533..d3fad1a766a 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall.py @@ -3,6 +3,16 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_DELEGATECALLFiller.json + +@manually-enhanced: Do not overwrite. The `gas_limit` is derived from +the fork intrinsic calculator instead of a hardcoded literal, so the +post-intrinsic execution budget stays fixed at 600_000 across forks: +`gas_limit = 600_000 + (intrinsic - 21_000)`, where `21_000` is the +pre-EIP-2780 baseline intrinsic. EIP-2780 lowers the intrinsic for +non-self, non-value txs, and the `SSTORE(0, GAS)` post assertion +(`0x8D5B6`) pins `Op.GAS` at a fixed execution point, so the remaining +gas after the intrinsic deduction must not shift. Do not hardcode the +gas limit. """ import pytest @@ -13,6 +23,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -30,6 +41,7 @@ def test_zero_value_delegatecall( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_delegatecall.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -69,11 +81,18 @@ def test_zero_value_delegatecall( address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=contract_0, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_empty_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_empty_paris.py index a92303da1cc..a858d9edcc5 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_empty_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_empty_paris.py @@ -3,6 +3,16 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_DELEGATECALL_ToEmpty_ParisFiller.json + +@manually-enhanced: Do not overwrite. The contract records `Op.GAS` into +storage slot 0, and the post-state asserts that value (`0x8D5B6`), so the +gas remaining at that fixed execution point must stay constant across +forks. The `gas_limit` is therefore derived from the fork as +`600_000 + (fork.transaction_intrinsic_cost_calculator()() - 21_000)`: +it re-adds the 600k post-intrinsic execution budget onto the fork +intrinsic and subtracts the pre-EIP-2780 baseline intrinsic `21_000`, +which EIP-2780 lowers for non-self non-value txs. Do not hardcode the +gas limit. """ import pytest @@ -12,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +42,7 @@ def test_zero_value_delegatecall_to_empty_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_delegatecall_to_empty_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -66,11 +78,18 @@ def test_zero_value_delegatecall_to_empty_paris( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_non_zero_balance.py b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_non_zero_balance.py index a02b7615016..2a06868f8a7 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_non_zero_balance.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_non_zero_balance.py @@ -3,6 +3,16 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_DELEGATECALL_ToNonZeroBalanceFiller.json + +@manually-enhanced: Do not overwrite. The contract stores `Op.GAS` into +slot 0 (asserted as 0x8D5B6), so the gas remaining at that fixed +execution point must be fork-invariant. The `gas_limit` is derived from +the fork: `600_000 + (intrinsic - 21_000)`, where `intrinsic` comes from +`fork.transaction_intrinsic_cost_calculator()()` and 21_000 is the +pre-EIP-2780 baseline intrinsic. EIP-2780's intrinsic decomposition +lowers the intrinsic for non-self non-value txs, so subtracting the old +21_000 literal keeps the post-intrinsic execution budget (600_000) +constant. Do not hardcode the gas_limit. """ import pytest @@ -12,6 +22,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -31,6 +42,7 @@ def test_zero_value_delegatecall_to_non_zero_balance( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_delegatecall_to_non_zero_balance.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -66,11 +78,18 @@ def test_zero_value_delegatecall_to_non_zero_balance( nonce=0, ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_one_storage_key_paris.py b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_one_storage_key_paris.py index d4e3d2fad43..10553129c76 100644 --- a/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_one_storage_key_paris.py +++ b/tests/ported_static/stZeroCallsTest/test_zero_value_delegatecall_to_one_storage_key_paris.py @@ -3,6 +3,16 @@ Ported from: state_tests/stZeroCallsTest/ZeroValue_DELEGATECALL_ToOneStorageKey_ParisFiller.json + +@manually-enhanced: Do not overwrite. Slot 0 asserts the `Op.GAS` +value (0x8D5B6) captured by the first SSTORE, so the gas remaining at +that fixed execution point must be constant across forks. The +`gas_limit` is derived from the fork intrinsic calculator +(`fork.transaction_intrinsic_cost_calculator()()`) as +`600_000 + (intrinsic - 21_000)`: subtracting the pre-EIP-2780 +baseline intrinsic 21_000 keeps the post-intrinsic execution budget +fixed at 600_000 even as EIP-2780 lowers the intrinsic for +non-value, non-self calls. Do not hardcode the gas_limit. """ import pytest @@ -13,6 +23,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -32,6 +43,7 @@ def test_zero_value_delegatecall_to_one_storage_key_paris( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """Test_zero_value_delegatecall_to_one_storage_key_paris.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -72,11 +84,18 @@ def test_zero_value_delegatecall_to_one_storage_key_paris( address=Address(0xC8881A7E48D37B4A4CDD6338CE7076D6A116283D), # noqa: E501 ) + # Preserve Cancun's post-intrinsic execution budget across + # forks; EIP-2780 lowers the intrinsic for non-self non-value + # txs, and the Op.GAS storage assertion depends on the + # remaining gas at a fixed execution point. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = 600_000 + (intrinsic - 21_000) + tx = Transaction( sender=sender, to=target, data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) post = { diff --git a/tests/ported_static/vmTests/test_suicide.py b/tests/ported_static/vmTests/test_suicide.py index 9cc37ebab8a..c562b182d0d 100644 --- a/tests/ported_static/vmTests/test_suicide.py +++ b/tests/ported_static/vmTests/test_suicide.py @@ -148,11 +148,14 @@ def test_suicide( # The CALL into the self-destructing contract touches a cold, already # existing account; EIP-8038 raises that cold account-access surcharge # from 2600 to 3000. The SELFDESTRUCT beneficiary is warm and - # non-empty, so its charge is unchanged. The sender pays this extra - # gas at the base fee (no priority fee), so its balance drops by the - # account-access delta times the gas price. + # non-empty, so its charge is unchanged. EIP-2780 separately reshapes + # the tx intrinsic for this non-self non-value call. The sender pays + # the combined delta at the base fee (no priority fee). cold_account_access_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 - caller_balance = 0x5AF31075D9DE - 10 * cold_account_access_delta + intrinsic_delta = fork.transaction_intrinsic_cost_calculator()() - 21_000 + caller_balance = ( + 0x5AF31075D9DE - 10 * cold_account_access_delta - 10 * intrinsic_delta + ) expect_entries_: list[dict] = [ { diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index e31a5323378..e7167078a04 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -36,6 +36,7 @@ Hash, Initcode, Op, + RecipientType, Requests, StateTestFiller, Storage, @@ -4001,13 +4002,16 @@ def test_many_delegations( max_gas = tx_gas_limit_cap else: max_gas = env.gas_limit - gas_for_delegations = max_gas - 21_000 - 20_000 - (3 * 2) + + success_slot = 1 + entry_code = Op.SSTORE(success_slot, 1) + Op.STOP + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + entry_code_gas = entry_code.gas_cost(fork) + gas_for_delegations = max_gas - intrinsic_gas - entry_code_gas gas_costs = fork.gas_costs() delegation_count = gas_for_delegations // gas_costs.AUTH_PER_EMPTY_ACCOUNT - success_slot = 1 - entry_code = Op.SSTORE(success_slot, 1) + Op.STOP entry_address = pre.deploy_contract(entry_code) signers = [pre.fund_eoa(signer_balance) for _ in range(delegation_count)] @@ -4098,6 +4102,7 @@ def test_invalid_transaction_after_authorization( def test_authorization_reusing_nonce( blockchain_test: BlockchainTestFiller, pre: Alloc, + fork: Fork, ) -> None: """ Test an authorization reusing the same nonce as a prior transaction @@ -4106,11 +4111,34 @@ def test_authorization_reusing_nonce( auth_signer = pre.fund_eoa() sender = pre.fund_eoa() recipient = pre.fund_eoa(amount=0) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + # Tx1: value transfer to an empty recipient -- pays the intrinsic + # value-transfer surcharges plus the top-frame ``NEW_ACCOUNT`` + # state-gas charge. + tx1_intrinsic = intrinsic_gas_calculator( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + tx1_top_frame_state = fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.EMPTY_ACCOUNT, + sends_value=True, + ) + tx1_gas = tx1_intrinsic + tx1_top_frame_state + + # Tx2: recipient is now alive (received 1 wei in tx1), so the + # recipient is an EOA and no top-frame charge fires. The auth + # list adds one ``AUTH_PER_EMPTY_ACCOUNT`` to intrinsic. + tx2_gas = intrinsic_gas_calculator( + recipient_type=RecipientType.EOA, + authorization_list_or_count=1, + ) + txs = [ Transaction( sender=auth_signer, nonce=0, - gas_limit=21_000, + gas_limit=tx1_gas, to=recipient, value=1, ), @@ -4118,6 +4146,7 @@ def test_authorization_reusing_nonce( sender=sender, to=recipient, value=0, + gas_limit=tx2_gas, authorization_list=[ AuthorizationTuple( address=Address(1), diff --git a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py index 8c4c62adf60..9d9a3074888 100644 --- a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py +++ b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py @@ -16,6 +16,7 @@ Fork, Hash, Op, + RecipientType, Transaction, TransactionException, Withdrawal, @@ -69,11 +70,16 @@ def recipient(self, pre: Alloc) -> EOA: return pre.fund_eoa(0) @pytest.fixture - def tx(self, sender: EOA, recipient: EOA) -> Transaction: # noqa: D102 + def tx( # noqa: D102 + self, sender: EOA, recipient: EOA, fork: Fork + ) -> Transaction: # Transaction sent from the `sender`, which has 1 wei balance at start + gas_limit = fork.transaction_intrinsic_cost_calculator()( + recipient_type=RecipientType.EOA, + ) return Transaction( gas_price=ONE_GWEI, - gas_limit=21_000, + gas_limit=gas_limit, to=recipient, sender=sender, ) From aed6293176d71de7c8740961505bfbbeb6efc1c0 Mon Sep 17 00:00:00 2001 From: spencer Date: Thu, 18 Jun 2026 23:53:05 +0100 Subject: [PATCH 03/14] chore(tests): fix failing ported static slow tests for EIP-8038 (#3019) Co-authored-by: marioevz --- .../test_static_call_change_revert.py | 284 +++--------------- .../stStaticCall/test_static_make_money.py | 72 ++--- .../test_static_raw_call_gas_ask.py | 211 ++----------- 3 files changed, 86 insertions(+), 481 deletions(-) diff --git a/tests/ported_static/stStaticCall/test_static_call_change_revert.py b/tests/ported_static/stStaticCall/test_static_call_change_revert.py index 2d6b7295118..c13ba330562 100644 --- a/tests/ported_static/stStaticCall/test_static_call_change_revert.py +++ b/tests/ported_static/stStaticCall/test_static_call_change_revert.py @@ -3,22 +3,18 @@ Ported from: state_tests/stStaticCall/static_callChangeRevertFiller.json + +@manually-enhanced: Do not overwrite. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, StateTestFiller, + Storage, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,267 +25,55 @@ ["state_tests/stStaticCall/static_callChangeRevertFiller.json"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.slow @pytest.mark.parametrize( - "d, g, v", + "sstore_in_static,oog", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param(False, False), + pytest.param(False, True), + pytest.param(True, False), ], ) -@pytest.mark.pre_alloc_mutable def test_static_call_change_revert( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + sstore_in_static: bool, + oog: bool, ) -> None: """Test_static_call_change_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + subcall_code = Op.MSTORE(offset=0x1, value=0x1) + if sstore_in_static: + subcall_code += Op.SSTORE(key=0x1, value=Op.SLOAD(key=0x1)) + subcall_code += Op.STOP + subcall_contract = pre.deploy_contract(subcall_code) - # Source: lll - # { (CALL 350000 (CALLDATALOAD 0) 0 0 0 0 0) } - target = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0x55730, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x492BB18ADCE7DA2BED3592742FB4E3DF9086FB4C), # noqa: E501 - ) - # Source: lll - # { (MSTORE 1 1) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP, - nonce=0, - address=Address(0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7), # noqa: E501 - ) - # Source: lll - # { (MSTORE 1 1) (SSTORE 1 (SLOAD 1)) } - addr_5 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x1, value=0x1) - + Op.SSTORE(key=0x1, value=Op.SLOAD(key=0x1)) - + Op.STOP, - nonce=0, - address=Address(0x47C4ED3D93429CB8304737E2327B522E8928C9F3), # noqa: E501 - ) - # Source: lll - # { [[ 0 ]] (CALL 100000 1 0 0 0 0) [[ 1 ]] (STATICCALL 100000 0 0 0 0) [[ 2 ]] (CALL 100000 1 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, - value=Op.CALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xE6F1FDAA1C99007971C641E10AF3A8FAC0B641C8), # noqa: E501 - ) - # Source: lll - # { [[ 0 ]] (CALL 100000 1 0 0 0 0) [[ 1 ]] (STATICCALL 100000 0 0 0 0) [[ 2 ]] (CALL 100000 1 0 0 0 0) (def 'i 0x80) (for {} (< @i 50000) [i](+ @i 1) (EXTCODESIZE 1)) } # noqa: E501 - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + caller_storage = Storage() + caller_code = ( + Op.SSTORE( + key=caller_storage.store_next(not oog), + value=Op.CALL(address=subcall_contract, value=0x1), ) + Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + key=caller_storage.store_next(not oog and not sstore_in_static), + value=Op.STATICCALL(address=subcall_contract), ) + Op.SSTORE( - key=0x2, - value=Op.CALL( - gas=0x186A0, - address=0xC031FC0AA7B61A5D7D962AFEE8838DEC6948ABB7, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.JUMPDEST - + Op.JUMPI( - pc=0x8F, condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)) + key=caller_storage.store_next(not oog), + value=Op.CALL(address=subcall_contract, value=0x1), ) - + Op.POP(Op.EXTCODESIZE(address=0x1)) - + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) - + Op.JUMP(pc=0x73) - + Op.JUMPDEST - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xEA22EC955AC71D8E4380541212BD20818D704567), # noqa: E501 - ) - # Source: lll - # { [[ 0 ]] (CALL 100000 1 0 0 0 0) [[ 1 ]] (STATICCALL 100000 0 0 0 0) [[ 2 ]] (CALL 100000 1 0 0 0 0) } # noqa: E501 - addr_4 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x186A0, - address=0x47C4ED3D93429CB8304737E2327B522E8928C9F3, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x186A0, - address=0x47C4ED3D93429CB8304737E2327B522E8928C9F3, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, - value=Op.CALL( - gas=0x186A0, - address=0x47C4ED3D93429CB8304737E2327B522E8928C9F3, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x2C004389EDAAE817E664B6D660F46735756B56D3), # noqa: E501 ) + if oog: + caller_code += Op.MLOAD(2**256 - 1) + caller_code += Op.STOP - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr: Account(storage={0: 1, 1: 1, 2: 1}), - addr_2: Account(balance=2), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_3: Account(storage={0: 0, 1: 0, 2: 0}), - addr_2: Account(balance=0), - }, - }, - { - "indexes": {"data": 2, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_4: Account(storage={0: 1, 1: 0, 2: 1}), - addr_5: Account(balance=2), - }, - }, - ] + caller_contract = pre.deploy_contract(caller_code, balance=2) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + post = { + caller_contract: Account(storage=caller_storage), + subcall_contract: Account(balance=2 if not oog else 0), + } - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_3, left_padding=True), - Hash(addr_4, left_padding=True), - ] - tx_gas = [1000000] - tx_value = [100000] - - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + tx = Transaction(sender=sender, to=caller_contract) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_make_money.py b/tests/ported_static/stStaticCall/test_static_make_money.py index add8a413112..4a921cd29f6 100644 --- a/tests/ported_static/stStaticCall/test_static_make_money.py +++ b/tests/ported_static/stStaticCall/test_static_make_money.py @@ -3,18 +3,12 @@ Ported from: state_tests/stStaticCall/static_makeMoneyFiller.json + +@manually-enhanced: Do not overwrite. """ import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) +from execution_testing import Account, Alloc, StateTestFiller, Transaction from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -25,65 +19,47 @@ ["state_tests/stStaticCall/static_makeMoneyFiller.json"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable def test_static_make_money( state_test: StateTestFiller, pre: Alloc, ) -> None: """Test_static_make_money.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x5F5E100) + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) + contracts_starting_balance = 1 - # Source: raw - # 0x600160015532600255 - addr = pre.deploy_contract( # noqa: F841 + subcall_contract = pre.deploy_contract( # noqa: F841 code=Op.SSTORE(key=0x1, value=0x1) + Op.SSTORE(key=0x2, value=Op.ORIGIN), - balance=0xDE0B6B3A7640000, - nonce=0, + balance=contracts_starting_balance, ) - # Source: lll - # { (MSTORE 0 0x601080600c6000396000f20060003554156009570060203560003555) (STATICCALL 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 + entry_contract = pre.deploy_contract( # noqa: F841 code=Op.MSTORE( offset=0x0, value=0x601080600C6000396000F20060003554156009570060203560003555, ) + Op.STATICCALL( - gas=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC, # noqa: E501 - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + gas=2**256 - 20, + address=subcall_contract, ) + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, + balance=contracts_starting_balance, ) - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=228500, - value=10, - ) + tx_value = 1 + tx = Transaction(sender=sender, to=entry_contract, value=tx_value) post = { - target: Account(balance=0xDE0B6B3A764000A), - sender: Account(balance=0x5D38038), - addr: Account(balance=0xDE0B6B3A7640000), + entry_contract: Account( + balance=contracts_starting_balance + tx_value, + ), + subcall_contract: Account( + balance=contracts_starting_balance, + storage={ + 1: 0, + 2: 0, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_raw_call_gas_ask.py b/tests/ported_static/stStaticCall/test_static_raw_call_gas_ask.py index 0ea735ae867..fa31ff1b4e6 100644 --- a/tests/ported_static/stStaticCall/test_static_raw_call_gas_ask.py +++ b/tests/ported_static/stStaticCall/test_static_raw_call_gas_ask.py @@ -8,17 +8,12 @@ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, + CodeGasMeasure, StateTestFiller, Transaction, ) from execution_testing.forks import Fork -from execution_testing.specs.static_state.expect_section import ( - resolve_expect_post, -) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,198 +26,48 @@ @pytest.mark.valid_from("Cancun") @pytest.mark.slow @pytest.mark.parametrize( - "d, g, v", + "mem_expansion", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), + pytest.param(False, id="without_mem_expansion"), + pytest.param(True, id="with_mem_expansion"), ], ) @pytest.mark.pre_alloc_mutable def test_static_raw_call_gas_ask( state_test: StateTestFiller, pre: Alloc, + mem_expansion: bool, fork: Fork, - d: int, - g: int, - v: int, ) -> None: """Test_static_raw_call_gas_ask.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x094F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000000) - contract_2 = Address(0x1000000000000000000000000000000000000001) - contract_3 = Address(0x2000000000000000000000000000000000000001) - contract_4 = Address(0x3000000000000000000000000000000000000001) - contract_5 = Address(0x4000000000000000000000000000000000000001) - sender = pre.fund_eoa(amount=0xE8D4A51000) + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + subcall_code = Op.MSTORE(0, Op.GAS, new_memory_size=32) + Op.STOP + subcall_contract = pre.deploy_contract(code=subcall_code) - # Source: lll - # { (MSTORE 0 (GAS)) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) + Op.STOP, - nonce=0, - address=Address(0x094F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # { (CALL (GAS) (CALLDATALOAD 0) 0 0 0 0 0) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=Op.GAS, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (STATICCALL 130000 0x094f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0) [[1]] (GAS) } # noqa: E501 - contract_3 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.STATICCALL( - gas=0x1FBD0, - address=0x94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + mem_expansion_size = 0x1F40 + static_call_code = ( + Op.STATICCALL( + address=subcall_contract, + args_size=mem_expansion_size, + ret_size=mem_expansion_size, + new_memory_size=mem_expansion_size, ) - + Op.SSTORE(key=0x1, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0x2000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # { (STATICCALL 130000 0x094f5374fce5edbc8e2a8697c15331677e6ebf0b 0 8000 0 8000) [[1]] (GAS) } # noqa: E501 - contract_5 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.STATICCALL( - gas=0x1FBD0, - address=0x94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) - ) - + Op.SSTORE(key=0x1, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0x4000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # { (STATICCALL 3000000 0x094f5374fce5edbc8e2a8697c15331677e6ebf0b 0 8000 0 8000) [[1]] (GAS) } # noqa: E501 - contract_4 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.STATICCALL( - gas=0x2DC6C0, - address=0x94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - args_offset=0x0, - args_size=0x1F40, - ret_offset=0x0, - ret_size=0x1F40, - ) + if mem_expansion + else Op.STATICCALL( + address=subcall_contract, ) - + Op.SSTORE(key=0x1, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0x3000000000000000000000000000000000000001), # noqa: E501 ) - # Source: lll - # { (STATICCALL 3000000 0x094f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0) [[1]] (GAS) } # noqa: E501 - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.STATICCALL( - gas=0x2DC6C0, - address=0x94F5374FCE5EDBC8E2A8697C15331677E6EBF0B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x1, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + static_call_contract = pre.deploy_contract( + code=CodeGasMeasure( + code=static_call_code, + extra_stack_items=1, + sstore_key=1, + ), ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_2: Account(storage={1: 0xE9F83})}, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_3: Account(storage={1: 0xE9F83})}, - }, - { - "indexes": {"data": 2, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_4: Account(storage={1: 0xE9C1B})}, - }, - { - "indexes": {"data": 3, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {contract_5: Account(storage={1: 0xE9C1B})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(contract_2, left_padding=True), - Hash(contract_3, left_padding=True), - Hash(contract_4, left_padding=True), - Hash(contract_5, left_padding=True), - ] - tx_gas = [1000000] - - tx = Transaction( - sender=sender, - to=contract_1, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, - ) + gas_cost = static_call_code.gas_cost(fork) + subcall_code.gas_cost(fork) + post = {static_call_contract: Account(storage={1: gas_cost})} + tx = Transaction(sender=sender, to=static_call_contract) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 5e306f73aa544380bd7295c144208825640f7394 Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:52:24 +0200 Subject: [PATCH 04/14] test(amsterdam): add EIP-8038 state-access gas cost tests (#3033) Co-authored-by: danceratopz Co-authored-by: marioevz --- .../forks/forks/eips/amsterdam/eip_8037.py | 39 + .../__init__.py | 3 + .../spec.py | 16 + .../test_access_list_gas.py | 329 ++++++++ .../test_call_gas.py | 768 ++++++++++++++++++ .../test_create_gas.py | 553 +++++++++++++ .../test_eip_mainnet.py | 265 ++++++ .../test_exact_balance_no_fallback.py | 222 +++++ .../test_ext_code_opcodes_gas.py | 471 +++++++++++ .../test_fork_transition.py | 523 ++++++++++++ .../test_selfdestruct_gas.py | 677 +++++++++++++++ .../test_set_code_auth_gas.py | 605 ++++++++++++++ .../test_set_code_auth_refunds.py | 242 ++++++ .../test_sload_gas.py | 192 +++++ .../test_sstore_gas.py | 215 +++++ .../test_sstore_refunds.py | 349 ++++++++ .../test_transient_storage_regression.py | 87 ++ 17 files changed, 5556 insertions(+) create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/__init__.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/spec.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sload_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py index bac17899e18..c9f5fd170be 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_8037.py @@ -146,6 +146,9 @@ def opcode_state_map( Opcodes.CREATE2: lambda op: cls._calculate_create_state_gas( op, gas_costs ), + Opcodes.SELFDESTRUCT: ( + lambda op: cls._calculate_selfdestruct_state_gas(op, gas_costs) + ), } @classmethod @@ -383,3 +386,39 @@ def _calculate_create_state_gas( """ del opcode return gas_costs.NEW_ACCOUNT + + @classmethod + def _calculate_selfdestruct_state_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate the SELFDESTRUCT state gas cost: `NEW_ACCOUNT` when a + positive balance funds a new account. Before EIP-8037 this was + folded into the regular SELFDESTRUCT cost; under EIP-8037 it is + exposed here as state gas (mirroring `_calculate_create_state_gas`) + so the regular cost matches the spec EVM + (`OPCODE_SELFDESTRUCT_BASE` + account access + the EIP-8038 + `ACCOUNT_WRITE` surcharge). + """ + if opcode.metadata["account_new"]: + return gas_costs.NEW_ACCOUNT + return 0 + + @classmethod + def _calculate_selfdestruct_gas( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate the regular SELFDESTRUCT gas cost. The Frontier base + calculation folds `NEW_ACCOUNT` into the regular cost when a + positive balance funds a new account; EIP-8038 (the mixin between + the base and EIP-8037 in the MRO) adds only the `ACCOUNT_WRITE` + surcharge. EIP-8037 moves that funding cost to the state-gas + dimension (see `_calculate_selfdestruct_state_gas`), so this + subtracts the `NEW_ACCOUNT` term back out of the inherited regular + cost; the EIP-8038 `ACCOUNT_WRITE` surcharge stays in regular gas. + """ + gas_cost = super()._calculate_selfdestruct_gas(opcode, gas_costs) + if opcode.metadata["account_new"]: + gas_cost -= gas_costs.NEW_ACCOUNT + return gas_cost diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/__init__.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/__init__.py new file mode 100644 index 00000000000..a84179b40a4 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). +""" diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/spec.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/spec.py new file mode 100644 index 00000000000..66a7606fd8a --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/spec.py @@ -0,0 +1,16 @@ +"""Defines the EIP-8038 reference specification.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_8038 = ReferenceSpec( + "EIPS/eip-8038.md", "a8862ae6653a12a2989b64a50eca5334cfe8b3cb" +) 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 new file mode 100644 index 00000000000..1337c77b230 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_access_list_gas.py @@ -0,0 +1,329 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Covers the EIP-8038 access-list repricing: + +* The intrinsic surcharge per access-list entry is + ``TX_ACCESS_LIST_ADDRESS`` (3000) per address and + ``TX_ACCESS_LIST_STORAGE_KEY`` (3000) per storage key, isolated from + the EIP-7981 calldata-floor tokens that the Amsterdam intrinsic + calculator also charges on access-list bytes. +* A storage slot named in the access list is *warm* on its first runtime + access (``SLOAD``/``SSTORE`` pays ``WARM_SLOAD`` rather than the cold + cost). +* Warmth is scoped to ``(address, slot)``: listing slot ``s`` of account + ``A`` does not warm slot ``s`` of account ``B``. +""" + +from typing import List + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Bytecode, + CodeGasMeasure, + Environment, + Fork, + Op, + StateTestFiller, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +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]: + """Build an access list of ``n_addr`` entries, each with keys.""" + entries: List[AccessList] = [] + for i in range(n_addr): + address = Address(0x1000) if duplicate else Address(0x1000 + i) + keys = [bytes([j]) * 32 for j in range(n_keys_each)] + entries.append(AccessList(address=address, storage_keys=keys)) + return entries + + +# (n_addr, n_keys_each, duplicate, id) +ACCESS_LIST_SHAPES = [ + pytest.param(0, 0, False, id="empty"), + pytest.param(1, 0, False, id="single_addr"), + pytest.param(1, 3, False, id="one_addr_three_keys"), + pytest.param(2, 0, False, id="two_addr"), + pytest.param(2, 1, True, id="duplicate_addr"), +] + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("n_addr,n_keys_each,duplicate", ACCESS_LIST_SHAPES) +def test_access_list_intrinsic_surcharge( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + n_addr: int, + n_keys_each: int, + duplicate: bool, +) -> None: + """ + Assert the per-entry intrinsic access-list surcharge. + + The intrinsic-cost delta from adding the access list, minus the + EIP-7981 floor-token contribution, must equal + ``n_addr * TX_ACCESS_LIST_ADDRESS + n_keys * TX_ACCESS_LIST_STORAGE_KEY``. + 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( + to=contract, + sender=pre.fund_eoa(), + access_list=access_list if access_list else None, + ) + + state_test(pre=pre, post={}, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_access_list_duplicate_address_key_intrinsic_and_warmth( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + A duplicated ``(address, storage_key)`` access-list entry is billed + twice intrinsically but warms the slot only once. + + The same ``(contract, slot)`` pair is listed twice. The intrinsic + surcharge (floor tokens isolated as in + ``test_access_list_intrinsic_surcharge``) bills both listings: + ``2 * TX_ACCESS_LIST_ADDRESS + 2 * TX_ACCESS_LIST_STORAGE_KEY``. At + 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. + measured_read = Op.SLOAD(slot) + overhead = measured_read.gas_cost(fork) - Op.SLOAD( + key_warm=False + ).gas_cost(fork) + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=measured_read, + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=1, + ), + storage={slot: 1}, + ) + + # Build the access list after deploying so the address is real, then + # list the identical (contract, slot) pair twice. + access_list = [ + AccessList(address=contract, storage_keys=[slot]), + 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, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + # Slot 1 holds the measured warm cost; the read slot keeps its value. + post = {contract: Account(storage={1: expected_gas, slot: 1})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("op", ["SLOAD", "SSTORE"], ids=["sload", "sstore"]) +def test_access_list_warms_storage_slot( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + op: str, +) -> None: + """ + A storage slot named in the access list is warm on first access. + + The first runtime ``SLOAD``/``SSTORE`` of an access-list slot pays + the warm cost: ``WARM_SLOAD`` for ``SLOAD``; for ``SSTORE`` an + 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 + slot = 0x42 + + if op == "SLOAD": + measured_code: Bytecode = Op.SLOAD(slot) + # Overhead is just the single PUSH (key); the stored value is the + # bare warm SLOAD access cost. + overhead_cost = 1 * very_low + extra_stack_items = 1 + expected_gas = Op.SLOAD(key_warm=True).gas_cost(fork) + else: + measured_code = Op.SSTORE(slot, 2) + # Overhead is the two PUSHes (key, value); the stored value is + # the bare warm SSTORE regular cost (overwrite of a non-zero + # original, no state gas). + overhead_cost = 2 * very_low + extra_stack_items = 0 + expected_gas = ( + Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=1, + new_value=2, + )(slot, 2).regular_cost(fork) + - 2 * very_low + ) + + code = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=extra_stack_items, + sstore_key=1, + ) + contract = pre.deploy_contract(code=code, storage={slot: 1}) + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + access_list=[AccessList(address=contract, storage_keys=[slot])], + ) + + # Slot 1 holds the measured warm cost. The data slot ends at its + # original (SLOAD) or the written value (SSTORE). + final_slot_value = 1 if op == "SLOAD" else 2 + post = { + contract: Account(storage={1: expected_gas, slot: final_slot_value}) + } + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_access_list_slot_warmth_is_address_scoped( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Access-list slot warmth is scoped to ``(address, slot)``. + + Slot ``s`` of account ``A`` is listed in the access list. Reading + slot ``s`` of ``A`` is warm (``WARM_SLOAD``); reading the same slot + number of a different account ``B`` is cold (``COLD_STORAGE_ACCESS``). + """ + slot = 0x42 + warm_gas = Op.SLOAD(key_warm=True).gas_cost(fork) + cold_gas = Op.SLOAD(key_warm=False).gas_cost(fork) + + # Both accounts read their own slot ``s`` with the same wrapper, so the + # overhead that strips the operand PUSH is identical for each. + measured_read = Op.SLOAD(slot) + overhead = measured_read.gas_cost(fork) - cold_gas + + # B reads its own slot ``s`` (cold), storing the result in B's slot 1. + account_b = pre.deploy_contract( + code=CodeGasMeasure( + code=measured_read, + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=1, + ), + storage={slot: 1}, + ) + + # A reads its own slot ``s`` (warm via the access list), then calls B. + account_a = pre.deploy_contract( + code=CodeGasMeasure( + code=measured_read, + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=1, + ) + + Op.POP(Op.CALL(gas=200_000, address=account_b)), + storage={slot: 1}, + ) + + tx = Transaction( + to=account_a, + sender=pre.fund_eoa(), + # Only A's slot is listed; B's identical slot stays cold. + access_list=[AccessList(address=account_a, storage_keys=[slot])], + ) + + post = { + account_a: Account(storage={1: warm_gas, slot: 1}), + account_b: Account(storage={1: cold_gas, slot: 1}), + } + state_test(env=env, pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..37e543f4376 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_call_gas.py @@ -0,0 +1,768 @@ +""" +Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) +``CALL``-family regular-gas dimension. + +Under EIP-8038 the call opcodes are repriced in their *regular* gas +dimension: + +- account access costs ``COLD_ACCOUNT_ACCESS`` (3,000) cold or + ``WARM_ACCESS`` (100) warm; +- a positive value transfer adds ``CALL_VALUE`` (``ACCOUNT_WRITE`` + + ``CALL_STIPEND`` = 10,300), charged only by ``CALL``/``CALLCODE``; +- a value transfer to a *new* account additionally creates the account, + whose ``GAS_NEW_ACCOUNT`` charge is the EIP-8037 *state* dimension and + is asserted via the block header ``max(regular, state)`` accounting, + never as regular gas; +- an EIP-7702 delegated target is double-accessed (target leaf plus + delegation leaf), each access cold or warm independently. + +These tests assert the EIP-8038 *regular* dimension; the EIP-8037 +*state* dimension for value-to-new-account is covered in +``eip8037_state_creation_gas_cost_increase/test_state_gas_call.py`` and +is only re-derived here at the seam to feed header gas accounting. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Bytecode, + CodeGasMeasure, + Environment, + Fork, + Header, + Op, + StateTestFiller, + Transaction, + TransactionReceipt, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _measure_call( + pre: Alloc, + fork: Fork, + measured_code: Bytecode, + own_cold_cost: Bytecode, + balance: int = 0, +) -> Address: + """ + Deploy a ``CodeGasMeasure`` contract around ``measured_code``. + + The overhead subtracts the call opcode's OWN cold cost (computed from + ``own_cold_cost``) so only the wrapping ``PUSH`` arguments remain in + the overhead; the measured value isolates the opcode's gas. The call + leaves exactly one stack item (its success flag). + """ + overhead_cost = measured_code.gas_cost(fork) - own_cold_cost.gas_cost(fork) + code_gas_measure = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + ) + return pre.deploy_contract(code=code_gas_measure, balance=balance) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_call_opcodes() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_call_access_gas( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, + warm: bool, +) -> None: + """ + Measure the access cost of every call opcode with no value transfer. + + 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) + cost_metadata = call_opcode(address_warm=warm) + measure_address = _measure_call( + 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) + + access_list = ( + [AccessList(address=target, storage_keys=[])] if warm else None + ) + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_call_opcodes() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_call_value_alive_target_gas( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, + warm: bool, +) -> None: + """ + Measure call cost with value transfer to an already-alive target. + + ``CALL``/``CALLCODE`` add ``CALL_VALUE`` (10,300) on top of the + access cost, where ``CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND``. + ``DELEGATECALL``/``STATICCALL`` never transfer value, so they pay + only the access cost regardless of any value argument. No new + account is created (the target is alive), so no state gas is charged. + + The ``CALL_STIPEND`` (2,300) is forwarded to the callee; with a + ``STOP`` callee it is unused and returned, so the gas *consumed* by + 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`. + 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) + + # Build the runnable call carrying the runtime metadata so that + # `measured_code.gas_cost(fork)` accounts for the value transfer. + if transfers_value: + measured_code = call_opcode.with_metadata( + address_warm=False, value_transfer=True + )( + gas=0, + address=target, + value=1, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, + ) + cost_metadata = call_opcode(address_warm=warm, value_transfer=True) + own_cold = call_opcode(address_warm=False, value_transfer=True) + else: + measured_code = call_opcode(gas=0, address=target) + cost_metadata = call_opcode(address_warm=warm) + own_cold = call_opcode(address_warm=False) + + # The measure contract needs balance to actually send the value. + measure_address = _measure_call( + 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) + 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 + ) + + access_list = ( + [AccessList(address=target, storage_keys=[])] if warm else None + ) + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + post = {measure_address: Account(storage={0: measured_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_callcode_value_to_nonexistent_no_new_account( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify CALLCODE value to a non-existent target charges CALL_VALUE + but not GAS_NEW_ACCOUNT. + + ``CALLCODE`` runs the callee's code in the caller's own context, so + the value never leaves the caller and no beneficiary account is + 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 + + # CALLCODE with value to a cold, non-existent target. The metadata + # carries `value_transfer` so `caller_code.gas_cost(fork)` reflects + # the CALL_VALUE charge; it must NOT carry `account_new` since the + # value stays with the caller and no beneficiary leaf is created. + callcode = Op.CALLCODE.with_metadata( + address_warm=False, value_transfer=True + )( + gas=0, + address=target, + value=1, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, + ) + 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_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. + expected_gas_used = ( + intrinsic + caller_code.gas_cost(fork) - gas_costs.CALL_STIPEND + ) + # Guard the no-state assertion: NEW_ACCOUNT would dominate if charged. + assert expected_gas_used < gas_costs.NEW_ACCOUNT + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used + ), + ) + + state_test(pre=pre, post={caller: Account(balance=1)}, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_call_value_to_new_account_seam( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify the CALL value-to-new-account regular/state seam. + + The EIP-8038 *regular* dimension is ``COLD_ACCOUNT_ACCESS`` + + ``CALL_VALUE`` = 13,300; the account creation charge + ``GAS_NEW_ACCOUNT`` (183,600) lands in the EIP-8037 *state* + 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. + call = Op.CALL.with_metadata( + address_warm=False, value_transfer=True, account_new=True + )( + gas=0, + address=target, + value=1, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, + ) + 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 + expected_gas_used = max(tx_regular, tx_state) + # State must dominate here, proving the 183,600 hit the state axis. + assert expected_gas_used == new_account_state_gas + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + state_gas_reservoir=new_account_state_gas, + ) + + state_test( + pre=pre, + post={target: Account(balance=1)}, + tx=tx, + blockchain_test_header_verify=Header(gas_used=expected_gas_used), + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_call_opcodes() +@pytest.mark.parametrize( + "target_warm", [False, True], ids=["target_cold", "target_warm"] +) +@pytest.mark.parametrize( + "delegate_warm", [False, True], ids=["delegate_cold", "delegate_warm"] +) +def test_call_to_delegated_target_double_access( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, + target_warm: bool, + delegate_warm: bool, +) -> None: + """ + Measure a call to a 7702-delegated target: 2x2 double access. + + The spec applies the delegation surcharge to every call opcode + (``CALL``/``CALLCODE``/``DELEGATECALL``/``STATICCALL``), so each + reads two account leaves: the target's leaf and the delegation's + leaf. Each is charged independently as ``WARM_ACCESS`` (100) or + ``COLD_ACCOUNT_ACCESS`` (3,000) by warmth. ``DELEGATECALL`` and + ``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`. + target = pre.fund_eoa(amount=0, delegation=delegate) + + measured_code = call_opcode(gas=0, address=target) + cost_metadata = call_opcode( + address_warm=target_warm, + delegated_address=True, + delegated_address_warm=delegate_warm, + ) + measure_address = _measure_call( + 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) + + # Warm the target and/or the delegate leaf via the access list. + access_entries = [] + if target_warm: + access_entries.append(AccessList(address=target, storage_keys=[])) + if delegate_warm: + access_entries.append(AccessList(address=delegate, storage_keys=[])) + access_list = access_entries or None + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.with_all_call_opcodes() +@pytest.mark.parametrize( + "sufficient_gas", [True, False], ids=["sufficient", "insufficient"] +) +def test_call_exact_gas_oog( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, + sufficient_gas: bool, +) -> None: + """ + Drive a cold call at exactly its gas (success) and one gas short (OOG). + + The caller forwards exactly enough gas for the inner call opcode (its + cold access cost plus the wrapping pushes). One gas short forces the + inner call to halt out-of-gas before executing, so the outer SSTORE + records 0; with the exact amount it records 1. + """ + target = pre.deploy_contract(Op.STOP) + + # Inner contract just performs the cold call to `target`. + inner_code = call_opcode(gas=0, address=target) + Op.STOP + inner = pre.deploy_contract(inner_code) + + # Exact regular gas for the inner frame: bytecode cost (which folds + # the cold call cost via the default metadata) under EIP-8038. + inner_gas_exact = inner_code.gas_cost(fork) + if not sufficient_gas: + inner_gas_exact -= 1 + + caller_code = Op.SSTORE(0, Op.CALL(gas=inner_gas_exact, address=inner)) + caller = pre.deploy_contract(caller_code) + + tx = Transaction(to=caller, sender=pre.fund_eoa()) + + post = {caller: Account(storage={0: 1 if sufficient_gas else 0})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_call_opcodes() +def test_call_self_is_warm( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, +) -> None: + """ + Verify a self-call is warm: the executing account is pre-warmed. + + 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, + # leaving the ADDRESS push and the other arg pushes as overhead. + measured_code = call_opcode(gas=0, address=Op.ADDRESS) + measure_address = _measure_call( + pre, fork, measured_code, call_opcode(address_warm=False) + ) + + 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()) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "sufficient_gas", [True, False], ids=["sufficient", "insufficient"] +) +def test_call_forwarded_gas_63_64( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + sufficient_gas: bool, +) -> None: + """ + Verify the 63/64 forwarding budget is computed after the repriced + cold access charge. + + A wrapper performs a cold, zero-value ``CALL`` requesting maximum + gas. The spec charges the repriced ``COLD_ACCOUNT_ACCESS`` (3,000) + up front and only then forwards ``floor(63/64 * gas_left)`` to the + child. The wrapper is handed an exact budget so that, net of the + access charge, ``gas_left`` equals ``child_regular * 64 // 63``; + forwarding then yields exactly the child's regular need + (``child_regular``) and its cold ``SSTORE`` takes effect. With one + gas less the floor drops below ``child_regular`` and the child OOGs, + so the slot stays zero. This pins that the floor is taken over + ``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 + + # Smallest budget whose 63/64 floor still reaches `child_regular`. + forward_budget = child_regular * 64 // 63 + if not sufficient_gas: + forward_budget -= 1 + + # 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, + ) + ) + + # 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 + + # Outer caller hands the wrapper exactly `wrapper_gas`. + caller = pre.deploy_contract( + Op.POP(Op.CALL(gas=wrapper_gas, address=wrapper)) + ) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + state_gas_reservoir=sstore_state_gas, + ) + + # Child SSTORE lands only when the forwarded floor reaches its need. + post = {child: Account(storage={0: 1 if sufficient_gas else 0})} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_account_warmth_reverts_on_subcall_revert( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Account warmth acquired inside a reverted sub-call does not persist. + + An inner contract reads an address's ``BALANCE`` via + ``DELEGATECALL`` (so the warmed address belongs to the shared + accessed-addresses set) then ``REVERT``s. Back in the outer frame, + that same address's first ``BALANCE`` is cold again and is charged + ``COLD_ACCOUNT_ACCESS`` (3,000), proving the warm-address set is + 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. + probed = pre.fund_eoa(amount=1) + + # Inner: warm `probed` by reading its balance, then revert. + inner = pre.deploy_contract( + code=Op.POP(Op.BALANCE(probed)) + Op.REVERT(0, 0), + ) + + # Outer: DELEGATECALL inner (which warms `probed` in the shared + # accessed set, then reverts, discarding that warmth), then measure + # its own first BALANCE of `probed`, which must be cold again. + measured_code = Op.BALANCE(probed) + overhead_cost = measured_code.gas_cost(fork) - Op.BALANCE( + address_warm=False + ).gas_cost(fork) + outer_code: Bytecode = Op.POP( + Op.DELEGATECALL(gas=100_000, address=inner) + ) + CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + ) + outer = pre.deploy_contract(code=outer_code) + + tx = Transaction(to=outer, sender=pre.fund_eoa()) + + # Slot 0 holds the measured (cold) BALANCE read. + post = {outer: Account(storage={0: cold_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_call_to_double_delegated_target_single_hop( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify delegation resolution is single-hop: A -> B -> C charges two. + + ``target`` (A) is an EOA delegated to ``mid`` (B), which is itself + an EOA delegated to ``final`` (C), a code-bearing account. A cold + ``CALL`` to ``target`` reads exactly two account leaves -- the + target's and its delegation's -- and is charged + ``2 * COLD_ACCOUNT_ACCESS`` (6,000). The chain is not followed a + 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. + final = pre.deploy_contract(Op.STOP) + mid = pre.fund_eoa(amount=0, delegation=final) + target = pre.fund_eoa(amount=0, delegation=mid) + + # Framework model: cold target leaf + cold delegation leaf, no third + # access for the second hop. + cost_metadata = Op.CALL( + address_warm=False, + delegated_address=True, + delegated_address_warm=False, + ) + expected_gas = 2 * gas_costs.COLD_ACCOUNT_ACCESS + assert expected_gas == cost_metadata.gas_cost(fork) + assert cost_metadata.state_cost(fork) == 0 + + measured_code = Op.CALL(gas=0, address=target) + measure_address = _measure_call( + pre, fork, measured_code, Op.CALL(address_warm=False) + ) + + tx = Transaction(to=measure_address, sender=pre.fund_eoa()) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_call_opcodes() +def test_call_precompile_is_warm( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + call_opcode: Op, +) -> None: + """ + Verify a call to a precompile is warm from the start. + + Precompiles are part of the accessed-addresses set from the start of + 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) + measure_address = _measure_call( + pre, fork, measured_code, call_opcode(address_warm=False) + ) + + 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()) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "value", [0, 1], ids=["no_value_no_stipend", "value_grants_stipend"] +) +def test_call_value_stipend_is_usable( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + value: int, +) -> None: + """ + The ``CALL`` value-transfer stipend (``CALL_STIPEND`` = 2,300) is + forwarded to the callee and usable for execution. + + The caller forwards ``gas=0``, so the callee receives only the stipend + (2,300) when a positive value is sent, and nothing otherwise. The + callee runs a small amount of work (well under 2,300 gas) then stops: + with the stipend the call succeeds (returns 1); without value (no + stipend, zero forwarded gas) the work runs out of gas and the call + fails (returns 0). This proves the stipend is not merely returned but + is spendable by the callee. + """ + # ~250 gas of cheap work: comfortably within the 2,300 stipend, far + # above the zero gas forwarded when no value (so no stipend) is sent. + work = (Op.PUSH1(0) + Op.POP) * 50 + Op.STOP + callee = pre.deploy_contract(code=work) + + caller = pre.deploy_contract( + code=Op.SSTORE(0, Op.CALL(0, callee, value, 0, 0, 0, 0)), + balance=1, + ) + + tx = Transaction(to=caller, sender=pre.fund_eoa()) + + # 1 when the stipend funded the callee's work, 0 when it ran out. + post = {caller: Account(storage={0: 1 if value else 0})} + state_test(env=env, pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..862b6fc77a7 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_create_gas.py @@ -0,0 +1,553 @@ +""" +Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) +``CREATE``/``CREATE2`` regular-gas dimension. + +Under EIP-8038 the contract-creation opcodes are repriced in their +*regular* gas dimension to ``CREATE_ACCESS`` (``ACCOUNT_WRITE`` + +``COLD_STORAGE_ACCESS`` = 11,000), on top of which the EIP-3860 init +code word cost (2 per word) and, for ``CREATE2`` only, an additional +keccak word cost (6 per word) are charged. The new-account creation +and per-byte code deposit charges are the EIP-8037 *state* dimension, +covered in +``eip8037_state_creation_gas_cost_increase/test_state_gas_create.py``. + +These tests isolate and assert the EIP-8038 *regular* dimension. At the +contract-creating-transaction boundary the state component is re-derived +only to feed the ``max(regular, state)`` block-header accounting. +""" + +from typing import List + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Bytecode, + CodeGasMeasure, + Fork, + Hash, + Header, + Initcode, + Op, + StateTestFiller, + Storage, + Transaction, + TransactionException, + compute_create_address, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.with_all_create_opcodes() +@pytest.mark.parametrize( + "init_code_size", + [ + pytest.param(0, id="empty"), + pytest.param(32, id="one_word"), + pytest.param(33, id="two_words"), + pytest.param(96, id="three_words"), + ], +) +def test_create_regular_gas( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + init_code_size: int, +) -> None: + """ + Measure the regular gas of CREATE/CREATE2 and assert the schedule. + + The EIP-8038 *regular* dimension is ``CREATE_ACCESS`` (11,000) plus + the EIP-3860 init code word cost (2 per word) plus, for ``CREATE2`` + only, an additional keccak word cost (6 per word). The EIP-8037 + 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). + create_meta = create_opcode(init_code_size=init_code_size) + regular_gas = create_meta.gas_cost(fork) - fork.create_state_gas( + code_size=0 + ) + # 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 + # reservoir. The initcode is brought into memory BEFORE the measured + # window, so the memory-expansion charge is excluded; the measured + # value is the CREATE opcode's regular cost exactly. The overhead + # subtracts the create-call argument pushes (the create leaves one + # stack item, its result). + # + # The initcode is all-zero bytes (`STOP`), so the child frame halts + # immediately consuming zero gas and deposits empty code. This keeps + # the measured value the CREATE opcode's own regular cost, with no + # child-execution gas folded in. `init_code_size` still drives the + # opcode's per-init-word charge. + padded_init = b"\x00" * init_code_size + + create_call = ( + Op.CREATE2(value=0, offset=0, size=init_code_size, salt=0) + 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 + + memory_setup = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE, new_memory_size=init_code_size) + if init_code_size + else Bytecode() + ) + storage = Storage() + measure = CodeGasMeasure( + code=create_call, + overhead_cost=arg_pushes, + extra_stack_items=1, + sstore_key=storage.store_next(regular_gas, "create_regular_gas"), + ) + factory = pre.deploy_contract(code=memory_setup + measure) + + tx = Transaction( + to=factory, + data=padded_init, + # Reservoir funds the account-creation state gas; leaving + # gas_limit unset keeps `Op.GAS` honest about gas_left. + state_gas_reservoir=fork.create_state_gas(code_size=0), + sender=pre.fund_eoa(), + ) + + post = {factory: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "init_code_size", + [ + pytest.param(32, id="one_word"), + pytest.param(64, id="two_words"), + pytest.param(128, id="four_words"), + ], +) +def test_create2_keccak_word_delta( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + init_code_size: int, +) -> None: + """ + Verify CREATE2 costs exactly the keccak word surcharge over CREATE. + + ``CREATE2`` hashes the init code to derive the salted address, adding + ``OPCODE_KECCAK256_PER_WORD`` (6) per init-code word on top of the + 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. + """ + 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. + padded = b"\x00" * init_code_size + + push4 = 4 * gas_costs.VERY_LOW + storage = Storage() + measure_create2 = CodeGasMeasure( + code=Op.CREATE2(value=0, offset=0, size=init_code_size, salt=0), + overhead_cost=push4, + extra_stack_items=1, + sstore_key=storage.store_next(create2_regular, "create2_regular"), + ) + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE, new_memory_size=init_code_size) + + measure_create2 + ) + factory = pre.deploy_contract(code=factory_code) + + tx = Transaction( + to=factory, + data=padded, + state_gas_reservoir=fork.create_state_gas(code_size=0), + sender=pre.fund_eoa(), + ) + + post = {factory: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +class TestCreateTxGasBoundary: + """ + Test the contract-creating-transaction gas boundary under EIP-8038. + + Four scenarios pin the boundary, mirroring EIP-3860's + ``TestContractCreationGasUsage`` but with the EIP-8037/8038 2D gas + split: + + 1. ``too_little_intrinsic_gas``: one below the total intrinsic; the + transaction is rejected (``INTRINSIC_GAS_TOO_LOW``). + 2. ``exact_intrinsic_gas``: exactly the intrinsic; the tx is valid + but the initcode runs out of execution gas. + 3. ``too_little_execution_gas``: one below the full execution gas; + creation fails but the tx is valid. + 4. ``exact_execution_gas``: exactly the full execution gas; creation + succeeds. + """ + + @pytest.fixture + def initcode(self) -> Initcode: + """Return a small initcode that deposits a multi-byte contract.""" + # Deploy 32 bytes (STOP + 31 padding) so code-deposit state gas + # is non-zero and the code-deposit branch is exercised. + return Initcode( + deploy_code=Op.STOP + Op.INVALID * 31, initcode_length=64 + ) + + @pytest.fixture + def tx_access_list(self) -> List[AccessList]: + """ + Return an access list to raise the intrinsic gas cost above the + EIP-7623 floor data cost, mirroring EIP-3860's fixture. + """ + return [ + AccessList(address=Address(i), storage_keys=[]) + for i in range(1, 642) + ] + + @pytest.fixture + def exact_intrinsic_gas( + self, + fork: Fork, + initcode: Initcode, + tx_access_list: List[AccessList], + ) -> int: + """Return the total (regular + state) intrinsic tx gas cost.""" + calc = fork.transaction_intrinsic_cost_calculator() + return calc( + calldata=initcode, + contract_creation=True, + access_list=tx_access_list, + ) + + @pytest.fixture + def exact_execution_gas( + self, fork: Fork, exact_intrinsic_gas: int, initcode: Initcode + ) -> int: + """ + Return the total execution gas: intrinsic plus the initcode + execution gas plus the code-deposit gas. + + ``deployment_gas`` is fork-aware: under EIP-8037 it splits the + deposit into the keccak word cost (regular) and the per-byte cost + (state), while on a fork without state-byte metering it is the + flat regular per-byte deposit cost. The single call is therefore + correct in either regime. + """ + execution = exact_intrinsic_gas + initcode.execution_gas(fork) + execution += initcode.deployment_gas(fork) + return execution + + @pytest.mark.parametrize( + "gas_test_case", + [ + pytest.param( + "too_little_intrinsic_gas", marks=pytest.mark.exception_test + ), + pytest.param("exact_intrinsic_gas"), + pytest.param("too_little_execution_gas"), + pytest.param("exact_execution_gas"), + ], + ) + @EIPChecklist.GasCostChanges.Test.OutOfGas() + def test_create_tx_gas_boundary( + self, + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + initcode: Initcode, + tx_access_list: List[AccessList], + exact_intrinsic_gas: int, + exact_execution_gas: int, + gas_test_case: str, + ) -> None: + """Drive a creation tx at each of the four gas boundary points.""" + sender = pre.fund_eoa() + create_address = compute_create_address(address=sender, nonce=0) + + if gas_test_case == "too_little_intrinsic_gas": + gas_limit = exact_intrinsic_gas - 1 + elif gas_test_case == "exact_intrinsic_gas": + gas_limit = exact_intrinsic_gas + elif gas_test_case == "too_little_execution_gas": + gas_limit = exact_execution_gas - 1 + else: + gas_limit = exact_execution_gas + + tx_error = ( + TransactionException.INTRINSIC_GAS_TOO_LOW + if gas_test_case == "too_little_intrinsic_gas" + else None + ) + + succeeds = gas_test_case == "exact_execution_gas" + post = { + create_address: ( + Account(code=initcode.deploy_code) + if succeeds + else Account.NONEXISTENT + ) + } + + tx = Transaction( + to=None, + data=initcode, + access_list=tx_access_list, + gas_limit=gas_limit, + error=tx_error, + sender=sender, + ) + + # 2D block accounting: gas_used = max(regular, state). The state + # axis carries the intrinsic NEW_ACCOUNT and (when the deposit + # succeeds) the per-byte code-deposit gas. + if tx_error is not None: + header_verify = None + else: + intrinsic_state = ( + fork.transaction_intrinsic_state_gas(contract_creation=True) + if hasattr(fork, "transaction_intrinsic_state_gas") + else 0 + ) + regular_used = gas_limit - intrinsic_state + state_used = intrinsic_state + if succeeds: + code_deposit_state = fork.code_deposit_state_gas( + code_size=len(initcode.deploy_code) + ) + state_used += code_deposit_state + regular_used -= code_deposit_state + header_verify = Header(gas_used=max(regular_used, state_used)) + + state_test( + pre=pre, + post=post, + tx=tx, + blockchain_test_header_verify=header_verify, + ) + + +@pytest.mark.with_all_create_opcodes() +@pytest.mark.parametrize( + "abort_mode", + [ + pytest.param("insufficient_balance", id="insufficient_balance"), + pytest.param("nonce_overflow", id="nonce_overflow"), + pytest.param(None, id="no_error"), + ], +) +def test_aborted_create_does_not_warm_address( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + abort_mode: str | None, +) -> None: + """ + Verify a silently-aborted CREATE does not warm the target address. + + When CREATE aborts before spawning the child frame (insufficient + balance for the endowment, or nonce overflow), the would-be address + is never added to the accessed-addresses set. A subsequent + ``BALANCE`` of that address is therefore charged the full + ``COLD_ACCOUNT_ACCESS`` (3,000), not ``WARM_ACCESS`` (100). + """ + init_code = Op.STOP + init_code_bytes = bytes(init_code) + init_code_len = len(init_code) + + create_value = 1 + create_call = create_opcode( + value=create_value, offset=0, size=init_code_len + ) + + # After the aborted CREATE, measure the BALANCE access of the + # would-be address (passed via calldata). + # The address should only be warm when the CREATE/CREATE2 opcode + # successfully reached initcode execution stage. + address_warm = abort_mode is None + balance_code = Op.BALANCE(Op.CALLDATALOAD(0), address_warm=address_warm) + measure = CodeGasMeasure(code=balance_code, extra_stack_items=1) + + setup = Op.MSTORE( + 0, + int.from_bytes(init_code_bytes, "big") << (256 - 8 * init_code_len), + ) + factory_code = setup + Op.POP(create_call) + measure + + factory_nonce = 2**64 - 1 if abort_mode == "nonce_overflow" else 1 + factory_balance = create_value + if abort_mode == "insufficient_balance": + factory_balance -= 1 + factory = pre.deploy_contract( + code=factory_code, nonce=factory_nonce, balance=factory_balance + ) + + target_address = compute_create_address( + address=factory, + salt=0, + initcode=init_code_bytes, + nonce=factory_nonce, + opcode=create_opcode, + ) + + tx = Transaction( + to=factory, + data=Hash(target_address, left_padding=True), + sender=pre.fund_eoa(), + ) + + # The BALANCE must be cold: in case of error, the aborted CREATE never + # warmed the would-be address. + post = { + factory: Account(storage={0: balance_code.gas_cost(fork)}), + target_address: Account(nonce=1) + if abort_mode is None + else Account.NONEXISTENT, + } + state_test(pre=pre, post=post, tx=tx) + + +@pytest.mark.pre_alloc_mutable +def test_create2_to_occupied_address( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify ``CREATE2`` to an occupied address creates nothing and refunds. + + When ``CREATE2`` targets an address that is not deployable (here an + already-deployed contract, whose ``code_hash`` is non-empty), the + creation aborts after the account-access charge: the opcode pushes + ``0``, bumps the factory's nonce, charges the message gas to the + regular dimension, and refunds the ``NEW_ACCOUNT`` *state* gas so no + net account-creation charge lands. No child frame runs, so the + occupied contract's code and storage are left untouched. + """ + # Initcode the factory passes to CREATE2; were the target free it + # would deposit a single STOP. The salt is fixed so the collision + # address is deterministic from the factory address. + init_code = Op.STOP + init_code_bytes = bytes(init_code) + init_code_len = len(init_code_bytes) + salt = 0 + + # Factory CREATE2s the calldata initcode and stores the pushed result; + # a collision pushes 0. The initcode is copied into memory before the + # CREATE2 so the address derivation hashes exactly ``init_code_bytes``. + storage = Storage() + factory_code = Op.CALLDATACOPY( + 0, 0, Op.CALLDATASIZE, new_memory_size=init_code_len + ) + Op.SSTORE( + storage.store_next(0, "create2_collision_result"), + Op.CREATE2(value=0, offset=0, size=init_code_len, salt=salt), + ) + factory = pre.deploy_contract(code=factory_code) + + # The address CREATE2 would compute from this factory, salt, and + # initcode. ``compute_create_address`` with ``opcode=Op.CREATE2`` is + # the unified EEST helper for the CREATE2 derivation. + collision_address = compute_create_address( + address=factory, + salt=salt, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + # Pre-occupy the collision address with a contract carrying distinct + # code and storage so a successful (and therefore incorrect) creation + # would be detectable. A non-empty ``code_hash`` makes the account + # non-deployable (``account_deployable`` is False). + # + # `address=` hard-codes the occupant at the derived collision address; + # it requires `pre_alloc_mutable`. This is the only way to pre-seat the + # exact CREATE2 target, mirroring the EIP-7610 collision suite. + occupant_code = Op.SSTORE(0, 0x42) + Op.STOP + occupant_storage = Storage({0x1: 0xCAFE}) # type: ignore[dict-item] + pre.deploy_contract( + code=occupant_code, + storage=occupant_storage, + nonce=1, + address=collision_address, + ) + + tx = Transaction( + to=factory, + data=init_code_bytes, + sender=pre.fund_eoa(), + ) + + # Factory stored a 0 result; the occupant is untouched (its initcode + # never ran, so slot 0 stays unset and slot 1 keeps its seeded value). + post = { + factory: Account(storage=storage), + collision_address: Account( + code=occupant_code, storage=occupant_storage + ), + } + state_test(pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..210879bf6f3 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_eip_mainnet.py @@ -0,0 +1,265 @@ +""" +Mainnet-marked happy-path smoke tests for +[EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +One minimal success per repriced dimension (no boundaries, no exact +magnitudes): a state slot is written, a value-bearing cold ``CALL`` +lands, an ``EXTCODESIZE`` runs, a ``CREATE`` deploys a contract, a +``SELFDESTRUCT`` funds a fresh account, a single ``7702`` authorization +installs a delegation, and a re-authorization of an already-delegated +authority applies the existing-authority refund. Gas limits are +deliberately generous so these prove the operation runs under the +EIP-8038 schedule without re-deriving any per-opcode cost (other files +own those matrices). +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + AuthorizationTuple, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = [pytest.mark.valid_at("Amsterdam"), pytest.mark.mainnet] + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_sstore_zero_to_nonzero( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + A zero-to-nonzero ``SSTORE`` pays the EIP-8038 storage write and + succeeds, leaving the slot set. + """ + storage = Storage() + contract = pre.deploy_contract(code=Op.SSTORE(storage.store_next(1), 1)) + + tx = Transaction( + to=contract, + gas_limit=1_000_000, + sender=pre.fund_eoa(), + ) + + post = {contract: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_cold_call_with_value( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + A value-bearing cold ``CALL`` pays ``COLD_ACCOUNT_ACCESS`` plus + ``CALL_VALUE`` and succeeds; the caller records the ``CALL`` success + flag and the callee receives the forwarded value. + """ + callee = pre.deploy_contract(code=Op.STOP, balance=0) + + caller_storage = Storage() + caller = pre.deploy_contract( + code=( + Op.SSTORE( + caller_storage.store_next(1), + Op.CALL(gas=100_000, address=callee, value=1), + ) + ), + ) + + tx = Transaction( + to=caller, + gas_limit=1_000_000, + value=1, + sender=pre.fund_eoa(), + ) + + post = { + caller: Account(storage=caller_storage), + callee: Account(balance=1), + } + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_extcodesize( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + ``EXTCODESIZE`` pays the EIP-8038 account access plus the code-read + surcharge and succeeds, returning the target's non-zero code size. + """ + target = pre.deploy_contract(code=Op.STOP * 3) + + storage = Storage() + contract = pre.deploy_contract( + code=Op.SSTORE(storage.store_next(3), Op.EXTCODESIZE(target)), + ) + + tx = Transaction( + to=contract, + gas_limit=1_000_000, + sender=pre.fund_eoa(), + ) + + post = {contract: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_create_deploys_contract( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + A factory ``CREATE``s a one-byte (``STOP``) contract under the + EIP-8038 schedule and succeeds; the factory records the ``CREATE`` + success flag in a slot. The transaction supplies the CREATE state + gas via the reservoir. + """ + init_code = Op.STOP + init_word = int.from_bytes(bytes(init_code), "big") << ( + 256 - 8 * len(init_code) + ) + + storage = Storage() + factory = pre.deploy_contract( + code=( + Op.MSTORE(0, init_word) + + Op.SSTORE( + storage.store_next(True), + Op.GT(Op.CREATE(0, 0, len(init_code)), 0), + ) + ), + ) + + tx = Transaction( + to=factory, + gas_limit=1_000_000, + state_gas_reservoir=fork.create_state_gas(code_size=0), + sender=pre.fund_eoa(), + ) + + post = {factory: Account(storage=storage)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_selfdestruct_funds_new_account( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + A balance-bearing contract ``SELFDESTRUCT``s to a fresh beneficiary + under the EIP-8038 schedule, forwarding its balance. The new-account + state gas is supplied via the reservoir; the beneficiary ends up + holding the transferred balance. + """ + beneficiary = pre.fund_eoa(amount=0) + + suicidal = pre.deploy_contract( + code=Op.SELFDESTRUCT(beneficiary), + balance=1, + ) + + tx = Transaction( + to=suicidal, + gas_limit=1_000_000, + state_gas_reservoir=fork.gas_costs().NEW_ACCOUNT, + sender=pre.fund_eoa(), + ) + + post = {beneficiary: Account(balance=1)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_auth_installs_delegation( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + A single valid ``7702`` authorization pays the EIP-8038 auth + intrinsic and installs a delegation designation on the authority. + """ + auth_signer = pre.fund_eoa() + set_code_to = pre.deploy_contract(code=Op.STOP) + + authorization_list = [ + AuthorizationTuple( + address=set_code_to, + nonce=0, + signer=auth_signer, + ), + ] + + tx = Transaction( + to=auth_signer, + gas_limit=1_000_000, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + ) + + post = { + auth_signer: Account( + nonce=1, + code=Spec7702.delegation_designation(set_code_to), + ), + } + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_existing_authority_refund( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Re-authorizing an already-delegated authority applies the + existing-authority refund and re-points the delegation; the tx + succeeds with the new designation installed. + """ + old_target = pre.deploy_contract(code=Op.STOP) + new_target = pre.deploy_contract(code=Op.STOP) + + # Authority already carries a delegation, so the new authorization + # triggers REFUND_AUTH_PER_EXISTING_ACCOUNT. + auth_signer = pre.fund_eoa(delegation=old_target) + + authorization_list = [ + AuthorizationTuple( + address=new_target, + nonce=1, + signer=auth_signer, + ), + ] + + tx = Transaction( + to=auth_signer, + gas_limit=1_000_000, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + ) + + post = { + auth_signer: Account( + nonce=2, + code=Spec7702.delegation_designation(new_target), + ), + } + state_test(pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..40376c8af55 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_exact_balance_no_fallback.py @@ -0,0 +1,222 @@ +""" +No-silent-fallback exact-balance tests for +[EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Each test funds the sender with *exactly* ``gas_limit * gas_price`` and +sets ``gas_limit`` one gas below the spec-correct Amsterdam intrinsic for +a single repriced dimension. A spec-correct client therefore rejects the +transaction with ``INTRINSIC_GAS_TOO_LOW``; a client that silently fell +back to the pre-Amsterdam value for that one constant would have computed +a strictly smaller intrinsic (``new - per_unit_delta``) and could have +executed the transaction. Because the sender holds no surplus wei, there +is no room for such a fallback to hide. + +The pre-Amsterdam (old) per-component value is read from the parent +fork's schedule (``fork.parent()``); the spec-correct intrinsic is read +from the active fork's intrinsic calculator. Nothing is hardcoded; the +gap is asserted to be positive so the construction is only emitted when +the dimension genuinely got more expensive. +""" + +import pytest +from execution_testing import ( + AccessList, + Alloc, + AuthorizationTuple, + Fork, + StateTestFiller, + Transaction, + TransactionException, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + +GAS_PRICE = 10 + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.exception_test +@pytest.mark.parametrize( + "num_addresses,num_keys", + [ + pytest.param(1, 0, id="one_address"), + pytest.param(2, 0, id="two_addresses"), + pytest.param(1, 1, id="one_address_one_key"), + ], +) +def test_access_list_no_fallback( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + num_addresses: int, + num_keys: int, +) -> None: + """ + Reject an access-list transaction whose ``gas_limit`` is one gas + below the Amsterdam intrinsic. + + EIP-8038 raises ``TX_ACCESS_LIST_ADDRESS`` (2400 -> 3000) and + ``TX_ACCESS_LIST_STORAGE_KEY`` (1900 -> 3000). A client reusing the + old per-address/per-key constants would compute an intrinsic smaller + 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)) + access_list = [ + AccessList( + address=pre.fund_eoa(amount=0), + storage_keys=storage_keys if index == 0 else [], + ) + for index in range(num_addresses) + ] + + intrinsic = fork.transaction_intrinsic_cost_calculator()( + 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`. + gas_limit = intrinsic - 1 + assert intrinsic - fallback_delta <= gas_limit < intrinsic + + sender = pre.fund_eoa(amount=gas_limit * GAS_PRICE) + tx = Transaction( + sender=sender, + to=pre.fund_eoa(amount=0), + access_list=access_list, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, post={}, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.exception_test +@pytest.mark.parametrize( + "num_auths", + [ + pytest.param(1, id="one_auth"), + pytest.param(2, id="two_auths"), + ], +) +def test_authorization_no_fallback( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + num_auths: int, +) -> None: + """ + Reject a ``7702`` set-code transaction whose ``gas_limit`` is one + gas below the Amsterdam intrinsic. + + EIP-8038 raises the per-authorization intrinsic + (``AUTH_PER_EMPTY_ACCOUNT``). A client reusing the old per-auth + constant would compute an intrinsic smaller by + ``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( + address=target, + nonce=0, + signer=pre.fund_eoa(), + ) + for _ in range(num_auths) + ] + + intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=authorization_list, + 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 + # protocol reserves is exactly gas_limit * GAS_PRICE. + sender = pre.fund_eoa(amount=gas_limit * GAS_PRICE) + tx = Transaction( + sender=sender, + to=pre.fund_eoa(amount=0), + authorization_list=authorization_list, + gas_limit=gas_limit, + max_fee_per_gas=GAS_PRICE, + max_priority_fee_per_gas=GAS_PRICE, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, post={}, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.exception_test +def test_cold_account_access_no_fallback( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Reject a plain call transaction whose ``gas_limit`` is one gas below + the Amsterdam intrinsic. + + Under EIP-2780 every non-create, non-self transaction pays one + ``COLD_ACCOUNT_ACCESS`` in its intrinsic for touching the recipient; + EIP-8038 raises that constant (2600 -> 3000). A client reusing the + old ``COLD_ACCOUNT_ACCESS`` would compute an intrinsic smaller by the + 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( + sender=sender, + to=pre.deploy_contract(code=b""), + gas_limit=gas_limit, + gas_price=GAS_PRICE, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, post={}, tx=tx) 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 new file mode 100644 index 00000000000..90f446398ce --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_ext_code_opcodes_gas.py @@ -0,0 +1,471 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Covers the EIP-8038 ``EXT*`` "double-read" surcharge: ``EXTCODESIZE`` and +``EXTCODECOPY`` perform two database reads (the account leaf and then the +code) and are therefore charged an extra ``WARM_ACCESS`` on top of the +account-access cost, whereas ``BALANCE`` and ``EXTCODEHASH`` read only the +account leaf and are charged the account-access cost alone. +""" + +from typing import Callable + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Bytecode, + CodeGasMeasure, + Environment, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +# Each parameter carries: +# - executable: builds the runnable opcode targeting ``target`` +# - cost_metadata: builds the metadata-only opcode for gas computation +# - extra_stack_items: stack items left by the opcode (for CodeGasMeasure) +# - code_read_surcharge: whether EIP-8038 adds the extra WARM_ACCESS read +EXT_OPCODES = [ + pytest.param( + lambda target: Op.EXTCODESIZE(target), + lambda warm: Op.EXTCODESIZE(address_warm=warm), + 1, + True, + id="EXTCODESIZE", + ), + pytest.param( + lambda target: Op.EXTCODECOPY(target, 0, 0, 0), + lambda warm: Op.EXTCODECOPY(address_warm=warm), + 0, + True, + id="EXTCODECOPY", + ), + pytest.param( + lambda target: Op.EXTCODEHASH(target), + lambda warm: Op.EXTCODEHASH(address_warm=warm), + 1, + False, + id="EXTCODEHASH", + ), + pytest.param( + lambda target: Op.BALANCE(target), + lambda warm: Op.BALANCE(address_warm=warm), + 1, + False, + id="BALANCE", + ), +] + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +@pytest.mark.parametrize( + "executable,cost_metadata,extra_stack_items,code_read_surcharge", + EXT_OPCODES, +) +def test_ext_code_opcode_gas( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, + executable: Callable[[object], Bytecode], + cost_metadata: Callable[[bool], Bytecode], + extra_stack_items: int, + code_read_surcharge: bool, +) -> None: + """ + Measure the exact gas of an external-code/account-access opcode and + assert it matches the EIP-8038 schedule. + + ``EXTCODESIZE``/``EXTCODECOPY`` must cost exactly one ``WARM_ACCESS`` + more than ``BALANCE``/``EXTCODEHASH`` at equal warmth (the second, + code-reading database access). + """ + gas_costs = fork.gas_costs() + + target = pre.deploy_contract(Op.STOP) + + measured_code = executable(target) + # Subtract the opcode's OWN cold cost (not BALANCE's) so the + # CodeGasMeasure overhead excludes only the PUSH wrapper; under + # EIP-8038 EXTCODESIZE/EXTCODECOPY have a higher cold cost than + # BALANCE because of the code-read surcharge. + overhead_cost = measured_code.gas_cost(fork) - cost_metadata( + False + ).gas_cost(fork) + + code_gas_measure = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=extra_stack_items, + ) + 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) + + # Warm the target via the access list when required; the cold case + # leaves it absent so its first runtime access is cold. + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=target, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +@pytest.mark.parametrize( + "copy_size", [32, 96], ids=["one_word", "three_words"] +) +def test_extcodecopy_nonzero_composes_additively( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, + copy_size: int, +) -> None: + """ + Verify the EIP-8038 ``EXTCODECOPY`` surcharge composes additively. + + With a non-zero copy size, ``EXTCODECOPY`` charges the account-access + cost, the EIP-8038 code-read ``WARM_ACCESS`` surcharge, the EIP-150 + per-word copy cost (``OPCODE_COPY_PER_WORD`` per word, driven by the + copied data size), and the memory-expansion cost. The surcharge is a + 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) + + # Runnable opcode copying ``copy_size`` bytes of the target's code into + # memory at offset 0. The metadata mirrors the runtime effect (warmth, + # copied byte count, and the 0 -> copy_size memory growth) so the + # opcode model agrees with execution and the overhead reduces to the + # operand pushes alone. + measured_code = Op.EXTCODECOPY.with_metadata( + address_warm=warm, + data_size=copy_size, + new_memory_size=copy_size, + old_memory_size=0, + )(target, 0, 0, copy_size) + + # Oracle: the same metadata-only opcode. Subtracting its cost from the + # measured code's cost yields the CodeGasMeasure overhead (the operand + # PUSHes only), so the stored value equals exactly this opcode cost. + oracle = Op.EXTCODECOPY.with_metadata( + address_warm=warm, + data_size=copy_size, + new_memory_size=copy_size, + old_memory_size=0, + ) + 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), + extra_stack_items=0, + ) + measure_address = pre.deploy_contract(code=code_gas_measure) + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=target, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_extcodehash_empty_account( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + Verify ``EXTCODEHASH`` of an empty account is priced without surcharge. + + ``EXTCODEHASH`` reads only the account leaf, so EIP-8038 adds no + code-read surcharge: the cost is exactly ``COLD_ACCOUNT_ACCESS`` (cold) + 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) + + # Measure the access cost, then store the returned hash so the + # empty-account 0 result is asserted alongside the pricing. The + # measured opcode carries the runtime warmth so the overhead reduces + # to the address PUSH alone. + # + # The empty-account hash is 0, which is also the default of an + # unwritten storage slot: a stranded hash store would leave slot 1 at + # 0 and pass vacuously (the original defect). Slot 1 is poisoned with + # a non-zero sentinel before the measured region, so the real store + # must overwrite it back to 0. If that store is ever stranded, slot 1 + # keeps the sentinel and the assertion fails instead of silently + # passing. + # The poison precedes the measured region and the hash store follows + # it, so neither touches 0xDEAD before the measured access nor + # perturbs the cold-case gas measurement. + storage = Storage() + measured_code = Op.EXTCODEHASH.with_metadata(address_warm=warm)(empty_addr) + gas_slot = storage.store_next(expected_gas, "extcodehash_empty_gas") + hash_slot = storage.store_next(0, "extcodehash_empty_hash") + hash_slot_sentinel = 0xBADC0FFEE + code = ( + Op.SSTORE(hash_slot, hash_slot_sentinel) + + CodeGasMeasure( + code=measured_code, + overhead_cost=measured_code.gas_cost(fork) + - Op.EXTCODEHASH(address_warm=warm).gas_cost(fork), + extra_stack_items=1, + sstore_key=gas_slot, + ) + + Op.SSTORE(hash_slot, Op.EXTCODEHASH(empty_addr)) + ) + measure_address = pre.deploy_contract(code=code) + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=empty_addr, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage=storage)} + state_test(env=env, pre=pre, post=post, tx=tx) + + +# The two surcharge opcodes only -- both always pay the second, +# code-reading access, so there is no no-surcharge variant here. +DOUBLE_READ_OPCODES = [Op.EXTCODESIZE, Op.EXTCODECOPY] + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +@pytest.mark.parametrize("opcode", DOUBLE_READ_OPCODES) +def test_ext_code_double_read_empty_account( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, + opcode: Op, +) -> None: + """ + Charge the EIP-8038 double-read surcharge on an empty target. + + ``EXTCODESIZE``/``EXTCODECOPY`` add the second, code-reading database + access unconditionally: the surcharge is charged before the account is + read, so an empty/non-existent target still costs + ``COLD_ACCOUNT_ACCESS + WARM_ACCESS`` (cold) or ``2 * WARM_ACCESS`` + (warm), i.e. 3100 / 200, exactly as for a code-bearing target. This + contrasts with ``EXTCODEHASH``/``BALANCE``, which read only the account + leaf and carry no surcharge (see ``test_extcodehash_empty_account``). A + client that skipped the second read for code-less accounts would be + caught here. + """ + # Never deployed: no code, no balance, non-existent account. + empty_addr = pre.nonexistent_account() + + measured_code = opcode(address=empty_addr) + # Subtract the opcode's OWN cold cost so the CodeGasMeasure overhead is + # only the operand PUSH wrapper; the surcharge is part of the cold cost. + overhead_cost = measured_code.gas_cost(fork) - opcode( + address_warm=False + ).gas_cost(fork) + + code_gas_measure = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=opcode.pushed_stack_items, + ) + measure_address = pre.deploy_contract(code=code_gas_measure) + expected_gas = opcode(address_warm=warm).gas_cost(fork) + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=empty_addr, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_extcodesize_empty_account_returns_zero( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + Pay the surcharge on an empty target while ``EXTCODESIZE`` returns 0. + + The size returned for an empty/non-existent account is ``0``, which + confirms the target genuinely has no code: the measured + ``COLD_ACCOUNT_ACCESS + WARM_ACCESS`` (cold) / ``2 * WARM_ACCESS`` + (warm) cost is therefore unambiguously the surcharge applied to an + empty account, not an artifact of the target accidentally holding code. + """ + # Never deployed: no code, no balance, non-existent account. + empty_addr = pre.nonexistent_account() + + expected_gas = Op.EXTCODESIZE(address_warm=warm).gas_cost(fork) + + # Measure the access cost and, separately, store the returned size so + # the empty-account 0 result is asserted alongside the pricing. The + # measured opcode carries the runtime warmth so the overhead reduces to + # the address PUSH alone. + storage = Storage() + measured_code = Op.EXTCODESIZE.with_metadata(address_warm=warm)(empty_addr) + gas_slot = storage.store_next(expected_gas, "extcodesize_empty_gas") + size_slot = storage.store_next(0, "extcodesize_empty_size") + code = CodeGasMeasure( + code=measured_code, + overhead_cost=measured_code.gas_cost(fork) + - Op.EXTCODESIZE(address_warm=warm).gas_cost(fork), + extra_stack_items=1, + sstore_key=gas_slot, + ) + Op.SSTORE(size_slot, Op.EXTCODESIZE(empty_addr)) + measure_address = pre.deploy_contract(code=code) + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=empty_addr, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage=storage)} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +@pytest.mark.parametrize( + "copy_size", [32, 96], ids=["one_word", "three_words"] +) +def test_extcodecopy_empty_account_composes_additively( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, + copy_size: int, +) -> None: + """ + Compose the surcharge additively when copying from an empty account. + + ``EXTCODECOPY`` of a non-existent source copies zero bytes into memory, + yet still charges the account-access cost, the EIP-8038 code-read + ``WARM_ACCESS`` surcharge, the EIP-150 per-word copy cost + (``OPCODE_COPY_PER_WORD`` per word, driven by the requested size, not + the source length), and the memory-expansion cost. The measured gas + must equal the sum of all four components, confirming the surcharge + composes additively even when there is no code to read. + """ + # Empty source: never deployed, no code. The copy yields zeros, but the + # cost is driven by the requested size, identical to a code-bearing + # source of the same length. + empty_addr = pre.nonexistent_account() + + oracle = Op.EXTCODECOPY.with_metadata( + address_warm=warm, + data_size=copy_size, + new_memory_size=copy_size, + old_memory_size=0, + ) + measured_code = oracle( + address=empty_addr, dest_offset=0, offset=0, size=copy_size + ) + + expected_gas = oracle.gas_cost(fork) + + code_gas_measure = CodeGasMeasure( + code=measured_code, + overhead_cost=measured_code.gas_cost(fork) - oracle.gas_cost(fork), + extra_stack_items=0, + ) + measure_address = pre.deploy_contract(code=code_gas_measure) + + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=[AccessList(address=empty_addr, storage_keys=[])] + if warm + else None, + ) + + post = {measure_address: Account(storage={0: expected_gas})} + state_test(env=env, pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..5ff91b0bbf6 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py @@ -0,0 +1,523 @@ +""" +Fork-transition tests for +[EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +"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. + +Two proof styles are used: + +* Account-access dimensions that are pure regular gas (``BALANCE`` cold + 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. +* 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. +""" + +from typing import List + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + AuthorizationTuple, + Block, + BlockchainTestFiller, + Bytecode, + CodeGasMeasure, + Fork, + Op, + Storage, + Transaction, + TransactionException, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") + +# Block timestamps straddling the Amsterdam activation. +BEFORE_TS = 14_999 +AFTER_TS = 15_000 + + +def _measure_contract( + pre: Alloc, measured: Bytecode, opcode_cost: int, fork: Fork +) -> Address: + """ + Deploy a contract that stores the exact gas consumed by the measured + opcode in slot 0. + + ``measured`` is the runnable expression (opcode plus its PUSH + operands); ``opcode_cost`` is the bare opcode's own gas at ``fork``. + The wrapper overhead (the PUSH operands) is the difference between + the two, so ``CodeGasMeasure`` strips it and slot 0 holds only the + opcode's own cost. The opcode leaves one stack item (its result). + """ + overhead = measured.gas_cost(fork) - opcode_cost + code = CodeGasMeasure( + code=measured, + overhead_cost=overhead, + extra_stack_items=1, + ) + return pre.deploy_contract(code=code) + + +def transition_blocks( + before_to: Address, + after_to: Address, + pre: Alloc, + *, + value: int = 0, +) -> List[Block]: + """ + Return the two blocks that straddle the Amsterdam activation. + + The first block runs at ``BEFORE_TS`` (pre-fork schedule) and the + second at ``AFTER_TS`` (EIP-8038 schedule). Each carries a single + transaction from a fresh sender to its respective ``to`` target, + forwarding ``value`` so a value-bearing operation is exercised in both + regimes. + """ + return [ + Block( + timestamp=BEFORE_TS, + txs=[ + Transaction(to=before_to, value=value, sender=pre.fund_eoa()), + ], + ), + Block( + timestamp=AFTER_TS, + txs=[ + Transaction(to=after_to, value=value, sender=pre.fund_eoa()), + ], + ), + ] + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_cold_account_access_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + ``BALANCE`` of a cold account costs ``COLD_ACCOUNT_ACCESS``, which + rises across the Amsterdam boundary (2600 -> 3000 on mainnet). The + same opcode is measured before and after; each block asserts its + regime's derived cost. + """ + 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 + assert cost_after > cost_before + + target = pre.deploy_contract(code=Op.STOP) + + # A distinct cold target per block keeps each measurement cold. + target_after = pre.deploy_contract(code=Op.STOP) + + # BALANCE has no code-read surcharge, so its bare cost equals + # COLD_ACCOUNT_ACCESS in each regime. + measure_before = _measure_contract( + pre, Op.BALANCE(target), cost_before, before + ) + measure_after = _measure_contract( + pre, Op.BALANCE(target_after), cost_after, after + ) + + blocks = transition_blocks(measure_before, measure_after, pre) + + post = { + measure_before: Account(storage={0: cost_before}), + measure_after: Account(storage={0: cost_after}), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_ext_code_surcharge_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The EIP-8038 ``EXT*`` code-read surcharge appears at the fork. The + surcharge equals ``EXTCODESIZE`` minus ``BALANCE`` at equal warmth: + it is zero before the fork and one ``WARM_ACCESS`` (100) after. That + comparison is computed from the opcode model. On-chain, each block + measures only a cold ``EXTCODESIZE`` (2600 before, 3100 after): its + rise reflects the surcharge on top of the cold-access repricing, and + ``BALANCE`` is never executed. + """ + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + + surcharge_before = Op.EXTCODESIZE(address_warm=True).gas_cost( + before + ) - Op.BALANCE(address_warm=True).gas_cost(before) + surcharge_after = Op.EXTCODESIZE(address_warm=True).gas_cost( + 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( + before + ) + extcodesize_cost_after = Op.EXTCODESIZE(address_warm=False).gas_cost(after) + assert extcodesize_cost_after > extcodesize_cost_before + + target = pre.deploy_contract(code=Op.STOP) + target_after = pre.deploy_contract(code=Op.STOP) + + measure_before = _measure_contract( + pre, Op.EXTCODESIZE(target), extcodesize_cost_before, before + ) + measure_after = _measure_contract( + pre, Op.EXTCODESIZE(target_after), extcodesize_cost_after, after + ) + + blocks = transition_blocks(measure_before, measure_after, pre) + + post = { + measure_before: Account(storage={0: extcodesize_cost_before}), + measure_after: Account(storage={0: extcodesize_cost_after}), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_call_value_cost_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + ``CALL_VALUE`` rises across the boundary (9000 -> 10300 on mainnet, + becoming ``ACCOUNT_WRITE + CALL_STIPEND``). The constant transition + is asserted from the derived schedules while a value-bearing ``CALL`` + 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) + + storage_before = Storage() + caller_before = pre.deploy_contract( + code=Op.SSTORE( + storage_before.store_next(1), + Op.CALL(gas=100_000, address=callee_before, value=1), + ), + ) + storage_after = Storage() + caller_after = pre.deploy_contract( + code=Op.SSTORE( + storage_after.store_next(1), + Op.CALL(gas=100_000, address=callee_after, value=1), + ), + ) + + blocks = transition_blocks(caller_before, caller_after, pre, value=1) + + post = { + caller_before: Account(storage=storage_before), + callee_before: Account(balance=1), + caller_after: Account(storage=storage_after), + callee_after: Account(balance=1), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_create_base_cost_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The ``CREATE`` regular base cost changes across the boundary + (``OPCODE_CREATE_BASE``: 32000 -> 11000 on mainnet, redefined as + ``ACCOUNT_WRITE + COLD_STORAGE_ACCESS``). The constant transition is + 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) + ) + + storage_before = Storage() + factory_before = pre.deploy_contract( + code=( + Op.MSTORE(0, init_word) + + Op.SSTORE( + storage_before.store_next(True), + Op.GT(Op.CREATE(0, 0, len(init_code)), 0), + ) + ), + ) + storage_after = Storage() + factory_after = pre.deploy_contract( + code=( + Op.MSTORE(0, init_word) + + Op.SSTORE( + storage_after.store_next(True), + Op.GT(Op.CREATE(0, 0, len(init_code)), 0), + ) + ), + ) + + blocks = transition_blocks(factory_before, factory_after, pre) + + post = { + factory_before: Account(storage=storage_before), + factory_after: Account(storage=storage_after), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_selfdestruct_account_write_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + ``SELFDESTRUCT`` gains an ``ACCOUNT_WRITE`` charge when it sends a + positive balance to an empty account, which is a new EIP-8038 + parameter (0 -> 8000 on mainnet). The constant transition is + asserted from the derived schedules and a value-bearing + ``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) + beneficiary_after = pre.fund_eoa(amount=0) + + suicidal_before = pre.deploy_contract( + code=Op.SELFDESTRUCT(beneficiary_before), + balance=1, + ) + suicidal_after = pre.deploy_contract( + code=Op.SELFDESTRUCT(beneficiary_after), + balance=1, + ) + + blocks = transition_blocks(suicidal_before, suicidal_after, pre) + + post = { + beneficiary_before: Account(balance=1), + beneficiary_after: Account(balance=1), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +def test_sstore_write_cost_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The ``SSTORE`` first-change cost is repriced across the Amsterdam + boundary, and EIP-8038 changes the *model*, not a single number. + + Before the fork (parent schedule) a zero-to-nonzero ``SSTORE`` is a + flat regular charge (``COLD_STORAGE_ACCESS + STORAGE_SET``) with no + state-gas dimension. After the fork the charge splits: the regular + portion drops to ``COLD_STORAGE_ACCESS + STORAGE_WRITE`` while the + bulk moves into the new state-gas dimension, and the clear refund + rises. Every magnitude is derived from the two schedules; nothing is + hardcoded. + + The transition is asserted at the derived-constant level (the + runtime opcode cost cannot isolate the regular portion without the + state-gas confounder) and a zero-to-nonzero ``SSTORE`` is exercised + in both blocks to prove it still sets the slot in each regime. + """ + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + + # First-change (zero -> nonzero, cold) SSTORE in each regime. + sstore = Op.SSTORE(new_value=1) + + regular_before = sstore.regular_cost(before) + regular_after = sstore.regular_cost(after) + state_before = sstore.state_cost(before) + state_after = sstore.state_cost(after) + total_before = sstore.gas_cost(before) + total_after = sstore.gas_cost(after) + + # The repricing changes the regular charge, introduces the state + # dimension, and therefore moves the total. + assert regular_after != regular_before + assert state_before == 0 + 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 + assert refund_after > refund_before + + # Exercise the zero-to-nonzero SSTORE in both regimes; the slot ends + # set in each block. + storage_before = Storage() + contract_before = pre.deploy_contract( + code=Op.SSTORE(storage_before.store_next(1), 1), + ) + storage_after = Storage() + contract_after = pre.deploy_contract( + code=Op.SSTORE(storage_after.store_next(1), 1), + ) + + blocks = transition_blocks(contract_before, contract_after, pre) + + post = { + contract_before: Account(storage=storage_before), + contract_after: Account(storage=storage_after), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@EIPChecklist.GasCostChanges.Test.ForkTransition.Before() +@EIPChecklist.GasCostChanges.Test.ForkTransition.After() +@pytest.mark.exception_test +def test_auth_intrinsic_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The ``7702`` authorization intrinsic rises across the boundary. A tx + whose ``gas_limit`` equals the pre-fork single-authorization + intrinsic is valid before the fork but is rejected with + ``INTRINSIC_GAS_TOO_LOW`` after, because the EIP-8038 auth intrinsic + is strictly larger. + """ + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + + intrinsic_before = before.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=1, + return_cost_deducted_prior_execution=True, + ) + intrinsic_after = after.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=1, + return_cost_deducted_prior_execution=True, + ) + # The pre-fork intrinsic is below the post-fork one, so the same + # gas_limit straddles validity at the boundary. + assert intrinsic_before < intrinsic_after + gas_limit = intrinsic_before + + target_before = pre.deploy_contract(code=Op.STOP) + target_after = pre.deploy_contract(code=Op.STOP) + + auth_before = pre.fund_eoa() + auth_after = pre.fund_eoa() + + blocks = [ + # Before the fork: gas_limit covers the old auth intrinsic. + Block( + timestamp=BEFORE_TS, + txs=[ + Transaction( + to=auth_before, + gas_limit=gas_limit, + authorization_list=[ + AuthorizationTuple( + address=target_before, + nonce=0, + signer=auth_before, + ), + ], + sender=pre.fund_eoa(), + ), + ], + ), + # After the fork: identical gas_limit is now below intrinsic. + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=auth_after, + gas_limit=gas_limit, + authorization_list=[ + AuthorizationTuple( + address=target_after, + nonce=0, + signer=auth_after, + ), + ], + sender=pre.fund_eoa(), + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ), + ], + exception=TransactionException.INTRINSIC_GAS_TOO_LOW, + ), + ] + + blockchain_test(pre=pre, blocks=blocks, post={}) 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 new file mode 100644 index 00000000000..63cf7e0544a --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py @@ -0,0 +1,677 @@ +""" +Tests for the EIP-8038 [State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038) +``SELFDESTRUCT`` regular-gas dimension. + +Under EIP-8038 ``SELFDESTRUCT`` is charged, in its *regular* gas +dimension: + +- ``OPCODE_SELFDESTRUCT_BASE`` (5,000); +- a ``COLD_ACCOUNT_ACCESS`` (3,000) surcharge when the beneficiary is + cold (a warm beneficiary adds nothing — SELFDESTRUCT has no + ``WARM_ACCESS`` surcharge); +- a net-new ``ACCOUNT_WRITE`` (8,000) when a positive balance is sent to + an empty (or non-existent) beneficiary, replacing the legacy combined + 25,000 regular account-creation cost. + +So ``regular = 5,000 + (3,000 if cold) + (8,000 if creating)``: 13,000 +warm / 16,000 cold when a new beneficiary is created, 5,000 warm / 8,000 +cold otherwise. + +The beneficiary account-creation charge ``GAS_NEW_ACCOUNT`` (183,600) is +the EIP-8037 *state* dimension (`charge_state_gas` in the spec), covered +in ``eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py``. + +``SELFDESTRUCT`` halts the frame, so it is driven via a wrapping ``CALL`` +and verified through block ``gas_used`` accounting and balances. Per +EIP-6780, a contract not created in the same transaction is not deleted, +but its balance is still transferred and the beneficiary creation charge +still applies. + +The framework opcode-gas model splits the two dimensions for +``SELFDESTRUCT`` exactly as the spec does: ``ACCOUNT_WRITE`` is charged +as regular gas and ``GAS_NEW_ACCOUNT`` as state gas, so +``Op.SELFDESTRUCT(account_new=True).regular_cost(fork)`` is the regular +charge (16,000 cold / 13,000 warm) and ``.state_cost(fork)`` is +``GAS_NEW_ACCOUNT``. These tests assert the regular dimension and verify +account-creation via balances; the state dimension is owned by +``eip8037_state_creation_gas_cost_increase/test_state_gas_selfdestruct.py``. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Bytecode, + Environment, + Fork, + Header, + Op, + StateTestFiller, + Storage, + Transaction, + TransactionReceipt, + compute_create_address, +) +from execution_testing.checklists import EIPChecklist + +from ..eip7708_eth_transfer_logs.spec import burn_log, transfer_log +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _selfdestruct_regular(fork: Fork, *, warm: bool, account_new: bool) -> int: + """ + Return the EIP-8038 *regular* gas charged by SELFDESTRUCT. + + ``OPCODE_SELFDESTRUCT_BASE + access + (ACCOUNT_WRITE if account_new)``; + 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( + 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( + beneficiary: Address | Bytecode, *, warm: bool, account_new: bool +) -> Bytecode: + """ + Build SELFDESTRUCT bytecode with metadata so ``regular_cost(fork)`` + folds the beneficiary PUSH and the correct access/account-write + charge (account-creation state gas excluded — it is charged + separately by the spec). + """ + return Op.SELFDESTRUCT.with_metadata( + address_warm=warm, account_new=account_new + )(beneficiary) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_selfdestruct_new_beneficiary_regular_gas( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + SELFDESTRUCT to an empty beneficiary with balance charges + ACCOUNT_WRITE. + + The destructor has a non-zero balance and targets an empty, + non-existent beneficiary, so the net-new ``ACCOUNT_WRITE`` applies: + ``regular = 5,000 + access + 8,000`` (13,000 warm, 16,000 cold). The + creation gas ``GAS_NEW_ACCOUNT`` is charged on the state axis (the + 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 + + regular = _selfdestruct_regular(fork, warm=warm, account_new=True) + assert regular == (13_000 if warm else 16_000) + + beneficiary = Address(0xDEAD) # empty, non-existent + + destructor_code = Op.SELFDESTRUCT(beneficiary) + destructor = pre.deploy_contract(code=destructor_code, balance=1) + + storage = Storage() + caller_code = Op.SSTORE( + storage.store_next(1, "call_succeeds"), + Op.CALL(gas=Op.GAS, address=destructor), + ) + caller = pre.deploy_contract(code=caller_code) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + access_list=[AccessList(address=beneficiary, storage_keys=[])] + if warm + else None, + state_gas_reservoir=new_account_state_gas, + ) + + state_test( + pre=pre, + post={ + caller: Account(storage=storage), + # New beneficiary created and credited the destructor balance. + beneficiary: Account(balance=1), + }, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_selfdestruct_alive_beneficiary_no_account_write( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + SELFDESTRUCT to an already-alive beneficiary charges no ACCOUNT_WRITE. + + The beneficiary already exists, so no account is created: regular = + ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 cold) and no state gas is + charged. The block header reflects the pure regular consumption. + """ + regular = _selfdestruct_regular(fork, warm=warm, account_new=False) + assert regular == (5_000 if warm else 8_000) + + beneficiary = pre.fund_eoa(amount=1) # alive + + destructor_code = _destructor_code( + beneficiary, warm=warm, account_new=False + ) + destructor = pre.deploy_contract(code=destructor_code, balance=1) + + caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=destructor)) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + access_list = ( + [AccessList(address=beneficiary, storage_keys=[])] if warm else None + ) + # Intrinsic must include the access-list cost that warms the + # beneficiary; pass the list so the calculator folds it in. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + access_list=access_list + ) + + # Pure regular: intrinsic + caller frame + destructor frame (whose + # regular_cost folds the SELFDESTRUCT charge and beneficiary PUSH). + expected_gas_used = ( + intrinsic + + caller_code.gas_cost(fork) + + destructor_code.regular_cost(fork) + ) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + access_list=access_list, + state_gas_reservoir=0, + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used, + ), + ) + + state_test( + pre=pre, + # EIP-6780: the pre-deployed destructor is not same-tx-created, + # so it is not deleted; its balance still transfers. + post={ + destructor: Account(balance=0, code=destructor_code), + beneficiary: Account(balance=2), + }, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_selfdestruct_codebearing_zero_balance_beneficiary_no_account_write( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + SELFDESTRUCT to a code-bearing zero-balance beneficiary: no + ACCOUNT_WRITE. + + The beneficiary is alive because it has code, not balance: it holds a + zero balance but a non-empty code (``Op.STOP``), so EIP-161 emptiness + does not apply and no account is created when a positive balance is + sent to it. Regular = ``5,000 + (3,000 if cold)`` (5,000 warm, 8,000 + cold) with no ACCOUNT_WRITE and no state gas — distinct from the + alive-via-balance case, which exercises the same path through a + different liveness source. + """ + regular = _selfdestruct_regular(fork, warm=warm, account_new=False) + assert regular == (5_000 if warm else 8_000) + + # Alive via code (non-empty code), with zero balance. + beneficiary = pre.deploy_contract(code=Op.STOP, balance=0) + + destructor_code = _destructor_code( + beneficiary, warm=warm, account_new=False + ) + destructor = pre.deploy_contract(code=destructor_code, balance=1) + + caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=destructor)) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + access_list = ( + [AccessList(address=beneficiary, storage_keys=[])] if warm else None + ) + # Intrinsic must include the access-list cost that warms the + # beneficiary; pass the list so the calculator folds it in. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + access_list=access_list + ) + + # Pure regular: intrinsic + caller frame + destructor frame (whose + # regular_cost folds the SELFDESTRUCT charge and beneficiary PUSH). + expected_gas_used = ( + intrinsic + + caller_code.gas_cost(fork) + + destructor_code.regular_cost(fork) + ) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + access_list=access_list, + state_gas_reservoir=0, + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used + ), + ) + + state_test( + pre=pre, + # EIP-6780: the pre-deployed destructor is not same-tx-created, + # so it is not deleted; its balance still transfers. + post={ + destructor: Account(balance=0, code=destructor_code), + # Code-bearing beneficiary credited the destructor balance. + beneficiary: Account(balance=1, code=Op.STOP), + }, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_selfdestruct_zero_balance_no_account_write( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + SELFDESTRUCT with a zero-balance destructor charges no ACCOUNT_WRITE. + + No value is transferred, so even a non-existent beneficiary is not + created: regular = ``5,000 + access`` and no state gas is charged. + """ + regular = _selfdestruct_regular(fork, warm=warm, account_new=False) + assert regular == (5_000 if warm else 8_000) + + beneficiary = Address(0xDEAD) # non-existent, but no value sent + + destructor_code = _destructor_code( + beneficiary, warm=warm, account_new=False + ) + destructor = pre.deploy_contract(code=destructor_code, balance=0) + + caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=destructor)) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + access_list = ( + [AccessList(address=beneficiary, storage_keys=[])] if warm else None + ) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + access_list=access_list + ) + + expected_gas_used = ( + intrinsic + + caller_code.gas_cost(fork) + + destructor_code.regular_cost(fork) + ) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + access_list=access_list, + state_gas_reservoir=0, + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used + ), + ) + + state_test( + pre=pre, + post={ + destructor: Account(balance=0, code=destructor_code), + beneficiary: Account.NONEXISTENT, + }, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "beneficiary_kind", + [ + pytest.param("self", id="self_beneficiary"), + pytest.param("precompile", id="precompile_beneficiary"), + ], +) +def test_selfdestruct_self_or_precompile_beneficiary( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + beneficiary_kind: str, +) -> None: + """ + SELFDESTRUCT to self or a precompile is warm and charges no + ACCOUNT_WRITE. + + The executing account is in the accessed set on entry (self), and + precompiles are pre-warmed from the start, so neither pays a cold + surcharge: regular = ``5,000`` (warm base, no ``WARM_ACCESS``) with no + state gas. + + The destructor balance is chosen so no account creation occurs: self + is alive (sending to itself never creates), and the precompile case + sends zero value (precompiles hold no state entry, so a value + 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). + destructor_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)( + Op.ADDRESS + ) + balance = 1 + else: + # Identity precompile (address 4) is pre-warmed. Zero balance so + # no value transfer and thus no account creation. + destructor_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)( + Address(4) + ) + balance = 0 + destructor = pre.deploy_contract(code=destructor_code, balance=balance) + + caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=destructor)) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + + expected_gas_used = ( + intrinsic + + caller_code.gas_cost(fork) + + destructor_code.regular_cost(fork) + ) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + state_gas_reservoir=0, + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_gas_used + ), + ) + + # EIP-6780: the pre-deployed destructor is not deleted. The self case + # keeps its balance (transferred to itself); the precompile case sent + # nothing. + post = {destructor: Account(balance=balance, code=destructor_code)} + + state_test( + pre=pre, + post=post, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.parametrize( + "sufficient_gas", [True, False], ids=["sufficient", "insufficient"] +) +def test_selfdestruct_oog_boundary( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + sufficient_gas: bool, +) -> None: + """ + Drive a cold SELFDESTRUCT that funds a new account at its exact total + gas and one short. + + The destructor sends value to an empty beneficiary, charging + ``5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000) in regular gas + and ``GAS_NEW_ACCOUNT`` in state gas. The child CALL frame has no state + reservoir of its own, so the state gas spills into the forwarded + regular gas and the frame needs its full ``gas_cost`` total. Forwarding + exactly that total lets the SELFDESTRUCT succeed (CALL returns 1); one + 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 + ) + destructor = pre.deploy_contract(code=destructor_code, balance=1) + + # The child CALL frame gets no state reservoir, so the NEW_ACCOUNT + # state gas spills into the forwarded regular gas: forward the full + # total. One gas short forces an out-of-gas before the value transfer. + forwarded = destructor_code.gas_cost(fork) + if not sufficient_gas: + forwarded -= 1 + + storage = Storage() + caller_code = Op.SSTORE( + storage.store_next(1 if sufficient_gas else 0, "sd_result"), + Op.CALL(gas=forwarded, address=destructor), + ) + caller = pre.deploy_contract(code=caller_code) + + tx = Transaction( + to=caller, + sender=pre.fund_eoa(), + state_gas_reservoir=0, + ) + + if sufficient_gas: + post: dict = { + caller: Account(storage=storage), + beneficiary: Account(balance=1), + } + else: + post = { + caller: Account(storage=storage), + beneficiary: Account.NONEXISTENT, + destructor: Account(balance=1), + } + + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.pre_alloc_mutable() +def test_same_tx_created_selfdestruct_self_burn( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + EIP-6780: a same-tx-created contract SELFDESTRUCTs to itself, burning + its balance, charged the warm base only. + + A creation transaction whose initcode SELFDESTRUCTs the new contract + to ITSELF: the originator is created in this transaction so it is + deleted, and because a same-tx-created contract holding balance is + alive, ``account_new`` is false for the self-beneficiary — + ``regular = 5,000`` (warm self, no ``ACCOUNT_WRITE``) and no + SELFDESTRUCT state gas. The originator balance is burnt (a ``Burn`` + log, not a ``Transfer``), distinguishing the same-tx self-destruct + path: the funding-empty-beneficiary charge keys on the beneficiary, + but same-tx deletion keys on the originator independently. + + No net state gas is charged: the only state cost is the intrinsic + creation ``NEW_ACCOUNT``, but the pre-funded created target is alive + at message entry, so EIP-8037 refunds it (the create-tx + ``created_target_alive`` refund), and the self-burn beneficiary + already exists. The block ``gas_used`` is therefore the pure regular + consumption. + """ + new_account_state_gas = fork.gas_costs().NEW_ACCOUNT + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + + amount = 1 + sender = pre.fund_eoa(amount=10**18) + created = compute_create_address(address=sender, nonce=0) + # Pre-fund the created address so its balance is present without an + # in-tx value transfer (which would emit its own Transfer log). The + # pre-funded target is alive at message entry, so the create-tx + # intrinsic NEW_ACCOUNT is refunded (EIP-8037). + pre.fund_address(created, amount) + + # Self is the executing account, warm on entry: no cold surcharge. + init_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)(Op.ADDRESS) + + # 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 + + intrinsic_total = intrinsic_calc( + calldata=bytes(init_code), contract_creation=True + ) + # The creation NEW_ACCOUNT is refunded (target alive at entry) and the + # self-burn adds no state gas, so net state gas is zero. + intrinsic_regular = intrinsic_total - new_account_state_gas + expected_regular = intrinsic_regular + init_code.regular_cost(fork) + expected_gas_used = expected_regular + + tx = Transaction( + to=None, + data=init_code, + # Slack covers the create-side NEW_ACCOUNT charged transiently + # before its refund, even though net state gas is zero. + sender=sender, + expected_receipt=TransactionReceipt( + logs=[burn_log(created, amount)], + cumulative_gas_used=expected_gas_used, + ), + ) + + state_test( + pre=pre, + # Same-tx-created originator is deleted; its balance is burnt. + post={created: Account.NONEXISTENT}, + tx=tx, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.pre_alloc_mutable() +def test_same_tx_created_selfdestruct_to_fresh_beneficiary( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + EIP-6780: a same-tx-created contract sends value to a fresh + beneficiary, charged ``ACCOUNT_WRITE`` and creation state gas. + + A creation transaction whose initcode SELFDESTRUCTs the new contract + to a fresh ``Address(0xDEAD)``: the fresh, non-existent beneficiary + receives a positive balance, so ``account_new`` is true — + ``regular = 5,000 + COLD_ACCOUNT_ACCESS + ACCOUNT_WRITE`` (16,000 + cold) plus a beneficiary ``NEW_ACCOUNT`` on the state axis. The + beneficiary creation charge keys on the beneficiary, while the + originator (created in this transaction) is still deleted: a + ``Transfer`` log is emitted (not a ``Burn``). + + The net state gas is a single beneficiary ``NEW_ACCOUNT``: the + intrinsic creation ``NEW_ACCOUNT`` is refunded because the pre-funded + 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 + beneficiary = Address(0xDEAD) # fresh, non-existent + sender = pre.fund_eoa(amount=10**18) + created = compute_create_address(address=sender, nonce=0) + # Pre-fund the created address so its balance is present without an + # in-tx value transfer (which would emit its own Transfer log). The + # pre-funded target is alive at message entry, so the create-tx + # intrinsic NEW_ACCOUNT is refunded (EIP-8037). + pre.fund_address(created, amount) + + # Cold beneficiary receiving value: account_new is true. + init_code = Op.SELFDESTRUCT.with_metadata( + address_warm=False, account_new=True + )(beneficiary) + + regular = _selfdestruct_regular(fork, warm=False, account_new=True) + assert regular == 16_000 + + intrinsic_total = 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) + + tx = Transaction( + to=None, + data=init_code, + sender=sender, + # Reservoir holds the beneficiary-creation state gas (above the + # creation's intrinsic NEW_ACCOUNT) so it does not spill into + # regular gas. + state_gas_reservoir=new_account_state_gas, + expected_receipt=TransactionReceipt( + logs=[transfer_log(created, beneficiary, amount)] + ), + ) + + state_test( + pre=pre, + # Same-tx-created originator is deleted; the fresh beneficiary is + # created and credited the originator balance. + post={ + created: Account.NONEXISTENT, + beneficiary: Account(balance=amount), + }, + tx=tx, + blockchain_test_header_verify=Header(gas_used=expected_gas_used), + ) 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 new file mode 100644 index 00000000000..da283a5fee1 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_gas.py @@ -0,0 +1,605 @@ +""" +Tests for the EIP-7702 authorization *regular*-gas repricing under +[EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +EIP-8037 splits each EIP-7702 authorization into a *state* component +(refunded against the state-gas reservoir, covered by the sibling +``eip8037_state_creation_gas_cost_increase`` suite) and a *regular* +component. This module pins the **regular** per-authorization intrinsic +magnitude and the repriced cold/warm account-access costs that an +authorized delegation incurs when later accessed by a ``CALL``. + +The regular per-authorization magnitude is derived purely from fork +helpers as:: + + regular_per_auth = ( + fork.gas_costs().AUTH_PER_EMPTY_ACCOUNT + - fork.transaction_intrinsic_state_gas(authorization_count=1) + ) + +which on Amsterdam equals ``ACCOUNT_WRITE`` (``8000``) plus the EIP-7702 +regular auth base cost (``7816``), i.e. ``15816``. The state portion that +this subtracts off (``transaction_intrinsic_state_gas``) is exactly what +the EIP-8037 suite asserts on the state channel; this suite never +re-asserts it. +""" + +from typing import List + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + AuthorizationTuple, + Bytecode, + CodeGasMeasure, + Environment, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, + TransactionException, + TransactionReceipt, +) +from execution_testing.checklists import EIPChecklist + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _regular_per_auth(fork: Fork) -> int: + """ + Return the EIP-8038 *regular* intrinsic gas charged per EIP-7702 + authorization, i.e. the total per-auth intrinsic less the EIP-8037 + state portion. + """ + return fork.gas_costs().AUTH_PER_EMPTY_ACCOUNT - ( + fork.transaction_intrinsic_state_gas(authorization_count=1) + ) + + +def _regular_intrinsic( + fork: Fork, + *, + n: int, + access_list: List[AccessList] | None = None, + calldata: bytes = b"", +) -> int: + """ + Return the regular (non-state) intrinsic gas of a set-code + transaction: the full intrinsic less the authorization state gas. + """ + total = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=n, + access_list=access_list, + calldata=calldata, + return_cost_deducted_prior_execution=True, + ) + return total - fork.transaction_intrinsic_state_gas( + authorization_count=n, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("n", [1, 2, 3]) +@pytest.mark.parametrize( + "authority_exists", + [ + pytest.param(False, id="new_authority"), + pytest.param(True, id="existing_authority"), + ], +) +@pytest.mark.parametrize( + "authority_in_access_list", + [ + pytest.param(False, id="empty_access_list"), + pytest.param(True, id="access_list_contains_authority"), + ], +) +def test_auth_regular_intrinsic_magnitude( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + n: int, + authority_exists: bool, + authority_in_access_list: bool, +) -> None: + """ + Assert the EIP-8038 *regular* per-authorization intrinsic magnitude. + + The regular intrinsic above the ``n=0`` base must equal + ``n * regular_per_auth`` plus the access-list delta (derived from + the calculator itself so the calldata-floor contribution of the + access-list bytes is accounted for). The state portion is excluded + via ``transaction_intrinsic_state_gas`` and is left to the EIP-8037 + suite. + """ + contract = pre.deploy_contract(code=Op.STOP) + + signers = [ + pre.fund_eoa() if authority_exists else pre.fund_eoa(amount=0) + for _ in range(n) + ] + authorization_list = [ + AuthorizationTuple(address=contract, nonce=0, signer=signer) + for signer in signers + ] + + access_list: List[AccessList] | None = None + if authority_in_access_list: + access_list = [ + AccessList(address=signer, storage_keys=[]) for signer in signers + ] + + base_regular = _regular_intrinsic(fork, n=0) + regular = _regular_intrinsic(fork, n=n, access_list=access_list) + + # Access-list delta is derived from the calculator (it folds in the + # calldata-floor cost of the access-list bytes), never hardcoded. + access_list_delta = _regular_intrinsic( + fork, n=0, access_list=access_list + ) - _regular_intrinsic(fork, n=0) + + expected_per_auth = _regular_per_auth(fork) + assert regular - base_regular == n * expected_per_auth + access_list_delta + + sender = pre.fund_eoa() + tx = Transaction( + to=contract, + authorization_list=authorization_list, + access_list=access_list, + sender=sender, + ) + + post = { + signer: Account(code=Spec7702.delegation_designation(contract)) + for signer in signers + } + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.OutOfGas() +@pytest.mark.exception_test +@pytest.mark.parametrize("n", [1, 3]) +def test_auth_intrinsic_oog_boundary( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + n: int, +) -> None: + """ + Reject a set-code transaction one gas below the full intrinsic. + + ``gas_limit`` is set to ``full_intrinsic - 1`` (full intrinsic = + regular + auth state gas). Catches an implementation that omits the + repriced regular per-authorization cost from the intrinsic check. + """ + contract = pre.deploy_contract(code=Op.STOP) + authorization_list = [ + AuthorizationTuple(address=contract, nonce=0, signer=pre.fund_eoa()) + for _ in range(n) + ] + + full_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=authorization_list, + ) + + tx = Transaction( + to=contract, + gas_limit=full_intrinsic - 1, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(env=env, pre=pre, post={}, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "invalidity", + [ + pytest.param("invalid_nonce", id="invalid_nonce"), + pytest.param("invalid_chain_id", id="invalid_chain_id"), + pytest.param("repeated_nonce", id="repeated_nonce"), + pytest.param("authority_is_contract", id="authority_is_contract"), + ], +) +@pytest.mark.pre_alloc_mutable +def test_invalid_auth_charged_intrinsic( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + invalidity: str, +) -> None: + """ + A skipped (invalid) authorization is still charged the full + intrinsic, and the invalid authority's account is left unchanged. + + Each invalidity kind (``INVALID_NONCE``, ``INVALID_CHAIN_ID``, + ``REPEATED_NONCE``, ``AUTHORITY_IS_CONTRACT``) makes the + authorization invalid during processing, so it is silently skipped, + but its regular + state intrinsic gas is still paid. The transaction + succeeds. + """ + contract = pre.deploy_contract(code=Op.STOP) + + # Build a (possibly multi-element) authorization list where the + # authority that *should* end up untouched is the invalid one. + authorization_list: List[AuthorizationTuple] = [] + + if invalidity == "invalid_nonce": + authority = pre.fund_eoa() + authorization_list.append( + AuthorizationTuple( + address=contract, + nonce=99, # wrong nonce -> skipped + signer=authority, + ) + ) + expected_code: bytes | Bytecode = b"" + elif invalidity == "invalid_chain_id": + authority = pre.fund_eoa() + authorization_list.append( + AuthorizationTuple( + address=contract, + nonce=0, + chain_id=9999, # wrong chain id -> skipped + signer=authority, + ) + ) + expected_code = b"" + elif invalidity == "repeated_nonce": + # First auth is valid and consumes nonce 0; the second reuses + # nonce 0 and is therefore skipped. The (single) signer ends up + # delegated by the first auth, so assert that delegation. + authority = pre.fund_eoa() + authorization_list.append( + AuthorizationTuple(address=contract, nonce=0, signer=authority) + ) + authorization_list.append( + AuthorizationTuple(address=contract, nonce=0, signer=authority) + ) + expected_code = Spec7702.delegation_designation(contract) + elif invalidity == "authority_is_contract": + # An authority that is already a (non-delegation) contract is an + # invalid authority; the authorization is skipped and the + # contract code is left intact. + authority = pre.fund_eoa(code=Op.STOP) + authorization_list.append( + AuthorizationTuple(address=contract, nonce=0, signer=authority) + ) + expected_code = Op.STOP + else: + raise ValueError(f"unknown invalidity: {invalidity!r}") + + # The full intrinsic (regular + state) is charged regardless of + # validity. Provide a comfortable gas limit and let the receipt + # accounting be verified by the framework; the key assertion is the + # untouched-authority post state. + full_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=authorization_list, + ) + assert full_intrinsic > 0 + + sender = pre.fund_eoa() + tx = Transaction( + to=contract, + authorization_list=authorization_list, + sender=sender, + ) + + post = {authority: Account(code=expected_code)} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "invalidity", + [ + pytest.param("invalid_nonce", id="invalid_nonce"), + pytest.param("invalid_chain_id", id="invalid_chain_id"), + pytest.param("repeated_nonce", id="repeated_nonce"), + pytest.param("authority_is_contract", id="authority_is_contract"), + ], +) +@pytest.mark.pre_alloc_mutable +def test_mixed_validity_multi_auth_receipt_gas( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + invalidity: str, +) -> None: + """ + Pin the exact receipt gas of a transaction carrying one valid and + one invalid authorization. + + Every authorization tuple, valid or invalid, is charged the full + regular + state per-authorization intrinsic. Refunds, however, fire + only for the *valid* authorization whose authority leaf already + exists: it refills ``NEW_ACCOUNT`` on the state channel (uncapped, + subtracted first) and returns ``ACCOUNT_WRITE`` on the regular + channel (one-fifth capped). The invalid tuple is silently skipped + during ``set_delegation`` and contributes no refund on either + channel. + + The dual-channel accounting mirrors ``process_transaction`` and the + sibling ``test_set_code_auth_refunds`` module: the state refill is + subtracted from ``gas_before_regular_refund`` first and uncapped, + then the regular refund clamps to + ``min(k * ACCOUNT_WRITE, gas_before_regular_refund // 5)`` where + ``k`` is the number of refundable (valid, existing-leaf) + authorizations. With no EVM execution, ``gas_before_regular_refund`` + reduces to the full per-authorization intrinsic less the state + refill, and the exact result is asserted via ``expected_receipt``. + + Each ``invalidity`` kind (``INVALID_NONCE``, ``INVALID_CHAIN_ID``, + ``REPEATED_NONCE``, ``AUTHORITY_IS_CONTRACT``) yields one valid and + one invalid tuple, so ``n = 2`` and ``k = 1`` uniformly and every + kind pins the same receipt gas. This is the numeric-receipt + companion to ``test_invalid_auth_charged_intrinsic`` (which asserts + only post state). + """ + gas_costs = fork.gas_costs() + account_write = gas_costs.ACCOUNT_WRITE + + delegate = pre.deploy_contract(code=Op.STOP) + + # The single refundable (valid, existing-leaf) authorization. + valid_signer = pre.fund_eoa() + valid_auth = AuthorizationTuple( + address=delegate, nonce=0, signer=valid_signer + ) + + # Build the authorization list: one valid tuple plus one invalid + # tuple of the requested kind. ``authority`` is the account that must + # end up untouched by the skipped (invalid) authorization. + authorization_list: List[AuthorizationTuple] + post: dict = { + valid_signer: Account( + code=Spec7702.delegation_designation(delegate), + ), + } + + if invalidity == "invalid_nonce": + authority = pre.fund_eoa() + authorization_list = [ + valid_auth, + AuthorizationTuple( + address=delegate, + nonce=99, # wrong nonce -> skipped + signer=authority, + ), + ] + post[authority] = Account(code=b"") + elif invalidity == "invalid_chain_id": + authority = pre.fund_eoa() + authorization_list = [ + valid_auth, + AuthorizationTuple( + address=delegate, + nonce=0, + chain_id=9999, # wrong chain id -> skipped + signer=authority, + ), + ] + post[authority] = Account(code=b"") + elif invalidity == "repeated_nonce": + # The valid tuple consumes the signer's nonce 0; a second tuple + # reusing nonce 0 on the same signer is skipped. The signer is + # the refundable authority, delegated by its first (valid) tuple. + authorization_list = [ + valid_auth, + AuthorizationTuple(address=delegate, nonce=0, signer=valid_signer), + ] + elif invalidity == "authority_is_contract": + # An authority that is already a (non-delegation) contract is an + # invalid authority; its authorization is skipped and the + # contract code is left intact. + authority = pre.fund_eoa(code=Op.STOP) + authorization_list = [ + valid_auth, + AuthorizationTuple(address=delegate, nonce=0, signer=authority), + ] + post[authority] = Account(code=Op.STOP) + else: + raise ValueError(f"unknown invalidity: {invalidity!r}") + + n = len(authorization_list) + refundable = 1 # exactly one valid, existing-leaf authorization + + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=n, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=n, + ) + # Only the valid existing-leaf authorization refills the state + # channel (NEW_ACCOUNT); the invalid tuple refunds nothing. The + # refill is subtracted first and is not subject to the one-fifth cap. + state_refund = gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT * refundable + + # No EVM execution (the target is a STOP), so the regular and state + # execution gas are both zero and ``gas_before_regular_refund`` + # reduces to the full per-auth intrinsic less the state refill. + gas_before_regular_refund = total_intrinsic - state_refund + regular_refund = min( + refundable * account_write, + gas_before_regular_refund // fork.max_refund_quotient(), + ) + # With only a single refundable authorization the one-fifth cap is + # generous, so the full ACCOUNT_WRITE clears on the regular channel. + assert regular_refund == refundable * account_write + cumulative_gas_used = gas_before_regular_refund - regular_refund + + tx = Transaction( + to=delegate, + state_gas_reservoir=intrinsic_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=cumulative_gas_used, + ), + ) + + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize( + "self_sponsored", + [ + pytest.param(False, id="external_sponsor"), + pytest.param(True, id="self_sponsor"), + ], +) +@pytest.mark.parametrize( + "delegation_in_access_list", + [ + pytest.param(False, id="delegation_cold"), + pytest.param(True, id="delegation_warm"), + ], +) +def test_auth_account_warming( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + self_sponsored: bool, + delegation_in_access_list: bool, +) -> None: + """ + A later ``CALL`` to an authorized authority pays the repriced + cold/warm account-access costs, plus the delegation double-charge. + + The authority itself is warmed by the authorization (added to + ``accessed_addresses`` during validation), so the ``CALL`` access + to it is ``WARM_ACCESS``. Because the authority carries a delegation + designator, accessing it triggers a *second* access to the + delegation target: ``WARM_ACCESS`` if that target is in the access + list (or is the authority itself, for self-delegation), else + ``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. + """ + gas_costs = fork.gas_costs() + cold = gas_costs.COLD_ACCOUNT_ACCESS + warm = gas_costs.WARM_ACCESS + + delegation_target = pre.deploy_contract(code=Op.STOP) + + if self_sponsored: + # Self-sponsored: the sender is the authority. fund_eoa with a + # delegation pre-installs the designator and sets nonce to 1. + sender = pre.fund_eoa(delegation=delegation_target) + authority: Address = sender + authorization_list = None + else: + sender = pre.fund_eoa() + authority = pre.fund_eoa() + authorization_list = [ + AuthorizationTuple( + address=delegation_target, + nonce=0, + signer=authority, + ) + ] + + access_list: List[AccessList] | None = None + if delegation_in_access_list: + access_list = [AccessList(address=delegation_target, storage_keys=[])] + + # Authority access: always warm (authorization or self-sponsor warms + # it). Delegation target double-charge: warm iff in the access list, + # else cold. + delegation_access = warm if delegation_in_access_list else cold + expected_cost = warm + delegation_access + + # 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) + storage = Storage() + callee_code = CodeGasMeasure( + code=Op.CALL(gas=0, address=authority), + overhead_cost=overhead_cost, + extra_stack_items=1, + sstore_key=storage.store_next(expected_cost), + ) + callee_address = pre.deploy_contract(callee_code) + + tx = Transaction( + to=callee_address, + authorization_list=authorization_list, + access_list=access_list, + sender=sender, + ) + + post = { + callee_address: Account(storage=storage), + authority: Account( + code=Spec7702.delegation_designation(delegation_target), + ), + } + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_many_auths_block_limit( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Pack many authorizations into a single transaction near the gas + limit cap and confirm it succeeds. + + The authorization count is sized from the per-authorization total + intrinsic (regular + state) and the transaction gas-limit cap, so it + automatically tracks the repriced cost. + """ + 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 + base = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=0, + ) + # Leave headroom for the base intrinsic and a little slack. + 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) + for signer in signers + ] + + sender = pre.fund_eoa() + tx = Transaction( + to=contract, + authorization_list=authorization_list, + sender=sender, + ) + + post = { + signer: Account(code=Spec7702.delegation_designation(contract)) + for signer in signers + } + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py new file mode 100644 index 00000000000..ba3dc5e5773 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_set_code_auth_refunds.py @@ -0,0 +1,242 @@ +""" +Tests for the EIP-7702 authorization *regular*-gas refund under +[EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +When an authority's account leaf already exists, ``set_delegation`` +refunds on two independent channels: + +* the **state** channel: ``StateGasCosts.NEW_ACCOUNT`` is refilled into + ``state_gas_reservoir`` / ``state_refund`` (and ``AUTH_BASE`` too when + the code slot already holds a delegation indicator). It is subtracted + from ``tx_state_gas`` *before* the regular refund is applied and is + **not** subject to the EIP-3529 one-fifth cap. This channel is the + subject of the EIP-8037 ``eip8037_state_creation_gas_cost_increase`` + suite. +* the **regular** channel: the worst-case ``GasCosts.ACCOUNT_WRITE`` + charged in the regular intrinsic is returned via the regular refund + counter, and **is** subject to the one-fifth cap. + +This module pins the *regular* ``ACCOUNT_WRITE`` refund. The dual-channel +accounting mirrors ``process_transaction``: + + gas_before_regular_refund = ( + intrinsic_regular + exec_regular + + intrinsic_state + exec_state + - state_refund # uncapped, subtracted first + ) + regular_refund = min( + n * ACCOUNT_WRITE, + gas_before_regular_refund // fork.max_refund_quotient(), + ) + cumulative_gas_used = gas_before_regular_refund - regular_refund + +Two regimes are exercised: + +* a non-clearing delegation on an existing leaf, padded with cold + SSTOREs so ``gas_before_regular_refund`` is large and the full + ``n * ACCOUNT_WRITE`` clears under the cap; and +* a *clearing* re-authorization of an existing-delegation authority, + where the state channel refunds the **full** per-auth state intrinsic + (``NEW_ACCOUNT + AUTH_BASE``). That collapses + ``gas_before_regular_refund`` to the regular intrinsic alone, so the + cap ``gas // 5`` becomes the binding term and the regular refund + clamps below ``ACCOUNT_WRITE``. +""" + +from typing import List + +import pytest +from execution_testing import ( + Account, + Alloc, + AuthorizationTuple, + Bytecode, + Environment, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, + TransactionReceipt, +) +from execution_testing.checklists import EIPChecklist + +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _sstore_state_per_op(fork: Fork) -> int: + """Return the state gas of one cold ``0 -> 1`` SSTORE.""" + return Op.SSTORE(new_value=1).state_cost(fork) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@pytest.mark.parametrize("n", [1, 2]) +def test_existing_authority_regular_refund_visible( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + n: int, +) -> None: + """ + Pin the full regular ``ACCOUNT_WRITE`` refund for set-code + authorizations whose authority leaves already exist. + + Each authority is an existing funded EOA delegating to a fresh + contract, so ``set_delegation`` refunds ``NEW_ACCOUNT`` on the state + channel (uncapped) and ``ACCOUNT_WRITE`` on the regular channel + (capped). The execution is padded with ten cold ``0 -> 1`` SSTOREs + so ``gas_before_regular_refund`` is large and the one-fifth cap + exceeds ``n * ACCOUNT_WRITE``; the entire regular refund is visible + in the receipt. + + The state refill is subtracted first and is not capped; it belongs + to the EIP-8037 suite and is only used here to size the receipt. + """ + gas_costs = fork.gas_costs() + account_write = gas_costs.ACCOUNT_WRITE + # Existing leaf overwritten with a fresh (non-clearing) delegation + # indicator: only NEW_ACCOUNT is refilled on the state channel. + state_refund = gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT * n + + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=n, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=n, + ) + + num_sstores = 10 + storage = Storage() + code = Bytecode() + for _ in range(num_sstores): + code += Op.SSTORE(storage.store_next(1), 1) + code += Op.STOP + contract = pre.deploy_contract(code=code) + + exec_state = _sstore_state_per_op(fork) * num_sstores + # The deployed bytecode's combined cost minus its state portion is + # the regular execution gas (includes the PUSHes for SSTORE args). + exec_regular = code.gas_cost(fork) - exec_state + + delegate = pre.deploy_contract(code=Op.STOP) + signers = [pre.fund_eoa() for _ in range(n)] + authorization_list = [ + AuthorizationTuple(address=delegate, nonce=0, signer=signer) + for signer in signers + ] + + gas_before_regular_refund = ( + total_intrinsic + exec_regular + exec_state - state_refund + ) + regular_refund = min( + n * account_write, + gas_before_regular_refund // fork.max_refund_quotient(), + ) + assert regular_refund == n * account_write + cumulative_gas_used = gas_before_regular_refund - regular_refund + + tx = Transaction( + to=contract, + state_gas_reservoir=intrinsic_state + exec_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=cumulative_gas_used, + ), + ) + + post: dict = {contract: Account(storage=storage)} + for signer in signers: + post[signer] = Account( + code=Spec7702.delegation_designation(delegate), + ) + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@pytest.mark.parametrize("n", [1, 3]) +def test_clearing_delegation_regular_refund_capped( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + n: int, +) -> None: + """ + Clearing a delegation refunds the full per-auth state intrinsic on + the state channel, which drives the regular refund into the + one-fifth cap. + + Each authority already holds a delegation and re-authorizes to the + reset (zero) address, clearing its code. The leaf exists, so + ``ACCOUNT_WRITE`` is refunded on the regular channel; the code slot + held a delegation indicator and the new indicator is empty, so both + ``NEW_ACCOUNT`` and ``AUTH_BASE`` are refilled on the state channel. + Refunding the full per-auth state intrinsic collapses + ``gas_before_regular_refund`` to the regular intrinsic alone, so the + cap ``gas // 5`` is below ``n * ACCOUNT_WRITE`` and the regular + refund clamps to ``gas // 5`` (cap-saturated). No execution padding + is used, so the contrast with the full-refund test is purely the + refunded state magnitude. + """ + gas_costs = fork.gas_costs() + account_write = gas_costs.ACCOUNT_WRITE + + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=n, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=n, + ) + # Clearing an existing delegation refills the full per-auth state + # intrinsic (NEW_ACCOUNT + AUTH_BASE) for every authorization. + state_refund = intrinsic_state + + contract = pre.deploy_contract(code=Op.STOP) + delegated_to = pre.deploy_contract(code=Op.STOP) + # Authorities that already delegate; fund_eoa(delegation=...) sets + # the authority nonce to 1, which is the expected auth nonce. + signers = [pre.fund_eoa(delegation=delegated_to) for _ in range(n)] + authorization_list: List[AuthorizationTuple] = [ + AuthorizationTuple( + address=Spec7702.RESET_DELEGATION_ADDRESS, + nonce=1, + signer=signer, + ) + for signer in signers + ] + + gas_before_regular_refund = total_intrinsic - state_refund + regular_refund = min( + n * account_write, + gas_before_regular_refund // fork.max_refund_quotient(), + ) + # The cap is the binding term: the refund clamps below ACCOUNT_WRITE. + assert regular_refund < n * account_write + assert regular_refund == gas_before_regular_refund // ( + fork.max_refund_quotient() + ) + cumulative_gas_used = gas_before_regular_refund - regular_refund + + tx = Transaction( + to=contract, + state_gas_reservoir=intrinsic_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=cumulative_gas_used, + ), + ) + + post: dict = {} + for signer in signers: + # Delegation cleared back to empty code, nonce incremented. + post[signer] = Account(nonce=2, code=b"") + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sload_gas.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sload_gas.py new file mode 100644 index 00000000000..26502836c7b --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sload_gas.py @@ -0,0 +1,192 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Covers the EIP-8038 ``SLOAD`` repricing: a cold storage slot read costs +``COLD_STORAGE_ACCESS`` (3000) and a warm read costs ``WARM_SLOAD`` (100). +A slot is warmed either by listing it in the transaction access list or by +a prior in-frame access; warmth acquired inside a sub-call that REVERTs is +discarded, so a subsequent read in the outer frame is cold again. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Alloc, + Bytecode, + CodeGasMeasure, + Environment, + Fork, + Op, + StateTestFiller, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _measure_sload(slot: int, fork: Fork) -> CodeGasMeasure: + """ + Build a ``CodeGasMeasure`` around a single ``SLOAD`` whose stored + result is the bare opcode cost (the PUSH wrapper is subtracted out). + + The runtime warmth of ``slot`` determines whether the measured value + lands at ``COLD_STORAGE_ACCESS`` or ``WARM_SLOAD``. + """ + measured_code = Op.SLOAD(slot) + # Subtract the SLOAD opcode's own cold cost so only the PUSH wrapper + # remains as overhead; the runtime access cost is what gets stored. + overhead_cost = measured_code.gas_cost(fork) - Op.SLOAD( + key_warm=False + ).gas_cost(fork) + return CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + ) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("warm", [False, True], ids=["cold", "warm"]) +def test_sload_gas( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, + warm: bool, +) -> None: + """ + Measure the gas of a ``SLOAD`` on a slot that is either cold or + pre-warmed via the transaction access list. + + A cold read must cost ``COLD_STORAGE_ACCESS`` (3000); a warm read + must cost ``WARM_SLOAD`` (100). + """ + slot = 0x42 + expected_gas = Op.SLOAD(key_warm=warm).gas_cost(fork) + + measure_address = pre.deploy_contract( + code=_measure_sload(slot, fork), + storage={slot: 1}, + ) + + # Warm the slot via the access list when required; the cold case + # leaves it unlisted so its first runtime read is cold. + access_list = ( + [AccessList(address=measure_address, storage_keys=[slot])] + if warm + else None + ) + tx = Transaction( + to=measure_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + + # Slot 0 holds the measured gas; the read slot keeps its value. + post = {measure_address: Account(storage={0: expected_gas, slot: 1})} + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_sload_warm_after_prior_touch( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + A first ``SLOAD`` on a cold slot warms it; the second in-frame + ``SLOAD`` of the same slot is charged ``WARM_SLOAD`` (100). + + Slot 0 records the cold first read and slot 1 the warm second read. + """ + slot = 0x42 + cold_gas = Op.SLOAD(key_warm=False).gas_cost(fork) + warm_gas = Op.SLOAD(key_warm=True).gas_cost(fork) + + measured_code = Op.SLOAD(slot) + overhead_cost = measured_code.gas_cost(fork) - Op.SLOAD( + key_warm=False + ).gas_cost(fork) + + # First measure (slot 0): cold read. Second measure (slot 1): the + # same slot is now warm. + code = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + sstore_key=0, + ) + CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + sstore_key=1, + ) + measure_address = pre.deploy_contract(code=code, storage={slot: 1}) + + tx = Transaction(to=measure_address, sender=pre.fund_eoa()) + + # Slots 0/1 hold the two measured reads; the read slot keeps its + # value. + post = { + measure_address: Account(storage={0: cold_gas, 1: warm_gas, slot: 1}) + } + state_test(env=env, pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_sload_warmth_reverts_on_subcall_revert( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Warmth acquired inside a reverted sub-call does not persist. + + An inner contract ``SLOAD``s the slot via ``DELEGATECALL`` (so the + warmed ``(address, slot)`` pair belongs to the outer account) then + ``REVERT``s. Back in the outer frame, that same slot's first + ``SLOAD`` is cold again and is charged ``COLD_STORAGE_ACCESS`` + (3000), proving the warm-slot set is rolled back on revert. + """ + slot = 0x42 + cold_gas = Op.SLOAD(key_warm=False).gas_cost(fork) + + # Inner: read the slot (warming it in the delegating account's + # context) then revert. + inner = pre.deploy_contract( + code=Op.SLOAD(slot) + Op.REVERT(0, 0), + ) + + # Outer: DELEGATECALL inner (which reverts), then measure its own + # first SLOAD of the slot. DELEGATECALL keeps the outer account's + # storage context, so inner's read warms (outer, slot); the revert + # discards that warmth, making the measured read cold. + measured_code = Op.SLOAD(slot) + overhead_cost = measured_code.gas_cost(fork) - Op.SLOAD( + key_warm=False + ).gas_cost(fork) + + outer_code: Bytecode = Op.POP( + Op.DELEGATECALL(gas=100_000, address=inner) + ) + CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=1, + ) + outer = pre.deploy_contract(code=outer_code, storage={slot: 1}) + + tx = Transaction(to=outer, sender=pre.fund_eoa()) + + # Slot 0 holds the measured (cold) read; the read slot keeps its + # value. + post = {outer: Account(storage={0: cold_gas, slot: 1})} + state_test(env=env, pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..e6ba6c912fe --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py @@ -0,0 +1,215 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Covers the EIP-8038 ``SSTORE`` *regular* (non-state) gas schedule. The +state-creation charge for a zero-to-nonzero write is owned by EIP-8037 +and is asserted separately; here every expectation is taken from the +``regular_cost`` dimension only. + +The regular ``SSTORE`` cost is the slot-access cost (``COLD_STORAGE_ACCESS`` +when the key is cold, else ``WARM_SLOAD``) plus, on the first change of the +slot in the transaction (``original == current != new``), the write cost +``STORAGE_WRITE`` (modeled as ``COLD_STORAGE_WRITE - COLD_STORAGE_ACCESS``). +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Alloc, + Bytecode, + CodeGasMeasure, + Fork, + Op, + StateTestFiller, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +# Each parameter: (key_warm, original, current, new). The id encodes the +# (original, current, new) triple, where ``0`` is the zero value and +# ``x``/``y``/``z`` are distinct non-zero values (1, 2, 3). The suffix marks +# the slot state at the measured write. A clean slot (current == original) +# is ``_cold`` or access-list ``_warm``; a dirty slot (current != original) +# is ``_dirty`` and has necessarily been warmed by the prior in-frame SSTORE. +SSTORE_ROWS = [ + pytest.param(False, 0, 0, 1, id="00x_cold"), + pytest.param(True, 0, 0, 1, id="00x_warm"), + pytest.param(True, 0, 1, 0, id="0x0_dirty"), + pytest.param(True, 1, 1, 0, id="xx0_warm"), + pytest.param(False, 1, 1, 2, id="xxy_cold"), + pytest.param(True, 1, 1, 2, id="xxy_warm"), + pytest.param(True, 1, 2, 3, id="xyz_dirty"), + pytest.param(True, 1, 2, 1, id="xyx_dirty"), + pytest.param(True, 1, 1, 1, id="xxx_warm"), + pytest.param(False, 1, 1, 1, id="xxx_cold"), +] + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +@pytest.mark.parametrize("key_warm,original,current,new", SSTORE_ROWS) +def test_sstore_regular_gas( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + key_warm: bool, + original: int, + current: int, + new: int, +) -> None: + """ + Measure the regular ``SSTORE`` gas for each EIP-8038 row and assert it. + + The final (measured) ``SSTORE`` is wrapped in ``CodeGasMeasure`` so the + executed regular cost is stored on-chain and asserted against + ``expected_regular`` (slot access plus write-on-first-change). The same + value is cross-checked against the framework opcode model's + ``regular_cost`` as a secondary guard. The state-gas dimension is owned + by EIP-8037 and funded from the reservoir, so it is excluded here. + """ + # Move the data off slot 0 so ``CodeGasMeasure`` can store the measured + # cost in slot 0. The bare (operand-free) opcode carries the metadata so + # the measure overhead resolves to just the two operand PUSHes, and + # ``regular_cost``/``gas_cost`` are exact. + data_slot = 0x42 + result_slot = 0 + measured_bare = Op.SSTORE.with_metadata( + key_warm=key_warm, + original_value=original, + current_value=current, + new_value=new, + ) + measured = measured_bare(data_slot, new) + + # Cross-check the oracle agrees with the hand-derived formula. + expected_regular = measured_bare.regular_cost(fork) + + # Reach ``current`` from ``original`` with an unmeasured prep SSTORE when + # they differ, then measure the write to ``new``. The slot is warmed for + # ``key_warm`` rows via the access list (and, where current != original, + # the prep SSTORE warms it too); cold rows have neither, so the measured + # write is cold. + code = Bytecode() + if current != original: + code += Op.SSTORE(data_slot, current) + code += CodeGasMeasure( + code=measured, + overhead_cost=measured.gas_cost(fork) - measured_bare.gas_cost(fork), + extra_stack_items=0, + sstore_key=result_slot, + ) + + contract = pre.deploy_contract( + code=code, + storage={data_slot: original} if original != 0 else {}, + ) + + # Warm the slot for ``key_warm`` rows that have no prep to warm it; + # harmless for prep rows (warmth is set membership). Built after + # ``deploy_contract`` so the address exists. + access_list = ( + [AccessList(address=contract, storage_keys=[data_slot])] + if key_warm + else None + ) + + # State gas (owned by EIP-8037) is funded from the reservoir so it never + # disturbs the regular gas this test isolates. ``gas_limit`` is left + # unset so the reservoir lands above the EIP-7825 cap and ``Op.GAS`` + # measures regular gas only; an explicit gas_limit below the cap would + # zero the reservoir and spill state gas into the measurement. + single_set_state_gas = Op.SSTORE(new_value=1).state_cost(fork) + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + access_list=access_list, + state_gas_reservoir=2 * single_set_state_gas, + ) + + # result_slot holds the measured regular cost; data_slot holds ``new`` + # (absent when new == 0, because the slot is cleared). + expected_storage = {result_slot: expected_regular} + if new != 0: + expected_storage[data_slot] = new + post = {contract: Account(storage=expected_storage)} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_sstore_cold_then_warm_same_slot( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + A first ``SSTORE`` on a cold slot warms it; the second in-frame + ``SSTORE`` of the same slot is charged only ``WARM_SLOAD`` (100). + + The slot starts non-zero (original 1) and is left unlisted, so the + first write is cold and is its first change (original == current != + new), costing ``COLD_STORAGE_ACCESS + STORAGE_WRITE`` (3000 + 10000). + That write warms the slot, so the second write -- which moves the slot + again without being a first change -- costs only ``WARM_SLOAD`` (100), + with no further ``STORAGE_WRITE``. Slot 0 records the cold first write + and slot 1 the warm second write; the data slot keeps its final value. + """ + data_slot = 0x42 + + # First write: cold, first change of a non-zero-original slot. The + # bare (operand-free) opcode carries the same metadata so that the + # CodeGasMeasure overhead resolves to just the two operand PUSHes. + first_bare = Op.SSTORE.with_metadata( + key_warm=False, + original_value=1, + current_value=1, + new_value=2, + ) + first = first_bare(data_slot, 2) + # Second write: same slot, now warm; not a first change, so the + # write cost is not re-charged and only the warm access applies. + second_bare = Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=2, + new_value=3, + ) + 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 + + # Each measured write stores its own runtime cost; the overhead + # subtraction strips the two operand PUSHes so the stored value is the + # bare SSTORE cost. The second write finds the slot warm. + code = CodeGasMeasure( + code=first, + overhead_cost=first.gas_cost(fork) - first_bare.gas_cost(fork), + extra_stack_items=0, + sstore_key=0, + ) + CodeGasMeasure( + code=second, + overhead_cost=second.gas_cost(fork) - second_bare.gas_cost(fork), + extra_stack_items=0, + sstore_key=1, + ) + + contract = pre.deploy_contract(code=code, storage={data_slot: 1}) + + tx = Transaction(to=contract, sender=pre.fund_eoa()) + + # Slots 0/1 hold the two measured writes; the data slot ends at its + # final written value. + post = { + contract: Account( + storage={0: expected_first, 1: expected_second, data_slot: 3} + ) + } + state_test(pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..7becbe2bb22 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_refunds.py @@ -0,0 +1,349 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +The headline mechanism of the pinned spec version ``a8862ae`` is the +``SSTORE`` clear-refund *reversal*: ``refund_counter`` is decremented by +``REFUND_STORAGE_CLEAR`` when a slot's original value is non-zero, its +current value is zero and the new value is non-zero (a slot cleared +earlier in the same transaction is restored). The spec reverses the clear +refund "so that clearing and then restoring a slot within the same +transaction is never net-profitable", closing the ``x -> 0 -> x`` round +trip; this reversal is exercised by +``test_sstore_clear_then_reset_nets_zero``. + +This module covers the EIP-8038 *regular* ``SSTORE`` refund schedule via +the transaction receipt's ``cumulative_gas_used``: + +* Clearing a slot whose original value is non-zero grants + ``REFUND_STORAGE_CLEAR`` (12480) to ``refund_counter`` (no EIP-8037 + state refund, since no state was created). +* Clearing then re-setting the same non-zero-original slot nets a zero + refund: the clear grant is reversed (``refund -= REFUND_STORAGE_CLEAR``) + exactly when ``original != 0 and current == 0`` and a non-zero value is + written back. +* Restoring a non-zero-original slot to its original value refunds the + write cost ``STORAGE_WRITE`` (10000). +* The applied refund is capped at ``gas_used // 5`` (EIP-3529 quotient). + +All refunds use a non-zero original so the state-creation refund owned by +EIP-8037 is never involved; only the EIP-8038 regular dimension is +exercised. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Bytecode, + Fork, + Op, + StateTestFiller, + Transaction, + TransactionReceipt, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _cumulative_gas_used(code: Bytecode, fork: Fork) -> int: + """ + Return the receipt ``cumulative_gas_used`` for a single transaction + whose execution is exactly ``code``. + + Mirrors the spec: gross gas is intrinsic plus the regular and state + gas of the code; the applied refund is ``min(gross // 5, refund)`` + (EIP-3529 quotient cap); the receipt reports gross minus the applied + refund. + """ + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True + ) + gross = intrinsic + code.regular_cost(fork) + code.state_cost(fork) + applied_refund = min(gross // 5, code.refund(fork)) + return gross - applied_refund + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation.Under() +def test_sstore_clear_grants_refund( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Clearing a non-zero-original slot grants ``REFUND_STORAGE_CLEAR``. + + Enough unrelated gas is burned so the EIP-3529 quotient cap + (``gas_used // 5``) does not bind, letting the full 12480 refund be + 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, + current_value=1, + new_value=0, + )(0, 0) + # Burn cheap gas (JUMPDEST = 1 gas, no stack effect) so that + # gas_used // 5 exceeds the refund and the full grant applies. + burn = Op.JUMPDEST * 60_000 + code = clear + burn + + 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 + 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()( + return_cost_deducted_prior_execution=True + ) + gross = intrinsic + code.regular_cost(fork) + assert gross // 5 > refund_clear + assert expected_cumulative == gross - refund_clear + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative + ), + ) + + post = {contract: Account(storage={0: 0})} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +def test_sstore_clear_then_reset_nets_zero( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Clearing then re-setting a non-zero-original slot nets zero refund. + + The clear grants ``REFUND_STORAGE_CLEAR``; re-setting the slot to a + non-zero value reverses it. ``refund_counter`` ends at zero, so + ``cumulative_gas_used`` equals the gross gas with no refund applied. + """ + code = Op.SSTORE.with_metadata( + key_warm=False, + original_value=1, + current_value=1, + new_value=0, + )(0, 0) + Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=0, + new_value=2, + )(0, 2) + + contract = pre.deploy_contract(code=code, storage={0: 1}) + + # The grant and its reversal cancel exactly. + assert code.refund(fork) == 0 + expected_cumulative = _cumulative_gas_used(code, fork) + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative + ), + ) + + post = {contract: Account(storage={0: 2})} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation.Under() +def test_sstore_restore_nonzero_refunds_write( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Restoring a non-zero-original slot refunds the write cost. + + The slot is changed (charging ``STORAGE_WRITE``) then restored to its + original non-zero value, refunding ``STORAGE_WRITE`` (10000). Gas is + 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, + current_value=1, + new_value=2, + )(0, 2) + Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=2, + new_value=1, + )(0, 1) + burn = Op.JUMPDEST * 60_000 + code += burn + + contract = pre.deploy_contract(code=code, storage={0: 1}) + + assert code.refund(fork) == storage_write + expected_cumulative = _cumulative_gas_used(code, fork) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True + ) + gross = intrinsic + code.regular_cost(fork) + assert gross // 5 > storage_write + assert expected_cumulative == gross - storage_write + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative + ), + ) + + post = {contract: Account(storage={0: 1})} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation.Exact() +@pytest.mark.parametrize("num_clears", [1, 8, 32]) +def test_sstore_refund_quotient_cap( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + num_clears: int, +) -> None: + """ + The applied refund saturates at the EIP-3529 quotient cap. + + ``num_clears`` distinct non-zero-original slots are each cleared, + accruing ``num_clears * REFUND_STORAGE_CLEAR`` into ``refund_counter``. + A single clear's gross gas is small enough that ``gas_used // 5`` is + 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( + key_warm=False, + original_value=1, + current_value=1, + new_value=0, + )(slot, 0) + + contract = pre.deploy_contract( + code=code, + storage=dict.fromkeys(range(num_clears), 1), + ) + + assert code.refund(fork) == accrued + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True + ) + gross = intrinsic + code.regular_cost(fork) + # The cap binds for every parametrization (single-clear gross is far + # below 5x a clear refund). + cap = gross // 5 + assert cap < accrued + applied_refund = min(cap, accrued) + expected_cumulative = gross - applied_refund + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative + ), + ) + + post = {contract: Account(storage=dict.fromkeys(range(num_clears), 0))} + state_test(pre=pre, post=post, tx=tx) + + +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation() +@EIPChecklist.GasRefundsChanges.Test.RefundCalculation.Exact() +def test_sstore_refund_cap_exact_equality( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The applied refund equals the EIP-3529 cap at exact equality. + + A single non-zero-original clear accrues ``REFUND_STORAGE_CLEAR``. + Cheap ``JUMPDEST`` gas (1 each) is burned so the gross gas lands at + exactly ``max_refund_quotient * accrued``; the quotient cap + ``gross // max_refund_quotient`` then equals the accrued refund + *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, + original_value=1, + current_value=1, + new_value=0, + )(0, 0) + + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True + ) + # Target the exact boundary: gross == quotient * accrued, so that + # gross // quotient == accrued with no slack. Solve for the JUMPDEST + # count from the remaining gas after intrinsic and the clear's + # regular cost; each JUMPDEST costs exactly 1 gas. + jumpdest_gas = Op.JUMPDEST.gas_cost(fork) + target_gross = quotient * accrued + base_gross = intrinsic + clear.regular_cost(fork) + burn_gas = target_gross - base_gross + num_jumpdest, remainder = divmod(burn_gas, jumpdest_gas) + # An exact integer JUMPDEST count must reach the boundary; otherwise + # the equality below would not hold and the test would (correctly) + # fail rather than silently approximate. + assert remainder == 0 + + 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 + assert gross // quotient == accrued + expected_cumulative = gross - accrued + + tx = Transaction( + to=contract, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=expected_cumulative + ), + ) + + post = {contract: Account(storage={0: 0})} + state_test(pre=pre, post=post, tx=tx) 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 new file mode 100644 index 00000000000..fc79ee51299 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_transient_storage_regression.py @@ -0,0 +1,87 @@ +""" +Tests for [EIP-8038: State Access Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-8038). + +Regression guard: EIP-8038 reprices persistent storage and account +access but must NOT touch transient storage. ``TLOAD`` and ``TSTORE`` +remain at their EIP-1153 cost of ``OPCODE_TLOAD`` / ``OPCODE_TSTORE`` +(100 each), unchanged by the persistent-storage repricing and distinct +from the (repriced) persistent ``COLD_STORAGE_WRITE``. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + CodeGasMeasure, + Environment, + Fork, + Op, + StateTestFiller, + Transaction, +) +from execution_testing.checklists import EIPChecklist + +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +def test_transient_storage_gas_unchanged( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + fork: Fork, +) -> None: + """ + Measure ``TLOAD`` and ``TSTORE`` gas and confirm EIP-8038 left them + at the transient-storage price of 100 each. + + The bare-opcode costs (excluding their PUSH wrappers) must equal + ``OPCODE_TLOAD`` / ``OPCODE_TSTORE``. The guard + ``OPCODE_TSTORE != COLD_STORAGE_WRITE`` ensures the persistent + 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. + assert gas_costs.OPCODE_TSTORE != gas_costs.COLD_STORAGE_WRITE + + # Measure TSTORE then TLOAD of the same transient slot in one frame. + tstore_code = CodeGasMeasure( + code=Op.TSTORE(0, 1), + overhead_cost=2 * very_low, + extra_stack_items=0, + sstore_key=0, + ) + tload_code = CodeGasMeasure( + code=Op.TLOAD(0), + overhead_cost=1 * very_low, + extra_stack_items=1, + sstore_key=1, + ) + contract = pre.deploy_contract(code=tstore_code + tload_code) + + tx = Transaction(to=contract, sender=pre.fund_eoa()) + + # Slot 0: measured TSTORE cost. Slot 1: measured TLOAD cost. + post = { + contract: Account( + storage={ + 0: gas_costs.OPCODE_TSTORE, + 1: gas_costs.OPCODE_TLOAD, + } + ) + } + state_test(env=env, pre=pre, post=post, tx=tx) From 4d090a1bdd41fe52d8951cb8fb846324aabdf7b8 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Wed, 24 Jun 2026 14:32:22 +0200 Subject: [PATCH 05/14] fix(tests): account for EIP-8246 in EIP-8038 selfdestruct gas test (#3044) EIP-8246 removes the `SELFDESTRUCT` balance burn, which changes the same-transaction self-destruct-to-self outcome. Gate the affected expectations in `test_selfdestruct_gas.py` on `fork.is_eip_enabled(8246)` so the test holds on forks with and without EIP-8246: - Pre-EIP-8246: The originator balance is burnt (a `Burn` log) and the same-transaction-created account is deleted. - EIP-8246 onwards: The burn is removed, so the self-send is a no-op; the balance stays in the (emptied) originator and no log is emitted. `burn_log` is imported lazily in the pre-EIP-8246 branch because EIP-8246 deletes the helper from the EIP-7708 spec. The charged gas is unchanged, so `cumulative_gas_used` is asserted identically on both sides. --- .../test_selfdestruct_gas.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) 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 63cf7e0544a..9349b191071 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 @@ -51,12 +51,13 @@ StateTestFiller, Storage, Transaction, + TransactionLog, TransactionReceipt, compute_create_address, ) from execution_testing.checklists import EIPChecklist -from ..eip7708_eth_transfer_logs.spec import burn_log, transfer_log +from ..eip7708_eth_transfer_logs.spec import transfer_log from .spec import ref_spec_8038 REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path @@ -526,25 +527,25 @@ def test_same_tx_created_selfdestruct_self_burn( fork: Fork, ) -> None: """ - EIP-6780: a same-tx-created contract SELFDESTRUCTs to itself, burning - its balance, charged the warm base only. + EIP-6780: a same-tx-created contract SELFDESTRUCTs to itself, charged + the warm base only. A creation transaction whose initcode SELFDESTRUCTs the new contract to ITSELF: the originator is created in this transaction so it is deleted, and because a same-tx-created contract holding balance is alive, ``account_new`` is false for the self-beneficiary — ``regular = 5,000`` (warm self, no ``ACCOUNT_WRITE``) and no - SELFDESTRUCT state gas. The originator balance is burnt (a ``Burn`` - log, not a ``Transfer``), distinguishing the same-tx self-destruct - path: the funding-empty-beneficiary charge keys on the beneficiary, - but same-tx deletion keys on the originator independently. - - No net state gas is charged: the only state cost is the intrinsic - creation ``NEW_ACCOUNT``, but the pre-funded created target is alive - at message entry, so EIP-8037 refunds it (the create-tx - ``created_target_alive`` refund), and the self-burn beneficiary - already exists. The block ``gas_used`` is therefore the pure regular - consumption. + SELFDESTRUCT state gas. + + EIP-8246 removes the SELFDESTRUCT burn, so the self-send is a no-op: + the balance stays in the (otherwise emptied) originator and no log is + emitted. + + No net state gas is charged either way: the only state cost is the + intrinsic creation ``NEW_ACCOUNT``, but the pre-funded created target + is alive at message entry, so EIP-8037 refunds it (the create-tx + ``created_target_alive`` refund). The block ``gas_used`` is therefore + the pure regular consumption regardless of the burn behavior. """ new_account_state_gas = fork.gas_costs().NEW_ACCOUNT intrinsic_calc = fork.transaction_intrinsic_cost_calculator() @@ -575,22 +576,25 @@ def test_same_tx_created_selfdestruct_self_burn( expected_regular = intrinsic_regular + init_code.regular_cost(fork) expected_gas_used = expected_regular + # EIP-8246 removes the SELFDESTRUCT burn: the self-send is a no-op, + # the balance stays in the (otherwise emptied) originator, and no + # log is emitted. + expected_logs: list[TransactionLog] = [] + created_post = Account(balance=amount, nonce=0, code=b"", storage={}) + tx = Transaction( to=None, data=init_code, - # Slack covers the create-side NEW_ACCOUNT charged transiently - # before its refund, even though net state gas is zero. sender=sender, expected_receipt=TransactionReceipt( - logs=[burn_log(created, amount)], + logs=expected_logs, cumulative_gas_used=expected_gas_used, ), ) state_test( pre=pre, - # Same-tx-created originator is deleted; its balance is burnt. - post={created: Account.NONEXISTENT}, + post={created: created_post}, tx=tx, ) From a558e9246ccfb3402e85e84123796e242239db4e Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:50:55 +0200 Subject: [PATCH 06/14] fix(spec-specs): EIP-2780 charge `NEW_ACCOUNT` for value transfer to zero balance precompile (#3048) * fix(amsterdam): charge NEW_ACCOUNT for value transfer to empty precompile EIP-2780 charges the NEW_ACCOUNT state cost when a transaction transfers value to a recipient that is empty per EIP-161. The top-frame charge previously carved out precompile recipients, but neither EIP-2780 nor EIP-161 authorizes that exemption: - EIP-2780 does not mention precompiles; its rule keys solely on "empty per EIP-161 and tx.value > 0". - EIP-161 defines empty structurally (no code, zero nonce, zero balance) with no precompile exception, so an unfunded precompile is empty and is created by the value transfer like any other account. Remove the `recipient_is_precompile` carve-out from the top-frame charge so an empty precompile receiving value pays NEW_ACCOUNT, drop the matching special-case from the testing framework's `transaction_top_frame_state_gas`, and rewrite `test_value_move_to_precompiles` to assert the charge fires for the not-funded precompile while a pre-funded (alive) precompile remains exempt by virtue of being non-empty. Co-authored-by: danceratopz --- .../forks/forks/eips/amsterdam/eip_2780.py | 7 +- .../forks/amsterdam/vm/interpreter.py | 11 +--- .../test_top_frame_charges.py | 55 ++++++++++++++++ .../test_value_moving_transactions.py | 66 ++++++++++++------- 4 files changed, 103 insertions(+), 36 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py index 3746a921b42..8ef87da63b0 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_2780.py @@ -146,16 +146,11 @@ def transaction_top_frame_state_gas( """ Return the state gas charged at the top-level transaction frame. Charges ``NEW_ACCOUNT`` when value is transferred to an - empty recipient; zero otherwise. Precompile recipients are - exempt: they are protocol-inherent rather than created by a - value transfer, so they never trigger ``NEW_ACCOUNT`` even - when state-empty. + empty recipient; zero otherwise. """ gas_costs = cls.gas_costs() if contract_creation: return 0 - if recipient_type == RecipientType.PRECOMPILE: - return 0 if sends_value and recipient_type == RecipientType.EMPTY_ACCOUNT: return gas_costs.NEW_ACCOUNT return 0 diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index a1c7265cc30..518a210c7d8 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -317,16 +317,11 @@ def process_message(message: Message) -> Evm: # opcode runs. Reads pre-value-transfer state so the # EIP-161-empty check sees the recipient as it was at the start # of the frame. Gated to non-create top-level frames; creates - # pay the equivalent NEW_ACCOUNT state gas. Precompile - # recipients are exempt from the NEW_ACCOUNT charge: they are - # protocol-inherent rather than created by a value transfer. + # pay the equivalent NEW_ACCOUNT state gas. if message.depth == Uint(0) and message.target != Bytes0(b""): recipient = message.current_target - recipient_is_precompile = recipient in PRE_COMPILED_CONTRACTS - if ( - message.value > U256(0) - and not recipient_is_precompile - and not is_account_alive(tx_state, recipient) + if message.value > U256(0) and not is_account_alive( + tx_state, recipient ): charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) recipient_code = get_code( diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index dd791c5e78d..c56c06439b0 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -19,6 +19,7 @@ import pytest from execution_testing import ( Account, + Address, Alloc, Fork, Op, @@ -104,6 +105,60 @@ def test_top_frame_state_charge( state_test(pre=pre, tx=tx, post=post) +def test_top_frame_state_charge_empty_precompile( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, +) -> None: + """ + An empty precompile recipient is still empty per EIP-161, so a + value-moving transaction to it must pay the top-frame + ``NEW_ACCOUNT`` state-gas charge. + + The gas limit is one short of covering that state charge. Without + the charge, the transaction would reach the identity precompile and + transfer value, which makes this a direct regression test for a + precompile carve-out. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + identity_precompile = Address(0x04) + + value = 1 + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.PRECOMPILE, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + assert top_frame_state_gas > 0, ( + "top-frame state gas must be non-zero for empty recipients" + ) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + top_frame_state_gas - 1 + tx = Transaction( + sender=sender, + to=identity_precompile, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + post = { + sender: Account( + nonce=1, + balance=sender_initial_balance - gas_limit * gas_price, + ), + identity_precompile: None, + } + + state_test(pre=pre, tx=tx, post=post) + + @pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"]) @pytest.mark.parametrize( "value", diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py index 77f5fc28de6..ace321454ab 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -300,18 +300,17 @@ def test_value_move_to_precompiles( intrinsic time as any other non-self target -- access lists do not warm transaction-level accounts. A value transfer to a precompile additionally pays the transfer-log and value-transfer - charges. The top-frame ``NEW_ACCOUNT`` state charge is suppressed - for precompile recipients: they are protocol-inherent rather than - created by a value transfer. - - The ``pre_funded`` parameter exercises both pre-tx states of the - precompile address: the ``empty`` variant proves the carve-out is - explicit -- without it, a value transfer to an empty precompile - would trip the ``NEW_ACCOUNT`` charge that ``is_account_alive`` - would otherwise demand. The gas limit is pinned to - ``intrinsic_gas`` plus a buffer that covers the precompile - execution body, so a miscalculated intrinsic or a missing - carve-out would trip the gas budget rather than pass silently. + charges. + + The top-frame ``NEW_ACCOUNT`` state charge keys solely on EIP-161 + emptiness; a precompile address is not special-cased. The + ``pre_funded`` parameter exercises both pre-tx states: + + - ``not_funded``: the precompile address is empty per EIP-161, so a + value transfer creates it and pays ``NEW_ACCOUNT`` -- exactly + like any other empty recipient. + - ``pre_funded``: the precompile already holds a balance and is + therefore alive, so no ``NEW_ACCOUNT`` charge applies. """ sender_initial_balance = 10**18 sender = pre.fund_eoa(sender_initial_balance) @@ -329,13 +328,17 @@ def test_value_move_to_precompiles( recipient_type=RecipientType.PRECOMPILE, return_cost_deducted_prior_execution=True, ) + # A value transfer to an empty (not pre-funded) precompile fires the + # top-frame ``NEW_ACCOUNT`` state charge, modelled via + # ``EMPTY_ACCOUNT``; a pre-funded precompile is alive and exempt. + state_recipient_type = ( + RecipientType.PRECOMPILE if pre_funded else RecipientType.EMPTY_ACCOUNT + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=bool(value), + recipient_type=state_recipient_type, + ) - # Precompile execution gas varies per precompile and input; pick - # a buffer large enough to cover the most expensive precompile in - # the matrix but still well under the ``NEW_ACCOUNT`` state - # charge, so a missing carve-out OOGs the ``empty`` variants. - precompile_execution_budget = 100_000 - tx_gas_limit = intrinsic_gas + precompile_execution_budget gas_price = 1_000_000_000 tx = Transaction( @@ -343,20 +346,39 @@ def test_value_move_to_precompiles( to=precompile, value=value, data=tx_data, - gas_limit=tx_gas_limit, gas_price=gas_price, ) - # Exact sender balance is not checked because precompile execution - # gas varies; verify value receipt and sender nonce instead. + # Exact sender balance is generally not checked because precompile + # execution gas varies across the matrix. For identity with empty + # calldata, the execution gas is deterministic, so pin the exact + # balance to make the empty-precompile ``NEW_ACCOUNT`` charge a + # source-level assertion. final_precompile_balance = pre_funded_amount + value expected_precompile: Account | None if final_precompile_balance > 0: expected_precompile = Account(balance=final_precompile_balance) else: expected_precompile = None + expected_sender = Account(nonce=1) + if precompile == Address(0x04): + gas_costs = fork.gas_costs() + precompile_execution_gas = ( + gas_costs.PRECOMPILE_IDENTITY_BASE + + gas_costs.PRECOMPILE_IDENTITY_PER_WORD + * ((len(tx_data) + 31) // 32) + ) + total_gas_cost = ( + intrinsic_gas + top_frame_state_gas + precompile_execution_gas + ) + expected_sender = Account( + nonce=1, + balance=( + sender_initial_balance - value - total_gas_cost * gas_price + ), + ) post = { - sender: Account(nonce=1), + sender: expected_sender, precompile: expected_precompile, } From 406c6305e8c9cfaa0e1e94b261a7b159ee49120a Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:39:30 +0200 Subject: [PATCH 07/14] feat(tests): add more EIP-2780 tests (#3055) Co-authored-by: danceratopz Co-authored-by: Louis Tsai <72684086+LouisTsai-Csie@users.noreply.github.com> --- .../eip2780_reduce_intrinsic_tx_gas/spec.py | 2 +- .../test_calldata_floor.py | 149 +++++++++++++++++ .../test_fork_transition.py | 154 ++++++++++++++++++ .../test_intrinsic_gas_boundary.py | 46 ++++++ .../test_top_frame_charges.py | 137 ++++++++++++++++ .../test_value_moving_transactions.py | 33 +++- 6 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py index 869b3d72bf0..e6fcb6bb528 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py @@ -13,5 +13,5 @@ class ReferenceSpec: ref_spec_2780 = ReferenceSpec( git_path="EIPS/eip-2780.md", - version="4b612eec2ef70611bba3e0819d137dcfb9b6cd81", + version="992074053f12f24fed9e6d6bf6099d3a44707dca", ) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py new file mode 100644 index 00000000000..0907cf64599 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py @@ -0,0 +1,149 @@ +"""EIP-2780 interaction with the EIP-7623/7976 calldata floor.""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Bytes, + Fork, + RecipientType, + StateTestFiller, + Transaction, + TransactionException, +) + +from ...prague.eip7623_increase_calldata_cost.helpers import ( + find_floor_cost_threshold, +) +from .helpers import EOA_INITIAL_BALANCE +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _floor_dominating_calldata(fork: Fork) -> Bytes: + """ + Return zero-byte calldata sized so its calldata floor strictly + exceeds the decomposed value-transfer intrinsic for a non-create + call to an existing EOA. + + Reuses the shared EIP-7623 ``find_floor_cost_threshold`` binary + search against this transaction shape, then steps one byte past the + threshold (the last size where the floor does not yet dominate) so + the floor strictly binds. + """ + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + floor_calc = fork.transaction_data_floor_cost_calculator() + + def intrinsic(byte_count: int) -> int: + return intrinsic_calc( + calldata=b"\x00" * byte_count, + sends_value=True, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + + def floor(byte_count: int) -> int: + return floor_calc(data=b"\x00" * byte_count) + + threshold = find_floor_cost_threshold( + floor_data_gas_cost_calculator=floor, + intrinsic_gas_cost_calculator=intrinsic, + ) + byte_count = threshold + 1 + + assert floor(byte_count) > intrinsic(byte_count) + return Bytes(b"\x00" * byte_count) + + +@pytest.mark.parametrize( + "gas_modifier", + [ + pytest.param(0, id="at_floor"), + pytest.param( + -1, + id="below_floor", + marks=pytest.mark.exception_test, + ), + ], +) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_calldata_floor( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + gas_modifier: int, + value: int, +) -> None: + """ + A data-heavy transaction to an existing EOA whose calldata floor + exceeds the decomposed value-transfer intrinsic. + + - ``at_floor``: with a gas limit exactly at the floor, ``gas_used`` + pins to the floor, so the value-transfer charges + (``TRANSFER_LOG_COST + TX_VALUE_COST``) folded into the intrinsic + ``value == 1`` and only the moved wei differs. + - ``below_floor``: a gas limit one short of the floor still covers + the (smaller) decomposed intrinsic, so the floor -- built on the + EIP-2780-lowered ``TX_BASE`` -- is the only thing that can reject + it, with ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST``. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + + calldata = _floor_dominating_calldata(fork) + calldata_floor = fork.transaction_data_floor_cost_calculator()( + data=calldata, + ) + gas_price = 1_000_000_000 + + post: dict[Address, Account] = {} + gas_limit = calldata_floor + gas_modifier + # Even at the reduced limit the decomposed intrinsic is still + # covered, so the calldata floor is the sole gate on the + # transaction. + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=calldata, + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + assert intrinsic_gas <= gas_limit, ( + "gas_limit must still cover the decomposed intrinsic so the " + "outcome is pinned to the calldata floor" + ) + + tx = Transaction( + sender=sender, + to=target, + value=value, + data=calldata, + gas_limit=gas_limit, + gas_price=gas_price, + error=( + TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST + if gas_modifier < 0 + else None + ), + ) + if gas_modifier == 0: + sender_final_balance = ( + sender_initial_balance - value - calldata_floor * gas_price + ) + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=EOA_INITIAL_BALANCE + value), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py new file mode 100644 index 00000000000..d94031fe34b --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py @@ -0,0 +1,154 @@ +""" +Fork-transition tests for EIP-2780. + +EIP-2780 reshapes the intrinsic transaction cost at the Amsterdam fork +boundary. These tests send identical transactions in a pre-fork block +and a post-fork block (straddling the transition timestamp) and assert +that the per-transaction gas paid changes by the EIP-2780 amount only +once the fork activates. + +For these shapes the post-fork intrinsic decomposes from the flat +pre-fork ``TX_BASE`` of 21_000 as follows: + +- A plain call to an existing account drops to ``TX_BASE`` (12_000) + plus the new ``COLD_ACCOUNT_ACCESS`` recipient charge; adding value + re-raises it to exactly 21_000 (the value-transfer cost is invariant + across the fork by design). +- A self-transfer is fully carved out post-fork: it pays only the + lowered ``TX_BASE`` with no recipient or value-transfer charge, + regardless of value, the largest reduction. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + RecipientType, + Transaction, + TransitionFork, +) + +from .helpers import EOA_INITIAL_BALANCE +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") + +# Transition forks switch at timestamp 15_000. +PRE_FORK_TIMESTAMP = 14_999 +POST_FORK_TIMESTAMP = 15_000 + + +@pytest.mark.parametrize( + "self_transfer", + [ + pytest.param(False, id="plain_call"), + pytest.param(True, id="self_transfer"), + ], +) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_reduction_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + self_transfer: bool, + value: int, +) -> None: + """ + Pin the EIP-2780 intrinsic change across the Amsterdam boundary. + + The same transaction shape is sent in a pre-fork block (Osaka + rules, flat 21_000 intrinsic) and a post-fork block (Amsterdam + rules, decomposed intrinsic). Each block uses a distinct sender so + its post-tx balance pins the fork-appropriate intrinsic; the + recipient is an existing EOA (or the sender itself for + ``self_transfer``), so neither block runs EVM bytecode and + ``gas_used`` equals the intrinsic exactly. + + The per-fork intrinsic returned by the calculator is also checked + against a hand-derived decomposition built from each fork's gas + constants, so a calculator regression fails here with a clear + message rather than only as a downstream balance mismatch. + """ + gas_price = 1_000_000_000 + recipient_type = RecipientType.SELF if self_transfer else RecipientType.EOA + + pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP) + post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP) + + # Pre-fork: flat ``TX_BASE`` regardless of recipient kind or value. + expected_pre = pre_fork.gas_costs().TX_BASE + # Post-fork: EIP-2780 decomposition. Self-transfers are fully + # carved out; other recipients pay the recipient access charge plus + # the value-transfer charges when value is moved. + post_gas_costs = post_fork.gas_costs() + expected_post = post_gas_costs.TX_BASE + if not self_transfer: + expected_post += post_gas_costs.COLD_ACCOUNT_ACCESS + if value: + expected_post += ( + post_gas_costs.TRANSFER_LOG_COST + post_gas_costs.TX_VALUE_COST + ) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_intrinsics = [expected_pre, expected_post] + blocks = [] + post: dict[Address, Account] = {} + + for timestamp, expected_intrinsic in zip( + timestamps, expected_intrinsics, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=recipient_type, + return_cost_deducted_prior_execution=True, + ) + assert intrinsic_gas == expected_intrinsic, ( + f"intrinsic at timestamp {timestamp} ({sub_fork}) is " + f"{intrinsic_gas}, expected {expected_intrinsic}" + ) + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + if self_transfer: + target = sender + else: + target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + + # No EVM bytecode runs (recipient is an EOA or the sender), so + # gas_used == intrinsic_gas; the gas limit is pinned to exactly + # the intrinsic, leaving no buffer. + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + # A self-transfer returns the value to the sender (net zero); + # a plain call moves ``value`` to the distinct recipient. + sender_value_delta = 0 if self_transfer else value + sender_final_balance = ( + sender_initial_balance + - sender_value_delta + - intrinsic_gas * gas_price + ) + post[sender] = Account(nonce=1, balance=sender_final_balance) + if not self_transfer: + post[target] = Account(balance=EOA_INITIAL_BALANCE + value) + + blockchain_test(pre=pre, blocks=blocks, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py index d07370531ff..e62a93c8999 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py @@ -11,6 +11,7 @@ from execution_testing import ( Alloc, Fork, + Op, RecipientType, StateTestFiller, Transaction, @@ -67,3 +68,48 @@ def test_intrinsic_gas_floor_boundary( ) state_test(pre=pre, tx=tx, post={}) + + +@pytest.mark.exception_test +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_gas_floor_boundary_contract_creation( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Reject a contract-creation transaction when + ``gas_limit = intrinsic_gas - 1``. + + A creation tx's intrinsic includes the ``NEW_ACCOUNT`` state gas, so + the pre-execution check rejects against the combined + ``regular + state`` intrinsic. The init code never runs. + """ + sender = pre.fund_eoa(10**18) + init_code = Op.STOP + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=init_code, + contract_creation=True, + sends_value=bool(value), + return_cost_deducted_prior_execution=True, + ) + + tx = Transaction( + sender=sender, + to=None, + value=value, + data=init_code, + gas_limit=intrinsic_gas - 1, + gas_price=1_000_000_000, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, tx=tx, post={}) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index c56c06439b0..2af1538f9c2 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -21,7 +21,10 @@ Account, Address, Alloc, + Block, + BlockchainTestFiller, Fork, + Header, Op, RecipientType, StateTestFiller, @@ -159,6 +162,140 @@ def test_top_frame_state_charge_empty_precompile( state_test(pre=pre, tx=tx, post=post) +def test_top_frame_new_account_charged_as_state_gas( + fork: Fork, + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + The top-frame ``NEW_ACCOUNT`` charge for a value transfer to an + empty recipient is *state* gas, not regular gas. This pins the + dimension via the block header ``gas_used``, which the spec + computes as ``max(block_regular_gas, block_state_gas)``. + + Correctly attributed, the ``NEW_ACCOUNT`` state gas dominates the + small regular intrinsic, so ``gas_used == NEW_ACCOUNT``. A + regression mis-classifying the charge as regular gas would instead + yield ``intrinsic_regular + NEW_ACCOUNT``. + + ``state_test``-based balance assertions (e.g. + ``test_top_frame_state_charge``) only observe the *sum* of the two + dimensions, so they cannot distinguish this; a block-level + ``gas_used`` assertion is required. + """ + sender = pre.fund_eoa(10**18) + target = pre.fund_eoa(amount=0) + value = 1 + + intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + return_cost_deducted_prior_execution=True, + ) + new_account_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + # The state charge must dominate the regular intrinsic for the + # header ``gas_used`` to distinguish a state vs regular + # mis-classification. + assert new_account_state_gas > intrinsic_regular, ( + "test only distinguishes the dimension when NEW_ACCOUNT " + f"({new_account_state_gas}) dominates the regular intrinsic " + f"({intrinsic_regular})" + ) + + # No EVM bytecode runs (empty recipient), so the only regular gas + # is the intrinsic and the only state gas is the top-frame + # ``NEW_ACCOUNT`` charge. + expected_gas_used = max(intrinsic_regular, new_account_state_gas) + + gas_price = 1_000_000_000 + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=intrinsic_regular + new_account_state_gas + 1000, + gas_price=gas_price, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_gas_used), + ), + ], + post={ + sender: Account(nonce=1), + target: Account(balance=value), + }, + ) + + +@pytest.mark.pre_alloc_mutable +def test_top_frame_new_account_skipped_for_nonce_only_recipient( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, +) -> None: + """ + A recipient that is alive only by its nonce (``nonce=1``, zero + balance, no code) is not empty per EIP-161, so a value transfer to + it does *not* incur the top-frame ``NEW_ACCOUNT`` charge. This pins + that the gate keys on ``is_account_alive``, not ``balance == 0``. + + Such an account is reachable on-chain: any EOA that has sent a + transaction (nonce bumped) and been fully drained sits at + ``nonce>0, balance=0, no code``. + + The gas limit is pinned to exactly the intrinsic, leaving no room + for any extra charge: an implementation that wrongly charged + ``NEW_ACCOUNT`` (keying on the zero balance) would out-of-gas + rather than succeed. The recipient has no code, so no EVM runs and + the intrinsic is fully consumed with nothing to refund. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + # Alive via nonce only: not empty per EIP-161 because nonce != 0. + target = pre.fund_eoa(amount=0, nonce=1) + value = 1 + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EOA, + ) + assert top_frame_state_gas == 0, ( + "a nonce-only-alive recipient must not incur the NEW_ACCOUNT charge" + ) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - intrinsic_gas * gas_price + ) + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(nonce=1, balance=value), + } + + state_test(pre=pre, tx=tx, post=post) + + @pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"]) @pytest.mark.parametrize( "value", diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py index ace321454ab..0a7533cbe79 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -22,9 +22,11 @@ RecipientType, StateTestFiller, Transaction, + TransactionReceipt, compute_create_address, ) +from ..eip7708_eth_transfer_logs.spec import transfer_log from .helpers import ( EOA_INITIAL_BALANCE, RECIPIENT_TYPES_NON_CREATE, @@ -63,6 +65,10 @@ def test_value_moving_transactions( delegations on the recipient surface as an extra top-frame ``COLD_ACCOUNT_ACCESS``; empty recipients trigger the top-frame ``NEW_ACCOUNT`` state charge when value is transferred. + + The EIP-7708 transfer log is asserted to fire exactly when + ``TRANSFER_LOG_COST`` is charged: for a non-self value transfer, + and never for a self-transfer (carve-out) or a zero-value tx. """ sender_initial_balance = 10**18 sender = pre.fund_eoa(sender_initial_balance) @@ -92,15 +98,25 @@ def test_value_moving_transactions( tx_gas_limit = total_gas_cost + 1000 # add a small buffer gas_price = 1_000_000_000 + is_self_transfer = recipient_type == RecipientType.SELF + + # A transfer log is emitted iff value moves to a distinct account, + # which is exactly when the intrinsic includes ``TRANSFER_LOG_COST``. + # ``logs=[]`` asserts no log fires for the carved-out cases. + if value > 0 and not is_self_transfer: + expected_logs = [transfer_log(sender, target, value)] + else: + expected_logs = [] + tx = Transaction( sender=sender, to=target, value=value, gas_limit=tx_gas_limit, gas_price=gas_price, + expected_receipt=TransactionReceipt(logs=expected_logs), ) - is_self_transfer = recipient_type == RecipientType.SELF sender_value_delta = 0 if is_self_transfer else value sender_final_balance = ( sender_initial_balance @@ -191,6 +207,13 @@ def test_value_contract_creation_tx( sender_value_delta = value expected_target = Account(code=code_to_deploy, balance=value) + expected_target_address = compute_create_address(address=sender, nonce=0) + + if value > 0 and not tx_reverts: + expected_logs = [transfer_log(sender, expected_target_address, value)] + else: + expected_logs = [] + gas_price = 1_000_000_000 gas_limit = intrinsic_gas + execution_gas + 1000 @@ -201,9 +224,9 @@ def test_value_contract_creation_tx( data=call_data, gas_limit=gas_limit, gas_price=gas_price, + expected_receipt=TransactionReceipt(logs=expected_logs), ) - expected_target_address = compute_create_address(address=sender, nonce=0) sender_final_balance = ( sender_initial_balance - sender_value_delta - gas_used * gas_price ) @@ -339,6 +362,11 @@ def test_value_move_to_precompiles( recipient_type=state_recipient_type, ) + if value > 0: + expected_logs = [transfer_log(sender, precompile, value)] + else: + expected_logs = [] + gas_price = 1_000_000_000 tx = Transaction( @@ -347,6 +375,7 @@ def test_value_move_to_precompiles( value=value, data=tx_data, gas_price=gas_price, + expected_receipt=TransactionReceipt(logs=expected_logs), ) # Exact sender balance is generally not checked because precompile From bdab7c8a3958a42b71a1eafcdbdd45a53147695e Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Thu, 2 Jul 2026 06:39:22 -0400 Subject: [PATCH 08/14] feat(specs): update EIP-8037 impl for ethereum/EIPs#11715 (#3021) Co-authored-by: spencer-tb --- src/ethereum/forks/amsterdam/state_tracker.py | 50 ++- .../forks/amsterdam/vm/eoa_delegation.py | 54 +-- .../test_state_gas_set_code.py | 315 ++++++++++++++++++ 3 files changed, 397 insertions(+), 22 deletions(-) diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index cda7bbf53ea..91610ace0ce 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -52,6 +52,11 @@ class BlockState: """ pre_state: PreState + """ + Accounts, storage, and code as they existed before the beginning of the + block. + """ + account_reads: Set[Address] = field(default_factory=set) account_writes: Dict[Address, Optional[Account]] = field( default_factory=dict @@ -92,6 +97,47 @@ class TransactionState: ) +def get_pre_state_account_optional( + tx_state: TransactionState, address: Address +) -> Optional[Account]: + """ + Get the `Account` object at an address that existed before the current + transaction, or `None` (rather than [`EMPTY_ACCOUNT`]) if there was no + account at the address at that point. + + Use [`get_pre_state_account()`][pre] if the difference between a + non-existent account and [`EMPTY_ACCOUNT`] isn't important. + + [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT + [pre]: ref:ethereum.forks.amsterdam.state_tracker.get_pre_state_account + """ + tx_state.account_reads.add(address) + if address in tx_state.parent.account_writes: + return tx_state.parent.account_writes[address] + return tx_state.parent.pre_state.get_account_optional(address) + + +def get_pre_state_account( + tx_state: TransactionState, address: Address +) -> Account: + """ + Get the `Account` object at an address that existed before the current + transaction, or [`EMPTY_ACCOUNT`]) if there was no account at the address + at that point. + + Use [`get_pre_state_account_optional()`][opt] if the difference between a + non-existent account and [`EMPTY_ACCOUNT`] is material. + + [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT + [opt]: ref:ethereum.forks.amsterdam.state_tracker.get_pre_state_account_optional + """ # noqa: E501 + account = get_pre_state_account_optional(tx_state, address) + if account is None: + return EMPTY_ACCOUNT + else: + return account + + def get_account_optional( tx_state: TransactionState, address: Address ) -> Optional[Account]: @@ -115,9 +161,7 @@ def get_account_optional( tx_state.account_reads.add(address) if address in tx_state.account_writes: return tx_state.account_writes[address] - if address in tx_state.parent.account_writes: - return tx_state.parent.account_writes[address] - return tx_state.parent.pre_state.get_account_optional(address) + return get_pre_state_account_optional(tx_state, address) def get_account(tx_state: TransactionState, address: Address) -> Account: diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index aa35e153a14..2060d5465d7 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -5,18 +5,20 @@ from typing import Optional, Tuple from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import keccak256 from ethereum.exceptions import InvalidBlock, InvalidSignatureError -from ethereum.state import EMPTY_CODE_HASH, Account, Address +from ethereum.state import Address -from ..fork_types import Authorization +from ..fork_types import Authorization, StateGas from ..state_tracker import ( account_exists, get_account, get_code, + get_pre_state_account, increment_nonce, set_code, ) @@ -157,11 +159,11 @@ def calculate_delegation_cost( def validate_authorization( message: Message, auth: Authorization -) -> None | Tuple[Address, Account]: +) -> None | Tuple[Address, Bytes]: """ Check if the given `Authorization` is valid against the current state. - Returns the `authority` address and its `Account`, or `None` if the + Returns the `authority` address and its code, or `None` if the validation was unsuccessful. """ tx_state = message.tx_env.state @@ -189,7 +191,7 @@ def validate_authorization( if authority_nonce != auth.nonce: return None - return (authority, authority_account) + return (authority, authority_code) def set_delegation(message: Message) -> Tuple[Uint, Uint]: @@ -224,37 +226,51 @@ def set_delegation(message: Message) -> Tuple[Uint, Uint]: for auth in message.tx_env.authorizations: match validate_authorization(message, auth): case None: + refund = StateGasCosts.AUTH_BASE + StateGasCosts.NEW_ACCOUNT + message.state_gas_reservoir += refund + state_refund += refund + regular_refund += GasCosts.ACCOUNT_WRITE continue - case (authority, authority_account): + case (authority, authority_code): pass + refund = StateGas(Uint(0)) + if account_exists(tx_state, authority): - refund = StateGasCosts.NEW_ACCOUNT - message.state_gas_reservoir += refund - state_refund += refund + refund += StateGasCosts.NEW_ACCOUNT # The new-account ACCOUNT_WRITE charged at intrinsic time is # not needed: refund it to the regular refund counter. regular_refund += GasCosts.ACCOUNT_WRITE - # No new delegation indicator bytes are written: either the - # authority already has one (overwrite in place / clear) or - # this auth clears against an authority with no prior code. - if ( - authority_account.code_hash != EMPTY_CODE_HASH - or auth.address == NULL_ADDRESS - ): - refund = StateGasCosts.AUTH_BASE - message.state_gas_reservoir += refund - state_refund += refund + pre_state_authority_account = get_pre_state_account( + tx_state, authority + ) + pre_state_authority_code = get_code( + tx_state, pre_state_authority_account.code_hash + ) + + delegated_before_tx = is_valid_delegation(pre_state_authority_code) + delegated_now = is_valid_delegation(authority_code) if auth.address == NULL_ADDRESS: + refund += StateGasCosts.AUTH_BASE + + if delegated_now and not delegated_before_tx: + refund += StateGasCosts.AUTH_BASE + code_to_set = b"" else: code_to_set = EOA_DELEGATION_MARKER + auth.address + if delegated_now or delegated_before_tx: + refund += StateGasCosts.AUTH_BASE + set_code(tx_state, authority, code_to_set) increment_nonce(tx_state, authority) + message.state_gas_reservoir += refund + state_refund += refund + if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") 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 d95a44acbdb..434de6023df 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 @@ -1556,3 +1556,318 @@ def test_auth_refund_reservoir_cannot_fund_regular_gas( gas_used=max(gas_limit - intrinsic_state, state_used), ), ) + + +@pytest.mark.parametrize( + "invalidity", + [ + pytest.param("nonce_mismatch", id="nonce_mismatch"), + pytest.param("nonce_at_u64_max", id="nonce_at_u64_max"), + pytest.param("chain_id_mismatch", id="chain_id_mismatch"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_invalid_auth_rule1_refill_by_reason( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + invalidity: str, +) -> None: + """ + Verify an invalid authorization refills its full intrinsic state gas. + + A rejected authorization is skipped during processing. Its whole + state portion of NEW_ACCOUNT plus AUTH_BASE refills the reservoir + and one ACCOUNT_WRITE refunds to the refund counter. The regular + per authorization base cost stays charged and the authority is + never created. Swept over the reasons an authorization is rejected. + """ + per_auth_state = fork.transaction_intrinsic_state_gas( + authorization_count=1, + ) + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=1, + ) + intrinsic_regular = total_intrinsic - per_auth_state + account_write = fork.gas_costs().ACCOUNT_WRITE + + target = pre.deploy_contract(code=Op.STOP) + signer = pre.fund_eoa(amount=0) + + if invalidity == "nonce_mismatch": + auth = AuthorizationTuple(address=target, nonce=99, signer=signer) + elif invalidity == "nonce_at_u64_max": + auth = AuthorizationTuple( + address=target, + nonce=2**64 - 1, + signer=signer, + ) + elif invalidity == "chain_id_mismatch": + auth = AuthorizationTuple( + address=target, + nonce=0, + chain_id=9999, + signer=signer, + ) + else: + raise ValueError(f"unknown invalidity: {invalidity!r}") + + # The skipped auth refills its whole state portion to the reservoir + # so the net state charge is zero, and one ACCOUNT_WRITE returns to + # the capped refund counter. + auth_refund = per_auth_state + refund_counter = account_write + + header_gas_used = max(intrinsic_regular, per_auth_state - auth_refund) + gas_used_before_refund = total_intrinsic - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + receipt_cumulative_gas_used = gas_used_before_refund - regular_refund + + tx = Transaction( + to=target, + state_gas_reservoir=per_auth_state, + authorization_list=[auth], + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=receipt_cumulative_gas_used, + ), + ) + + state_test( + pre=pre, + post={signer: Account.NONEXISTENT}, + tx=tx, + blockchain_test_header_verify=Header(gas_used=header_gas_used), + ) + + +@pytest.mark.valid_from("EIP8037") +def test_same_tx_create_then_clear_double_auth_base_refill( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify the create then clear double AUTH_BASE refill in one tx. + + A fresh authority is delegated by the first authorization then + cleared by the second within one transaction. The clear refills + AUTH_BASE twice. Once because the clear writes no indicator bytes. + Once because the delegation it removes was created earlier in this + same transaction. Net AUTH_BASE charged is zero and only the + NEW_ACCOUNT leaf cost remains. + """ + per_auth_state = fork.transaction_intrinsic_state_gas( + authorization_count=1, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=2, + ) + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=2, + ) + intrinsic_regular = total_intrinsic - intrinsic_state + new_account_refund = fork.gas_costs().REFUND_AUTH_PER_EXISTING_ACCOUNT + account_write = fork.gas_costs().ACCOUNT_WRITE + auth_base_refund = per_auth_state - new_account_refund + + contract_a = pre.deploy_contract(code=Op.STOP) + target = pre.deploy_contract(code=Op.STOP) + + signer = pre.fund_eoa(amount=0) + authorization_list = [ + AuthorizationTuple(address=contract_a, nonce=0, signer=signer), + AuthorizationTuple( + address=Spec7702.RESET_DELEGATION_ADDRESS, + nonce=1, + signer=signer, + ), + ] + + # The first auth creates the leaf and writes the indicator with no + # refill. The second auth refills NEW_ACCOUNT, AUTH_BASE twice, and + # one ACCOUNT_WRITE. + auth_refund = new_account_refund + 2 * auth_base_refund + refund_counter = account_write + + header_gas_used = max(intrinsic_regular, intrinsic_state - auth_refund) + gas_used_before_refund = total_intrinsic - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + receipt_cumulative_gas_used = gas_used_before_refund - regular_refund + + tx = Transaction( + to=target, + state_gas_reservoir=intrinsic_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=receipt_cumulative_gas_used, + ), + ) + + state_test( + pre=pre, + post={signer: Account(nonce=2, code=b"")}, + tx=tx, + blockchain_test_header_verify=Header(gas_used=header_gas_used), + ) + + +@pytest.mark.valid_from("EIP8037") +def test_same_tx_clear_then_reset_pre_delegated( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify clear then reset of a pre delegated authority in one tx. + + An authority delegated before the transaction is cleared by the + first authorization then set to a new target by the second. The + reset refills AUTH_BASE through the pre delegated term even though + the current code was empty at that point. Net AUTH_BASE charged is + zero because the authority started and ended delegated. + """ + per_auth_state = fork.transaction_intrinsic_state_gas( + authorization_count=1, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=2, + ) + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=2, + ) + intrinsic_regular = total_intrinsic - intrinsic_state + new_account_refund = fork.gas_costs().REFUND_AUTH_PER_EXISTING_ACCOUNT + account_write = fork.gas_costs().ACCOUNT_WRITE + auth_base_refund = per_auth_state - new_account_refund + + contract_a = pre.deploy_contract(code=Op.STOP) + contract_b = pre.deploy_contract(code=Op.STOP) + target = pre.deploy_contract(code=Op.STOP) + + signer = pre.fund_eoa(delegation=contract_a) + authorization_list = [ + AuthorizationTuple( + address=Spec7702.RESET_DELEGATION_ADDRESS, + nonce=1, + signer=signer, + ), + AuthorizationTuple(address=contract_b, nonce=2, signer=signer), + ] + + # Both auths refill NEW_ACCOUNT and one AUTH_BASE each. The leaf + # already exists so each also refunds one ACCOUNT_WRITE. + auth_refund = 2 * (new_account_refund + auth_base_refund) + refund_counter = 2 * account_write + + header_gas_used = max(intrinsic_regular, intrinsic_state - auth_refund) + gas_used_before_refund = total_intrinsic - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + receipt_cumulative_gas_used = gas_used_before_refund - regular_refund + + tx = Transaction( + to=target, + state_gas_reservoir=intrinsic_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=receipt_cumulative_gas_used, + ), + ) + + state_test( + pre=pre, + post={ + signer: Account( + nonce=3, + code=Spec7702.delegation_designation(contract_b), + ), + }, + tx=tx, + blockchain_test_header_verify=Header(gas_used=header_gas_used), + ) + + +@pytest.mark.valid_from("EIP8037") +def test_same_authority_increasing_nonce_net_once( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Verify the per authority once invariant across valid auths. + + The same fresh authority is delegated by three authorizations with + increasing nonces in one transaction. The account leaf and its + delegation indicator are written once. NEW_ACCOUNT and AUTH_BASE are + each charged once across the batch while ACCOUNT_WRITE is refunded + for every auth after the leaf is created. + """ + num_auths = 3 + per_auth_state = fork.transaction_intrinsic_state_gas( + authorization_count=1, + ) + intrinsic_state = fork.transaction_intrinsic_state_gas( + authorization_count=num_auths, + ) + total_intrinsic = fork.transaction_intrinsic_cost_calculator()( + authorization_list_or_count=num_auths, + ) + intrinsic_regular = total_intrinsic - intrinsic_state + new_account_refund = fork.gas_costs().REFUND_AUTH_PER_EXISTING_ACCOUNT + account_write = fork.gas_costs().ACCOUNT_WRITE + auth_base_refund = per_auth_state - new_account_refund + + targets = [pre.deploy_contract(code=Op.STOP) for _ in range(num_auths)] + call_target = pre.deploy_contract(code=Op.STOP) + + signer = pre.fund_eoa(amount=0) + authorization_list = [ + AuthorizationTuple(address=targets[i], nonce=i, signer=signer) + for i in range(num_auths) + ] + + # The first auth creates the leaf with no refill. Each later auth + # refills NEW_ACCOUNT, one AUTH_BASE, and one ACCOUNT_WRITE. + auth_refund = (num_auths - 1) * (new_account_refund + auth_base_refund) + refund_counter = (num_auths - 1) * account_write + + header_gas_used = max(intrinsic_regular, intrinsic_state - auth_refund) + gas_used_before_refund = total_intrinsic - auth_refund + regular_refund = min( + gas_used_before_refund // fork.max_refund_quotient(), + refund_counter, + ) + receipt_cumulative_gas_used = gas_used_before_refund - regular_refund + + tx = Transaction( + to=call_target, + state_gas_reservoir=intrinsic_state, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + cumulative_gas_used=receipt_cumulative_gas_used, + ), + ) + + state_test( + pre=pre, + post={ + signer: Account( + nonce=num_auths, + code=Spec7702.delegation_designation(targets[-1]), + ), + }, + tx=tx, + blockchain_test_header_verify=Header(gas_used=header_gas_used), + ) From ada583d827fd27aba83faa0284aae56cec6c6a98 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 2 Jul 2026 19:20:49 +0200 Subject: [PATCH 09/14] fix(tests): align Amsterdam (gas) tests with EIP-2780/8037/8038 (#3088) Co-authored-by: marioevz --- .../test_block_2d_gas_accounting.py | 59 +++++++++++++++---- .../test_state_gas_selfdestruct.py | 6 +- .../test_set_code_auth_gas.py | 46 ++++++++------- .../test_create2check_fields_in_initcode.py | 3 +- .../test_coinbase_warm_account_call_gas.py | 9 ++- 5 files changed, 86 insertions(+), 37 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 50d8547e2cb..42775006b8c 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 @@ -41,9 +41,9 @@ def sstore_tx_gas(fork: Fork, num_sstores: int = 1) -> tuple[int, int]: """Return (regular, state) gas for a tx with N cold SSTOREs.""" intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() - evm_total = num_sstores * Op.SSTORE(0, 1).gas_cost(fork) + evm_total = num_sstores * Op.SSTORE(0, 1).regular_cost(fork) state = num_sstores * Op.SSTORE(new_value=1).state_cost(fork) - return intrinsic_gas + evm_total - state, state + return intrinsic_gas + evm_total, state def sstore_txs( @@ -793,18 +793,26 @@ def test_receipt_cumulative_differs_from_header_gas_used( @pytest.mark.parametrize("dominant_dimension", ["state", "regular"]) +@pytest.mark.parametrize( + "single_tx", + [ + pytest.param(True, id="single_tx"), + pytest.param(False, id="multiple_txs"), + ], +) @pytest.mark.valid_from("EIP8037") def test_base_fee_per_gas_follows_dominant_dimension( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, dominant_dimension: str, + single_tx: bool, ) -> None: """ Verify the child block's base fee follows the bottleneck dimension. Block 1 exceeds the gas target on one dimension only: state, via - SSTORE-set txs that spill, or regular, via STOP txs. Its header + SSTORE-set txs that spill, or regular, via STOP/MSTORE txs. Its header gas_used = max(regular, state) is then set by that dimension alone, which lifts empty block 2's base fee under the EIP-1559 update. """ @@ -815,30 +823,53 @@ def test_base_fee_per_gas_follows_dominant_dimension( txs: list[Transaction] = [] post: dict = {} + num_sstores = 0 if dominant_dimension == "state": - num_txs = 5 - tx_regular, tx_state = sstore_tx_gas(fork) + if single_tx: + num_txs = 1 + num_sstores = target // sstore_tx_gas(fork, num_sstores=1)[1] + 1 + tx_regular, tx_state = sstore_tx_gas(fork, num_sstores=num_sstores) + else: + num_sstores = 1 + tx_regular, tx_state = sstore_tx_gas(fork, num_sstores=num_sstores) + while tx_regular >= tx_state: + num_sstores += 1 + tx_regular, tx_state = sstore_tx_gas( + fork, num_sstores=num_sstores + ) + num_txs = target // tx_state + 1 block_regular = num_txs * tx_regular block_state = num_txs * tx_state tx_gas_limit = tx_regular + tx_state assert block_state > target > block_regular else: - num_txs = 15 - tx_gas_limit = fork.transaction_intrinsic_cost_calculator()() + if single_tx: + num_txs = 1 + # Just consume all gas + regular_contract = pre.deploy_contract( + code=Op.MSTORE(offset=2**256 - 1, value=1) + Op.STOP + ) + tx_gas_limit = target + 1 + else: + tx_gas_limit = fork.transaction_intrinsic_cost_calculator()() + # Enough STOP txs that regular gas alone clears the target. + regular_contract = pre.deploy_contract(code=Op.STOP) + num_txs = target // tx_gas_limit + 1 block_regular = num_txs * tx_gas_limit block_state = 0 - stop_contract = pre.deploy_contract(code=Op.STOP) assert block_regular > target > block_state for _ in range(num_txs): if dominant_dimension == "state": storage = Storage() - contract = pre.deploy_contract( - code=Op.SSTORE(storage.store_next(1), 1) + Op.STOP, - ) + code = Bytecode() + for _ in range(num_sstores): + code += Op.SSTORE(storage.store_next(1), 1) + code += Op.STOP + contract = pre.deploy_contract(code=code) post[contract] = Account(storage=storage) else: - contract = stop_contract + contract = regular_contract txs.append( Transaction( to=contract, @@ -850,6 +881,10 @@ def test_base_fee_per_gas_follows_dominant_dimension( ) block_1_gas_used = max(block_regular, block_state) + assert block_1_gas_used < gas_limit, ( + "test needs update: gas_limit reached by usage, simply raise the " + "anchored gas_limit value" + ) base_fee_calc = fork.base_fee_per_gas_calculator() block_1_base_fee = base_fee_calc( parent_base_fee_per_gas=genesis_base_fee, 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 df255111a23..ff0fec86530 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 @@ -227,8 +227,9 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( The inner frame spills the NEW_ACCOUNT charge and self-destructs successfully, then the caller reverts: the beneficiary creation - rolls back and the spilled charge is refilled, so only regular gas - is billed. + rolls back and the spilled state charge is refilled. The EIP-8038 + regular account-write charge for the attempted empty-account value + transfer remains billed. """ beneficiary = 0xDEAD inner_code = Op.SELFDESTRUCT(beneficiary) @@ -240,6 +241,7 @@ def test_selfdestruct_state_gas_refilled_on_ancestor_revert( fork.transaction_intrinsic_cost_calculator()() + caller_code.gas_cost(fork) + inner_code.gas_cost(fork) + + fork.gas_costs().ACCOUNT_WRITE ) tx = Transaction(to=caller, sender=pre.fund_eoa()) 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 da283a5fee1..61cd928c2a5 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 @@ -327,27 +327,28 @@ def test_mixed_validity_multi_auth_receipt_gas( one invalid authorization. Every authorization tuple, valid or invalid, is charged the full - regular + state per-authorization intrinsic. Refunds, however, fire - only for the *valid* authorization whose authority leaf already - exists: it refills ``NEW_ACCOUNT`` on the state channel (uncapped, - subtracted first) and returns ``ACCOUNT_WRITE`` on the regular - channel (one-fifth capped). The invalid tuple is silently skipped - during ``set_delegation`` and contributes no refund on either - channel. + regular + state per-authorization intrinsic. The valid + authorization whose authority leaf already exists refills + ``NEW_ACCOUNT`` on the state channel (uncapped, subtracted first) + and returns ``ACCOUNT_WRITE`` on the regular channel (one-fifth + capped). The invalid tuple is silently skipped during + ``set_delegation``, refilling the full per-auth state intrinsic and + returning its regular ``ACCOUNT_WRITE`` charge. The dual-channel accounting mirrors ``process_transaction`` and the sibling ``test_set_code_auth_refunds`` module: the state refill is subtracted from ``gas_before_regular_refund`` first and uncapped, then the regular refund clamps to ``min(k * ACCOUNT_WRITE, gas_before_regular_refund // 5)`` where - ``k`` is the number of refundable (valid, existing-leaf) - authorizations. With no EVM execution, ``gas_before_regular_refund`` - reduces to the full per-authorization intrinsic less the state - refill, and the exact result is asserted via ``expected_receipt``. + ``k`` is the number of authorizations that return the regular + account-write charge. With no EVM execution, + ``gas_before_regular_refund`` reduces to the full per-authorization + intrinsic less the state refill, and the exact result is asserted + via ``expected_receipt``. Each ``invalidity`` kind (``INVALID_NONCE``, ``INVALID_CHAIN_ID``, ``REPEATED_NONCE``, ``AUTHORITY_IS_CONTRACT``) yields one valid and - one invalid tuple, so ``n = 2`` and ``k = 1`` uniformly and every + one invalid tuple, so ``n = 2`` and ``k = 2`` uniformly and every kind pins the same receipt gas. This is the numeric-receipt companion to ``test_invalid_auth_charged_intrinsic`` (which asserts only post state). @@ -418,7 +419,7 @@ def test_mixed_validity_multi_auth_receipt_gas( raise ValueError(f"unknown invalidity: {invalidity!r}") n = len(authorization_list) - refundable = 1 # exactly one valid, existing-leaf authorization + regular_refundable = 2 total_intrinsic = fork.transaction_intrinsic_cost_calculator()( authorization_list_or_count=n, @@ -426,22 +427,25 @@ def test_mixed_validity_multi_auth_receipt_gas( intrinsic_state = fork.transaction_intrinsic_state_gas( authorization_count=n, ) - # Only the valid existing-leaf authorization refills the state - # channel (NEW_ACCOUNT); the invalid tuple refunds nothing. The - # refill is subtracted first and is not subject to the one-fifth cap. - state_refund = gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT * refundable + # The valid existing-leaf authorization refills NEW_ACCOUNT. The + # invalid skipped tuple refills the full per-auth state intrinsic. + # State refills are subtracted first and are not subject to the + # one-fifth cap. + state_refund = gas_costs.REFUND_AUTH_PER_EXISTING_ACCOUNT + ( + intrinsic_state // n + ) # No EVM execution (the target is a STOP), so the regular and state # execution gas are both zero and ``gas_before_regular_refund`` # reduces to the full per-auth intrinsic less the state refill. gas_before_regular_refund = total_intrinsic - state_refund regular_refund = min( - refundable * account_write, + regular_refundable * account_write, gas_before_regular_refund // fork.max_refund_quotient(), ) - # With only a single refundable authorization the one-fifth cap is - # generous, so the full ACCOUNT_WRITE clears on the regular channel. - assert regular_refund == refundable * account_write + # The one-fifth cap is generous, so both ACCOUNT_WRITE refunds clear + # on the regular channel. + assert regular_refund == regular_refundable * account_write cumulative_gas_used = gas_before_regular_refund - regular_refund tx = Transaction( diff --git a/tests/ported_static/stCreate2/test_create2check_fields_in_initcode.py b/tests/ported_static/stCreate2/test_create2check_fields_in_initcode.py index da4c2cabd35..8ee471f590e 100644 --- a/tests/ported_static/stCreate2/test_create2check_fields_in_initcode.py +++ b/tests/ported_static/stCreate2/test_create2check_fields_in_initcode.py @@ -3,6 +3,8 @@ Ported from: state_tests/stCreate2/create2checkFieldsInInitcodeFiller.json +@manually-enhanced: Do not overwrite. The env `gas_limit` is omitted so +the framework default supplies ample gas for EIP-8037 state accounting. """ import pytest @@ -115,7 +117,6 @@ def test_create2check_fields_in_initcode( timestamp=1000, prev_randao=0x20000, base_fee_per_gas=10, - gas_limit=1000000, ) pre[sender] = Account(balance=0x56BC75E2D63100000) diff --git a/tests/ported_static/stEIP3651_warmcoinbase/test_coinbase_warm_account_call_gas.py b/tests/ported_static/stEIP3651_warmcoinbase/test_coinbase_warm_account_call_gas.py index 7e8250c3ad0..1316363c63c 100644 --- a/tests/ported_static/stEIP3651_warmcoinbase/test_coinbase_warm_account_call_gas.py +++ b/tests/ported_static/stEIP3651_warmcoinbase/test_coinbase_warm_account_call_gas.py @@ -3,6 +3,8 @@ Ported from: state_tests/Shanghai/stEIP3651_warmcoinbase/coinbaseWarmAccountCallGasFiller.yml +@manually-enhanced: Do not overwrite. When EIP-8038 is enabled, +EXTCODESIZE and EXTCODECOPY charge an extra warm code-read. """ import pytest @@ -278,6 +280,11 @@ def test_coinbase_warm_account_call_gas( nonce=1, ) - post = {target: Account(storage={0: 100})} + warm_access = fork.gas_costs().WARM_ACCESS + # EIP-8038 charges EXTCODESIZE (d0) and EXTCODECOPY (d1) a second + # WARM_ACCESS for the account code read; other opcodes are unchanged. + ext_code_read = d in (0, 1) and fork.is_eip_enabled(8038) + expected_gas = warm_access + (warm_access if ext_code_read else 0) + post = {target: Account(storage={0: expected_gas})} state_test(env=env, pre=pre, post=post, tx=tx) From 51ade6382b64e5c19dc07430b25b78c74161567a Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 2 Jul 2026 16:00:35 -0600 Subject: [PATCH 10/14] Apply suggestions from code review (packates/testing) Co-authored-by: Mario Vega --- .../src/execution_testing/forks/gas_costs.py | 11 ----------- .../tools/utility/generators.py | 16 ---------------- 2 files changed, 27 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index 0899e2e1402..a899d5a129a 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -36,20 +36,9 @@ class GasCosts: CALL_VALUE: int CALL_STIPEND: int NEW_ACCOUNT: int - # Surcharge for the first write to an account leaf; 0 before the - # state-access repricing introduces it as a standalone parameter. ACCOUNT_WRITE: int = 0 - # Access cost for a contract-creation slot; 0 before the - # state-access repricing introduces it as a standalone parameter. CREATE_ACCESS: int = 0 - # Per-transfer log cost charged when a value-transferring - # transaction emits the synthetic transfer log; 0 before the - # intrinsic gas decomposition introduces it. TRANSFER_LOG_COST: int = 0 - # Per-transfer value cost charged on top of the transfer log when a - # non-create transaction transfers a non-zero value to an account - # other than the sender; 0 before the intrinsic gas decomposition - # introduces it. TX_VALUE_COST: int = 0 # Contract Creation diff --git a/packages/testing/src/execution_testing/tools/utility/generators.py b/packages/testing/src/execution_testing/tools/utility/generators.py index 53dc2cdc090..64b00df95fb 100644 --- a/packages/testing/src/execution_testing/tools/utility/generators.py +++ b/packages/testing/src/execution_testing/tools/utility/generators.py @@ -407,25 +407,9 @@ def wrapper( # Simple test transaction to verify the block failed to modify the # state. value_receiver = pre.fund_eoa(amount=0) - # value_receiver is empty, so a value transfer to it pays - # the value-transfer intrinsic surcharges plus the - # top-frame ``NEW_ACCOUNT`` state-gas charge introduced by - # EIP-2780. Derive ``gas_limit`` from the fork so the tx - # is provisioned correctly across all forks. - test_tx_intrinsic_calc = ( - fork.transaction_intrinsic_cost_calculator() - ) - test_tx_gas_limit = test_tx_intrinsic_calc( - sends_value=True, - recipient_type=RecipientType.EMPTY_ACCOUNT, - ) + fork.transaction_top_frame_state_gas( - sends_value=True, - recipient_type=RecipientType.EMPTY_ACCOUNT, - ) test_tx = Transaction( to=value_receiver, value=1, - gas_limit=test_tx_gas_limit, sender=pre.fund_eoa(), ) post = Alloc() From dd3c2f258d156f964477e0af5e4e246320f1a368 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 2 Jul 2026 16:01:47 -0600 Subject: [PATCH 11/14] Apply suggestions from code review (specs) Co-authored-by: Mario Vega --- src/ethereum/forks/amsterdam/fork.py | 3 -- src/ethereum/forks/amsterdam/state_tracker.py | 31 ++++++++++++++++--- src/ethereum/forks/amsterdam/vm/gas.py | 31 ------------------- .../forks/amsterdam/vm/instructions/system.py | 1 - .../forks/amsterdam/vm/interpreter.py | 6 ---- 5 files changed, 26 insertions(+), 46 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 7172a39f3f6..c2001554bd1 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -1024,9 +1024,6 @@ def process_transaction( encode_transaction(tx), ) - # `chain_id` also validates a legacy signature's `v` (raising - # `InvalidSignatureError` when `v` is neither 27/28 nor >= 35), so it - # must be called before `recover_sender`. tx_chain_id = chain_id(tx) if tx_chain_id is not None and tx_chain_id != block_env.chain_id: raise WrongChainIdError( diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index 91610ace0ce..fc8ce5b72c9 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -52,11 +52,6 @@ class BlockState: """ pre_state: PreState - """ - Accounts, storage, and code as they existed before the beginning of the - block. - """ - account_reads: Set[Address] = field(default_factory=set) account_writes: Dict[Address, Optional[Account]] = field( default_factory=dict @@ -110,6 +105,19 @@ def get_pre_state_account_optional( [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT [pre]: ref:ethereum.forks.amsterdam.state_tracker.get_pre_state_account + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Optional[Account]`` + Account at address before the current transaction. + """ tx_state.account_reads.add(address) if address in tx_state.parent.account_writes: @@ -130,6 +138,19 @@ def get_pre_state_account( [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT [opt]: ref:ethereum.forks.amsterdam.state_tracker.get_pre_state_account_optional + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Account`` + Account at address before the current transaction. + """ # noqa: E501 account = get_pre_state_account_optional(tx_state, address) if account is None: diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 83fe851d992..91a5810b7d2 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -139,26 +139,12 @@ class GasCosts: # Authorization AUTH_TUPLE_BYTES: Final[Uint] = Uint(101) - """ - Calldata bytes charged for one [EIP-7702] authorization tuple. - - Counts the tuple's fields: the chain id, authority address, nonce, - signature parity, and the two signature scalars. Charged at the - calldata floor rate (`TX_DATA_TOKEN_FLOOR`). - - [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 - """ - REGULAR_PER_AUTH_BASE_COST: Final[Uint] = ( AUTH_TUPLE_BYTES * TX_DATA_TOKEN_FLOOR + PRECOMPILE_ECRECOVER + COLD_ACCOUNT_ACCESS + Uint(2) * WARM_ACCESS ) - """ - Regular gas charged per EIP-7702 authorization, in addition to - `ACCOUNT_WRITE`. - """ # Block LIMIT_ADJUSTMENT_FACTOR: Final[Uint] = Uint(1024) @@ -225,24 +211,7 @@ class GasCosts: OPCODE_SWAPN: Final[Uint] = VERY_LOW OPCODE_EXCHANGE: Final[Uint] = VERY_LOW OPCODE_TLOAD: Final[Uint] = Uint(100) - """ - Cost of the opcode that reads from transient storage. - - Transient storage is in-memory only; its cost is independent of - state-access pricing ([EIP-7971] proposes dedicated values). - - [EIP-7971]: https://eips.ethereum.org/EIPS/eip-7971 - """ - OPCODE_TSTORE: Final[Uint] = Uint(100) - """ - Cost of the opcode that writes to transient storage. - - Transient storage is in-memory only; its cost is independent of - state-access pricing ([EIP-7971] proposes dedicated values). - - [EIP-7971]: https://eips.ethereum.org/EIPS/eip-7971 - """ # Dynamic Opcode Components OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index a41e41a719f..a3587e4f238 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -674,7 +674,6 @@ def selfdestruct(evm: Evm) -> None: and get_account(tx_state, evm.message.current_target).balance != 0 ): state_gas = StateGasCosts.NEW_ACCOUNT - # EIP-8038: positive balance sent to an empty account. account_write_gas = GasCosts.ACCOUNT_WRITE # Charge regular gas before state gas so that a regular-gas OOG diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 518a210c7d8..921873a06bd 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -312,12 +312,6 @@ def process_message(message: Message) -> Evm: # Execute message code and handle errors try: - # EIP-2780 top-frame charges: applied at the top of a - # transaction's call frame, after authorizations and before any - # opcode runs. Reads pre-value-transfer state so the - # EIP-161-empty check sees the recipient as it was at the start - # of the frame. Gated to non-create top-level frames; creates - # pay the equivalent NEW_ACCOUNT state gas. if message.depth == Uint(0) and message.target != Bytes0(b""): recipient = message.current_target if message.value > U256(0) and not is_account_alive( From d587095758e7eac965461b422703c73a69dd9f23 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 2 Jul 2026 16:02:04 -0600 Subject: [PATCH 12/14] Apply suggestions from code review (tests) Co-authored-by: Mario Vega --- tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py index 78350d79de9..4c90175c2a5 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/helpers.py @@ -31,7 +31,7 @@ def setup_target( case RecipientType.CONTRACT: return pre.deploy_contract(code=Op.STOP) case RecipientType.EMPTY_ACCOUNT: - return pre.fund_eoa(amount=0) + return pre.nonexistent_account() case RecipientType.SELF: return sender case RecipientType.DELEGATION_7702: From 4cac8f4b8800c49aa34551d542eadad236088ab2 Mon Sep 17 00:00:00 2001 From: marioevz Date: Thu, 2 Jul 2026 16:05:02 -0600 Subject: [PATCH 13/14] fix(specs): Lint --- src/ethereum/forks/amsterdam/state_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index fc8ce5b72c9..5f7d0eaf33c 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -105,7 +105,7 @@ def get_pre_state_account_optional( [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT [pre]: ref:ethereum.forks.amsterdam.state_tracker.get_pre_state_account - + Parameters ---------- tx_state : From 60a2c7ce4bcce19ef48696b98b08e6bc3537f62c Mon Sep 17 00:00:00 2001 From: marioevz Date: Thu, 2 Jul 2026 16:05:09 -0600 Subject: [PATCH 14/14] fix(test-tools): Lint --- .../testing/src/execution_testing/tools/utility/generators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/testing/src/execution_testing/tools/utility/generators.py b/packages/testing/src/execution_testing/tools/utility/generators.py index 64b00df95fb..a70b3831ce2 100644 --- a/packages/testing/src/execution_testing/tools/utility/generators.py +++ b/packages/testing/src/execution_testing/tools/utility/generators.py @@ -12,7 +12,6 @@ from execution_testing.exceptions import BlockException from execution_testing.forks import Berlin, Fork, TransitionFork from execution_testing.forks.base_fork import BaseFork -from execution_testing.recipient_type import RecipientType from execution_testing.specs import BlockchainTestFiller, StateTestFiller from execution_testing.specs.blockchain import Block from execution_testing.test_types import Alloc, Transaction