From 24857eeac40d3437f8cd7a970d8777938dcd8d00 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 10:32:57 +0200 Subject: [PATCH 1/8] refactor(spec-specs): rename prepare_dispatch, simplify set_delegation Rename `prepare_dispatch` to `resolve_dispatch`: it resolves the top frame's code and charges the state-dependent dispatch costs, and the name frees "prepare" for a later `prepare_message_call` split. Replace the `set_delegation` match/case on an optional with a plain `if authority is None: continue`. Groundwork for merging Message into the Evm call frame; no behavior change (Amsterdam fixtures fill byte-identically). --- src/ethereum/forks/amsterdam/vm/eoa_delegation.py | 8 +++----- src/ethereum/forks/amsterdam/vm/interpreter.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index f2c69d5e7d..61cc47232c 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -244,11 +244,9 @@ def set_delegation(evm: Evm) -> None: # Authorities a delegation was set for earlier in this transaction. delegation_set_for: Set[Address] = set() for auth in message.tx_env.authorizations: - match validate_authorization(message, auth): - case None: - continue - case authority: - pass + authority = validate_authorization(message, auth) + if authority is None: + continue if not account_exists(tx_state, authority): charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 8d0a3fe26c..9fb5512c3c 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -247,7 +247,7 @@ def process_create_message(message: Message) -> Evm: return evm -def prepare_dispatch(evm: Evm) -> None: +def resolve_dispatch(evm: Evm) -> None: """ Charge the state-dependent dispatch costs and resolve the code the top frame will run. @@ -369,7 +369,7 @@ def process_message(message: Message) -> Evm: # non-refillable; a later failure restores only to the # post-commit baseline. commit_state_gas(evm.gas_meter) - prepare_dispatch(evm) + resolve_dispatch(evm) except ExceptionalHalt as error: evm_trace(evm, OpException(error)) restore_tx_state(tx_state, prep_snapshot) From d8bb4b5423003c2fca60d7e63f7767538c9cf765 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 10:32:57 +0200 Subject: [PATCH 2/8] refactor(spec-specs): return the transaction environment from check_transaction `check_transaction` now assembles and returns the `TransactionEnvironment` instead of an ad-hoc tuple, consuming the `IntrinsicGasCost` from `validate_transaction` and calling `allocate_execution_gas` for the regular/state gas split. The access-list prewarming and authorizations move in with it, and the transactions-trie write moves after validation -- a transaction that fails validation aborts the block, so recording it first was an unobservable mutation. The chain-id check stays in `process_transaction` before `recover_sender`: for a legacy transaction `chain_id` also validates the `v` value that `recover_sender` then relies on, so it cannot move into `check_transaction` (which runs after `recover_sender`). No behavior change (Amsterdam fixtures fill byte-identically). --- src/ethereum/forks/amsterdam/fork.py | 143 +++++++++++++++------------ 1 file changed, 81 insertions(+), 62 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 302e0887ed..d9c2e99e02 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -59,7 +59,7 @@ TransactionTypeContractCreationError, WrongChainIdError, ) -from .fork_types import Authorization, BlockAccessIndex, VersionedHash +from .fork_types import Authorization, BlockAccessIndex from .requests import ( BUILDER_DEPOSIT_REQUEST_TYPE, BUILDER_EXIT_REQUEST_TYPE, @@ -85,6 +85,7 @@ TX_MAX_GAS_LIMIT, BlobTransaction, FeeMarketCapableTransaction, + IntrinsicGasCost, LegacyTransaction, SetCodeTransaction, Transaction, @@ -506,10 +507,28 @@ def check_transaction( block_output: vm.BlockOutput, tx: Transaction, sender: Address, + intrinsic: IntrinsicGasCost, + index: Uint, tx_state: TransactionState, -) -> Tuple[Uint, Tuple[VersionedHash, ...], U64]: +) -> vm.TransactionEnvironment: """ - Check if the transaction is includable in the block. + Check if the transaction is includable in the block and assemble + its [`TransactionEnvironment`][tx-env]. + + Inclusion is checked against the block's remaining capacity in each + gas dimension (regular, state, and blob), the sender's account + (correct nonce, sufficient balance for the maximum possible charge, + and code that is empty or a valid delegation), the transaction's fee + caps against the current base fee and blob gas price, and + type-specific rules. Any violation raises a subclass of + [`InvalidBlock`] naming the failure. + + On success, the returned environment carries everything execution + needs: the effective gas price, the execution gas split into the + regular gas pool and the state gas reservoir (the portion of the + transaction's gas above the per-transaction regular gas cap), the + addresses and storage keys to prewarm, and the transaction's + authorizations. Parameters ---------- @@ -521,17 +540,18 @@ def check_transaction( The transaction. sender : The recovered sender address of the transaction. + intrinsic : + The transaction's intrinsic gas cost, as returned by + `validate_transaction`. + index : + Index of the transaction in the block. tx_state : The transaction state tracker. Returns ------- - effective_gas_price : - The price to charge for gas when the transaction is executed. - blob_versioned_hashes : - The blob versioned hashes of the transaction. - tx_blob_gas_used: - The blob gas used by the transaction. + tx_env : `ethereum.forks.amsterdam.vm.TransactionEnvironment` + The transaction's execution environment. Raises ------ @@ -567,6 +587,8 @@ def check_transaction( If the transaction is a SetCodeTransaction and the authorization list is empty. + [tx-env]: ref:ethereum.forks.amsterdam.vm.TransactionEnvironment + [`InvalidBlock`]: ref:ethereum.exceptions.InvalidBlock """ regular_gas_available = ( block_env.block_gas_limit - block_output.block_gas_used @@ -659,10 +681,37 @@ def check_transaction( ): raise InvalidSenderError("not EOA") - return ( - effective_gas_price, - blob_versioned_hashes, - tx_blob_gas_used, + # Split execution gas into a regular grant (capped by the remaining + # regular-gas budget) and a state gas reservoir. + allocation = allocate_execution_gas(tx.gas, intrinsic) + + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) + if has_access_list(tx): + for access in tx.access_list: + access_list_addresses.add(access.account) + for slot in access.slots: + access_list_storage_keys.add((access.account, slot)) + + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + + return vm.TransactionEnvironment( + origin=sender, + recipient=tx.to, + value=tx.value, + gas_price=effective_gas_price, + gas=allocation.regular_gas, + state_gas_reservoir=allocation.state_gas_reservoir, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + state=tx_state, + blob_versioned_hashes=blob_versioned_hashes, + authorizations=authorizations, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), ) @@ -1018,12 +1067,9 @@ def process_transaction( ) tx_state = TransactionState(parent=block_env.state) - trie_set( - block_output.transactions_trie, - rlp.encode(index), - encode_transaction(tx), - ) - + # The chain id is checked before ``recover_sender`` because, for a + # legacy transaction, ``chain_id`` also validates the ``v`` value + # that ``recover_sender`` then relies on. tx_chain_id = chain_id(tx) if tx_chain_id is not None and tx_chain_id != block_env.chain_id: raise WrongChainIdError( @@ -1034,18 +1080,24 @@ def process_transaction( sender = recover_sender(tx) intrinsic = validate_transaction(tx, sender) - ( - effective_gas_price, - blob_versioned_hashes, - tx_blob_gas_used, - ) = check_transaction( + tx_env = check_transaction( block_env=block_env, block_output=block_output, tx=tx, sender=sender, + intrinsic=intrinsic, + index=index, tx_state=tx_state, ) + # The transaction has passed validation and is included in the + # block, so it is recorded in the transactions trie. + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + sender_account = get_account(tx_state, sender) if isinstance(tx, BlobTransaction): @@ -1053,11 +1105,7 @@ def process_transaction( else: blob_gas_fee = Uint(0) - effective_gas_fee = tx.gas * effective_gas_price - - # Split execution gas into a regular grant (capped by the remaining - # regular-gas budget) and a state gas reservoir. - allocation = allocate_execution_gas(tx.gas, intrinsic) + effective_gas_fee = tx.gas * tx_env.gas_price increment_nonce(tx_state, sender) @@ -1066,35 +1114,6 @@ def process_transaction( ) set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) - access_list_addresses = set() - access_list_storage_keys = set() - access_list_addresses.add(block_env.coinbase) - if has_access_list(tx): - for access in tx.access_list: - access_list_addresses.add(access.account) - for slot in access.slots: - access_list_storage_keys.add((access.account, slot)) - - authorizations: Tuple[Authorization, ...] = () - if isinstance(tx, SetCodeTransaction): - authorizations = tx.authorizations - - tx_env = vm.TransactionEnvironment( - origin=sender, - recipient=tx.to, - value=tx.value, - gas_price=effective_gas_price, - gas=allocation.regular_gas, - state_gas_reservoir=allocation.state_gas_reservoir, - access_list_addresses=access_list_addresses, - access_list_storage_keys=access_list_storage_keys, - state=tx_state, - blob_versioned_hashes=blob_versioned_hashes, - authorizations=authorizations, - index_in_block=index, - tx_hash=get_transaction_hash(encode_transaction(tx)), - ) - message = prepare_message(block_env, tx_env, tx) tx_output = process_message_call(message) @@ -1108,10 +1127,10 @@ def process_transaction( tx_output.state_gas_used, ) - gas_refund_amount = settlement.gas_left * effective_gas_price + gas_refund_amount = settlement.gas_left * tx_env.gas_price - # For non-1559 transactions effective_gas_price == tx.gas_price - priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + # For non-1559 transactions tx_env.gas_price == tx.gas_price + priority_fee_per_gas = tx_env.gas_price - block_env.base_fee_per_gas transaction_fee = settlement.gas_used * priority_fee_per_gas # refund gas @@ -1122,7 +1141,7 @@ def process_transaction( block_output.block_gas_used += settlement.regular_gas_used block_output.block_state_gas_used += settlement.state_gas_used - block_output.blob_gas_used += tx_blob_gas_used + block_output.blob_gas_used += calculate_total_blob_gas(tx) block_output.cumulative_gas_used += settlement.gas_used receipt = make_receipt( From eec721bda83053dbab7106fe8b0768bdcd44a8ae Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 11:41:07 +0200 Subject: [PATCH 3/8] refactor(spec-specs, tools): merge Message into the Evm call frame Message and Evm were two mutable halves of one frame: the warm sets were aliased, resolve_dispatch double-wrote the code, and the top frame mutated the supposedly immutable message. Fold Message's call parameters onto the Evm dataclass so callers construct child frames directly and the Message->Evm hand-off disappears. - The two entry grants (entry_gas, state_gas_grant) live on the GasMeter, which becomes self-describing: tx_state_gas_used and restore_state_gas_to_entry no longer take the grant as a parameter. - MessageCallOutput is retired; process_message_call returns the finished frame and normalizes error results on it. - prepare_message becomes create_top_level_evm in fork.py and utils/message.py is deleted; system transactions build the frame inline. - The unreachable stack-depth guard moves inside process_message's try, so a fire is settled as the exceptional halt it is rather than escaping the frame. - The EIP-3155 and opcode-count tracers resolve the message-scoped fields via getattr(evm, "message", evm), keeping older forks traced through the same protocols; the Evm trace protocol drops its stale `message` member. No behavior change: the full Amsterdam suite (52,812 cases) fills byte-identically; ruff and mypy clean. --- src/ethereum/forks/amsterdam/fork.py | 189 +++++++++++---- src/ethereum/forks/amsterdam/utils/message.py | 92 -------- src/ethereum/forks/amsterdam/vm/__init__.py | 60 +++-- .../forks/amsterdam/vm/eoa_delegation.py | 27 +-- src/ethereum/forks/amsterdam/vm/gas.py | 61 +++-- .../forks/amsterdam/vm/instructions/block.py | 18 +- .../amsterdam/vm/instructions/environment.py | 38 ++- .../forks/amsterdam/vm/instructions/log.py | 4 +- .../amsterdam/vm/instructions/storage.py | 34 ++- .../forks/amsterdam/vm/instructions/system.py | 145 +++++++----- .../forks/amsterdam/vm/interpreter.py | 223 ++++++------------ .../vm/precompiled_contracts/alt_bn128.py | 6 +- .../vm/precompiled_contracts/blake2f.py | 2 +- .../bls12_381/bls12_381_g1.py | 6 +- .../bls12_381/bls12_381_g2.py | 6 +- .../bls12_381/bls12_381_pairing.py | 2 +- .../vm/precompiled_contracts/ecrecover.py | 2 +- .../vm/precompiled_contracts/identity.py | 2 +- .../vm/precompiled_contracts/modexp.py | 2 +- .../vm/precompiled_contracts/p256verify.py | 2 +- .../precompiled_contracts/point_evaluation.py | 2 +- .../vm/precompiled_contracts/ripemd160.py | 2 +- .../vm/precompiled_contracts/sha256.py | 2 +- .../evm_tools/t8n/evm_trace/count.py | 9 +- .../evm_tools/t8n/evm_trace/eip3155.py | 28 ++- .../evm_tools/t8n/evm_trace/protocols.py | 9 +- 26 files changed, 486 insertions(+), 487 deletions(-) delete mode 100644 src/ethereum/forks/amsterdam/utils/message.py diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index d9c2e99e02..c4de8bd218 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -15,7 +15,7 @@ from typing import Final, List, Optional, Tuple, final from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes +from ethereum_types.bytes import Bytes, Bytes0 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint, ulen @@ -97,12 +97,12 @@ recover_sender, validate_transaction, ) +from .utils.address import compute_contract_address from .utils.hexadecimal import hex_to_address -from .utils.message import prepare_message -from .vm import Message from .vm.eoa_delegation import is_valid_delegation from .vm.gas import ( GasCosts, + GasMeter, StateGasCosts, allocate_execution_gas, calculate_blob_gas_price, @@ -110,8 +110,10 @@ calculate_excess_blob_gas, calculate_total_blob_gas, settle_transaction_gas, + tx_state_gas_used, ) -from .vm.interpreter import MessageCallOutput, process_message_call +from .vm.interpreter import process_message_call +from .vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) ELASTICITY_MULTIPLIER = Uint(2) @@ -589,6 +591,7 @@ def check_transaction( [tx-env]: ref:ethereum.forks.amsterdam.vm.TransactionEnvironment [`InvalidBlock`]: ref:ethereum.exceptions.InvalidBlock + """ regular_gas_available = ( block_env.block_gas_limit - block_output.block_gas_used @@ -756,7 +759,7 @@ def process_checked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, -) -> MessageCallOutput: +) -> vm.Evm: """ Process a system transaction and raise an error if the contract does not contain code or if the transaction fails. @@ -772,8 +775,8 @@ def process_checked_system_transaction( Returns ------- - system_tx_output : `MessageCallOutput` - Output of processing the system transaction. + system_tx_frame : `vm.Evm` + The finished frame of the system transaction. """ # Pre-check that the system contract has code. We use a throwaway @@ -796,26 +799,26 @@ def process_checked_system_transaction( "contain code" ) - system_tx_output = process_unchecked_system_transaction( + system_tx_frame = process_unchecked_system_transaction( block_env, target_address, data, ) - if system_tx_output.error: + if system_tx_frame.error: raise InvalidBlock( f"System contract ({target_address.hex()}) call failed: " - f"{system_tx_output.error}" + f"{system_tx_frame.error}" ) - return system_tx_output + return system_tx_frame def process_unchecked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, -) -> MessageCallOutput: +) -> vm.Evm: """ Process a system transaction without checking if the contract contains code or if the transaction fails. @@ -831,8 +834,8 @@ def process_unchecked_system_transaction( Returns ------- - system_tx_output : `MessageCallOutput` - Output of processing the system transaction. + system_tx_frame : `vm.Evm` + The finished frame of the system transaction. """ system_tx_state = TransactionState(parent=block_env.state) @@ -841,15 +844,17 @@ def process_unchecked_system_transaction( get_account(system_tx_state, target_address).code_hash, ) + system_state_gas_reservoir = ( + StateGasCosts.STORAGE_SET * SYSTEM_MAX_SSTORES_PER_CALL + ) + 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=( - StateGasCosts.STORAGE_SET * SYSTEM_MAX_SSTORES_PER_CALL - ), + state_gas_reservoir=system_state_gas_reservoir, access_list_addresses=set(), access_list_storage_keys=set(), state=system_tx_state, @@ -859,36 +864,49 @@ def process_unchecked_system_transaction( tx_hash=None, ) - system_tx_message = Message( + system_tx_frame = vm.Evm( block_env=block_env, tx_env=tx_env, caller=SYSTEM_ADDRESS, target=target_address, - gas=SYSTEM_TRANSACTION_GAS, - state_gas_reservoir=( - StateGasCosts.STORAGE_SET * SYSTEM_MAX_SSTORES_PER_CALL - ), + current_target=target_address, + code_address=target_address, value=U256(0), data=data, - code=system_contract_code, depth=Uint(0), - current_target=target_address, - code_address=target_address, should_transfer_value=False, is_static=False, - accessed_addresses=set(), - accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, + pc=Uint(0), + stack=[], + memory=bytearray(), + code=system_contract_code, + gas_meter=GasMeter( + entry_gas=SYSTEM_TRANSACTION_GAS, + state_gas_grant=system_state_gas_reservoir, + gas_left=SYSTEM_TRANSACTION_GAS, + state_gas_left=system_state_gas_reservoir, + state_gas_baseline=system_state_gas_reservoir, + ), + valid_jump_destinations=set(), + logs=(), + running=True, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=set(), + accessed_storage_keys=set(), ) - system_tx_output = process_message_call(system_tx_message) + system_tx_frame = process_message_call(system_tx_frame) incorporate_tx_into_block( system_tx_state, block_env.block_access_list_builder ) - return system_tx_output + return system_tx_frame def apply_body( @@ -990,9 +1008,9 @@ def process_general_purpose_requests( data=b"", ) - if len(system_withdrawal_tx_output.return_data) > 0: + if len(system_withdrawal_tx_output.output) > 0: requests_from_execution.append( - WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.output ) system_consolidation_tx_output = process_checked_system_transaction( @@ -1001,10 +1019,9 @@ def process_general_purpose_requests( data=b"", ) - if len(system_consolidation_tx_output.return_data) > 0: + if len(system_consolidation_tx_output.output) > 0: requests_from_execution.append( - CONSOLIDATION_REQUEST_TYPE - + system_consolidation_tx_output.return_data + CONSOLIDATION_REQUEST_TYPE + system_consolidation_tx_output.output ) system_builder_deposit_tx_output = process_checked_system_transaction( @@ -1013,10 +1030,10 @@ def process_general_purpose_requests( data=b"", ) - if len(system_builder_deposit_tx_output.return_data) > 0: + if len(system_builder_deposit_tx_output.output) > 0: requests_from_execution.append( BUILDER_DEPOSIT_REQUEST_TYPE - + system_builder_deposit_tx_output.return_data + + system_builder_deposit_tx_output.output ) system_builder_exit_tx_output = process_checked_system_transaction( @@ -1025,13 +1042,88 @@ def process_general_purpose_requests( data=b"", ) - if len(system_builder_exit_tx_output.return_data) > 0: + if len(system_builder_exit_tx_output.output) > 0: requests_from_execution.append( - BUILDER_EXIT_REQUEST_TYPE - + system_builder_exit_tx_output.return_data + BUILDER_EXIT_REQUEST_TYPE + system_builder_exit_tx_output.output ) +def create_top_level_evm( + block_env: vm.BlockEnvironment, + tx_env: vm.TransactionEnvironment, + tx: Transaction, +) -> vm.Evm: + """ + Assemble the transaction's top-level frame. + + The frame is created with the transaction's gas split and with the + origin, the precompiles, and the access-list entries prewarmed. For a + contract creation the frame runs the transaction's init code at the + newly computed contract address; for a message call the frame's code + is resolved at dispatch, after any authorizations have been applied. + """ + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): + current_target = compute_contract_address( + tx_env.origin, + get_account(tx_env.state, tx_env.origin).nonce - Uint(1), + ) + msg_data = Bytes(b"") + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + # Placeholder; ``resolve_dispatch`` resolves the code the frame + # runs once any authorizations have been applied. + code = Bytes(b"") + code_address = tx.to + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses.add(current_target) + + return vm.Evm( + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + current_target=current_target, + code_address=code_address, + value=tx.value, + data=msg_data, + depth=Uint(0), + should_transfer_value=True, + is_static=False, + disable_precompiles=False, + parent_evm=None, + pc=Uint(0), + stack=[], + memory=bytearray(), + code=code, + gas_meter=GasMeter( + entry_gas=tx_env.gas, + state_gas_grant=tx_env.state_gas_reservoir, + gas_left=tx_env.gas, + state_gas_left=tx_env.state_gas_reservoir, + state_gas_baseline=tx_env.state_gas_reservoir, + ), + valid_jump_destinations=set(), + logs=(), + running=True, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(tx_env.access_list_storage_keys), + ) + + def process_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, @@ -1114,17 +1206,18 @@ def process_transaction( ) set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) - message = prepare_message(block_env, tx_env, tx) + tx_frame = create_top_level_evm(block_env, tx_env, tx) - tx_output = process_message_call(message) + tx_frame = process_message_call(tx_frame) + gas_meter = tx_frame.gas_meter settlement = settle_transaction_gas( tx.gas, intrinsic, - tx_output.gas_left, - tx_output.state_gas_left, - tx_output.refund_counter, - tx_output.state_gas_used, + gas_meter.gas_left, + gas_meter.state_gas_left, + U256(gas_meter.refund_counter), + tx_state_gas_used(gas_meter), ) gas_refund_amount = settlement.gas_left * tx_env.gas_price @@ -1145,7 +1238,7 @@ def process_transaction( block_output.cumulative_gas_used += settlement.gas_used receipt = make_receipt( - tx, tx_output.error, block_output.cumulative_gas_used, tx_output.logs + tx, tx_frame.error, block_output.cumulative_gas_used, tx_frame.logs ) receipt_key = rlp.encode(Uint(index)) @@ -1157,9 +1250,9 @@ def process_transaction( receipt, ) - block_output.block_logs += tx_output.logs + block_output.block_logs += tx_frame.logs - for address in tx_output.accounts_to_delete: + for address in tx_frame.accounts_to_delete: clear_account_preserving_balance(tx_state, address) incorporate_tx_into_block(tx_state, block_env.block_access_list_builder) diff --git a/src/ethereum/forks/amsterdam/utils/message.py b/src/ethereum/forks/amsterdam/utils/message.py deleted file mode 100644 index a0387240d0..0000000000 --- a/src/ethereum/forks/amsterdam/utils/message.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Hardfork Utility Functions For The Message Data-structure. - -.. contents:: Table of Contents - :backlinks: none - :local: - -Introduction ------------- - -Message specific functions used in this amsterdam version of -specification. -""" - -from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import Uint - -from ethereum.state import Address - -from ..state_tracker import get_account -from ..transactions import Transaction -from ..vm import BlockEnvironment, Message, TransactionEnvironment -from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from .address import compute_contract_address - - -def prepare_message( - block_env: BlockEnvironment, - tx_env: TransactionEnvironment, - tx: Transaction, -) -> Message: - """ - Execute a transaction against the provided environment. - - Parameters - ---------- - block_env : - Environment for the Ethereum Virtual Machine. - tx_env : - Environment for the transaction. - tx : - Transaction to be executed. - - Returns - ------- - message: `ethereum.forks.amsterdam.vm.Message` - Items containing contract creation or message call specific data. - - """ - accessed_addresses = set() - accessed_addresses.add(tx_env.origin) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(tx_env.access_list_addresses) - - if isinstance(tx.to, Bytes0): - current_target = compute_contract_address( - tx_env.origin, - get_account(tx_env.state, tx_env.origin).nonce - Uint(1), - ) - msg_data = Bytes(b"") - code = tx.data - code_address = None - elif isinstance(tx.to, Address): - current_target = tx.to - msg_data = tx.data - code = None - code_address = tx.to - else: - raise AssertionError("Target must be address or empty bytes") - - accessed_addresses.add(current_target) - - return Message( - block_env=block_env, - tx_env=tx_env, - caller=tx_env.origin, - target=tx.to, - gas=tx_env.gas, - state_gas_reservoir=tx_env.state_gas_reservoir, - value=tx.value, - data=msg_data, - code=code, - depth=Uint(0), - current_target=current_target, - code_address=code_address, - should_transfer_value=True, - is_static=False, - accessed_addresses=accessed_addresses, - accessed_storage_keys=set(tx_env.access_list_storage_keys), - disable_precompiles=False, - parent_evm=None, - ) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index c7563ab5fd..87dc187632 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -8,8 +8,24 @@ Introduction ------------ -The abstract computer which runs the code stored in an +The abstract computer that runs the code stored in an `.fork_types.Account`. + +Execution is organized around a single call frame, the [`Evm`][evm]: one +object carrying a call's parameters, its machine state (program counter, +stack, memory), the gas it has left, and the effects it accrues (logs, +refunds, accounts to delete). A call spawns a child frame, and each +top-level call is a frame at depth zero; a transaction executes one or +more such frames, so `.interpreter` drives every frame the same way. + +Each frame meters gas in two pools, bundled in its [`GasMeter`][meter]. +Regular gas pays for computation; the state gas reservoir pays the +state-growth costs, and when it is exhausted a charge spills into regular +gas, with a revert refilling both pools in LIFO order so the two never +drift. `.gas` holds both the charges and that pool accounting. + +[evm]: ref:ethereum.forks.amsterdam.vm.Evm +[meter]: ref:ethereum.forks.amsterdam.vm.gas.GasMeter """ from dataclasses import dataclass, field @@ -31,7 +47,12 @@ from ..transactions import LegacyTransaction from .gas import GasMeter -__all__ = ("Environment", "Evm", "Message") +__all__ = ( + "BlockEnvironment", + "BlockOutput", + "TransactionEnvironment", + "Evm", +) TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)") SYSTEM_ADDRESS = Address( bytes.fromhex("fffffffffffffffffffffffffffffffffffffffe") @@ -137,45 +158,50 @@ class TransactionEnvironment: @final @dataclass -class Message: +class Evm: """ - Items that are used by contract creation or message call. + A single call frame: its parameters, gas meter, machine state, and + accrued effects. + + A call spawns a child frame and each top-level call is a frame at + depth zero, so one dataclass describes them all. What was once a + separate ``Message`` -- the call's fixed parameters -- now lives on + the frame directly, alongside its mutable machine state. """ + # Environment. block_env: BlockEnvironment tx_env: TransactionEnvironment + + # Call parameters, fixed at frame creation. caller: Address target: Bytes0 | Address current_target: Address - gas: Uint - state_gas_reservoir: Uint + code_address: Optional[Address] value: U256 data: Bytes - code_address: Optional[Address] - code: Optional[Bytes] depth: Uint should_transfer_value: bool is_static: bool - accessed_addresses: Set[Address] - accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - -@final -@dataclass -class Evm: - """The internal state of the virtual machine.""" - + # Machine state, gas meter, and accrued effects. Field order follows + # the predecessor fork's `Evm` for diff stability. The frame's gas -- + # its entry grants and everything it consumes -- lives on the + # [`GasMeter`][meter]. + # + # [meter]: ref:ethereum.forks.amsterdam.vm.gas.GasMeter pc: Uint stack: List[U256] memory: bytearray + # Init code for a creation; for a call a placeholder that + # `resolve_dispatch` replaces once authorizations are applied. code: Bytes gas_meter: GasMeter valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] running: bool - message: Message output: Bytes accounts_to_delete: Set[Address] return_data: Bytes diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 61cc47232c..63d318a175 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -28,7 +28,7 @@ charge_gas, charge_state_gas, ) -from . import Evm, Message +from . import Evm SET_CODE_TX_MAGIC = b"\x05" EOA_DELEGATION_MARKER = b"\xef\x01\x00" @@ -141,7 +141,7 @@ def calculate_delegation_cost( The delegation address and access gas cost. """ - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state code = get_code(tx_state, get_account(tx_state, address).code_hash) @@ -158,18 +158,16 @@ def calculate_delegation_cost( return True, delegated_address, delegation_gas_cost -def validate_authorization( - message: Message, auth: Authorization -) -> Optional[Address]: +def validate_authorization(evm: Evm, auth: Authorization) -> Optional[Address]: """ Check if the given `Authorization` is valid against the current state. Returns the `authority` address, or `None` if the validation was unsuccessful. """ - tx_state = message.tx_env.state + tx_state = evm.tx_env.state - if auth.chain_id not in (message.block_env.chain_id, U256(0)): + if auth.chain_id not in (evm.block_env.chain_id, U256(0)): return None if auth.nonce >= U64.MAX_VALUE: @@ -180,7 +178,7 @@ def validate_authorization( except InvalidSignatureError: return None - message.accessed_addresses.add(authority) + evm.accessed_addresses.add(authority) authority_account = get_account(tx_state, authority) authority_code = get_code(tx_state, authority_account.code_hash) @@ -231,20 +229,19 @@ def set_delegation(evm: Evm) -> None: The top-level transaction frame. """ - message = evm.message - tx_state = message.tx_env.state + tx_state = evm.tx_env.state # Accounts whose write the transaction has already priced: the # sender's leaf was written at inclusion (nonce bump and fee # deduction), and a value-bearing transaction prepays the # recipient's balance write -- the transfer itself only happens at # frame entry, after these charges. - written_accounts: Set[Address] = {message.tx_env.origin} - if evm.message.tx_env.value > U256(0): - written_accounts.add(evm.message.current_target) + written_accounts: Set[Address] = {evm.tx_env.origin} + if evm.value > U256(0): + written_accounts.add(evm.current_target) # Authorities a delegation was set for earlier in this transaction. delegation_set_for: Set[Address] = set() - for auth in message.tx_env.authorizations: - authority = validate_authorization(message, auth) + for auth in evm.tx_env.authorizations: + authority = validate_authorization(evm, auth) if authority is None: continue diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 028b4ac431..4169f1eb9e 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -244,15 +244,38 @@ class GasCosts: @dataclass class GasMeter: """ - Track a frame's gas consumption across both gas dimensions. + Track a frame's gas grants and consumption across both gas + dimensions. - Bundle every mutable gas quantity a frame maintains, so the frame + Bundle every gas quantity a frame maintains -- the immutable grants + it entered with and everything it mutates as it runs -- so the frame and its settlement work against one object instead of a scatter of - fields on the [`Evm`]. + fields on the [`Evm`]. A meter is created with `gas_left` and + `state_gas_left` at the grants (`entry_gas`, `state_gas_grant`) and + `state_gas_baseline` at the state grant. [`Evm`]: ref:ethereum.forks.amsterdam.vm.Evm """ + entry_gas: Uint + """ + Regular gas granted at frame entry. Immutable: the frozen snapshot + of `gas_left` at entry, against which the top frame's total + consumption is measured for its closing trace. + """ + + state_gas_grant: Uint + """ + State gas reservoir granted at frame entry. Immutable: the amount + [`tx_state_gas_used`][used] and [`restore_state_gas_to_entry`][entry] + measure against. Distinct from the mutable `state_gas_baseline`, + which starts equal to it but the top frame moves down as its EIP-7702 + authorizations commit. + + [used]: ref:ethereum.forks.amsterdam.vm.gas.tx_state_gas_used + [entry]: ref:ethereum.forks.amsterdam.vm.gas.restore_state_gas_to_entry + """ + gas_left: Uint """ Gas still available from the frame's regular grant. Pays regular @@ -269,10 +292,11 @@ class GasMeter: state_gas_baseline: Uint """ - Reservoir level a rollback refills to: the frame's grant at entry, - moved down by [`commit_state_gas`][commit] when charges become - non-refillable. + Reservoir level a rollback refills to: starts at the frame's + [`state_gas_grant`][grant], moved down by [`commit_state_gas`][commit] + when charges become non-refillable. + [grant]: ref:ethereum.forks.amsterdam.vm.gas.GasMeter.state_gas_grant [commit]: ref:ethereum.forks.amsterdam.vm.gas.commit_state_gas """ @@ -465,30 +489,27 @@ def restore_state_gas(gas_meter: GasMeter) -> None: gas_meter.refund_counter = 0 -def restore_state_gas_to_entry( - gas_meter: GasMeter, state_gas_reservoir: Uint -) -> None: +def restore_state_gas_to_entry(gas_meter: GasMeter) -> None: """ Roll the frame's state gas back to frame entry, undoing any commit. Used when the transaction-state rollback also reverts the applied delegations a [`commit_state_gas`][commit] protected: every state charge refills -- all spill, committed or not, returns to - `gas_left` -- and the baseline resets to the frame's [grant]. + `gas_left` -- and the baseline resets to the frame's + [`state_gas_grant`][grant]. Parameters ---------- gas_meter : The frame's gas meter. - state_gas_reservoir : - The frame's immutable state gas grant. [commit]: ref:ethereum.forks.amsterdam.vm.gas.commit_state_gas - [grant]: ref:ethereum.forks.amsterdam.vm.Message.state_gas_reservoir + [grant]: ref:ethereum.forks.amsterdam.vm.gas.GasMeter.state_gas_grant """ # The baseline starts at the grant and only ever moves down. - assert gas_meter.state_gas_baseline <= state_gas_reservoir + assert gas_meter.state_gas_baseline <= gas_meter.state_gas_grant # Only pre-dispatch failures roll back to entry, and no refund # accrues before dispatch. assert gas_meter.refund_counter == 0 @@ -497,11 +518,11 @@ def restore_state_gas_to_entry( ) gas_meter.state_gas_spilled = Uint(0) gas_meter.state_gas_committed_spill = Uint(0) - gas_meter.state_gas_left = state_gas_reservoir - gas_meter.state_gas_baseline = state_gas_reservoir + gas_meter.state_gas_left = gas_meter.state_gas_grant + gas_meter.state_gas_baseline = gas_meter.state_gas_grant -def tx_state_gas_used(gas_meter: GasMeter, state_gas_reservoir: Uint) -> int: +def tx_state_gas_used(gas_meter: GasMeter) -> int: """ Return the net state gas a transaction's execution consumed. @@ -514,8 +535,6 @@ def tx_state_gas_used(gas_meter: GasMeter, state_gas_reservoir: Uint) -> int: ---------- gas_meter : The top frame's finished gas meter. - state_gas_reservoir : - The transaction's immutable state gas grant. Returns ------- @@ -526,9 +545,9 @@ def tx_state_gas_used(gas_meter: GasMeter, state_gas_reservoir: Uint) -> int: """ # The baseline starts at the grant and only ever moves down. - assert gas_meter.state_gas_baseline <= state_gas_reservoir + assert gas_meter.state_gas_baseline <= gas_meter.state_gas_grant return ( - int(state_gas_reservoir) + int(gas_meter.state_gas_grant) - int(gas_meter.state_gas_left) + int(gas_meter.state_gas_spilled) + int(gas_meter.state_gas_committed_spill) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/block.py b/src/ethereum/forks/amsterdam/vm/instructions/block.py index 24c524673e..fa286c439c 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/block.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/block.py @@ -44,7 +44,7 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - current_block_number = evm.message.block_env.number + current_block_number = evm.block_env.number if ( current_block_number <= block_number or current_block_number > max_block_number @@ -54,7 +54,7 @@ def block_hash(evm: Evm) -> None: # or if the block's age is more than 256. current_block_hash = b"\x00" else: - current_block_hash = evm.message.block_env.block_hashes[ + current_block_hash = evm.block_env.block_hashes[ -(current_block_number - block_number) ] @@ -92,7 +92,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_COINBASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +126,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_TIMESTAMP) # OPERATION - push(evm.stack, evm.message.block_env.time) + push(evm.stack, evm.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -159,7 +159,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_NUMBER) # OPERATION - push(evm.stack, U256(evm.message.block_env.number)) + push(evm.stack, U256(evm.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -192,7 +192,7 @@ def prev_randao(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_PREVRANDAO) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) + push(evm.stack, U256.from_be_bytes(evm.block_env.prev_randao)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -225,7 +225,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_GASLIMIT) # OPERATION - push(evm.stack, U256(evm.message.block_env.block_gas_limit)) + push(evm.stack, U256(evm.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -255,7 +255,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_CHAINID) # OPERATION - push(evm.stack, U256(evm.message.block_env.chain_id)) + push(evm.stack, U256(evm.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -288,7 +288,7 @@ def slot_number(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_SLOTNUM) # OPERATION - push(evm.stack, U256(evm.message.block_env.slot_number)) + push(evm.stack, U256(evm.block_env.slot_number)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py index 8a7e9ec148..cd71bb081c 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/environment.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -48,7 +48,7 @@ def address(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_ADDRESS) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + push(evm.stack, U256.from_be_bytes(evm.current_target)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -76,7 +76,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state balance = get_account(tx_state, address).balance push(evm.stack, balance) @@ -103,7 +103,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_ORIGIN) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) + push(evm.stack, U256.from_be_bytes(evm.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +126,7 @@ def caller(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_CALLER) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.message.caller)) + push(evm.stack, U256.from_be_bytes(evm.caller)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -149,7 +149,7 @@ def callvalue(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_CALLVALUE) # OPERATION - push(evm.stack, evm.message.value) + push(evm.stack, evm.value) # PROGRAM COUNTER evm.pc += Uint(1) @@ -173,7 +173,7 @@ def calldataload(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_CALLDATALOAD) # OPERATION - value = buffer_read(evm.message.data, start_index, U256(32)) + value = buffer_read(evm.data, start_index, U256(32)) push(evm.stack, U256.from_be_bytes(value)) @@ -198,7 +198,7 @@ def calldatasize(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_CALLDATASIZE) # OPERATION - push(evm.stack, U256(len(evm.message.data))) + push(evm.stack, U256(len(evm.data))) # PROGRAM COUNTER evm.pc += Uint(1) @@ -235,7 +235,7 @@ def calldatacopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - value = buffer_read(evm.message.data, data_start_index, size) + value = buffer_read(evm.data, data_start_index, size) memory_write(evm.memory, memory_start_index, value) # PROGRAM COUNTER @@ -320,7 +320,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_GASPRICE) # OPERATION - push(evm.stack, U256(evm.message.tx_env.gas_price)) + push(evm.stack, U256(evm.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -349,7 +349,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state code_hash = get_account(tx_state, address).code_hash code = get_code(tx_state, code_hash) @@ -396,7 +396,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state code_hash = get_account(tx_state, address).code_hash code = get_code(tx_state, code_hash) @@ -493,7 +493,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state account = get_account(tx_state, address) if account == EMPTY_ACCOUNT: @@ -525,9 +525,7 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account( - evm.message.tx_env.state, evm.message.current_target - ).balance + balance = get_account(evm.tx_env.state, evm.current_target).balance push(evm.stack, balance) @@ -552,7 +550,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_BASEFEE) # OPERATION - push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) + push(evm.stack, U256(evm.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -575,8 +573,8 @@ def blob_hash(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_BLOBHASH) # OPERATION - if int(index) < len(evm.message.tx_env.blob_versioned_hashes): - blob_hash = evm.message.tx_env.blob_versioned_hashes[index] + if int(index) < len(evm.tx_env.blob_versioned_hashes): + blob_hash = evm.tx_env.blob_versioned_hashes[index] else: blob_hash = Bytes32(b"\x00" * 32) push(evm.stack, U256.from_be_bytes(blob_hash)) @@ -602,9 +600,7 @@ def blob_base_fee(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_BLOBBASEFEE) # OPERATION - blob_base_fee = calculate_blob_gas_price( - evm.message.block_env.excess_blob_gas - ) + blob_base_fee = calculate_blob_gas_price(evm.block_env.excess_blob_gas) push(evm.stack, U256(blob_base_fee)) # PROGRAM COUNTER diff --git a/src/ethereum/forks/amsterdam/vm/instructions/log.py b/src/ethereum/forks/amsterdam/vm/instructions/log.py index 695f4de735..d5f016817e 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/log.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/log.py @@ -66,10 +66,10 @@ def log_n(evm: Evm, num_topics: int) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext log_entry = Log( - address=evm.message.current_target, + address=evm.current_target, topics=tuple(topics), data=memory_read_bytes(evm.memory, memory_start_index, size), ) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index b63a8ea2a4..1036741e70 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -49,15 +49,15 @@ def sload(evm: Evm) -> None: key = pop(evm.stack).to_be_bytes32() # GAS - if (evm.message.current_target, key) in evm.accessed_storage_keys: + if (evm.current_target, key) in evm.accessed_storage_keys: charge_gas(evm, GasCosts.WARM_ACCESS) else: - evm.accessed_storage_keys.add((evm.message.current_target, key)) + evm.accessed_storage_keys.add((evm.current_target, key)) charge_gas(evm, GasCosts.COLD_STORAGE_ACCESS) # OPERATION - tx_state = evm.message.tx_env.state - value = get_storage(tx_state, evm.message.current_target, key) + tx_state = evm.tx_env.state + value = get_storage(tx_state, evm.current_target, key) push(evm.stack, value) @@ -75,7 +75,7 @@ def sstore(evm: Evm) -> None: The current EVM frame. """ - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext # STACK @@ -89,7 +89,7 @@ def sstore(evm: Evm) -> None: # Access cost: cold or warm, always charged. is_cold_access = ( - evm.message.current_target, + evm.current_target, key, ) not in evm.accessed_storage_keys if is_cold_access: @@ -108,13 +108,11 @@ def sstore(evm: Evm) -> None: # the slot's original and current values, adjusting the # transaction's refunds. if is_cold_access: - evm.accessed_storage_keys.add((evm.message.current_target, key)) + evm.accessed_storage_keys.add((evm.current_target, key)) - tx_state = evm.message.tx_env.state - original_value = get_storage_original( - tx_state, evm.message.current_target, key - ) - current_value = get_storage(tx_state, evm.message.current_target, key) + tx_state = evm.tx_env.state + original_value = get_storage_original(tx_state, evm.current_target, key) + current_value = get_storage(tx_state, evm.current_target, key) state_gas = StateGas(Uint(0)) @@ -154,7 +152,7 @@ def sstore(evm: Evm) -> None: # reservoir on frame failure. charge_gas(evm, gas_cost) charge_state_gas(evm, state_gas) - set_storage(tx_state, evm.message.current_target, key, new_value) + set_storage(tx_state, evm.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,9 +176,7 @@ def tload(evm: Evm) -> None: charge_gas(evm, GasCosts.OPCODE_TLOAD) # OPERATION - value = get_transient_storage( - evm.message.tx_env.state, evm.message.current_target, key - ) + value = get_transient_storage(evm.tx_env.state, evm.current_target, key) push(evm.stack, value) # PROGRAM COUNTER @@ -197,7 +193,7 @@ def tstore(evm: Evm) -> None: The current EVM frame. """ - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext # STACK @@ -207,8 +203,8 @@ def tstore(evm: Evm) -> None: # GAS charge_gas(evm, GasCosts.OPCODE_TSTORE) set_transient_storage( - evm.message.tx_env.state, - evm.message.current_target, + evm.tx_env.state, + evm.current_target, key, new_value, ) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 7d5a23a017..203dddf066 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -40,13 +40,13 @@ from .. import ( CALL_SUCCESS, Evm, - Message, emit_transfer_log, incorporate_child, ) from ..exceptions import OutOfGasError, Revert, WriteInStaticContext from ..gas import ( GasCosts, + GasMeter, StateGasCosts, calculate_gas_extend_memory, calculate_message_call_gas, @@ -83,7 +83,7 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size @@ -94,13 +94,13 @@ def generic_create( # PREFLIGHT # Abort without spawning the child: nothing has been charged or # withheld for it yet. - sender_address = evm.message.current_target + sender_address = evm.current_target sender = get_account(tx_state, sender_address) if ( sender.balance < endowment or sender.nonce == Uint(2**64 - 1) - or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT + or evm.depth + Uint(1) > STACK_DEPTH_LIMIT ): push(evm.stack, U256(0)) return @@ -138,27 +138,42 @@ def generic_create( # DISPATCH - child_message = Message( - block_env=evm.message.block_env, - tx_env=evm.message.tx_env, - caller=evm.message.current_target, + child_evm = Evm( + block_env=evm.block_env, + tx_env=evm.tx_env, + caller=evm.current_target, target=Bytes0(), - gas=create_message_gas, - state_gas_reservoir=create_message_state_gas_reservoir, - value=endowment, - data=b"", - code=call_data, current_target=contract_address, - depth=evm.message.depth + Uint(1), code_address=None, + value=endowment, + data=b"", + depth=evm.depth + Uint(1), should_transfer_value=True, is_static=False, - accessed_addresses=evm.accessed_addresses.copy(), - accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, + pc=Uint(0), + stack=[], + memory=bytearray(), + code=call_data, + gas_meter=GasMeter( + entry_gas=create_message_gas, + state_gas_grant=create_message_state_gas_reservoir, + gas_left=create_message_gas, + state_gas_left=create_message_state_gas_reservoir, + state_gas_baseline=create_message_state_gas_reservoir, + ), + valid_jump_destinations=set(), + logs=(), + running=True, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), ) - child_evm = process_create_message(child_message) + child_evm = process_create_message(child_evm) # OUTCOME # The child settled its own gas; absorb it and resolve the @@ -172,7 +187,7 @@ def generic_create( push(evm.stack, U256(0)) else: evm.return_data = b"" - push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + push(evm.stack, U256.from_be_bytes(child_evm.current_target)) def create(evm: Evm) -> None: @@ -189,7 +204,7 @@ def create(evm: Evm) -> None: # if it's not moved inside this method from ...vm.interpreter import MAX_INIT_CODE_SIZE - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext # STACK @@ -213,10 +228,8 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( - evm.message.current_target, - get_account( - evm.message.tx_env.state, evm.message.current_target - ).nonce, + evm.current_target, + get_account(evm.tx_env.state, evm.current_target).nonce, ) generic_create( @@ -248,7 +261,7 @@ def create2(evm: Evm) -> None: # if it's not moved inside this method from ...vm.interpreter import MAX_INIT_CODE_SIZE - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext # STACK @@ -277,7 +290,7 @@ def create2(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_create2_contract_address( - evm.message.current_target, + evm.current_target, salt, memory_read_bytes(evm.memory, memory_start_position, memory_size), ) @@ -372,10 +385,7 @@ def generic_call(evm: Evm, params: GenericCall) -> None: # PREFLIGHT # Abort without spawning the child: both grants return untouched # and any account-creation charge refills. - if ( - evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT - or params.insufficient_balance - ): + if evm.depth + Uint(1) > STACK_DEPTH_LIMIT or params.insufficient_balance: restore_child_gas( evm.gas_meter, params.gas, params.state_gas_reservoir ) @@ -391,28 +401,43 @@ def generic_call(evm: Evm, params: GenericCall) -> None: params.memory_input_size, ) - child_message = Message( - block_env=evm.message.block_env, - tx_env=evm.message.tx_env, + child_evm = Evm( + block_env=evm.block_env, + tx_env=evm.tx_env, caller=params.caller, target=params.to, - gas=params.gas, - state_gas_reservoir=params.state_gas_reservoir, - value=params.value, - data=call_data, - code=params.code, current_target=params.to, - depth=evm.message.depth + Uint(1), code_address=params.code_address, + value=params.value, + data=call_data, + depth=evm.depth + Uint(1), should_transfer_value=params.should_transfer_value, - is_static=params.is_staticcall or evm.message.is_static, - accessed_addresses=evm.accessed_addresses.copy(), - accessed_storage_keys=evm.accessed_storage_keys.copy(), + is_static=params.is_staticcall or evm.is_static, disable_precompiles=params.disable_precompiles, parent_evm=evm, + pc=Uint(0), + stack=[], + memory=bytearray(), + code=params.code, + gas_meter=GasMeter( + entry_gas=params.gas, + state_gas_grant=params.state_gas_reservoir, + gas_left=params.gas, + state_gas_left=params.state_gas_reservoir, + state_gas_baseline=params.state_gas_reservoir, + ), + valid_jump_destinations=set(), + logs=(), + running=True, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), ) - child_evm = process_message(child_message) + child_evm = process_message(child_evm) # OUTCOME # The child settled its own gas; absorb it and resolve the @@ -455,7 +480,7 @@ def call(evm: Evm) -> None: memory_output_start_position = pop(evm.stack) memory_output_size = pop(evm.stack) - if evm.message.is_static and value != U256(0): + if evm.is_static and value != U256(0): raise WriteInStaticContext # GAS (STATE-INDEPENDENT) @@ -486,7 +511,7 @@ def call(evm: Evm) -> None: # Perform the accesses and complete the state-dependent pricing -- # a delegation adds its access cost -- then charge the regular # gas. - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state if is_cold_access: evm.accessed_addresses.add(to) @@ -535,7 +560,7 @@ def call(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - sender_balance = get_account(tx_state, evm.message.current_target).balance + sender_balance = get_account(tx_state, evm.current_target).balance generic_call( evm, @@ -543,7 +568,7 @@ def call(evm: Evm) -> None: gas=message_call_gas.sub_call, state_gas_reservoir=call_state_gas_reservoir, value=value, - caller=evm.message.current_target, + caller=evm.current_target, to=to, code_address=code_address, should_transfer_value=True, @@ -585,7 +610,7 @@ def callcode(evm: Evm) -> None: # GAS (STATE-INDEPENDENT) # Price what is computable without touching state, and check it is # affordable before any state access is performed. - to = evm.message.current_target + to = evm.current_target extend_memory = calculate_gas_extend_memory( evm.memory, @@ -612,7 +637,7 @@ def callcode(evm: Evm) -> None: # Perform the accesses and complete the state-dependent pricing -- # a delegation adds its access cost; the regular gas is charged # with the child grant. - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state if is_cold_access: evm.accessed_addresses.add(code_address) @@ -650,7 +675,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - sender_balance = get_account(tx_state, evm.message.current_target).balance + sender_balance = get_account(tx_state, evm.current_target).balance generic_call( evm, @@ -658,7 +683,7 @@ def callcode(evm: Evm) -> None: gas=message_call_gas.sub_call, state_gas_reservoir=call_state_gas_reservoir, value=value, - caller=evm.message.current_target, + caller=evm.current_target, to=to, code_address=code_address, should_transfer_value=True, @@ -687,7 +712,7 @@ def selfdestruct(evm: Evm) -> None: The current EVM frame. """ - if evm.message.is_static: + if evm.is_static: raise WriteInStaticContext # STACK @@ -707,7 +732,7 @@ def selfdestruct(evm: Evm) -> None: # STATE ACCESS (STATE-DEPENDENT GAS) # Perform the access; the pricing completes with the state gas # below. - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state if is_cold_access: evm.accessed_addresses.add(beneficiary) @@ -719,7 +744,7 @@ def selfdestruct(evm: Evm) -> None: account_write_gas = Uint(0) if ( not is_account_alive(tx_state, beneficiary) - and get_account(tx_state, evm.message.current_target).balance != 0 + and get_account(tx_state, evm.current_target).balance != 0 ): state_gas = StateGasCosts.NEW_ACCOUNT account_write_gas = GasCosts.ACCOUNT_WRITE @@ -731,7 +756,7 @@ def selfdestruct(evm: Evm) -> None: charge_state_gas(evm, state_gas) # OPERATION - originator = evm.message.current_target + originator = evm.current_target originator_balance = get_account(tx_state, originator).balance # Transfer balance @@ -810,7 +835,7 @@ def delegatecall(evm: Evm) -> None: if code_address not in evm.accessed_addresses: evm.accessed_addresses.add(code_address) - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state code_hash = get_account(tx_state, code_address).code_hash code = get_code(tx_state, code_hash) @@ -836,9 +861,9 @@ def delegatecall(evm: Evm) -> None: GenericCall( gas=message_call_gas.sub_call, state_gas_reservoir=call_state_gas_reservoir, - value=evm.message.value, - caller=evm.message.caller, - to=evm.message.current_target, + value=evm.value, + caller=evm.caller, + to=evm.current_target, code_address=code_address, should_transfer_value=False, is_staticcall=False, @@ -913,7 +938,7 @@ def staticcall(evm: Evm) -> None: if code_address not in evm.accessed_addresses: evm.accessed_addresses.add(code_address) - tx_state = evm.message.tx_env.state + tx_state = evm.tx_env.state code_hash = get_account(tx_state, code_address).code_hash code = get_code(tx_state, code_hash) @@ -940,7 +965,7 @@ def staticcall(evm: Evm) -> None: gas=message_call_gas.sub_call, state_gas_reservoir=call_state_gas_reservoir, value=U256(0), - caller=evm.message.current_target, + caller=evm.current_target, to=to, code_address=code_address, should_transfer_value=True, diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 9fb5512c3c..b9cf13ab5f 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -11,14 +11,10 @@ A straightforward interpreter that executes EVM code. """ -from dataclasses import dataclass -from typing import Optional, Set, Tuple, final - -from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen -from ethereum.exceptions import EthereumException -from ethereum.state import EMPTY_ACCOUNT, Address +from ethereum.state import EMPTY_ACCOUNT from ethereum.trace import ( EvmStop, OpEnd, @@ -31,7 +27,6 @@ ) from ethereum.utils.numeric import ceil32 -from ..blocks import Log from ..state_tracker import ( account_deployable, copy_tx_state, @@ -46,11 +41,9 @@ restore_tx_state, set_code, ) -from ..vm import Message from ..vm.eoa_delegation import get_delegated_code_address, set_delegation from ..vm.gas import ( GasCosts, - GasMeter, StateGasCosts, charge_gas, charge_state_gas, @@ -58,7 +51,6 @@ forfeit_remaining_gas, restore_state_gas, restore_state_gas_to_entry, - tx_state_gas_used, ) from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import ( @@ -82,116 +74,72 @@ MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE -@final -@dataclass -class MessageCallOutput: - """ - Output of a particular message call. - - Contains the following: - - 1. `gas_left`: remaining gas after execution. - 2. `refund_counter`: gas to refund after execution. - 3. `logs`: list of `Log` generated during execution. - 4. `accounts_to_delete`: Contracts which have self-destructed. - 5. `error`: The error from the execution if any. - 6. `return_data`: The output of the execution. - 7. `state_gas_left`: remaining state gas after execution. - 8. `state_gas_used`: State gas used during execution. +def process_message_call(evm: Evm) -> Evm: """ + Dispatch the transaction's top-level frame and close it out. - gas_left: Uint - refund_counter: U256 - logs: Tuple[Log, ...] - accounts_to_delete: Set[Address] - error: Optional[EthereumException] - return_data: Bytes - state_gas_left: Uint - state_gas_used: int - - -def process_message_call(message: Message) -> MessageCallOutput: - """ - If `message.target` is empty then it creates a smart contract - else it executes a call from the `message.caller` to the `message.target`. + If `evm.target` is empty the dispatch creates a smart contract, + else it executes a call from the `evm.caller` to the `evm.target`. Parameters ---------- - message : - Transaction specific items. + evm : + The transaction's top-level frame. Returns ------- - output : `MessageCallOutput` - Output of the message call + evm : `ethereum.forks.amsterdam.vm.Evm` + The finished frame. On failure its `logs` and + `accounts_to_delete` are cleared, and its meter arrives settled + with a zero refund counter. """ - tx_state = message.tx_env.state - if message.target == Bytes0(b""): - if account_deployable(tx_state, message.current_target): - evm = process_create_message(message) + tx_state = evm.tx_env.state + if evm.target == Bytes0(b""): + if account_deployable(tx_state, evm.current_target): + evm = process_create_message(evm) else: - return MessageCallOutput( - gas_left=Uint(0), - refund_counter=U256(0), - logs=tuple(), - accounts_to_delete=set(), - error=AddressCollision(), - return_data=Bytes(b""), - state_gas_left=message.state_gas_reservoir, - state_gas_used=0, - ) + evm.error = AddressCollision() + forfeit_remaining_gas(evm.gas_meter) + return evm else: # Authorizations and delegation resolution are handled at the # top frame inside ``process_message`` (depth 0), so their # state-dependent gas charges go through the EVM gas pools and # an out-of-gas there halts the frame cleanly. - evm = process_message(message) + evm = process_message(evm) if evm.error: - logs: Tuple[Log, ...] = () - accounts_to_delete = set() - else: - logs = evm.logs - accounts_to_delete = evm.accounts_to_delete + evm.logs = () + evm.accounts_to_delete = set() + gas_meter = evm.gas_meter tx_end = TransactionEnd( - int(message.gas) - int(evm.gas_meter.gas_left), evm.output, evm.error + int(gas_meter.entry_gas) - int(gas_meter.gas_left), + evm.output, + evm.error, ) evm_trace(evm, tx_end) - # A failed frame settles its meter with a zero refund counter, so - # the refunds can be read unconditionally. - return MessageCallOutput( - gas_left=evm.gas_meter.gas_left, - refund_counter=U256(evm.gas_meter.refund_counter), - logs=logs, - accounts_to_delete=accounts_to_delete, - error=evm.error, - return_data=evm.output, - state_gas_left=evm.gas_meter.state_gas_left, - state_gas_used=tx_state_gas_used( - evm.gas_meter, message.state_gas_reservoir - ), - ) + return evm -def process_create_message(message: Message) -> Evm: +def process_create_message(evm: Evm) -> Evm: """ Executes a call to create a smart contract. Parameters ---------- - message : - Transaction specific items. + evm : + The contract-creation frame. Returns ------- evm: :py:class:`~ethereum.forks.amsterdam.vm.Evm` - Items containing execution specific objects. + The finished frame. """ - tx_state = message.tx_env.state + tx_state = evm.tx_env.state # take snapshot of state before processing the message snapshot = copy_tx_state(tx_state) @@ -202,17 +150,17 @@ def process_create_message(message: Message) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(tx_state, message.current_target) + destroy_storage(tx_state, evm.current_target) # In the previously mentioned edge case the preexisting storage is ignored # for gas refund purposes. In order to do this we must track created # accounts. This tracking is also needed to respect the constraints # added to SELFDESTRUCT by EIP-6780. - mark_account_created(tx_state, message.current_target) + mark_account_created(tx_state, evm.current_target) - increment_nonce(tx_state, message.current_target) + increment_nonce(tx_state, evm.current_target) - evm = process_message(message) + evm = process_message(evm) if not evm.error: contract_code = evm.output try: @@ -241,7 +189,7 @@ def process_create_message(message: Message) -> Evm: evm.output = b"" evm.error = error else: - set_code(tx_state, message.current_target, contract_code) + set_code(tx_state, evm.current_target, contract_code) else: restore_tx_state(tx_state, snapshot) return evm @@ -282,20 +230,17 @@ def resolve_dispatch(evm: Evm) -> None: back the whole preparation -- including the applied authorizations -- and halts the frame without dispatching. """ - message = evm.message - tx_state = message.tx_env.state + tx_state = evm.tx_env.state - if message.target == Bytes0(b""): + if evm.target == Bytes0(b""): if ( - get_pre_state_account(tx_state, message.current_target) + get_pre_state_account(tx_state, evm.current_target) == EMPTY_ACCOUNT ): charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) else: - recipient = message.current_target - if message.value > U256(0) and not is_account_alive( - tx_state, recipient - ): + recipient = evm.current_target + if evm.value > U256(0) 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 @@ -308,61 +253,37 @@ def resolve_dispatch(evm: Evm) -> None: charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) evm.accessed_addresses.add(delegated_address) - message.disable_precompiles = True - message.code_address = delegated_address - message.code = get_code( + evm.disable_precompiles = True + evm.code_address = delegated_address + evm.code = get_code( tx_state, get_account(tx_state, delegated_address).code_hash, ) else: - message.code = recipient_code + evm.code = recipient_code -def process_message(message: Message) -> Evm: +def process_message(evm: Evm) -> Evm: """ Move ether and execute the relevant code. Parameters ---------- - message : - Transaction specific items. + evm : + The frame to execute. Returns ------- evm: :py:class:`~ethereum.forks.amsterdam.vm.Evm` - Items containing execution specific objects + The finished frame. """ - tx_state = message.tx_env.state - if message.depth > STACK_DEPTH_LIMIT: - raise StackDepthLimitError("Stack depth limit reached") - - evm = Evm( - pc=Uint(0), - stack=[], - memory=bytearray(), - code=Bytes(b""), - gas_meter=GasMeter( - gas_left=message.gas, - state_gas_left=message.state_gas_reservoir, - state_gas_baseline=message.state_gas_reservoir, - ), - valid_jump_destinations=set(), - logs=(), - running=True, - message=message, - output=b"", - accounts_to_delete=set(), - return_data=b"", - error=None, - accessed_addresses=message.accessed_addresses, - accessed_storage_keys=message.accessed_storage_keys, - ) + tx_state = evm.tx_env.state - if message.depth == Uint(0): + if evm.depth == Uint(0): prep_snapshot = copy_tx_state(tx_state) try: - if message.tx_env.authorizations != (): + if evm.tx_env.authorizations != (): set_delegation(evm) # The applied delegations outlive a failure of the # dispatched code, so their state gas is committed as @@ -376,39 +297,41 @@ def process_message(message: Message) -> Evm: # The rollback reverts any applied delegations, so the # commit above is undone with it: roll state gas back to # frame entry, refilling every state charge. - restore_state_gas_to_entry( - evm.gas_meter, message.state_gas_reservoir - ) + restore_state_gas_to_entry(evm.gas_meter) forfeit_remaining_gas(evm.gas_meter) evm.error = error return evm - assert message.code is not None - evm.code = message.code - evm.valid_jump_destinations = get_valid_jump_destinations(message.code) + evm.valid_jump_destinations = get_valid_jump_destinations(evm.code) snapshot = copy_tx_state(tx_state) # Execute message code and handle errors try: - if message.should_transfer_value and message.value != 0: + # A frame is only spawned when the depth limit allows it, so + # this is a defensive invariant; keeping it inside the halt + # handling means a fire is settled as the exceptional halt it is + # rather than escaping the frame. + if evm.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + if evm.should_transfer_value and evm.value != 0: move_ether( tx_state, - message.caller, - message.current_target, - message.value, + evm.caller, + evm.current_target, + evm.value, ) - if message.caller != message.current_target: + if evm.caller != evm.current_target: emit_transfer_log( evm, - message.caller, - message.current_target, - message.value, + evm.caller, + evm.current_target, + evm.value, ) - if evm.message.code_address in PRE_COMPILED_CONTRACTS: - if not message.disable_precompiles: - evm_trace(evm, PrecompileStart(evm.message.code_address)) - PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + if evm.code_address in PRE_COMPILED_CONTRACTS: + if not evm.disable_precompiles: + evm_trace(evm, PrecompileStart(evm.code_address)) + PRE_COMPILED_CONTRACTS[evm.code_address](evm) evm_trace(evm, PrecompileEnd()) else: while evm.running and evm.pc < ulen(evm.code): diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py index 862506c54c..336033f81d 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py @@ -146,7 +146,7 @@ def alt_bn128_add(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS charge_gas(evm, GasCosts.PRECOMPILE_ECADD) @@ -174,7 +174,7 @@ def alt_bn128_mul(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS charge_gas(evm, GasCosts.PRECOMPILE_ECMUL) @@ -202,7 +202,7 @@ def alt_bn128_pairing_check(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS charge_gas( diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py index ae53b1ab4b..c38f814964 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py @@ -28,7 +28,7 @@ def blake2f(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data if len(data) != 213: raise InvalidParameter diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py index d1f63224a0..1f4c1a6eff 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py @@ -53,7 +53,7 @@ def bls12_g1_add(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) != 256: raise InvalidParameter("Invalid Input Length") @@ -88,7 +88,7 @@ def bls12_g1_msm(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: raise InvalidParameter("Invalid Input Length") @@ -133,7 +133,7 @@ def bls12_map_fp_to_g1(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) != 64: raise InvalidParameter("Invalid Input Length") diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py index 2fd32313f8..fde9ece44a 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py @@ -54,7 +54,7 @@ def bls12_g2_add(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) != 512: raise InvalidParameter("Invalid Input Length") @@ -89,7 +89,7 @@ def bls12_g2_msm(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: raise InvalidParameter("Invalid Input Length") @@ -134,7 +134,7 @@ def bls12_map_fp2_to_g2(evm: Evm) -> None: If the input length is invalid. """ - data = evm.message.data + data = evm.data if len(data) != 128: raise InvalidParameter("Invalid Input Length") diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py index c7a62cb49c..79fa7fe07f 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py @@ -36,7 +36,7 @@ def bls12_pairing(evm: Evm) -> None: If the input length is invalid or if the subgroup check fails. """ - data = evm.message.data + data = evm.data if len(data) == 0 or len(data) % 384 != 0: raise InvalidParameter("Invalid Input Length") diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py index 17a0174f6e..2924689a8c 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py @@ -34,7 +34,7 @@ def ecrecover(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS charge_gas(evm, GasCosts.PRECOMPILE_ECRECOVER) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py index b763173607..6217915f6e 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py @@ -32,7 +32,7 @@ def identity(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS word_count = ceil32(ulen(data)) // Uint(32) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py index bf828ee8f6..036874b0d3 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py @@ -25,7 +25,7 @@ def modexp(evm: Evm) -> None: Calculates `(base**exp) % modulus` for arbitrary sized `base`, `exp` and `modulus`. The return value is the same length as the modulus. """ - data = evm.message.data + data = evm.data # GAS base_length = U256.from_be_bytes(buffer_read(data, U256(0), U256(32))) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py index 29c2e91e0f..0c17adcba5 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py @@ -38,7 +38,7 @@ def p256verify(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS charge_gas(evm, GasCosts.PRECOMPILE_P256VERIFY) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py index d2d105ba13..7a50806eb4 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py @@ -40,7 +40,7 @@ def point_evaluation(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data if len(data) != 192: raise KZGProofError diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py index c82c9bd534..33d8282723 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py @@ -35,7 +35,7 @@ def ripemd160(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS word_count = ceil32(ulen(data)) // Uint(32) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py index 9d467d7e95..a1358888fe 100644 --- a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py @@ -34,7 +34,7 @@ def sha256(evm: Evm) -> None: The current EVM frame. """ - data = evm.message.data + data = evm.data # GAS word_count = ceil32(ulen(data)) // Uint(32) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/count.py b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/count.py index d922d78827..7b6bd7e6bc 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/count.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/count.py @@ -3,6 +3,7 @@ """ from collections import defaultdict +from typing import Any from ethereum.trace import EvmTracer, OpStart, TraceEvent @@ -31,9 +32,13 @@ def __call__(self, evm: object, event: TraceEvent) -> None: assert isinstance(evm, Evm) - if self.transaction_environment is not evm.message.tx_env: + # Recent forks merge the message fields into the frame itself; + # older forks keep them on `evm.message`. + message: Any = getattr(evm, "message", evm) + + if self.transaction_environment is not message.tx_env: self.active_traces = defaultdict(lambda: 0) - self.transaction_environment = evm.message.tx_env + self.transaction_environment = message.tx_env self.active_traces[event.op.name] += 1 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/eip3155.py b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/eip3155.py index b2b26008a8..c1591333fb 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/eip3155.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/eip3155.py @@ -117,28 +117,32 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: """ Create a trace of the event. """ + # Recent forks merge the message fields into the frame itself; + # older forks keep them on `evm.message`. + message = getattr(evm, "message", evm) + # System Transaction do not have a tx_hash or index if ( - evm.message.tx_env.index_in_block is None - or evm.message.tx_env.tx_hash is None + message.tx_env.index_in_block is None + or message.tx_env.tx_hash is None ): return assert isinstance(evm, Evm) - if self.transaction_environment is not evm.message.tx_env: + if self.transaction_environment is not message.tx_env: self.active_traces = [] - self.transaction_environment = evm.message.tx_env + self.transaction_environment = message.tx_env last_trace = None if self.active_traces: last_trace = self.active_traces[-1] refund_counter = evm_refund_counter(evm) - parent_evm = evm.message.parent_evm + parent_evm = message.parent_evm while parent_evm is not None: refund_counter += evm_refund_counter(parent_evm) - parent_evm = parent_evm.message.parent_evm + parent_evm = getattr(parent_evm, "message", parent_evm).parent_evm len_memory = len(evm.memory) @@ -162,8 +166,8 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: output_traces( self.active_traces, - evm.message.tx_env.index_in_block, - evm.message.tx_env.tx_hash, + message.tx_env.index_in_block, + message.tx_env.tx_hash, self.output_basedir, ) elif isinstance(event, PrecompileStart): @@ -176,7 +180,7 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: memSize=len_memory, stack=stack, returnData=return_data, - depth=int(evm.message.depth) + 1, + depth=int(message.depth) + 1, refund=refund_counter, opName="0x" + event.address.hex().lstrip("0"), precompile=True, @@ -208,7 +212,7 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: memSize=len_memory, stack=stack, returnData=return_data, - depth=int(evm.message.depth) + 1, + depth=int(message.depth) + 1, refund=refund_counter, opName=str(event.op).split(".")[-1], stateGas=state_gas, @@ -235,7 +239,7 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: # The first opcode in a child message is an InvalidOpcode. # This case has to be explicitly handled since the first # two conditions do not cover it. - or last_trace.depth == evm.message.depth + or last_trace.depth == message.depth ): if not hasattr(event.error, "code"): name = event.error.__class__.__name__ @@ -253,7 +257,7 @@ def __call__(self, evm: Any, event: TraceEvent) -> None: memSize=len_memory, stack=stack, returnData=return_data, - depth=int(evm.message.depth) + 1, + depth=int(message.depth) + 1, refund=refund_counter, opName="InvalidOpcode", gasCostTraced=True, diff --git a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/protocols.py b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/protocols.py index 1b0a2271ef..8230fec24e 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/protocols.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace/protocols.py @@ -33,6 +33,14 @@ class Message(Protocol): class Evm(Protocol): """ The class describes the EVM interface common to every fork's trace. + + The message-scoped fields (`depth`, `tx_env`, `parent_evm`) are + described by [`Message`][msg]. Older forks carry them on + `evm.message`; forks that merge the message into the frame expose + them on `evm` itself, so `evm` satisfies both protocols. Tracers + resolve the carrier with `getattr(evm, "message", evm)`. + + [msg]: ref:ethereum_spec_tools.evm_tools.t8n.evm_trace.protocols.Message """ pc: Uint @@ -40,7 +48,6 @@ class Evm(Protocol): memory: bytearray code: Bytes running: bool - message: Message @runtime_checkable From 5cb47679836a595255c7c0efaedf2dea566ac845 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 13:40:10 +0200 Subject: [PATCH 4/8] refactor(spec-specs): extract charge_sender, resolve blob fee at inclusion Drop the dead TransactionEnvironment.recipient/.value fields (no readers), and hoist the transaction's resolved blob fee onto the environment as blob_gas_fee, computed once in check_transaction at the current blob gas price. Extract the upfront sender debit into a charge_sender helper that reads that fee, and derive the top frame's create address from tx.nonce directly rather than reading the already-incremented sender nonce back. Byte-identical on Amsterdam fixtures. --- src/ethereum/forks/amsterdam/fork.py | 71 +++++++++++++-------- src/ethereum/forks/amsterdam/vm/__init__.py | 3 +- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index c4de8bd218..6a04dc3064 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -106,7 +106,6 @@ StateGasCosts, allocate_execution_gas, calculate_blob_gas_price, - calculate_data_fee, calculate_excess_blob_gas, calculate_total_blob_gas, settle_transaction_gas, @@ -526,11 +525,11 @@ def check_transaction( [`InvalidBlock`] naming the failure. On success, the returned environment carries everything execution - needs: the effective gas price, the execution gas split into the - regular gas pool and the state gas reservoir (the portion of the - transaction's gas above the per-transaction regular gas cap), the - addresses and storage keys to prewarm, and the transaction's - authorizations. + needs: the effective gas price, the blob fee resolved at the current + blob gas price, the execution gas split into the regular gas pool and + the state gas reservoir (the portion of the transaction's gas above + the per-transaction regular gas cap), the addresses and storage keys + to prewarm, and the transaction's authorizations. Parameters ---------- @@ -659,8 +658,10 @@ def check_transaction( max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( tx.max_fee_per_blob_gas ) + blob_gas_fee = Uint(calculate_total_blob_gas(tx)) * blob_gas_price blob_versioned_hashes = tx.blob_versioned_hashes else: + blob_gas_fee = Uint(0) blob_versioned_hashes = () if isinstance(tx, (BlobTransaction, SetCodeTransaction)): @@ -703,9 +704,8 @@ def check_transaction( return vm.TransactionEnvironment( origin=sender, - recipient=tx.to, - value=tx.value, gas_price=effective_gas_price, + blob_gas_fee=blob_gas_fee, gas=allocation.regular_gas, state_gas_reservoir=allocation.state_gas_reservoir, access_list_addresses=access_list_addresses, @@ -850,9 +850,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, + blob_gas_fee=Uint(0), gas=SYSTEM_TRANSACTION_GAS, state_gas_reservoir=system_state_gas_reservoir, access_list_addresses=set(), @@ -1070,7 +1069,7 @@ def create_top_level_evm( if isinstance(tx.to, Bytes0): current_target = compute_contract_address( tx_env.origin, - get_account(tx_env.state, tx_env.origin).nonce - Uint(1), + Uint(tx.nonce), ) msg_data = Bytes(b"") code = tx.data @@ -1124,6 +1123,40 @@ def create_top_level_evm( ) +def charge_sender( + tx_env: vm.TransactionEnvironment, + tx: Transaction, +) -> None: + """ + Debit the sender for the transaction's maximum possible gas fee. + + Increment the sender's nonce and deduct the largest fee the + transaction could incur -- its gas limit priced at the effective gas + price, plus the blob fee resolved at inclusion -- up front. + Execution later refunds whatever regular gas was not spent. + + Parameters + ---------- + tx_env : + The transaction's execution environment. + tx : + The transaction being charged. + + """ + tx_state = tx_env.state + sender = tx_env.origin + sender_account = get_account(tx_state, sender) + + effective_gas_fee = tx.gas * tx_env.gas_price + + increment_nonce(tx_state, sender) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - tx_env.blob_gas_fee + ) + set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) + + def process_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, @@ -1190,21 +1223,7 @@ def process_transaction( encode_transaction(tx), ) - sender_account = get_account(tx_state, sender) - - if isinstance(tx, BlobTransaction): - blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) - else: - blob_gas_fee = Uint(0) - - effective_gas_fee = tx.gas * tx_env.gas_price - - increment_nonce(tx_state, sender) - - sender_balance_after_gas_fee = ( - Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee - ) - set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) + charge_sender(tx_env, tx) tx_frame = create_top_level_evm(block_env, tx_env, tx) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 87dc187632..0717d7a407 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -142,9 +142,8 @@ class TransactionEnvironment: """ origin: Address - recipient: Bytes0 | Address - value: U256 gas_price: Uint + blob_gas_fee: Uint gas: Uint state_gas_reservoir: Uint access_list_addresses: Set[Address] From 23099ce1531936b82bb0b60f096aae0b5fd79a8d Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 13:53:47 +0200 Subject: [PATCH 5/8] refactor(spec-specs): hoist top-frame preparation out of process_message Move the depth-0 preparation block -- EIP-7702 delegation application and dispatch resolution -- out of process_message and up into process_message_call, where it runs once before the create-vs-call dispatch. process_message now executes only the code of whichever frame it is given, top-level or child. Byte-identical on the full Amsterdam fixture set. --- .../forks/amsterdam/vm/interpreter.py | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index b9cf13ab5f..6091c8ac74 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -76,10 +76,14 @@ def process_message_call(evm: Evm) -> Evm: """ - Dispatch the transaction's top-level frame and close it out. + Prepare the transaction's top-level frame, dispatch it, and close + it out. - If `evm.target` is empty the dispatch creates a smart contract, - else it executes a call from the `evm.caller` to the `evm.target`. + Preparation applies any EIP-7702 authorizations and resolves the + code the frame runs; if it exhausts the frame's gas the frame is + settled without dispatching. Otherwise, if `evm.target` is empty the + dispatch creates a smart contract, else it executes a call from the + `evm.caller` to the `evm.target`. Parameters ---------- @@ -95,19 +99,40 @@ def process_message_call(evm: Evm) -> Evm: """ tx_state = evm.tx_env.state - if evm.target == Bytes0(b""): - if account_deployable(tx_state, evm.current_target): - evm = process_create_message(evm) - else: - evm.error = AddressCollision() - forfeit_remaining_gas(evm.gas_meter) - return evm + + # Preparation runs once per top-level frame, before dispatch: apply + # any EIP-7702 authorizations and resolve the code the frame runs. + # Its state-dependent gas charges go through the frame's gas pools, + # so an out-of-gas here settles the frame cleanly without dispatch. + prep_snapshot = copy_tx_state(tx_state) + try: + if evm.tx_env.authorizations != (): + set_delegation(evm) + # The applied delegations outlive a failure of the + # dispatched code, so their state gas is committed as + # non-refillable; a later failure restores only to the + # post-commit baseline. + commit_state_gas(evm.gas_meter) + resolve_dispatch(evm) + except ExceptionalHalt as error: + evm_trace(evm, OpException(error)) + restore_tx_state(tx_state, prep_snapshot) + # The rollback reverts any applied delegations, so the commit + # above is undone with it: roll state gas back to frame entry, + # refilling every state charge. + restore_state_gas_to_entry(evm.gas_meter) + forfeit_remaining_gas(evm.gas_meter) + evm.error = error else: - # Authorizations and delegation resolution are handled at the - # top frame inside ``process_message`` (depth 0), so their - # state-dependent gas charges go through the EVM gas pools and - # an out-of-gas there halts the frame cleanly. - evm = process_message(evm) + if evm.target == Bytes0(b""): + if account_deployable(tx_state, evm.current_target): + evm = process_create_message(evm) + else: + evm.error = AddressCollision() + forfeit_remaining_gas(evm.gas_meter) + return evm + else: + evm = process_message(evm) if evm.error: evm.logs = () @@ -280,28 +305,6 @@ def process_message(evm: Evm) -> Evm: """ tx_state = evm.tx_env.state - if evm.depth == Uint(0): - prep_snapshot = copy_tx_state(tx_state) - try: - if evm.tx_env.authorizations != (): - set_delegation(evm) - # The applied delegations outlive a failure of the - # dispatched code, so their state gas is committed as - # non-refillable; a later failure restores only to the - # post-commit baseline. - commit_state_gas(evm.gas_meter) - resolve_dispatch(evm) - except ExceptionalHalt as error: - evm_trace(evm, OpException(error)) - restore_tx_state(tx_state, prep_snapshot) - # The rollback reverts any applied delegations, so the - # commit above is undone with it: roll state gas back to - # frame entry, refilling every state charge. - restore_state_gas_to_entry(evm.gas_meter) - forfeit_remaining_gas(evm.gas_meter) - evm.error = error - return evm - evm.valid_jump_destinations = get_valid_jump_destinations(evm.code) snapshot = copy_tx_state(tx_state) From 1adc8baf9f8bcb2524cac5a821f784fe300f3765 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 14:14:19 +0200 Subject: [PATCH 6/8] refactor(spec-specs): split top-level dispatch into ready + run Split process_message_call into ready_top_level_evm (apply authorizations and resolve dispatch) and run_top_level_evm (dispatch to create or call, then close out). Both mutate the frame in place and are called as a pair by process_transaction and process_unchecked_system_transaction. Everything transaction-scoped -- sender charging, settlement, receipt assembly -- stays outside the pair, so a future frame loop can call it once per top-level frame. Byte-identical on Amsterdam fixtures. --- src/ethereum/forks/amsterdam/fork.py | 8 ++- .../forks/amsterdam/vm/interpreter.py | 56 ++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 6a04dc3064..da22a8b025 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -111,7 +111,7 @@ settle_transaction_gas, tx_state_gas_used, ) -from .vm.interpreter import process_message_call +from .vm.interpreter import ready_top_level_evm, run_top_level_evm from .vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) @@ -899,7 +899,8 @@ def process_unchecked_system_transaction( accessed_storage_keys=set(), ) - system_tx_frame = process_message_call(system_tx_frame) + ready_top_level_evm(system_tx_frame) + run_top_level_evm(system_tx_frame) incorporate_tx_into_block( system_tx_state, block_env.block_access_list_builder @@ -1227,7 +1228,8 @@ def process_transaction( tx_frame = create_top_level_evm(block_env, tx_env, tx) - tx_frame = process_message_call(tx_frame) + ready_top_level_evm(tx_frame) + run_top_level_evm(tx_frame) gas_meter = tx_frame.gas_meter settlement = settle_transaction_gas( diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 6091c8ac74..5cd4064c08 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -74,36 +74,24 @@ MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE -def process_message_call(evm: Evm) -> Evm: +def ready_top_level_evm(evm: Evm) -> None: """ - Prepare the transaction's top-level frame, dispatch it, and close - it out. + Ready the transaction's top-level frame for dispatch. - Preparation applies any EIP-7702 authorizations and resolves the - code the frame runs; if it exhausts the frame's gas the frame is - settled without dispatching. Otherwise, if `evm.target` is empty the - dispatch creates a smart contract, else it executes a call from the - `evm.caller` to the `evm.target`. + Apply any EIP-7702 authorizations and resolve the code the frame + runs. These state-dependent gas charges go through the frame's gas + pools; if they exhaust it, roll the preparation back, settle the + meter, and record the error on the frame so ``run_top_level_evm`` + skips dispatch. Parameters ---------- evm : The transaction's top-level frame. - Returns - ------- - evm : `ethereum.forks.amsterdam.vm.Evm` - The finished frame. On failure its `logs` and - `accounts_to_delete` are cleared, and its meter arrives settled - with a zero refund counter. - """ tx_state = evm.tx_env.state - # Preparation runs once per top-level frame, before dispatch: apply - # any EIP-7702 authorizations and resolve the code the frame runs. - # Its state-dependent gas charges go through the frame's gas pools, - # so an out-of-gas here settles the frame cleanly without dispatch. prep_snapshot = copy_tx_state(tx_state) try: if evm.tx_env.authorizations != (): @@ -123,16 +111,36 @@ def process_message_call(evm: Evm) -> Evm: restore_state_gas_to_entry(evm.gas_meter) forfeit_remaining_gas(evm.gas_meter) evm.error = error - else: + + +def run_top_level_evm(evm: Evm) -> None: + """ + Dispatch a readied top-level frame and close it out. + + If readying already failed there is nothing to dispatch. Otherwise, + if `evm.target` is empty the dispatch creates a smart contract, else + it executes a call from the `evm.caller` to the `evm.target`. On + failure the frame's `logs` and `accounts_to_delete` are cleared and + its meter arrives settled with a zero refund counter. + + Parameters + ---------- + evm : + The readied top-level frame, mutated in place. + + """ + tx_state = evm.tx_env.state + + if not evm.error: if evm.target == Bytes0(b""): if account_deployable(tx_state, evm.current_target): - evm = process_create_message(evm) + process_create_message(evm) else: evm.error = AddressCollision() forfeit_remaining_gas(evm.gas_meter) - return evm + return else: - evm = process_message(evm) + process_message(evm) if evm.error: evm.logs = () @@ -146,8 +154,6 @@ def process_message_call(evm: Evm) -> Evm: ) evm_trace(evm, tx_end) - return evm - def process_create_message(evm: Evm) -> Evm: """ From 0409887c26ba4fe08556f475b69473bcbf24d94b Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 14:31:47 +0200 Subject: [PATCH 7/8] refactor(spec-specs): settle transaction gas from the finished meter Pass the top frame's finished GasMeter to settle_transaction_gas and derive execution gas used, refund counter, and net state gas from it inside the function, rather than threading each figure through the call. The pre-refund usage is now the intrinsic regular cost plus the execution gas the frame consumed across both dimensions. Byte-identical on the full Amsterdam fixture set. --- src/ethereum/forks/amsterdam/fork.py | 11 +-------- src/ethereum/forks/amsterdam/vm/gas.py | 33 +++++++++++++------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index da22a8b025..e467681753 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -109,7 +109,6 @@ calculate_excess_blob_gas, calculate_total_blob_gas, settle_transaction_gas, - tx_state_gas_used, ) from .vm.interpreter import ready_top_level_evm, run_top_level_evm from .vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS @@ -1231,15 +1230,7 @@ def process_transaction( ready_top_level_evm(tx_frame) run_top_level_evm(tx_frame) - gas_meter = tx_frame.gas_meter - settlement = settle_transaction_gas( - tx.gas, - intrinsic, - gas_meter.gas_left, - gas_meter.state_gas_left, - U256(gas_meter.refund_counter), - tx_state_gas_used(gas_meter), - ) + settlement = settle_transaction_gas(tx.gas, intrinsic, tx_frame.gas_meter) gas_refund_amount = settlement.gas_left * tx_env.gas_price diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 4169f1eb9e..0cf23af9d7 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -1003,18 +1003,16 @@ class TransactionGasSettlement: def settle_transaction_gas( tx_gas: Uint, intrinsic: IntrinsicGasCost, - gas_left: Uint, - state_gas_left: Uint, - refund_counter: U256, - state_gas_used: int, + gas_meter: GasMeter, ) -> TransactionGasSettlement: """ Settle a transaction's gas after execution. Compute, in order: - - the gas used before refunds, from the gas limit less the regular - gas and reservoir the top frame returned; + - the gas used before refunds, as the intrinsic regular cost plus + the execution gas the top frame consumed across both dimensions + (its grants less what it returned); - the refund, capped at one fifth of that pre-refund usage; - the gas used, taken as the larger of the post-refund usage and the calldata floor, so a transaction never pays below the floor; and @@ -1030,14 +1028,8 @@ def settle_transaction_gas( The transaction's gas limit. intrinsic : The transaction's intrinsic gas cost. - gas_left : - Regular gas the top frame returned. - state_gas_left : - State gas reservoir the top frame returned. - refund_counter : - The refund the top frame accrued. - state_gas_used : - Net state gas the top frame consumed, possibly negative. + gas_meter : + The top frame's finished gas meter. Returns ------- @@ -1047,12 +1039,19 @@ def settle_transaction_gas( [EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778 """ - gas_used_before_refund = tx_gas - gas_left - state_gas_left - gas_refund = min(gas_used_before_refund // Uint(5), Uint(refund_counter)) + execution_gas_used = (gas_meter.entry_gas + gas_meter.state_gas_grant) - ( + gas_meter.gas_left + gas_meter.state_gas_left + ) + + gas_used_before_refund = Uint(intrinsic.regular) + execution_gas_used + gas_refund = min( + gas_used_before_refund // Uint(5), + Uint(gas_meter.refund_counter), + ) gas_used_after_refund = gas_used_before_refund - gas_refund gas_used = max(gas_used_after_refund, intrinsic.calldata_floor) - settled_state_gas_used = Uint(max(0, state_gas_used)) + settled_state_gas_used = Uint(max(0, tx_state_gas_used(gas_meter))) regular_gas_used = max( gas_used_before_refund - settled_state_gas_used, intrinsic.calldata_floor, From 98fe743a08b17fd060f2311f2f11de8aa1f77da2 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 17 Jul 2026 14:39:09 +0200 Subject: [PATCH 8/8] refactor(spec-specs): extract payer-parameterized disburse_gas_fees Move the two post-settlement fee transfers -- the unspent-gas refund and the coinbase priority fee -- into disburse_gas_fees, parameterized on the payer that receives the refund (the sender on the standard path). Pairs with charge_sender: one fronts the maximum fee at inclusion, the other returns the remainder and pays the miner. Byte-identical on Amsterdam fixtures. --- src/ethereum/forks/amsterdam/fork.py | 51 ++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index e467681753..eedfca9bee 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -104,6 +104,7 @@ GasCosts, GasMeter, StateGasCosts, + TransactionGasSettlement, allocate_execution_gas, calculate_blob_gas_price, calculate_excess_blob_gas, @@ -1157,6 +1158,44 @@ def charge_sender( set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) +def disburse_gas_fees( + block_env: vm.BlockEnvironment, + tx_env: vm.TransactionEnvironment, + settlement: TransactionGasSettlement, + payer: Address, +) -> None: + """ + Refund the payer's unspent gas and pay the priority fee. + + Return the gas the transaction did not use to the ``payer`` that + fronted the maximum fee at inclusion, priced at the effective gas + price, and credit the coinbase with the priority fee on the gas that + was used. + + Parameters + ---------- + block_env : + The block scoped environment. + tx_env : + The transaction's execution environment. + settlement : + The settled gas amounts. + payer : + The account that fronted the maximum gas fee and receives the + refund. + + """ + tx_state = tx_env.state + gas_refund_amount = settlement.gas_left * tx_env.gas_price + + # For non-1559 transactions tx_env.gas_price == tx.gas_price + priority_fee_per_gas = tx_env.gas_price - block_env.base_fee_per_gas + transaction_fee = settlement.gas_used * priority_fee_per_gas + + create_ether(tx_state, payer, U256(gas_refund_amount)) + create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) + + def process_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, @@ -1232,17 +1271,7 @@ def process_transaction( settlement = settle_transaction_gas(tx.gas, intrinsic, tx_frame.gas_meter) - gas_refund_amount = settlement.gas_left * tx_env.gas_price - - # For non-1559 transactions tx_env.gas_price == tx.gas_price - priority_fee_per_gas = tx_env.gas_price - block_env.base_fee_per_gas - transaction_fee = settlement.gas_used * priority_fee_per_gas - - # refund gas - create_ether(tx_state, sender, U256(gas_refund_amount)) - - # transfer miner fees - create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) + disburse_gas_fees(block_env, tx_env, settlement, sender) block_output.block_gas_used += settlement.regular_gas_used block_output.block_state_gas_used += settlement.state_gas_used