From 06d947663e6087c2f324a4d5c127a332fc5da4e0 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Dec 2025 18:09:02 -0700 Subject: [PATCH 1/3] refactor(spec-specs): Refactor state changes and their frames --- src/ethereum/forks/amsterdam/fork.py | 77 ++-- src/ethereum/forks/amsterdam/state.py | 104 +---- src/ethereum/forks/amsterdam/state_tracker.py | 388 +++++++++--------- src/ethereum/forks/amsterdam/utils/message.py | 11 +- src/ethereum/forks/amsterdam/vm/__init__.py | 4 +- .../forks/amsterdam/vm/eoa_delegation.py | 40 +- .../amsterdam/vm/instructions/storage.py | 15 +- .../forks/amsterdam/vm/instructions/system.py | 96 ++++- .../forks/amsterdam/vm/interpreter.py | 152 ++++--- 9 files changed, 454 insertions(+), 433 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 2e5aca21f9..681acd1483 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -69,12 +69,13 @@ commit_transaction_frame, create_child_frame, get_block_access_index, - handle_in_transaction_selfdestruct, 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, @@ -647,8 +648,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 +672,14 @@ def process_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - transaction_state_changes=system_tx_state_changes, + 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 @@ -909,7 +914,9 @@ def process_transaction( # 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) + block_access_index = get_block_access_index(block_env.block_state_changes) + # Capture coinbase pre-balance for net-zero filtering coinbase_pre_balance = get_account( block_env.state, block_env.coinbase ).balance @@ -947,16 +954,30 @@ 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), block_access_index + ) + + # 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, + block_access_index, ) access_list_addresses = set() @@ -991,13 +1012,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 +1048,12 @@ 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, + block_access_index, ) coinbase_balance_after_mining_fee = get_account( @@ -1039,10 +1061,13 @@ 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, + block_access_index, ) if coinbase_balance_after_mining_fee == 0 and account_exists_and_is_empty( @@ -1078,19 +1103,17 @@ def process_transaction( # 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) - ), + tx_env.state_changes, + block_access_index, 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( + track_selfdestruct( block_env.block_state_changes, address, BlockAccessIndex( @@ -1107,6 +1130,10 @@ def process_withdrawals( """ Increase the balance of the withdrawing account. """ + # Get block access index for withdrawals (post-exec phase) + block_access_index = get_block_access_index(block_env.block_state_changes) + + # 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 @@ -1129,7 +1156,10 @@ 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.block_state_changes, + wd.address, + new_balance, + block_access_index, ) if account_exists_and_is_empty(block_env.state, wd.address): @@ -1137,12 +1167,9 @@ def increase_recipient_balance(recipient: Account) -> None: # 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_access_index, block_env.state, ) diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index c1d331942a..341792cac7 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,22 +537,11 @@ 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: """ Sets the balance of an account. @@ -567,32 +552,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 +577,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 +584,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 +600,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 +607,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 +626,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 +633,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..d05995e1e8 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,8 +29,9 @@ 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 @@ -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 @@ -93,12 +85,12 @@ def get_block_frame(state_changes: StateChanges) -> StateChanges: def get_block_access_index(root_frame: StateChanges) -> BlockAccessIndex: """ - Get current block access index from root frame. + Get the current block access index from the root frame. Parameters ---------- root_frame : - The root (block-level) state changes frame. + The root block-level frame. Returns ------- @@ -111,12 +103,12 @@ def get_block_access_index(root_frame: StateChanges) -> BlockAccessIndex: def increment_block_access_index(root_frame: StateChanges) -> None: """ - Increment block access index in root frame. + Increment the block access index in the root frame. Parameters ---------- root_frame : - The root (block-level) state changes frame to increment. + The root block-level frame. """ root_frame._block_access_index = BlockAccessIndex( @@ -124,92 +116,113 @@ def increment_block_access_index(root_frame: StateChanges) -> None: ) +def get_transaction_frame(state_changes: StateChanges) -> StateChanges: + """ + Walk to the transaction-level frame (child of block frame). + + Parameters + ---------- + state_changes : + Any frame in the hierarchy. + + Returns + ------- + tx_frame : StateChanges + The transaction-level frame. + + """ + 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 +239,7 @@ def track_storage_read( state_changes: StateChanges, address: Address, key: Bytes32 ) -> None: """ - Track a storage read operation. + Record a storage read operation. Parameters ---------- @@ -246,9 +259,10 @@ def track_storage_write( address: Address, key: Bytes32, value: U256, + block_access_index: BlockAccessIndex, ) -> None: """ - Track a storage write operation with block access index. + Record a storage write keyed by (address, key, block_access_index). Parameters ---------- @@ -260,21 +274,21 @@ def track_storage_write( The storage key that was written. value : The new storage value. + block_access_index : + The current block access index. """ - block_frame = get_block_frame(state_changes) - state_changes.storage_writes[ - (address, key, get_block_access_index(block_frame)) - ] = value + state_changes.storage_writes[(address, key, block_access_index)] = value def track_balance_change( state_changes: StateChanges, address: Address, new_balance: U256, + block_access_index: BlockAccessIndex, ) -> None: """ - Track balance change keyed by (address, index). + Record a balance change keyed by (address, block_access_index). Parameters ---------- @@ -284,21 +298,21 @@ def track_balance_change( The address whose balance changed. new_balance : The new balance value. + block_access_index : + The current block access index. """ - block_frame = get_block_frame(state_changes) - state_changes.balance_changes[ - (address, get_block_access_index(block_frame)) - ] = new_balance + state_changes.balance_changes[(address, block_access_index)] = new_balance def track_nonce_change( state_changes: StateChanges, address: Address, new_nonce: U64, + block_access_index: BlockAccessIndex, ) -> None: """ - Track a nonce change. + Record a nonce change as (address, block_access_index, new_nonce). Parameters ---------- @@ -308,21 +322,21 @@ def track_nonce_change( The address whose nonce changed. new_nonce : The new nonce value. + block_access_index : + The current block access index. """ - block_frame = get_block_frame(state_changes) - state_changes.nonce_changes.add( - (address, get_block_access_index(block_frame), new_nonce) - ) + state_changes.nonce_changes.add((address, block_access_index, new_nonce)) def track_code_change( state_changes: StateChanges, address: Address, new_code: Bytes, + block_access_index: BlockAccessIndex, ) -> None: """ - Track a code change. + Record a code change keyed by (address, block_access_index). Parameters ---------- @@ -332,21 +346,57 @@ def track_code_change( The address whose code changed. new_code : The new code value. + block_access_index : + The current block access index. + + """ + state_changes.code_changes[(address, block_access_index)] = new_code + + +def track_selfdestruct( + state_changes: StateChanges, + address: Address, + current_block_access_index: BlockAccessIndex, +) -> 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. + current_block_access_index : + The current block access index (transaction index). """ - block_frame = get_block_frame(state_changes) - state_changes.code_changes[ - (address, get_block_access_index(block_frame)) - ] = new_code + # 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)) 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. + Filters net-zero changes by comparing against tx frame's pre-state. Parameters ---------- @@ -356,34 +406,26 @@ def merge_on_success(child_frame: StateChanges) -> None: """ assert child_frame.parent is not None parent_frame = child_frame.parent + + # Get the transaction frame for pre-state lookups + tx_frame = get_transaction_frame(child_frame) + # 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 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: + if (addr, key) in tx_frame.pre_storage: + if tx_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: + # Net-zero write - convert to read and remove any stale + # parent write (child's value supersedes parent's) parent_frame.storage_reads.add((addr, key)) + if (addr, key, idx) in parent_frame.storage_writes: + del parent_frame.storage_writes[(addr, key, idx)] else: # No pre-state captured, merge as-is parent_frame.storage_writes[(addr, key, idx)] = value @@ -391,8 +433,8 @@ def merge_on_success(child_frame: StateChanges) -> None: # 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: + if addr in tx_frame.pre_balances: + if tx_frame.pre_balances[addr] != final_balance: parent_frame.balance_changes[(addr, idx)] = final_balance # else: Net-zero change - skip entirely else: @@ -415,23 +457,44 @@ def merge_on_success(child_frame: StateChanges) -> None: # 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"") + pre_code = tx_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 -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. - 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. + Only reads merge; writes are discarded (converted to reads). - This is different from merge_on_success() which filters net-zero - changes within a single transaction's execution. + 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 + + +def commit_transaction_frame(tx_frame: StateChanges) -> None: + """ + Commit transaction frame to block frame. + + Unlike merge_on_success(), this merges ALL changes without net-zero + filtering (each tx's changes recorded at their respective index). Parameters ---------- @@ -467,37 +530,9 @@ def commit_transaction_frame(tx_frame: StateChanges) -> None: # 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 - - def create_child_frame(parent: StateChanges) -> StateChanges: """ - Create a child frame for nested execution. + Create a child frame linked to the given parent. Parameters ---------- @@ -513,72 +548,23 @@ def create_child_frame(parent: StateChanges) -> StateChanges: 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)) - - def normalize_balance_changes_for_transaction( - block_frame: StateChanges, + tx_frame: StateChanges, current_block_access_index: BlockAccessIndex, state: "State", ) -> None: """ - Normalize balance changes for the current transaction. - - Removes balance changes where post-transaction balance equals - pre-transaction balance. This handles net-zero transfers across - the entire transaction. + Remove net-zero balance changes before committing to block frame. - 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 pre vs post balance for each address; removes if equal. Parameters ---------- - block_frame : StateChanges - The block-level state changes frame. - current_block_access_index : BlockAccessIndex + tx_frame : + The transaction-level state changes frame. + current_block_access_index : The current transaction's block access index. - state : State + state : The current state to read final balances from. """ @@ -588,18 +574,18 @@ def normalize_balance_changes_for_transaction( # Collect addresses that have balance changes in this transaction addresses_to_check = [ addr - for (addr, idx) in block_frame.balance_changes.keys() + for (addr, idx) in tx_frame.balance_changes.keys() if idx == current_block_access_index ] # 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[ + del tx_frame.balance_changes[ (addr, current_block_access_index) ] 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..7fc3a746e5 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -117,6 +117,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 +143,8 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - transaction_state_changes: StateChanges + is_create: bool = False + 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..5529535450 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,13 @@ increment_nonce, set_authority_code, ) -from ..state_tracker import capture_pre_code, track_address +from ..state_tracker import ( + capture_pre_code, + get_block_access_index, + 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 +195,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 +239,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 +256,29 @@ 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 + block_access_index = get_block_access_index( + message.block_env.block_state_changes ) # Capture pre-code before any changes (first-write-wins) - capture_pre_code(state_changes, authority, authority_code) + capture_pre_code(tx_frame, authority, authority_code) # 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 - ) + set_authority_code(state, authority, code_to_set) - increment_nonce(state, authority, state_changes) + # Track code change if different from current + if authority_code != code_to_set: + track_code_change( + tx_frame, authority, code_to_set, block_access_index + ) + + # Track nonce increment + increment_nonce(state, authority) + nonce_after = get_account(state, authority).nonce + track_nonce_change( + tx_frame, authority, U64(nonce_after), block_access_index + ) 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..568c976acb 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -22,6 +22,7 @@ ) from ...state_tracker import ( capture_pre_storage, + get_block_access_index, track_storage_read, track_storage_write, ) @@ -127,7 +128,10 @@ def sstore(evm: Evm) -> None: # 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, @@ -164,8 +168,15 @@ def sstore(evm: Evm) -> None: # OPERATION set_storage(state, evm.message.current_target, key, new_value) + block_access_index = get_block_access_index( + evm.message.block_env.block_state_changes + ) track_storage_write( - evm.state_changes, evm.message.current_target, key, new_value + evm.state_changes, + evm.message.current_target, + key, + new_value, + block_access_index, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 1fca8b1459..f5d1a449cc 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,14 @@ move_ether, set_account_balance, ) -from ...state_tracker import capture_pre_balance, track_address +from ...state_tracker import ( + capture_pre_balance, + create_child_frame, + get_block_access_index, + track_address, + track_balance_change, + track_nonce_change, +) from ...utils.address import ( compute_contract_address, compute_create2_contract_address, @@ -116,15 +123,37 @@ def generic_create( evm.accessed_addresses.add(contract_address) track_address(evm.state_changes, contract_address) + block_access_index = get_block_access_index( + evm.message.block_env.block_state_changes + ) 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) + # Track nonce increment even on collision + 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), + block_access_index, + ) 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), + block_access_index, + ) + + # 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 +173,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 +351,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 +372,7 @@ 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, + state_changes=child_state_changes, ) child_evm = process_message(child_message) @@ -570,11 +601,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,24 +676,47 @@ 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 + block_access_index = get_block_access_index( + evm.message.block_env.block_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, + block_access_index, + ) + track_balance_change( evm.state_changes, + beneficiary, + beneficiary_new_balance, + block_access_index, ) # 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), block_access_index ) evm.accounts_to_delete.add(originator) diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 57f890a12e..0ae7966cac 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,15 @@ ) from ..state_tracker import ( StateChanges, - create_child_frame, + capture_pre_balance, + capture_pre_code, + get_block_access_index, 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 +78,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 +123,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), @@ -263,10 +211,20 @@ 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) + block_access_index = get_block_access_index( + message.block_env.block_state_changes + ) + + # Track nonce increment for contract creation + 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), + block_access_index, + ) - increment_nonce(state, message.current_target, create_frame) evm = process_message(message) if not evm.error: contract_code = evm.output @@ -280,19 +238,29 @@ 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 + # Track code change for contract creation + pre_code = get_account(state, message.current_target).code + capture_pre_code( + message.tx_env.state_changes, message.current_target, pre_code ) + set_code(state, message.current_target, contract_code) + if pre_code != contract_code: + track_code_change( + message.state_changes, + message.current_target, + contract_code, + block_access_index, + ) 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 @@ -318,28 +286,56 @@ def process_message(message: Message) -> Evm: 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) + block_access_index = get_block_access_index( + message.block_env.block_state_changes + ) + 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), + block_access_index, + ) + track_balance_change( + message.state_changes, message.current_target, - message.value, - state_changes, + U256(recipient_new_balance), + block_access_index, ) - 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 From 4569117b74c2980a313f91b0ef0f4da67f39e2b8 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 3 Dec 2025 11:47:06 -0700 Subject: [PATCH 2/3] chore(spec-specs): cleanup BAL logic; organize gas check for SSTORE --- src/ethereum/forks/amsterdam/fork.py | 5 -- src/ethereum/forks/amsterdam/state.py | 6 +- .../forks/amsterdam/vm/eoa_delegation.py | 6 +- .../amsterdam/vm/instructions/storage.py | 65 +++++++++---------- .../forks/amsterdam/vm/instructions/system.py | 1 - .../forks/amsterdam/vm/interpreter.py | 4 +- 6 files changed, 36 insertions(+), 51 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 681acd1483..a788b43876 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -70,7 +70,6 @@ create_child_frame, get_block_access_index, increment_block_access_index, - merge_on_success, normalize_balance_changes_for_transaction, track_address, track_balance_change, @@ -1093,9 +1092,6 @@ 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) @@ -1130,7 +1126,6 @@ def process_withdrawals( """ Increase the balance of the withdrawing account. """ - # Get block access index for withdrawals (post-exec phase) block_access_index = get_block_access_index(block_env.block_state_changes) # Capture pre-state for withdrawal balance filtering diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 341792cac7..fcf12e971b 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -538,11 +538,7 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, recipient_address, increase_recipient_balance) -def set_account_balance( - state: State, - address: Address, - amount: U256, -) -> None: +def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 5529535450..ecf8869dd6 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -261,19 +261,17 @@ def set_delegation(message: Message) -> U256: message.block_env.block_state_changes ) - # Capture pre-code before any changes (first-write-wins) + # EIP-7928: Capture pre-code before any changes capture_pre_code(tx_frame, authority, authority_code) - # Set delegation code set_authority_code(state, authority, code_to_set) - # Track code change if different from current if authority_code != code_to_set: + # Track code change if different from current track_code_change( tx_frame, authority, code_to_set, block_access_index ) - # Track nonce increment increment_nonce(state, authority) nonce_after = get_account(state, authority).nonce track_nonce_change( diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 568c976acb..d95673635c 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -27,7 +27,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, @@ -56,25 +56,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 @@ -94,19 +95,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 = ( @@ -117,16 +113,15 @@ 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.message.tx_env.state_changes, evm.message.current_target, @@ -138,10 +133,14 @@ def sstore(evm: Evm) -> None: 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) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index f5d1a449cc..3b74c5e967 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -130,7 +130,6 @@ def generic_create( if account_has_code_or_nonce( state, contract_address ) or account_has_storage(state, contract_address): - # Track nonce increment even on collision increment_nonce(state, evm.message.current_target) nonce_after = get_account(state, evm.message.current_target).nonce track_nonce_change( diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 0ae7966cac..5d486523b8 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -145,8 +145,6 @@ 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 ) @@ -215,7 +213,6 @@ def process_create_message(message: Message) -> Evm: message.block_env.block_state_changes ) - # Track nonce increment for contract creation increment_nonce(state, message.current_target) nonce_after = get_account(state, message.current_target).nonce track_nonce_change( @@ -284,6 +281,7 @@ 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) block_access_index = get_block_access_index( From 2478c13b7e03cef8a450d5bfa85710a9cd454ca9 Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 5 Dec 2025 17:01:51 -0700 Subject: [PATCH 3/3] refactor(spec-specs): Changes from comments on PR #1841 --- src/ethereum/forks/amsterdam/fork.py | 64 ++---- src/ethereum/forks/amsterdam/state_tracker.py | 195 +++++++----------- src/ethereum/forks/amsterdam/vm/__init__.py | 6 +- .../forks/amsterdam/vm/eoa_delegation.py | 13 +- .../amsterdam/vm/instructions/storage.py | 5 - .../forks/amsterdam/vm/instructions/system.py | 16 +- .../forks/amsterdam/vm/interpreter.py | 25 +-- .../evm_tools/t8n/__init__.py | 6 +- 8 files changed, 110 insertions(+), 220 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index a788b43876..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,9 +67,8 @@ capture_pre_balance, commit_transaction_frame, create_child_frame, - get_block_access_index, + filter_net_zero_frame_changes, increment_block_access_index, - normalize_balance_changes_for_transaction, track_address, track_balance_change, track_nonce_change, @@ -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, @@ -671,6 +669,7 @@ def process_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, + is_create=False, state_changes=call_frame, ) @@ -818,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) @@ -826,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 @@ -911,9 +910,8 @@ 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) - block_access_index = get_block_access_index(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( @@ -957,9 +955,7 @@ def process_transaction( # 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), block_access_index - ) + 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 @@ -976,7 +972,6 @@ def process_transaction( tx_state_changes, sender, U256(sender_balance_after_gas_fee), - block_access_index, ) access_list_addresses = set() @@ -1052,7 +1047,6 @@ def process_transaction( tx_env.state_changes, sender, sender_balance_after_refund, - block_access_index, ) coinbase_balance_after_mining_fee = get_account( @@ -1066,7 +1060,6 @@ def process_transaction( tx_env.state_changes, block_env.coinbase, coinbase_balance_after_mining_fee, - block_access_index, ) if coinbase_balance_after_mining_fee == 0 and account_exists_and_is_empty( @@ -1095,27 +1088,16 @@ def process_transaction( 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_env.state_changes, - block_access_index, - 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_env.state_changes) # 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: - track_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( @@ -1126,16 +1108,12 @@ def process_withdrawals( """ Increase the balance of the withdrawing account. """ - block_access_index = get_block_access_index(block_env.block_state_changes) - # 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) @@ -1151,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, + block_env.state_changes, wd.address, new_balance, - block_access_index, ) 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, - block_access_index, - 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_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py index d05995e1e8..19a929d0dd 100644 --- a/src/ethereum/forks/amsterdam/state_tracker.py +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -35,7 +35,7 @@ class StateChanges: """ 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) @@ -83,24 +83,6 @@ def get_block_frame(state_changes: StateChanges) -> StateChanges: return block_frame -def get_block_access_index(root_frame: StateChanges) -> BlockAccessIndex: - """ - Get the current block access index from the root frame. - - Parameters - ---------- - root_frame : - The root block-level frame. - - Returns - ------- - index : BlockAccessIndex - The current block access index. - - """ - return root_frame._block_access_index - - def increment_block_access_index(root_frame: StateChanges) -> None: """ Increment the block access index in the root frame. @@ -111,8 +93,8 @@ def increment_block_access_index(root_frame: StateChanges) -> None: The root block-level frame. """ - root_frame._block_access_index = BlockAccessIndex( - root_frame._block_access_index + Uint(1) + root_frame.block_access_index = BlockAccessIndex( + root_frame.block_access_index + Uint(1) ) @@ -259,7 +241,6 @@ def track_storage_write( address: Address, key: Bytes32, value: U256, - block_access_index: BlockAccessIndex, ) -> None: """ Record a storage write keyed by (address, key, block_access_index). @@ -274,18 +255,16 @@ def track_storage_write( The storage key that was written. value : The new storage value. - block_access_index : - The current block access index. """ - state_changes.storage_writes[(address, key, block_access_index)] = value + idx = state_changes.block_access_index + state_changes.storage_writes[(address, key, idx)] = value def track_balance_change( state_changes: StateChanges, address: Address, new_balance: U256, - block_access_index: BlockAccessIndex, ) -> None: """ Record a balance change keyed by (address, block_access_index). @@ -298,18 +277,16 @@ def track_balance_change( The address whose balance changed. new_balance : The new balance value. - block_access_index : - The current block access index. """ - state_changes.balance_changes[(address, block_access_index)] = new_balance + idx = state_changes.block_access_index + state_changes.balance_changes[(address, idx)] = new_balance def track_nonce_change( state_changes: StateChanges, address: Address, new_nonce: U64, - block_access_index: BlockAccessIndex, ) -> None: """ Record a nonce change as (address, block_access_index, new_nonce). @@ -322,18 +299,16 @@ def track_nonce_change( The address whose nonce changed. new_nonce : The new nonce value. - block_access_index : - The current block access index. """ - state_changes.nonce_changes.add((address, block_access_index, new_nonce)) + idx = state_changes.block_access_index + state_changes.nonce_changes.add((address, idx, new_nonce)) def track_code_change( state_changes: StateChanges, address: Address, new_code: Bytes, - block_access_index: BlockAccessIndex, ) -> None: """ Record a code change keyed by (address, block_access_index). @@ -346,17 +321,15 @@ def track_code_change( The address whose code changed. new_code : The new code value. - block_access_index : - The current block access index. """ - state_changes.code_changes[(address, block_access_index)] = new_code + idx = state_changes.block_access_index + state_changes.code_changes[(address, idx)] = new_code def track_selfdestruct( state_changes: StateChanges, address: Address, - current_block_access_index: BlockAccessIndex, ) -> None: """ Handle selfdestruct of account created in same transaction. @@ -370,25 +343,25 @@ def track_selfdestruct( The state changes tracker. address : The address that self-destructed. - current_block_access_index : - The current block access index (transaction index). """ + idx = state_changes.block_access_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) + (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, current_block_access_index) in state_changes.code_changes: - del state_changes.code_changes[(address, current_block_access_index)] + 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, 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)] + 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)) @@ -396,7 +369,9 @@ def merge_on_success(child_frame: StateChanges) -> None: """ Merge child frame into parent on success. - Filters net-zero changes by comparing against tx frame's pre-state. + Child values overwrite parent values (most recent wins). No net-zero + filtering here - that happens once at transaction commit via + normalize_transaction(). Parameters ---------- @@ -407,41 +382,19 @@ def merge_on_success(child_frame: StateChanges) -> None: assert child_frame.parent is not None parent_frame = child_frame.parent - # Get the transaction frame for pre-state lookups - tx_frame = get_transaction_frame(child_frame) - # Merge address accesses parent_frame.touched_addresses.update(child_frame.touched_addresses) - # 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 tx_frame.pre_storage: - if tx_frame.pre_storage[(addr, key)] != value: - parent_frame.storage_writes[(addr, key, idx)] = value - else: - # Net-zero write - convert to read and remove any stale - # parent write (child's value supersedes parent's) - parent_frame.storage_reads.add((addr, key)) - if (addr, key, idx) in parent_frame.storage_writes: - del parent_frame.storage_writes[(addr, key, idx)] - 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 tx_frame.pre_balances: - if tx_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 ( @@ -449,18 +402,12 @@ 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 = tx_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 merge_on_failure(child_frame: StateChanges) -> None: @@ -521,19 +468,18 @@ 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 + block_frame.code_changes[(addr, idx)] = final_code def create_child_frame(parent: StateChanges) -> StateChanges: """ 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 ---------- parent : @@ -542,50 +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) + return StateChanges( + parent=parent, + block_access_index=parent.block_access_index, + ) -def normalize_balance_changes_for_transaction( +def filter_net_zero_frame_changes( tx_frame: StateChanges, - current_block_access_index: BlockAccessIndex, state: "State", ) -> None: """ - Remove net-zero balance changes before committing to block frame. + Filter net-zero changes from transaction frame before commit. - Compares pre vs post balance for each address; removes if equal. + 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 ---------- tx_frame : The transaction-level state changes frame. - current_block_access_index : - The current transaction's block access index. state : - The current state to read final balances from. + 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 tx_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 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 tx_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/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 7fc3a746e5..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 @@ -143,7 +141,7 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - is_create: bool = False + is_create: bool state_changes: "StateChanges" = field(default_factory=StateChanges) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index ecf8869dd6..1f1aac9d97 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -21,7 +21,6 @@ ) from ..state_tracker import ( capture_pre_code, - get_block_access_index, track_address, track_code_change, track_nonce_change, @@ -257,10 +256,6 @@ def set_delegation(message: Message) -> U256: code_to_set = EOA_DELEGATION_MARKER + auth.address tx_frame = message.tx_env.state_changes - block_access_index = get_block_access_index( - message.block_env.block_state_changes - ) - # EIP-7928: Capture pre-code before any changes capture_pre_code(tx_frame, authority, authority_code) @@ -268,15 +263,11 @@ def set_delegation(message: Message) -> U256: if authority_code != code_to_set: # Track code change if different from current - track_code_change( - tx_frame, authority, code_to_set, block_access_index - ) + track_code_change(tx_frame, authority, code_to_set) increment_nonce(state, authority) nonce_after = get_account(state, authority).nonce - track_nonce_change( - tx_frame, authority, U64(nonce_after), block_access_index - ) + 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 d95673635c..de7ef935f5 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -22,7 +22,6 @@ ) from ...state_tracker import ( capture_pre_storage, - get_block_access_index, track_storage_read, track_storage_write, ) @@ -167,15 +166,11 @@ def sstore(evm: Evm) -> None: # OPERATION set_storage(state, evm.message.current_target, key, new_value) - block_access_index = get_block_access_index( - evm.message.block_env.block_state_changes - ) track_storage_write( evm.state_changes, evm.message.current_target, key, new_value, - block_access_index, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 3b74c5e967..89e9572e59 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -30,7 +30,6 @@ from ...state_tracker import ( capture_pre_balance, create_child_frame, - get_block_access_index, track_address, track_balance_change, track_nonce_change, @@ -123,9 +122,6 @@ def generic_create( evm.accessed_addresses.add(contract_address) track_address(evm.state_changes, contract_address) - block_access_index = get_block_access_index( - evm.message.block_env.block_state_changes - ) if account_has_code_or_nonce( state, contract_address @@ -136,7 +132,6 @@ def generic_create( evm.state_changes, evm.message.current_target, U64(nonce_after), - block_access_index, ) push(evm.stack, U256(0)) return @@ -148,7 +143,6 @@ def generic_create( evm.state_changes, evm.message.current_target, U64(nonce_after), - block_access_index, ) # Create call frame as child of parent EVM's frame @@ -371,6 +365,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, + is_create=False, state_changes=child_state_changes, ) @@ -682,9 +677,6 @@ def selfdestruct(evm: Evm) -> None: # Get tracking context tx_frame = evm.message.tx_env.state_changes - block_access_index = get_block_access_index( - evm.message.block_env.block_state_changes - ) # Capture pre-balances for net-zero filtering track_address(evm.state_changes, originator) @@ -701,22 +693,18 @@ def selfdestruct(evm: Evm) -> None: evm.state_changes, originator, originator_new_balance, - block_access_index, ) track_balance_change( evm.state_changes, beneficiary, beneficiary_new_balance, - block_access_index, ) # register account for deletion only if it was created # in the same transaction if originator in state.created_accounts: set_account_balance(state, originator, U256(0)) - track_balance_change( - evm.state_changes, originator, U256(0), block_access_index - ) + 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 5d486523b8..154c56de11 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -47,8 +47,6 @@ from ..state_tracker import ( StateChanges, capture_pre_balance, - capture_pre_code, - get_block_access_index, merge_on_failure, merge_on_success, track_address, @@ -145,9 +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 - track_address( - message.block_env.block_state_changes, delegated_address - ) + track_address(message.block_env.state_changes, delegated_address) evm = process_message(message) @@ -209,17 +205,12 @@ def process_create_message(message: Message) -> Evm: # added to SELFDESTRUCT by EIP-6780. mark_account_created(state, message.current_target) - block_access_index = get_block_access_index( - message.block_env.block_state_changes - ) - 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), - block_access_index, ) evm = process_message(message) @@ -240,18 +231,13 @@ def process_create_message(message: Message) -> Evm: evm.output = b"" evm.error = error else: - # Track code change for contract creation - pre_code = get_account(state, message.current_target).code - capture_pre_code( - message.tx_env.state_changes, message.current_target, pre_code - ) + # Note: No need to capture pre code since it's always b"" here set_code(state, message.current_target, contract_code) - if pre_code != contract_code: + if contract_code != b"": track_code_change( message.state_changes, message.current_target, contract_code, - block_access_index, ) commit_transaction(state, transient_storage) merge_on_success(message.state_changes) @@ -284,9 +270,6 @@ def process_message(message: Message) -> Evm: # take snapshot of state before processing the message begin_transaction(state, transient_storage) - block_access_index = get_block_access_index( - message.block_env.block_state_changes - ) track_address(message.state_changes, message.current_target) if message.should_transfer_value and message.value != 0: @@ -317,13 +300,11 @@ def process_message(message: Message) -> Evm: message.state_changes, message.caller, U256(sender_new_balance), - block_access_index, ) track_balance_change( message.state_changes, message.current_target, U256(recipient_new_balance), - block_access_index, ) evm = execute_code(message, message.state_changes) 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: