diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 2e5aca21f9..6268146e24 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -30,7 +30,6 @@ from . import vm from .block_access_lists.builder import build_block_access_list -from .block_access_lists.rlp_types import BlockAccessIndex from .block_access_lists.rlp_utils import compute_block_access_list_hash from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom @@ -68,13 +67,12 @@ capture_pre_balance, commit_transaction_frame, create_child_frame, - get_block_access_index, - handle_in_transaction_selfdestruct, + filter_net_zero_frame_changes, increment_block_access_index, - merge_on_success, - normalize_balance_changes_for_transaction, track_address, track_balance_change, + track_nonce_change, + track_selfdestruct, ) from .transactions import ( AccessListTransaction, @@ -634,7 +632,7 @@ def process_system_transaction( """ # EIP-7928: Create a child frame for system transaction # This allows proper pre-state capture for net-zero filtering - system_tx_state_changes = create_child_frame(block_env.block_state_changes) + system_tx_state_changes = create_child_frame(block_env.state_changes) tx_env = vm.TransactionEnvironment( origin=SYSTEM_ADDRESS, @@ -647,8 +645,12 @@ def process_system_transaction( authorizations=(), index_in_block=None, tx_hash=None, + state_changes=system_tx_state_changes, ) + # Create call frame as child of tx frame + call_frame = create_child_frame(tx_env.state_changes) + system_tx_message = Message( block_env=block_env, tx_env=tx_env, @@ -667,14 +669,15 @@ def process_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - transaction_state_changes=system_tx_state_changes, + is_create=False, + state_changes=call_frame, ) system_tx_output = process_message_call(system_tx_message) - # Merge system transaction changes back to block frame + # Commit system transaction changes to block frame # System transactions always succeed (or block is invalid) - merge_on_success(system_tx_state_changes) + commit_transaction_frame(tx_env.state_changes) return system_tx_output @@ -814,7 +817,7 @@ def apply_body( # EIP-7928: Increment block frame to post-execution index # After N transactions, block frame is at index N # Post-execution operations (withdrawals, etc.) use index N+1 - increment_block_access_index(block_env.block_state_changes) + increment_block_access_index(block_env.state_changes) process_withdrawals(block_env, block_output, withdrawals) @@ -822,9 +825,9 @@ def apply_body( block_env=block_env, block_output=block_output, ) - # Build block access list from block_env.block_state_changes + # Build block access list from block_env.state_changes block_output.block_access_list = build_block_access_list( - block_env.block_state_changes + block_env.state_changes ) return block_output @@ -907,9 +910,10 @@ def process_transaction( """ # EIP-7928: Create a transaction-level StateChanges frame # The frame will read the current block_access_index from the block frame - increment_block_access_index(block_env.block_state_changes) - tx_state_changes = create_child_frame(block_env.block_state_changes) + increment_block_access_index(block_env.state_changes) + tx_state_changes = create_child_frame(block_env.state_changes) + # Capture coinbase pre-balance for net-zero filtering coinbase_pre_balance = get_account( block_env.state, block_env.coinbase ).balance @@ -947,16 +951,27 @@ def process_transaction( effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(block_env.state, sender, tx_state_changes) + + # Track sender nonce increment + increment_nonce(block_env.state, sender) + sender_nonce_after = get_account(block_env.state, sender).nonce + track_nonce_change(tx_state_changes, sender, U64(sender_nonce_after)) + + # Track sender balance deduction for gas fee + sender_balance_before = get_account(block_env.state, sender).balance + track_address(tx_state_changes, sender) + capture_pre_balance(tx_state_changes, sender, sender_balance_before) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) set_account_balance( - block_env.state, + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) + track_balance_change( + tx_state_changes, sender, U256(sender_balance_after_gas_fee), - tx_state_changes, ) access_list_addresses = set() @@ -991,13 +1006,13 @@ def process_transaction( authorizations=authorizations, index_in_block=index, tx_hash=get_transaction_hash(encode_transaction(tx)), + state_changes=tx_state_changes, ) message = prepare_message( block_env, tx_env, tx, - tx_state_changes, ) tx_output = process_message_call(message) @@ -1027,11 +1042,11 @@ def process_transaction( sender_balance_after_refund = get_account( block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance( - block_env.state, + set_account_balance(block_env.state, sender, sender_balance_after_refund) + track_balance_change( + tx_env.state_changes, sender, sender_balance_after_refund, - tx_state_changes, ) coinbase_balance_after_mining_fee = get_account( @@ -1039,10 +1054,12 @@ def process_transaction( ).balance + U256(transaction_fee) set_account_balance( - block_env.state, + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee + ) + track_balance_change( + tx_env.state_changes, block_env.coinbase, coinbase_balance_after_mining_fee, - tx_state_changes, ) if coinbase_balance_after_mining_fee == 0 and account_exists_and_is_empty( @@ -1068,35 +1085,19 @@ def process_transaction( block_output.block_logs += tx_output.logs - # EIP-7928: Handle in-transaction self-destruct BEFORE normalization - # Destroy accounts first so normalization sees correct post-tx state - # Only accounts created in same tx are in accounts_to_delete per EIP-6780 for address in tx_output.accounts_to_delete: destroy_account(block_env.state, address) - # EIP-7928: Normalize balance changes for this transaction before merging - # into block frame. Must happen AFTER destroy_account so net-zero filtering - # sees the correct post-transaction balance (0 for destroyed accounts). - normalize_balance_changes_for_transaction( - tx_state_changes, - BlockAccessIndex( - get_block_access_index(block_env.block_state_changes) - ), - block_env.state, - ) + # EIP-7928: Filter net-zero changes before committing to block frame. + # Must happen AFTER destroy_account so filtering sees correct state. + filter_net_zero_frame_changes(tx_env.state_changes, block_env.state) - commit_transaction_frame(tx_state_changes) + commit_transaction_frame(tx_env.state_changes) - # EIP-7928: Handle in-transaction self-destruct normalization AFTER merge + # EIP-7928: Track in-transaction self-destruct normalization AFTER merge # Convert storage writes to reads and remove nonce/code changes for address in tx_output.accounts_to_delete: - handle_in_transaction_selfdestruct( - block_env.block_state_changes, - address, - BlockAccessIndex( - get_block_access_index(block_env.block_state_changes) - ), - ) + track_selfdestruct(block_env.state_changes, address) def process_withdrawals( @@ -1107,13 +1108,12 @@ def process_withdrawals( """ Increase the balance of the withdrawing account. """ + # Capture pre-state for withdrawal balance filtering withdrawal_addresses = {wd.address for wd in withdrawals} for address in withdrawal_addresses: pre_balance = get_account(block_env.state, address).balance - track_address(block_env.block_state_changes, address) - capture_pre_balance( - block_env.block_state_changes, address, pre_balance - ) + track_address(block_env.state_changes, address) + capture_pre_balance(block_env.state_changes, address, pre_balance) def increase_recipient_balance(recipient: Account) -> None: recipient.balance += wd.amount * U256(10**9) @@ -1129,22 +1129,16 @@ def increase_recipient_balance(recipient: Account) -> None: new_balance = get_account(block_env.state, wd.address).balance track_balance_change( - block_env.block_state_changes, wd.address, new_balance + block_env.state_changes, + wd.address, + new_balance, ) if account_exists_and_is_empty(block_env.state, wd.address): destroy_account(block_env.state, wd.address) - # EIP-7928: Normalize balance changes after all withdrawals - # Filters out net-zero changes - - normalize_balance_changes_for_transaction( - block_env.block_state_changes, - BlockAccessIndex( - get_block_access_index(block_env.block_state_changes) - ), - block_env.state, - ) + # EIP-7928: Filter net-zero balance changes for withdrawals + filter_net_zero_frame_changes(block_env.state_changes, block_env.state) def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index c1d331942a..fcf12e971b 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -21,17 +21,9 @@ from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.frozen import modify -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U256, Uint from .fork_types import EMPTY_ACCOUNT, Account, Address, Root -from .state_tracker import ( - StateChanges, - capture_pre_balance, - track_address, - track_balance_change, - track_code_change, - track_nonce_change, -) from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set if TYPE_CHECKING: @@ -517,18 +509,22 @@ def move_ether( sender_address: Address, recipient_address: Address, amount: U256, - state_changes: StateChanges, ) -> None: """ Move funds between accounts. - """ - sender_balance = get_account(state, sender_address).balance - recipient_balance = get_account(state, recipient_address).balance - track_address(state_changes, sender_address) - capture_pre_balance(state_changes, sender_address, sender_balance) - track_address(state_changes, recipient_address) - capture_pre_balance(state_changes, recipient_address, recipient_balance) + Parameters + ---------- + state: + The current state. + sender_address: + Address of the sender. + recipient_address: + Address of the recipient. + amount: + The amount to transfer. + + """ def reduce_sender_balance(sender: Account) -> None: if sender.balance < amount: @@ -541,23 +537,8 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, sender_address, reduce_sender_balance) modify_state(state, recipient_address, increase_recipient_balance) - sender_new_balance = get_account(state, sender_address).balance - recipient_new_balance = get_account(state, recipient_address).balance - - track_balance_change( - state_changes, sender_address, U256(sender_new_balance) - ) - track_balance_change( - state_changes, recipient_address, U256(recipient_new_balance) - ) - -def set_account_balance( - state: State, - address: Address, - amount: U256, - state_changes: StateChanges, -) -> None: +def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. @@ -567,32 +548,20 @@ def set_account_balance( The current state. address: - Address of the account whose nonce needs to be incremented. + Address of the account whose balance needs to be set. amount: The amount that needs to set in balance. - state_changes: - State changes frame for tracking (EIP-7928). - """ - current_balance = get_account(state, address).balance - - track_address(state_changes, address) - capture_pre_balance(state_changes, address, current_balance) def set_balance(account: Account) -> None: account.balance = amount modify_state(state, address, set_balance) - track_balance_change(state_changes, address, amount) -def increment_nonce( - state: State, - address: Address, - state_changes: "StateChanges", -) -> None: +def increment_nonce(state: State, address: Address) -> None: """ Increments the nonce of an account. @@ -604,9 +573,6 @@ def increment_nonce( address: Address of the account whose nonce needs to be incremented. - state_changes: - State changes frame for tracking (EIP-7928). - """ def increase_nonce(sender: Account) -> None: @@ -614,16 +580,8 @@ def increase_nonce(sender: Account) -> None: modify_state(state, address, increase_nonce) - account = get_account(state, address) - track_nonce_change(state_changes, address, U64(account.nonce)) - -def set_code( - state: State, - address: Address, - code: Bytes, - state_changes: StateChanges, -) -> None: +def set_code(state: State, address: Address, code: Bytes) -> None: """ Sets Account code. @@ -638,9 +596,6 @@ def set_code( code: The bytecode that needs to be set. - state_changes: - State changes frame for tracking (EIP-7928). - """ def write_code(sender: Account) -> None: @@ -648,27 +603,13 @@ def write_code(sender: Account) -> None: modify_state(state, address, write_code) - # Only track code change if it's not net-zero within this frame - # Compare against pre-code captured in this frame, default to b"" - pre_code = state_changes.pre_code.get(address, b"") - if pre_code != code: - track_code_change(state_changes, address, code) - -def set_authority_code( - state: State, - address: Address, - code: Bytes, - state_changes: StateChanges, - current_code: Bytes, -) -> None: +def set_authority_code(state: State, address: Address, code: Bytes) -> None: """ Sets authority account code for EIP-7702 delegation. This function is used specifically for setting authority code within - EIP-7702 Set Code Transactions. Unlike set_code(), it tracks changes based - on the current code rather than pre_code to handle multiple authorizations - to the same address within a single transaction correctly. + EIP-7702 Set Code Transactions. Parameters ---------- @@ -681,13 +622,6 @@ def set_authority_code( code: The delegation designation bytecode to set. - state_changes: - State changes frame for tracking (EIP-7928). - - current_code: - The current code before this change. Used to determine if tracking - is needed (only track if code actually changes from current value). - """ def write_code(sender: Account) -> None: @@ -695,12 +629,6 @@ def write_code(sender: Account) -> None: modify_state(state, address, write_code) - # Only track if code is actually changing from current value - # This allows multiple auths to same address to be tracked individually - # Net-zero filtering happens in commit_transaction_frame - if current_code != code: - track_code_change(state_changes, address, code) - def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: """ diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index 7b98396318..19a929d0dd 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -1,21 +1,12 @@ """ -Hierarchical state change tracking for EIP-7928 Block Access Lists. +EIP-7928 Block Access Lists: Hierarchical State Change Tracking. -Implements a frame-based hierarchy: Block → Transaction → Call frames. -Each frame tracks state changes and merges upward on completion: -- Success: merge all changes (reads + writes) -- Failure: merge only reads (writes discarded) +Frame hierarchy mirrors EVM execution: Block -> Transaction -> Call frames. +Each frame tracks state accesses and merges to parent on completion. -Frame Hierarchy: - Block Frame: Root, lifetime = entire block, index 0..N+1 - Transaction Frame: Child of block, lifetime = single transaction - Call Frame: Child of transaction/call, lifetime = single message - -Block Access Index: 0=pre-exec, 1..N=transactions, N+1=post-exec -Stored in root frame, passed explicitly to operations. - -Pre-State Tracking: Values captured before modifications to enable -net-zero filtering. +On success, changes merge upward with net-zero filtering (pre-state vs final). +On failure, only reads merge (writes discarded). Pre-state captures use +first-write-wins semantics and are stored at the transaction frame level. [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 """ @@ -38,12 +29,13 @@ class StateChanges: """ Tracks state changes within a single execution frame. - Frames form a hierarchy: Block → Transaction → Call frames. - Each frame holds a reference to its parent for upward traversal. + Frames form a hierarchy (Block -> Transaction -> Call) linked by parent + references. The block_access_index is stored at the root frame. Pre-state + captures (pre_balances, etc.) are only populated at the transaction level. """ parent: Optional["StateChanges"] = None - _block_access_index: BlockAccessIndex = BlockAccessIndex(0) + block_access_index: BlockAccessIndex = BlockAccessIndex(0) touched_addresses: Set[Address] = field(default_factory=set) storage_reads: Set[Tuple[Address, Bytes32]] = field(default_factory=set) @@ -61,7 +53,7 @@ class StateChanges: default_factory=dict ) - # Pre-state captures for net-zero filtering + # Pre-state captures (transaction-scoped, only populated at tx frame) pre_balances: Dict[Address, U256] = field(default_factory=dict) pre_nonces: Dict[Address, U64] = field(default_factory=dict) pre_storage: Dict[Tuple[Address, Bytes32], U256] = field( @@ -72,17 +64,17 @@ class StateChanges: def get_block_frame(state_changes: StateChanges) -> StateChanges: """ - Walk to block-level frame. + Walk to the root (block-level) frame. Parameters ---------- state_changes : - Any state changes frame. + Any frame in the hierarchy. Returns ------- block_frame : StateChanges - The block-level frame. + The root block-level frame. """ block_frame = state_changes @@ -91,125 +83,128 @@ def get_block_frame(state_changes: StateChanges) -> StateChanges: return block_frame -def get_block_access_index(root_frame: StateChanges) -> BlockAccessIndex: +def increment_block_access_index(root_frame: StateChanges) -> None: """ - Get current block access index from root frame. + Increment the block access index in the root frame. Parameters ---------- root_frame : - The root (block-level) state changes frame. - - Returns - ------- - index : BlockAccessIndex - The current block access index. + The root block-level frame. """ - return root_frame._block_access_index + root_frame.block_access_index = BlockAccessIndex( + root_frame.block_access_index + Uint(1) + ) -def increment_block_access_index(root_frame: StateChanges) -> None: +def get_transaction_frame(state_changes: StateChanges) -> StateChanges: """ - Increment block access index in root frame. + Walk to the transaction-level frame (child of block frame). Parameters ---------- - root_frame : - The root (block-level) state changes frame to increment. + state_changes : + Any frame in the hierarchy. + + Returns + ------- + tx_frame : StateChanges + The transaction-level frame. """ - root_frame._block_access_index = BlockAccessIndex( - root_frame._block_access_index + Uint(1) - ) + tx_frame = state_changes + while tx_frame.parent is not None and tx_frame.parent.parent is not None: + tx_frame = tx_frame.parent + return tx_frame def capture_pre_balance( - state_changes: StateChanges, address: Address, balance: U256 + tx_frame: StateChanges, address: Address, balance: U256 ) -> None: """ - Capture pre-balance (first-write-wins for net-zero filtering). + Capture pre-balance if not already captured (first-write-wins). Parameters ---------- - state_changes : - The state changes frame. + tx_frame : + The transaction-level frame. address : - The address whose balance is being captured. + The address whose balance to capture. balance : - The balance value before modification. + The current balance value. """ - if address not in state_changes.pre_balances: - state_changes.pre_balances[address] = balance + if address not in tx_frame.pre_balances: + tx_frame.pre_balances[address] = balance def capture_pre_nonce( - state_changes: StateChanges, address: Address, nonce: U64 + tx_frame: StateChanges, address: Address, nonce: U64 ) -> None: """ - Capture pre-nonce (first-write-wins). + Capture pre-nonce if not already captured (first-write-wins). Parameters ---------- - state_changes : - The state changes frame. + tx_frame : + The transaction-level frame. address : - The address whose nonce is being captured. + The address whose nonce to capture. nonce : - The nonce value before modification. + The current nonce value. """ - if address not in state_changes.pre_nonces: - state_changes.pre_nonces[address] = nonce + if address not in tx_frame.pre_nonces: + tx_frame.pre_nonces[address] = nonce def capture_pre_storage( - state_changes: StateChanges, address: Address, key: Bytes32, value: U256 + tx_frame: StateChanges, address: Address, key: Bytes32, value: U256 ) -> None: """ - Capture pre-storage (first-write-wins for noop filtering). + Capture pre-storage value if not already captured (first-write-wins). Parameters ---------- - state_changes : - The state changes frame. + tx_frame : + The transaction-level frame. address : - The address whose storage is being captured. + The address whose storage to capture. key : The storage key. value : - The storage value before modification. + The current storage value. """ slot = (address, key) - if slot not in state_changes.pre_storage: - state_changes.pre_storage[slot] = value + if slot not in tx_frame.pre_storage: + tx_frame.pre_storage[slot] = value def capture_pre_code( - state_changes: StateChanges, address: Address, code: Bytes + tx_frame: StateChanges, address: Address, code: Bytes ) -> None: """ - Capture pre-code (first-write-wins). + Capture pre-code if not already captured (first-write-wins). Parameters ---------- - state_changes : - The state changes frame. + tx_frame : + The transaction-level frame. address : - The address whose code is being captured. + The address whose code to capture. code : - The code value before modification. + The current code value. """ - if address not in state_changes.pre_code: - state_changes.pre_code[address] = code + if address not in tx_frame.pre_code: + tx_frame.pre_code[address] = code def track_address(state_changes: StateChanges, address: Address) -> None: """ - Track that an address was accessed. + Record that an address was accessed. Parameters ---------- @@ -226,7 +221,7 @@ def track_storage_read( state_changes: StateChanges, address: Address, key: Bytes32 ) -> None: """ - Track a storage read operation. + Record a storage read operation. Parameters ---------- @@ -248,7 +243,7 @@ def track_storage_write( value: U256, ) -> None: """ - Track a storage write operation with block access index. + Record a storage write keyed by (address, key, block_access_index). Parameters ---------- @@ -262,10 +257,8 @@ def track_storage_write( The new storage value. """ - block_frame = get_block_frame(state_changes) - state_changes.storage_writes[ - (address, key, get_block_access_index(block_frame)) - ] = value + idx = state_changes.block_access_index + state_changes.storage_writes[(address, key, idx)] = value def track_balance_change( @@ -274,7 +267,7 @@ def track_balance_change( new_balance: U256, ) -> None: """ - Track balance change keyed by (address, index). + Record a balance change keyed by (address, block_access_index). Parameters ---------- @@ -286,10 +279,8 @@ def track_balance_change( The new balance value. """ - block_frame = get_block_frame(state_changes) - state_changes.balance_changes[ - (address, get_block_access_index(block_frame)) - ] = new_balance + idx = state_changes.block_access_index + state_changes.balance_changes[(address, idx)] = new_balance def track_nonce_change( @@ -298,7 +289,7 @@ def track_nonce_change( new_nonce: U64, ) -> None: """ - Track a nonce change. + Record a nonce change as (address, block_access_index, new_nonce). Parameters ---------- @@ -310,10 +301,8 @@ def track_nonce_change( The new nonce value. """ - block_frame = get_block_frame(state_changes) - state_changes.nonce_changes.add( - (address, get_block_access_index(block_frame), new_nonce) - ) + idx = state_changes.block_access_index + state_changes.nonce_changes.add((address, idx, new_nonce)) def track_code_change( @@ -322,7 +311,7 @@ def track_code_change( new_code: Bytes, ) -> None: """ - Track a code change. + Record a code change keyed by (address, block_access_index). Parameters ---------- @@ -334,19 +323,55 @@ def track_code_change( The new code value. """ - block_frame = get_block_frame(state_changes) - state_changes.code_changes[ - (address, get_block_access_index(block_frame)) - ] = new_code + idx = state_changes.block_access_index + state_changes.code_changes[(address, idx)] = new_code + + +def track_selfdestruct( + state_changes: StateChanges, + address: Address, +) -> None: + """ + Handle selfdestruct of account created in same transaction. + + Per EIP-7928/EIP-6780: removes nonce/code changes, converts storage + writes to reads. Balance changes handled by net-zero filtering. + + Parameters + ---------- + state_changes : + The state changes tracker. + address : + The address that self-destructed. + + """ + idx = state_changes.block_access_index + + # Remove nonce changes from current transaction + state_changes.nonce_changes = { + (addr, i, nonce) + for addr, i, nonce in state_changes.nonce_changes + if not (addr == address and i == idx) + } + + # Remove code changes from current transaction + if (address, idx) in state_changes.code_changes: + del state_changes.code_changes[(address, idx)] + + # Convert storage writes from current transaction to reads + for addr, key, i in list(state_changes.storage_writes.keys()): + if addr == address and i == idx: + del state_changes.storage_writes[(addr, key, i)] + state_changes.storage_reads.add((addr, key)) def merge_on_success(child_frame: StateChanges) -> None: """ - Merge child frame's changes into parent on successful completion. + Merge child frame into parent on success. - Merges all tracked changes (reads and writes) from the child frame - into the parent frame. Filters out net-zero changes based on - captured pre-state values by comparing initial vs final values. + Child values overwrite parent values (most recent wins). No net-zero + filtering here - that happens once at transaction commit via + normalize_transaction(). Parameters ---------- @@ -356,50 +381,20 @@ def merge_on_success(child_frame: StateChanges) -> None: """ assert child_frame.parent is not None parent_frame = child_frame.parent + # Merge address accesses parent_frame.touched_addresses.update(child_frame.touched_addresses) - # Merge pre-state captures for transaction-level normalization - # Only if parent doesn't have value (first capture wins) - for addr, balance in child_frame.pre_balances.items(): - if addr not in parent_frame.pre_balances: - parent_frame.pre_balances[addr] = balance - for addr, nonce in child_frame.pre_nonces.items(): - if addr not in parent_frame.pre_nonces: - parent_frame.pre_nonces[addr] = nonce - for slot, value in child_frame.pre_storage.items(): - if slot not in parent_frame.pre_storage: - parent_frame.pre_storage[slot] = value - for addr, code in child_frame.pre_code.items(): - if addr not in parent_frame.pre_code: - capture_pre_code(parent_frame, addr, code) - - # Merge storage operations, filtering noop writes + # Merge storage: reads union, writes overwrite (child supersedes parent) parent_frame.storage_reads.update(child_frame.storage_reads) - for (addr, key, idx), value in child_frame.storage_writes.items(): - # Only merge if value actually changed from pre-state - if (addr, key) in child_frame.pre_storage: - if child_frame.pre_storage[(addr, key)] != value: - parent_frame.storage_writes[(addr, key, idx)] = value - # If equal, it's a noop write - convert to read only - else: - parent_frame.storage_reads.add((addr, key)) - else: - # No pre-state captured, merge as-is - parent_frame.storage_writes[(addr, key, idx)] = value - - # Merge balance changes - filter net-zero changes - # balance_changes keyed by (address, index) - for (addr, idx), final_balance in child_frame.balance_changes.items(): - if addr in child_frame.pre_balances: - if child_frame.pre_balances[addr] != final_balance: - parent_frame.balance_changes[(addr, idx)] = final_balance - # else: Net-zero change - skip entirely - else: - # No pre-balance captured, merge as-is - parent_frame.balance_changes[(addr, idx)] = final_balance - - # Merge nonce changes - keep only highest nonce per address + for storage_key, storage_value in child_frame.storage_writes.items(): + parent_frame.storage_writes[storage_key] = storage_value + + # Merge balance changes: child overwrites parent for same key + for balance_key, balance_value in child_frame.balance_changes.items(): + parent_frame.balance_changes[balance_key] = balance_value + + # Merge nonce changes: keep highest nonce per address address_final_nonces: Dict[Address, Tuple[BlockAccessIndex, U64]] = {} for addr, idx, nonce in child_frame.nonce_changes: if ( @@ -407,31 +402,46 @@ def merge_on_success(child_frame: StateChanges) -> None: or nonce > address_final_nonces[addr][1] ): address_final_nonces[addr] = (idx, nonce) - - # Merge final nonces (no net-zero filtering - nonces never decrease) for addr, (idx, final_nonce) in address_final_nonces.items(): parent_frame.nonce_changes.add((addr, idx, final_nonce)) - # Merge code changes - filter net-zero changes - # code_changes keyed by (address, index) - for (addr, idx), final_code in child_frame.code_changes.items(): - pre_code = child_frame.pre_code.get(addr, b"") - if pre_code != final_code: - parent_frame.code_changes[(addr, idx)] = final_code - # else: Net-zero change - skip entirely + # Merge code changes: child overwrites parent for same key + for code_key, code_value in child_frame.code_changes.items(): + parent_frame.code_changes[code_key] = code_value -def commit_transaction_frame(tx_frame: StateChanges) -> None: +def merge_on_failure(child_frame: StateChanges) -> None: """ - Commit a transaction frame's changes to the block frame. + Merge child frame into parent on failure/revert. + + Only reads merge; writes are discarded (converted to reads). + + Parameters + ---------- + child_frame : + The failed child frame. + + """ + assert child_frame.parent is not None + parent_frame = child_frame.parent + # Only merge reads and address accesses on failure + parent_frame.touched_addresses.update(child_frame.touched_addresses) + parent_frame.storage_reads.update(child_frame.storage_reads) + + # Convert writes to reads (failed writes still accessed the slots) + for address, key, _idx in child_frame.storage_writes.keys(): + parent_frame.storage_reads.add((address, key)) + + # Note: balance_changes, nonce_changes, and code_changes are NOT + # merged on failure - they are discarded + - Merges ALL changes from the transaction frame into the block frame - without net-zero filtering. Each transaction's changes are recorded - at their respective transaction index, even if a later transaction - reverts a change back to its original value. +def commit_transaction_frame(tx_frame: StateChanges) -> None: + """ + Commit transaction frame to block frame. - This is different from merge_on_success() which filters net-zero - changes within a single transaction's execution. + Unlike merge_on_success(), this merges ALL changes without net-zero + filtering (each tx's changes recorded at their respective index). Parameters ---------- @@ -458,46 +468,17 @@ def commit_transaction_frame(tx_frame: StateChanges) -> None: for addr, idx, nonce in tx_frame.nonce_changes: block_frame.nonce_changes.add((addr, idx, nonce)) - # Merge code changes - filter net-zero changes within the transaction - # Compare final code against transaction's pre-code + # Merge code changes (net-zero filtering done in normalize_transaction) for (addr, idx), final_code in tx_frame.code_changes.items(): - pre_code = tx_frame.pre_code.get(addr, b"") - if pre_code != final_code: - block_frame.code_changes[(addr, idx)] = final_code - # else: Net-zero change within this transaction - skip - - -def merge_on_failure(child_frame: StateChanges) -> None: - """ - Merge child frame's changes into parent on failed completion. - - Merges only read operations from the child frame into the parent. - Write operations are discarded since the frame reverted. - This is called when a call frame fails/reverts. - - Parameters - ---------- - child_frame : - The failed child frame. - - """ - assert child_frame.parent is not None - parent_frame = child_frame.parent - # Only merge reads and address accesses on failure - parent_frame.touched_addresses.update(child_frame.touched_addresses) - parent_frame.storage_reads.update(child_frame.storage_reads) - - # Convert writes to reads (failed writes still accessed the slots) - for address, key, _idx in child_frame.storage_writes.keys(): - parent_frame.storage_reads.add((address, key)) - - # Note: balance_changes, nonce_changes, and code_changes are NOT - # merged on failure - they are discarded + block_frame.code_changes[(addr, idx)] = final_code def create_child_frame(parent: StateChanges) -> StateChanges: """ - Create a child frame for nested execution. + Create a child frame linked to the given parent. + + Inherits block_access_index from parent so track functions can + access it directly without walking up the frame hierarchy. Parameters ---------- @@ -507,99 +488,69 @@ def create_child_frame(parent: StateChanges) -> StateChanges: Returns ------- child : StateChanges - A new child frame with parent reference set. + A new child frame with parent reference and inherited + block_access_index. """ - return StateChanges(parent=parent) - - -def handle_in_transaction_selfdestruct( - state_changes: StateChanges, - address: Address, - current_block_access_index: BlockAccessIndex, -) -> None: - """ - Handle account self-destructed in same transaction as creation. - - Per EIP-7928 and EIP-6780, accounts destroyed within their creation - transaction must have: - - Nonce changes from current transaction removed - - Code changes from current transaction removed - - Storage writes from current transaction converted to reads - - Balance changes handled by net-zero filtering - - Parameters - ---------- - state_changes : StateChanges - The state changes tracker (typically the block-level frame). - address : Address - The address that self-destructed. - current_block_access_index : BlockAccessIndex - The current block access index (transaction index). - - """ - # Remove nonce changes from current transaction - state_changes.nonce_changes = { - (addr, idx, nonce) - for addr, idx, nonce in state_changes.nonce_changes - if not (addr == address and idx == current_block_access_index) - } - - # Remove code changes from current transaction - if (address, current_block_access_index) in state_changes.code_changes: - del state_changes.code_changes[(address, current_block_access_index)] - - # Convert storage writes from current transaction to reads - for addr, key, idx in list(state_changes.storage_writes.keys()): - if addr == address and idx == current_block_access_index: - del state_changes.storage_writes[(addr, key, idx)] - state_changes.storage_reads.add((addr, key)) + return StateChanges( + parent=parent, + block_access_index=parent.block_access_index, + ) -def normalize_balance_changes_for_transaction( - block_frame: StateChanges, - current_block_access_index: BlockAccessIndex, +def filter_net_zero_frame_changes( + tx_frame: StateChanges, state: "State", ) -> None: """ - Normalize balance changes for the current transaction. + Filter net-zero changes from transaction frame before commit. - Removes balance changes where post-transaction balance equals - pre-transaction balance. This handles net-zero transfers across - the entire transaction. - - This function should be called after merging transaction frames - into the block frame to filter out addresses where balance didn't - actually change from transaction start to transaction end. + Compares final values against pre-tx state for storage, balance, and code. + Net-zero storage writes are converted to reads. Net-zero balance/code + changes are removed entirely. Nonces are not filtered (only increment). Parameters ---------- - block_frame : StateChanges - The block-level state changes frame. - current_block_access_index : BlockAccessIndex - The current transaction's block access index. - state : State - The current state to read final balances from. + tx_frame : + The transaction-level state changes frame. + state : + The current state to read final values from. """ # Import locally to avoid circular import from .state import get_account - # Collect addresses that have balance changes in this transaction + idx = tx_frame.block_access_index + + # Filter storage: compare against pre_storage, convert net-zero to reads + for addr, key, i in list(tx_frame.storage_writes.keys()): + if i != idx: + continue + final_value = tx_frame.storage_writes[(addr, key, i)] + if (addr, key) in tx_frame.pre_storage: + if tx_frame.pre_storage[(addr, key)] == final_value: + # Net-zero write - convert to read + del tx_frame.storage_writes[(addr, key, i)] + tx_frame.storage_reads.add((addr, key)) + + # Filter balance: compare pre vs post, remove if equal addresses_to_check = [ - addr - for (addr, idx) in block_frame.balance_changes.keys() - if idx == current_block_access_index + addr for (addr, i) in tx_frame.balance_changes.keys() if i == idx ] - - # For each address, compare pre vs post balance for addr in addresses_to_check: - if addr in block_frame.pre_balances: - pre_balance = block_frame.pre_balances[addr] + if addr in tx_frame.pre_balances: + pre_balance = tx_frame.pre_balances[addr] post_balance = get_account(state, addr).balance - if pre_balance == post_balance: - # Remove balance change for this address - net-zero transfer - del block_frame.balance_changes[ - (addr, current_block_access_index) - ] + del tx_frame.balance_changes[(addr, idx)] + + # Filter code: compare pre vs post, remove if equal + for addr, i in list(tx_frame.code_changes.keys()): + if i != idx: + continue + final_code = tx_frame.code_changes[(addr, i)] + pre_code = tx_frame.pre_code.get(addr, b"") + if pre_code == final_code: + del tx_frame.code_changes[(addr, i)] + + # Nonces: no filtering needed (nonces only increment, never net-zero) diff --git a/src/ethereum/forks/amsterdam/utils/message.py b/src/ethereum/forks/amsterdam/utils/message.py index def5b36e20..130532fef6 100644 --- a/src/ethereum/forks/amsterdam/utils/message.py +++ b/src/ethereum/forks/amsterdam/utils/message.py @@ -17,7 +17,7 @@ from ..fork_types import Address from ..state import get_account -from ..state_tracker import StateChanges +from ..state_tracker import create_child_frame from ..transactions import Transaction from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS @@ -28,7 +28,6 @@ def prepare_message( block_env: BlockEnvironment, tx_env: TransactionEnvironment, tx: Transaction, - transaction_state_changes: StateChanges, ) -> Message: """ Execute a transaction against the provided environment. @@ -41,8 +40,6 @@ def prepare_message( Environment for the transaction. tx : Transaction to be executed. - transaction_state_changes : - State changes specific to this transaction. Returns ------- @@ -73,6 +70,9 @@ def prepare_message( accessed_addresses.add(current_target) + # Create call frame as child of transaction frame + call_frame = create_child_frame(tx_env.state_changes) + return Message( block_env=block_env, tx_env=tx_env, @@ -91,5 +91,6 @@ def prepare_message( accessed_storage_keys=set(tx_env.access_list_storage_keys), disable_precompiles=False, parent_evm=None, - transaction_state_changes=transaction_state_changes, + is_create=isinstance(tx.to, Bytes0), + state_changes=call_frame, ) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index d414aa50f9..016d8007c6 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -49,9 +49,7 @@ class BlockEnvironment: prev_randao: Bytes32 excess_blob_gas: U64 parent_beacon_block_root: Hash32 - block_state_changes: StateChanges = field( - default_factory=lambda: StateChanges() - ) + state_changes: StateChanges = field(default_factory=lambda: StateChanges()) @dataclass @@ -117,6 +115,7 @@ class TransactionEnvironment: authorizations: Tuple[Authorization, ...] index_in_block: Optional[Uint] tx_hash: Optional[Hash32] + state_changes: "StateChanges" = field(default_factory=StateChanges) @dataclass @@ -142,7 +141,8 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - transaction_state_changes: StateChanges + is_create: bool + state_changes: "StateChanges" = field(default_factory=StateChanges) @dataclass diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index ec95fd1a47..1f1aac9d97 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -12,7 +12,6 @@ from ethereum.crypto.hash import keccak256 from ethereum.exceptions import InvalidBlock, InvalidSignatureError -# track_address_access removed - now using state_changes.track_address() from ..fork_types import Address, Authorization from ..state import ( account_exists, @@ -20,7 +19,12 @@ increment_nonce, set_authority_code, ) -from ..state_tracker import capture_pre_code, track_address +from ..state_tracker import ( + capture_pre_code, + track_address, + track_code_change, + track_nonce_change, +) from ..utils.hexadecimal import hex_to_address from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS from . import Evm, Message @@ -190,11 +194,9 @@ def read_delegation_target(evm: Evm, delegated_address: Address) -> Bytes: """ state = evm.message.block_env.state - # Add to accessed addresses for warm/cold gas accounting if delegated_address not in evm.accessed_addresses: evm.accessed_addresses.add(delegated_address) - # Track the address for BAL track_address(evm.state_changes, delegated_address) return get_account(state, delegated_address).code @@ -236,7 +238,7 @@ def set_delegation(message: Message) -> U256: authority_account = get_account(state, authority) authority_code = authority_account.code - track_address(message.block_env.block_state_changes, authority) + track_address(message.tx_env.state_changes, authority) if authority_code and not is_valid_delegation(authority_code): continue @@ -253,22 +255,19 @@ def set_delegation(message: Message) -> U256: else: code_to_set = EOA_DELEGATION_MARKER + auth.address - state_changes = ( - message.transaction_state_changes - or message.block_env.block_state_changes - ) + tx_frame = message.tx_env.state_changes + # EIP-7928: Capture pre-code before any changes + capture_pre_code(tx_frame, authority, authority_code) - # Capture pre-code before any changes (first-write-wins) - capture_pre_code(state_changes, authority, authority_code) + set_authority_code(state, authority, code_to_set) - # Set delegation code - # Uses authority_code (current) for tracking to handle multiple auths - # Net-zero filtering happens in commit_transaction_frame - set_authority_code( - state, authority, code_to_set, state_changes, authority_code - ) + if authority_code != code_to_set: + # Track code change if different from current + track_code_change(tx_frame, authority, code_to_set) - increment_nonce(state, authority, state_changes) + increment_nonce(state, authority) + nonce_after = get_account(state, authority).nonce + track_nonce_change(tx_frame, authority, U64(nonce_after)) if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 8edff23534..de7ef935f5 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -26,7 +26,7 @@ track_storage_write, ) from .. import Evm -from ..exceptions import OutOfGasError, WriteInStaticContext +from ..exceptions import WriteInStaticContext from ..gas import ( GAS_CALL_STIPEND, GAS_COLD_SLOAD, @@ -55,25 +55,26 @@ def sload(evm: Evm) -> None: key = pop(evm.stack).to_be_bytes32() # GAS - gas_cost = ( - GAS_WARM_ACCESS - if (evm.message.current_target, key) in evm.accessed_storage_keys - else GAS_COLD_SLOAD - ) - check_gas(evm, gas_cost) - if (evm.message.current_target, key) not in evm.accessed_storage_keys: - evm.accessed_storage_keys.add((evm.message.current_target, key)) - track_storage_read( - evm.state_changes, + is_cold_access = ( evm.message.current_target, key, - ) + ) not in evm.accessed_storage_keys + gas_cost = GAS_COLD_SLOAD if is_cold_access else GAS_WARM_ACCESS + charge_gas(evm, gas_cost) # OPERATION state = evm.message.block_env.state value = get_storage(state, evm.message.current_target, key) + if is_cold_access: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + track_storage_read( + evm.state_changes, + evm.message.current_target, + key, + ) + push(evm.stack, value) # PROGRAM COUNTER @@ -93,19 +94,14 @@ def sstore(evm: Evm) -> None: # STACK key = pop(evm.stack).to_be_bytes32() new_value = pop(evm.stack) - if evm.gas_left <= GAS_CALL_STIPEND: - raise OutOfGasError - # Check static context before accessing storage + # check we have at least the stipend gas + check_gas(evm, GAS_CALL_STIPEND + Uint(1)) + + # check static context before accessing storage if evm.message.is_static: raise WriteInStaticContext - state = evm.message.block_env.state - original_value = get_storage_original( - state, evm.message.current_target, key - ) - current_value = get_storage(state, evm.message.current_target, key) - # GAS gas_cost = Uint(0) is_cold_access = ( @@ -116,28 +112,34 @@ def sstore(evm: Evm) -> None: if is_cold_access: gas_cost += GAS_COLD_SLOAD - if original_value == current_value and current_value != new_value: - if original_value == 0: - gas_cost += GAS_STORAGE_SET - else: - gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - else: - gas_cost += GAS_WARM_ACCESS + state = evm.message.block_env.state + original_value = get_storage_original( + state, evm.message.current_target, key + ) + current_value = get_storage(state, evm.message.current_target, key) + + if is_cold_access: + evm.accessed_storage_keys.add((evm.message.current_target, key)) - # Track storage access BEFORE checking gas (EIP-7928) - # Even if we run out of gas, the access attempt should be tracked capture_pre_storage( - evm.state_changes, evm.message.current_target, key, current_value + evm.message.tx_env.state_changes, + evm.message.current_target, + key, + current_value, ) track_storage_read( evm.state_changes, evm.message.current_target, key, ) - check_gas(evm, gas_cost) - if is_cold_access: - evm.accessed_storage_keys.add((evm.message.current_target, key)) + if original_value == current_value and current_value != new_value: + if original_value == 0: + gas_cost += GAS_STORAGE_SET + else: + gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD + else: + gas_cost += GAS_WARM_ACCESS charge_gas(evm, gas_cost) @@ -165,7 +167,10 @@ def sstore(evm: Evm) -> None: # OPERATION set_storage(state, evm.message.current_target, key, new_value) track_storage_write( - evm.state_changes, evm.message.current_target, key, new_value + evm.state_changes, + evm.message.current_target, + key, + new_value, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 1fca8b1459..89e9572e59 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -12,7 +12,7 @@ """ from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.utils.numeric import ceil32 @@ -27,7 +27,13 @@ move_ether, set_account_balance, ) -from ...state_tracker import capture_pre_balance, track_address +from ...state_tracker import ( + capture_pre_balance, + create_child_frame, + track_address, + track_balance_change, + track_nonce_change, +) from ...utils.address import ( compute_contract_address, compute_create2_contract_address, @@ -120,11 +126,27 @@ def generic_create( if account_has_code_or_nonce( state, contract_address ) or account_has_storage(state, contract_address): - increment_nonce(state, evm.message.current_target, evm.state_changes) + increment_nonce(state, evm.message.current_target) + nonce_after = get_account(state, evm.message.current_target).nonce + track_nonce_change( + evm.state_changes, + evm.message.current_target, + U64(nonce_after), + ) push(evm.stack, U256(0)) return - increment_nonce(state, evm.message.current_target, evm.state_changes) + # Track nonce increment for CREATE + increment_nonce(state, evm.message.current_target) + nonce_after = get_account(state, evm.message.current_target).nonce + track_nonce_change( + evm.state_changes, + evm.message.current_target, + U64(nonce_after), + ) + + # Create call frame as child of parent EVM's frame + child_state_changes = create_child_frame(evm.state_changes) child_message = Message( block_env=evm.message.block_env, @@ -144,7 +166,8 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - transaction_state_changes=evm.message.transaction_state_changes, + is_create=True, + state_changes=child_state_changes, ) child_evm = process_create_message(child_message) @@ -321,8 +344,9 @@ def generic_call( evm.memory, memory_input_start_position, memory_input_size ) - # EIP-7928: Child message inherits transaction_state_changes from parent - # The actual child frame will be created automatically in process_message + # Create call frame as child of parent EVM's frame + child_state_changes = create_child_frame(evm.state_changes) + child_message = Message( block_env=evm.message.block_env, tx_env=evm.message.tx_env, @@ -341,7 +365,8 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - transaction_state_changes=evm.message.transaction_state_changes, + is_create=False, + state_changes=child_state_changes, ) child_evm = process_message(child_message) @@ -570,11 +595,13 @@ def callcode(evm: Evm) -> None: ).balance # EIP-7928: For CALLCODE with value transfer, capture pre-balance - # in parent frame. CALLCODE transfers value from/to current_target + # in transaction frame. CALLCODE transfers value from/to current_target # (same address), affecting current storage context, not child frame if value != 0 and sender_balance >= value: capture_pre_balance( - evm.state_changes, evm.message.current_target, sender_balance + evm.message.tx_env.state_changes, + evm.message.current_target, + sender_balance, ) if sender_balance < value: @@ -643,25 +670,41 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) + state = evm.message.block_env.state originator = evm.message.current_target - originator_balance = get_account( - evm.message.block_env.state, originator - ).balance + originator_balance = get_account(state, originator).balance + beneficiary_balance = get_account(state, beneficiary).balance + + # Get tracking context + tx_frame = evm.message.tx_env.state_changes + + # Capture pre-balances for net-zero filtering + track_address(evm.state_changes, originator) + capture_pre_balance(tx_frame, originator, originator_balance) + capture_pre_balance(tx_frame, beneficiary, beneficiary_balance) - move_ether( - evm.message.block_env.state, + # Transfer balance + move_ether(state, originator, beneficiary, originator_balance) + + # Track balance changes + originator_new_balance = get_account(state, originator).balance + beneficiary_new_balance = get_account(state, beneficiary).balance + track_balance_change( + evm.state_changes, originator, - beneficiary, - originator_balance, + originator_new_balance, + ) + track_balance_change( evm.state_changes, + beneficiary, + beneficiary_new_balance, ) # register account for deletion only if it was created # in the same transaction - if originator in evm.message.block_env.state.created_accounts: - set_account_balance( - evm.message.block_env.state, originator, U256(0), evm.state_changes - ) + if originator in state.created_accounts: + set_account_balance(state, originator, U256(0)) + track_balance_change(evm.state_changes, originator, U256(0)) evm.accounts_to_delete.add(originator) # HALT the execution diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 57f890a12e..154c56de11 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -15,7 +15,7 @@ from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint, ulen +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.exceptions import EthereumException from ethereum.trace import ( @@ -46,10 +46,13 @@ ) from ..state_tracker import ( StateChanges, - create_child_frame, + capture_pre_balance, merge_on_failure, merge_on_success, track_address, + track_balance_change, + track_code_change, + track_nonce_change, ) from ..vm import Message from ..vm.eoa_delegation import get_delegated_code_address, set_delegation @@ -73,61 +76,6 @@ MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE -def get_parent_frame(message: Message) -> StateChanges: - """ - Get the appropriate parent frame for a message's state changes. - - Frame selection logic: - - Nested calls: Parent EVM's frame - - Top-level calls: Transaction frame - - System transactions: Block frame - - Parameters - ---------- - message : - The message being processed. - - Returns - ------- - parent_frame : StateChanges - The parent frame to use for creating child frames. - - """ - if message.parent_evm is not None: - return message.parent_evm.state_changes - elif message.transaction_state_changes is not None: - return message.transaction_state_changes - else: - return message.block_env.block_state_changes - - -def get_message_state_frame(message: Message) -> StateChanges: - """ - Determine and create the appropriate state tracking frame for a message. - - Creates a call frame as a child of the appropriate parent frame. - - Parameters - ---------- - message : - The message being processed. - - Returns - ------- - state_frame : StateChanges - The state tracking frame to use for this message execution. - - """ - parent_frame = get_parent_frame(message) - if ( - message.parent_evm is not None - or message.transaction_state_changes is not None - ): - return create_child_frame(parent_frame) - else: - return parent_frame - - @dataclass class MessageCallOutput: """ @@ -173,9 +121,7 @@ def process_message_call(message: Message) -> MessageCallOutput: is_collision = account_has_code_or_nonce( block_env.state, message.current_target ) or account_has_storage(block_env.state, message.current_target) - track_address( - message.transaction_state_changes, message.current_target - ) + track_address(message.tx_env.state_changes, message.current_target) if is_collision: return MessageCallOutput( Uint(0), @@ -197,11 +143,7 @@ def process_message_call(message: Message) -> MessageCallOutput: message.accessed_addresses.add(delegated_address) message.code = get_account(block_env.state, delegated_address).code message.code_address = delegated_address - - # EIP-7928: Track delegation target when loaded as call target - track_address( - message.block_env.block_state_changes, delegated_address - ) + track_address(message.block_env.state_changes, delegated_address) evm = process_message(message) @@ -263,10 +205,14 @@ def process_create_message(message: Message) -> Evm: # added to SELFDESTRUCT by EIP-6780. mark_account_created(state, message.current_target) - parent_frame = get_parent_frame(message) - create_frame = create_child_frame(parent_frame) + increment_nonce(state, message.current_target) + nonce_after = get_account(state, message.current_target).nonce + track_nonce_change( + message.state_changes, + message.current_target, + U64(nonce_after), + ) - increment_nonce(state, message.current_target, create_frame) evm = process_message(message) if not evm.error: contract_code = evm.output @@ -280,19 +226,24 @@ def process_create_message(message: Message) -> Evm: raise OutOfGasError except ExceptionalHalt as error: rollback_transaction(state, transient_storage) - merge_on_failure(create_frame) + merge_on_failure(message.state_changes) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code( - state, message.current_target, contract_code, create_frame - ) + # Note: No need to capture pre code since it's always b"" here + set_code(state, message.current_target, contract_code) + if contract_code != b"": + track_code_change( + message.state_changes, + message.current_target, + contract_code, + ) commit_transaction(state, transient_storage) - merge_on_success(create_frame) + merge_on_success(message.state_changes) else: rollback_transaction(state, transient_storage) - merge_on_failure(create_frame) + merge_on_failure(message.state_changes) return evm @@ -316,30 +267,54 @@ def process_message(message: Message) -> Evm: if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") + # take snapshot of state before processing the message begin_transaction(state, transient_storage) - parent_frame = get_parent_frame(message) - state_changes = get_message_state_frame(message) - - track_address(state_changes, message.current_target) + track_address(message.state_changes, message.current_target) if message.should_transfer_value and message.value != 0: + # Track value transfer + sender_balance = get_account(state, message.caller).balance + recipient_balance = get_account(state, message.current_target).balance + + track_address(message.state_changes, message.caller) + capture_pre_balance( + message.tx_env.state_changes, message.caller, sender_balance + ) + capture_pre_balance( + message.tx_env.state_changes, + message.current_target, + recipient_balance, + ) + move_ether( - state, + state, message.caller, message.current_target, message.value + ) + + sender_new_balance = get_account(state, message.caller).balance + recipient_new_balance = get_account( + state, message.current_target + ).balance + + track_balance_change( + message.state_changes, message.caller, + U256(sender_new_balance), + ) + track_balance_change( + message.state_changes, message.current_target, - message.value, - state_changes, + U256(recipient_new_balance), ) - evm = execute_code(message, state_changes) + evm = execute_code(message, message.state_changes) if evm.error: rollback_transaction(state, transient_storage) - if state_changes != parent_frame: + if not message.is_create: merge_on_failure(evm.state_changes) else: commit_transaction(state, transient_storage) - if state_changes != parent_frame: + if not message.is_create: merge_on_success(evm.state_changes) return evm diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 9d229496fe..9f02e722f8 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -293,7 +293,7 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: increment_block_access_index, ) - increment_block_access_index(block_env.block_state_changes) + increment_block_access_index(block_env.state_changes) if not self.fork.is_after_fork("paris"): if self.options.state_reward is None: @@ -312,9 +312,9 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: self.fork.process_general_purpose_requests(block_env, block_output) if self.fork.is_after_fork("amsterdam"): - # Build block access list from block_env.block_state_changes + # Build block access list from block_env.state_changes block_output.block_access_list = self.fork.build_block_access_list( - block_env.block_state_changes + block_env.state_changes ) def run_blockchain_test(self) -> None: