diff --git a/pyproject.toml b/pyproject.toml index a188a57c84..f5a711714c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ packages = [ "ethereum_spec_tools.lint", "ethereum_spec_tools.lint.lints", "ethereum", + "ethereum.state_oracle", "ethereum.frontier", "ethereum.frontier.utils", "ethereum.frontier.vm", diff --git a/src/ethereum/osaka/fork.py b/src/ethereum/osaka/fork.py index a4f3c231f4..972be555af 100644 --- a/src/ethereum/osaka/fork.py +++ b/src/ethereum/osaka/fork.py @@ -51,17 +51,7 @@ compute_requests_hash, parse_deposit_requests, ) -from .state import ( - State, - TransientStorage, - account_exists_and_is_empty, - destroy_account, - get_account, - increment_nonce, - modify_state, - set_account_balance, - state_root, -) +from .state import State, TransientStorage from .transactions import ( AccessListTransaction, BlobTransaction, @@ -210,6 +200,8 @@ def state_transition(chain: BlockChain, block: Block) -> None: History and current state. block : Block to apply to `chain`. + oracle : MerkleOracle + State oracle for accessing blockchain state. Must be provided. """ if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") @@ -237,7 +229,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: transactions=block.transactions, withdrawals=block.withdrawals, ) - block_state_root = state_root(block_env.state) + block_state_root = block_env.get_oracle().state_root() transactions_root = root(block_output.transactions_trie) receipt_root = root(block_output.receipts_trie) block_logs_bloom = logs_bloom(block_output.block_logs) @@ -461,7 +453,7 @@ def check_transaction( raise BlobGasLimitExceededError("blob gas limit exceeded") sender_address = recover_sender(block_env.chain_id, tx) - sender_account = get_account(block_env.state, sender_address) + sender_account = block_env.get_oracle().get_account(sender_address) if isinstance( tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) @@ -666,7 +658,9 @@ def process_checked_system_transaction( system_tx_output : `MessageCallOutput` Output of processing the system transaction. """ - system_contract_code = get_account(block_env.state, target_address).code + system_contract_code = ( + block_env.get_oracle().get_account(target_address).code + ) if len(system_contract_code) == 0: raise InvalidBlock( @@ -713,7 +707,9 @@ def process_unchecked_system_transaction( system_tx_output : `MessageCallOutput` Output of processing the system transaction. """ - system_contract_code = get_account(block_env.state, target_address).code + system_contract_code = ( + block_env.get_oracle().get_account(target_address).code + ) return process_system_transaction( block_env, target_address, @@ -870,7 +866,7 @@ def process_transaction( tx=tx, ) - sender_account = get_account(block_env.state, sender) + sender_account = block_env.get_oracle().get_account(sender) if isinstance(tx, BlobTransaction): blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) @@ -880,13 +876,13 @@ def process_transaction( effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(block_env.state, sender) + block_env.get_oracle().increment_nonce(sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) - set_account_balance( - block_env.state, sender, U256(sender_balance_after_gas_fee) + block_env.get_oracle().set_account_balance( + sender, U256(sender_balance_after_gas_fee) ) access_list_addresses = set() @@ -949,26 +945,33 @@ def process_transaction( transaction_fee = tx_gas_used_after_refund * priority_fee_per_gas # refund gas - sender_balance_after_refund = get_account( - block_env.state, sender - ).balance + U256(gas_refund_amount) - set_account_balance(block_env.state, sender, sender_balance_after_refund) + current_sender_balance = block_env.get_oracle().get_account(sender).balance + sender_balance_after_refund = current_sender_balance + U256( + gas_refund_amount + ) + block_env.get_oracle().set_account_balance( + sender, sender_balance_after_refund + ) # transfer miner fees - coinbase_balance_after_mining_fee = get_account( - block_env.state, block_env.coinbase - ).balance + U256(transaction_fee) + current_coinbase_balance = ( + block_env.get_oracle().get_account(block_env.coinbase).balance + ) + coinbase_balance_after_mining_fee = current_coinbase_balance + U256( + transaction_fee + ) if coinbase_balance_after_mining_fee != 0: - set_account_balance( - block_env.state, + block_env.get_oracle().set_account_balance( block_env.coinbase, coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(block_env.state, block_env.coinbase): - destroy_account(block_env.state, block_env.coinbase) + elif block_env.get_oracle().account_exists_and_is_empty( + block_env.coinbase + ): + block_env.get_oracle().destroy_account(block_env.coinbase) for address in tx_output.accounts_to_delete: - destroy_account(block_env.state, address) + block_env.get_oracle().destroy_account(address) block_output.block_gas_used += tx_gas_used_after_refund block_output.blob_gas_used += tx_blob_gas_used @@ -1008,10 +1011,12 @@ def increase_recipient_balance(recipient: Account) -> None: rlp.encode(wd), ) - modify_state(block_env.state, wd.address, increase_recipient_balance) + block_env.get_oracle().modify_state( + wd.address, increase_recipient_balance + ) - if account_exists_and_is_empty(block_env.state, wd.address): - destroy_account(block_env.state, wd.address) + if block_env.get_oracle().account_exists_and_is_empty(wd.address): + block_env.get_oracle().destroy_account(wd.address) def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: diff --git a/src/ethereum/osaka/utils/message.py b/src/ethereum/osaka/utils/message.py index 7d0ee65131..c7a2fc3322 100644 --- a/src/ethereum/osaka/utils/message.py +++ b/src/ethereum/osaka/utils/message.py @@ -17,7 +17,6 @@ from ethereum_types.numeric import Uint from ..fork_types import Address -from ..state import get_account from ..transactions import Transaction from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS @@ -54,7 +53,7 @@ def prepare_message( if isinstance(tx.to, Bytes0): current_target = compute_contract_address( tx_env.origin, - get_account(block_env.state, tx_env.origin).nonce - Uint(1), + block_env.get_oracle().get_account(tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") code = tx.data @@ -62,7 +61,7 @@ def prepare_message( elif isinstance(tx.to, Address): current_target = tx.to msg_data = tx.data - code = get_account(block_env.state, tx.to).code + code = block_env.get_oracle().get_account(tx.to).code code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") diff --git a/src/ethereum/osaka/vm/__init__.py b/src/ethereum/osaka/vm/__init__.py index 033293a5fd..7fd61c1606 100644 --- a/src/ethereum/osaka/vm/__init__.py +++ b/src/ethereum/osaka/vm/__init__.py @@ -14,7 +14,7 @@ """ from dataclasses import dataclass, field -from typing import List, Optional, Set, Tuple +from typing import Any, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint @@ -38,6 +38,7 @@ class BlockEnvironment: """ chain_id: U64 + # TODO: Remove, no longer used. Kept so tests don't break for now. state: State block_gas_limit: Uint block_hashes: List[Hash32] @@ -49,6 +50,16 @@ class BlockEnvironment: excess_blob_gas: U64 parent_beacon_block_root: Hash32 + def get_oracle(self) -> Any: + """ + Get the state oracle. + + Returns the global oracle (raises error if not set). + """ + from ethereum.state_oracle import get_state_oracle + + return get_state_oracle() + @dataclass class BlockOutput: diff --git a/src/ethereum/osaka/vm/eoa_delegation.py b/src/ethereum/osaka/vm/eoa_delegation.py index 1fe2e1e7bd..701cdc5f75 100644 --- a/src/ethereum/osaka/vm/eoa_delegation.py +++ b/src/ethereum/osaka/vm/eoa_delegation.py @@ -2,7 +2,6 @@ Set EOA account code. """ - from typing import Optional, Tuple from ethereum_rlp import rlp @@ -14,7 +13,6 @@ from ethereum.exceptions import InvalidBlock, InvalidSignatureError from ..fork_types import Address, Authorization -from ..state import account_exists, get_account, increment_nonce, set_code from ..utils.hexadecimal import hex_to_address from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS from . import Evm, Message @@ -130,8 +128,8 @@ def access_delegation( delegation : `Tuple[bool, Address, Bytes, Uint]` The delegation address, code, and access gas cost. """ - state = evm.message.block_env.state - code = get_account(state, address).code + oracle = evm.message.block_env.get_oracle() + code = oracle.get_account(address).code if not is_valid_delegation(code): return False, address, code, Uint(0) @@ -141,7 +139,7 @@ def access_delegation( else: evm.accessed_addresses.add(address) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS - code = get_account(state, address).code + code = oracle.get_account(address).code return True, address, code, access_gas_cost @@ -162,7 +160,7 @@ def set_delegation(message: Message) -> U256: refund_counter: `U256` Refund from authority which already exists in state. """ - state = message.block_env.state + oracle = message.block_env.get_oracle() refund_counter = U256(0) for auth in message.tx_env.authorizations: if auth.chain_id not in (message.block_env.chain_id, U256(0)): @@ -178,7 +176,7 @@ def set_delegation(message: Message) -> U256: message.accessed_addresses.add(authority) - authority_account = get_account(state, authority) + authority_account = oracle.get_account(authority) authority_code = authority_account.code if authority_code and not is_valid_delegation(authority_code): @@ -188,20 +186,20 @@ def set_delegation(message: Message) -> U256: if authority_nonce != auth.nonce: continue - if account_exists(state, authority): + if oracle.account_exists(authority): refund_counter += U256(PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST) if auth.address == NULL_ADDRESS: code_to_set = b"" else: code_to_set = EOA_DELEGATION_MARKER + auth.address - set_code(state, authority, code_to_set) + oracle.set_code(authority, code_to_set) - increment_nonce(state, authority) + oracle.increment_nonce(authority) if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") - message.code = get_account(state, message.code_address).code + message.code = oracle.get_account(message.code_address).code return refund_counter diff --git a/src/ethereum/osaka/vm/instructions/environment.py b/src/ethereum/osaka/vm/instructions/environment.py index 226b3d3bb3..c8ac3812ee 100644 --- a/src/ethereum/osaka/vm/instructions/environment.py +++ b/src/ethereum/osaka/vm/instructions/environment.py @@ -19,7 +19,6 @@ from ethereum.utils.numeric import ceil32 from ...fork_types import EMPTY_ACCOUNT -from ...state import get_account from ...utils.address import to_address_masked from ...vm.memory import buffer_read, memory_write from .. import Evm @@ -84,8 +83,9 @@ def balance(evm: Evm) -> None: charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) # OPERATION + oracle = evm.message.block_env.get_oracle() # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.message.block_env.state, address).balance + balance = oracle.get_account(address).balance push(evm.stack, balance) @@ -351,7 +351,8 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.message.block_env.state, address).code + oracle = evm.message.block_env.get_oracle() + code = oracle.get_account(address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -393,7 +394,8 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.message.block_env.state, address).code + oracle = evm.message.block_env.get_oracle() + code = oracle.get_account(address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -479,7 +481,8 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.message.block_env.state, address) + oracle = evm.message.block_env.get_oracle() + account = oracle.get_account(address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -510,10 +513,9 @@ def self_balance(evm: Evm) -> None: charge_gas(evm, GAS_FAST_STEP) # OPERATION + oracle = evm.message.block_env.get_oracle() # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account( - evm.message.block_env.state, evm.message.current_target - ).balance + balance = oracle.get_account(evm.message.current_target).balance push(evm.stack, balance) diff --git a/src/ethereum/osaka/vm/instructions/memory.py b/src/ethereum/osaka/vm/instructions/memory.py index 89533af37e..f3c4013d27 100644 --- a/src/ethereum/osaka/vm/instructions/memory.py +++ b/src/ethereum/osaka/vm/instructions/memory.py @@ -11,6 +11,7 @@ Implementations of the EVM Memory instructions. """ + from ethereum_types.bytes import Bytes from ethereum_types.numeric import U256, Uint diff --git a/src/ethereum/osaka/vm/instructions/storage.py b/src/ethereum/osaka/vm/instructions/storage.py index 65a0d5a9b6..f583bb90cd 100644 --- a/src/ethereum/osaka/vm/instructions/storage.py +++ b/src/ethereum/osaka/vm/instructions/storage.py @@ -11,15 +11,10 @@ Implementations of the EVM storage related instructions. """ -from ethereum_types.numeric import Uint - -from ...state import ( - get_storage, - get_storage_original, - get_transient_storage, - set_storage, - set_transient_storage, -) + +from ethereum_types.numeric import U256, Uint + +from ...state import get_transient_storage, set_transient_storage from .. import Evm from ..exceptions import OutOfGasError, WriteInStaticContext from ..gas import ( @@ -56,9 +51,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage( - evm.message.block_env.state, evm.message.current_target, key - ) + oracle = evm.message.block_env.get_oracle() + value_bytes = oracle.get_storage(evm.message.current_target, key) + value = U256.from_be_bytes(value_bytes) push(evm.stack, value) @@ -82,11 +77,13 @@ def sstore(evm: Evm) -> None: if evm.gas_left <= GAS_CALL_STIPEND: raise OutOfGasError - state = evm.message.block_env.state - original_value = get_storage_original( - state, evm.message.current_target, key + oracle = evm.message.block_env.get_oracle() + original_value_bytes = oracle.get_storage_original( + evm.message.current_target, key ) - current_value = get_storage(state, evm.message.current_target, key) + original_value = U256.from_be_bytes(original_value_bytes) + current_value_bytes = oracle.get_storage(evm.message.current_target, key) + current_value = U256.from_be_bytes(current_value_bytes) gas_cost = Uint(0) @@ -126,7 +123,9 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(state, evm.message.current_target, key, new_value) + oracle.set_storage_value( + evm.message.current_target, key, new_value.to_be_bytes32() + ) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/osaka/vm/instructions/system.py b/src/ethereum/osaka/vm/instructions/system.py index d7308821bd..a575c13174 100644 --- a/src/ethereum/osaka/vm/instructions/system.py +++ b/src/ethereum/osaka/vm/instructions/system.py @@ -18,15 +18,6 @@ from ethereum.utils.numeric import ceil32 from ...fork_types import Address -from ...state import ( - account_has_code_or_nonce, - account_has_storage, - get_account, - increment_nonce, - is_account_alive, - move_ether, - set_account_balance, -) from ...utils.address import ( compute_contract_address, compute_create2_contract_address, @@ -91,7 +82,8 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.message.block_env.state, sender_address) + oracle = evm.message.block_env.get_oracle() + sender = oracle.get_account(sender_address) if ( sender.balance < endowment @@ -104,16 +96,14 @@ def generic_create( evm.accessed_addresses.add(contract_address) - if account_has_code_or_nonce( - evm.message.block_env.state, contract_address - ) or account_has_storage(evm.message.block_env.state, contract_address): - increment_nonce( - evm.message.block_env.state, evm.message.current_target - ) + if oracle.account_has_code_or_nonce( + contract_address + ) or oracle.account_has_storage(contract_address): + oracle.increment_nonce(evm.message.current_target) push(evm.stack, U256(0)) return - increment_nonce(evm.message.block_env.state, evm.message.current_target) + oracle.increment_nonce(evm.message.current_target) child_message = Message( block_env=evm.message.block_env, @@ -169,12 +159,11 @@ def create(evm: Evm) -> None: charge_gas(evm, GAS_CREATE + extend_memory.cost + init_code_gas) # OPERATION + oracle = evm.message.block_env.get_oracle() evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account( - evm.message.block_env.state, evm.message.current_target - ).nonce, + oracle.get_account(evm.message.current_target).nonce, ) generic_create( @@ -385,8 +374,9 @@ def call(evm: Evm) -> None: ) = access_delegation(evm, code_address) access_gas_cost += delegated_access_gas_cost + oracle = evm.message.block_env.get_oracle() create_gas_cost = GAS_NEW_ACCOUNT - if value == 0 or is_account_alive(evm.message.block_env.state, to): + if value == 0 or oracle.is_account_alive(to): create_gas_cost = Uint(0) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE message_call_gas = calculate_message_call_gas( @@ -400,9 +390,7 @@ def call(evm: Evm) -> None: if evm.message.is_static and value != U256(0): raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by - sender_balance = get_account( - evm.message.block_env.state, evm.message.current_target - ).balance + sender_balance = oracle.get_account(evm.message.current_target).balance if sender_balance < value: push(evm.stack, U256(0)) evm.return_data = b"" @@ -483,10 +471,9 @@ def callcode(evm: Evm) -> None: charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION + oracle = evm.message.block_env.get_oracle() evm.memory += b"\x00" * extend_memory.expand_by - sender_balance = get_account( - evm.message.block_env.state, evm.message.current_target - ).balance + sender_balance = oracle.get_account(evm.message.current_target).balance if sender_balance < value: push(evm.stack, U256(0)) evm.return_data = b"" @@ -531,13 +518,10 @@ def selfdestruct(evm: Evm) -> None: evm.accessed_addresses.add(beneficiary) gas_cost += GAS_COLD_ACCOUNT_ACCESS - if ( - not is_account_alive(evm.message.block_env.state, beneficiary) - and get_account( - evm.message.block_env.state, evm.message.current_target - ).balance - != 0 - ): + oracle = evm.message.block_env.get_oracle() + current_balance = oracle.get_account(evm.message.current_target).balance + + if not oracle.is_account_alive(beneficiary) and current_balance != 0: gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT charge_gas(evm, gas_cost) @@ -545,12 +529,9 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - originator_balance = get_account( - evm.message.block_env.state, originator - ).balance + originator_balance = oracle.get_account(originator).balance - move_ether( - evm.message.block_env.state, + oracle.move_ether( originator, beneficiary, originator_balance, @@ -558,10 +539,10 @@ def selfdestruct(evm: Evm) -> None: # register account for deletion only if it was created # in the same transaction - if originator in evm.message.block_env.state.created_accounts: + if oracle.is_created_account(originator): # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.message.block_env.state, originator, U256(0)) + oracle.set_account_balance(originator, U256(0)) evm.accounts_to_delete.add(originator) # HALT the execution diff --git a/src/ethereum/osaka/vm/interpreter.py b/src/ethereum/osaka/vm/interpreter.py index 18225616d6..ca62ec5761 100644 --- a/src/ethereum/osaka/vm/interpreter.py +++ b/src/ethereum/osaka/vm/interpreter.py @@ -32,19 +32,7 @@ from ..blocks import Log from ..fork_types import Address -from ..state import ( - account_has_code_or_nonce, - account_has_storage, - begin_transaction, - commit_transaction, - destroy_storage, - get_account, - increment_nonce, - mark_account_created, - move_ether, - rollback_transaction, - set_code, -) +from ..state import begin_transaction, commit_transaction, rollback_transaction from ..vm import Message from ..vm.eoa_delegation import get_delegated_code_address, set_delegation from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas @@ -108,9 +96,9 @@ def process_message_call(message: Message) -> MessageCallOutput: block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): - is_collision = account_has_code_or_nonce( - block_env.state, message.current_target - ) or account_has_storage(block_env.state, message.current_target) + is_collision = block_env.get_oracle().account_has_code_or_nonce( + message.current_target + ) or block_env.get_oracle().account_has_storage(message.current_target) if is_collision: return MessageCallOutput( Uint(0), @@ -130,7 +118,9 @@ def process_message_call(message: Message) -> MessageCallOutput: if delegated_address is not None: message.disable_precompiles = True message.accessed_addresses.add(delegated_address) - message.code = get_account(block_env.state, delegated_address).code + message.code = ( + block_env.get_oracle().get_account(delegated_address).code + ) message.code_address = delegated_address evm = process_message(message) @@ -172,7 +162,7 @@ def process_create_message(message: Message) -> Evm: evm: :py:class:`~ethereum.osaka.vm.Evm` Items containing execution specific objects. """ - state = message.block_env.state + state = message.block_env.get_oracle().state transient_storage = message.tx_env.transient_storage # take snapshot of state before processing the message begin_transaction(state, transient_storage) @@ -184,15 +174,15 @@ def process_create_message(message: Message) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(state, message.current_target) + message.block_env.get_oracle().destroy_storage(message.current_target) # In the previously mentioned edge case the preexisting storage is ignored # for gas refund purposes. In order to do this we must track created # accounts. This tracking is also needed to respect the constraints # added to SELFDESTRUCT by EIP-6780. - mark_account_created(state, message.current_target) + message.block_env.get_oracle().add_created_account(message.current_target) - increment_nonce(state, message.current_target) + message.block_env.get_oracle().increment_nonce(message.current_target) evm = process_message(message) if not evm.error: contract_code = evm.output @@ -210,7 +200,9 @@ def process_create_message(message: Message) -> Evm: evm.output = b"" evm.error = error else: - set_code(state, message.current_target, contract_code) + message.block_env.get_oracle().set_code( + message.current_target, contract_code + ) commit_transaction(state, transient_storage) else: rollback_transaction(state, transient_storage) @@ -231,7 +223,7 @@ def process_message(message: Message) -> Evm: evm: :py:class:`~ethereum.osaka.vm.Evm` Items containing execution specific objects """ - state = message.block_env.state + state = message.block_env.get_oracle().state transient_storage = message.tx_env.transient_storage if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") @@ -240,8 +232,8 @@ def process_message(message: Message) -> Evm: begin_transaction(state, transient_storage) if message.should_transfer_value and message.value != 0: - move_ether( - state, message.caller, message.current_target, message.value + message.block_env.get_oracle().move_ether( + message.caller, message.current_target, message.value ) evm = execute_code(message) diff --git a/src/ethereum/state_oracle/__init__.py b/src/ethereum/state_oracle/__init__.py new file mode 100644 index 0000000000..933cd84505 --- /dev/null +++ b/src/ethereum/state_oracle/__init__.py @@ -0,0 +1,47 @@ +""" +State Oracle Interface for Ethereum Execution Specs. + +Provides an abstract interface for state access that can be implemented +by different state management strategies. +""" + +from typing import Optional + +from .interface import MerkleOracle +from .memory_oracle import MemoryMerkleOracle + +_state_oracle: Optional[MerkleOracle] = None + + +def set_state_oracle(oracle: MerkleOracle) -> Optional[MerkleOracle]: + """ + Set the global state oracle. + + Returns the previous oracle if any. + """ + global _state_oracle + old = _state_oracle + _state_oracle = oracle + return old + + +def get_state_oracle() -> MerkleOracle: + """ + Get the current global state oracle. + + Raises RuntimeError if no oracle has been set. + """ + global _state_oracle + if _state_oracle is None: + raise RuntimeError( + "No global state oracle set. Call set_state_oracle() first." + ) + return _state_oracle + + +__all__ = [ + "MerkleOracle", + "MemoryMerkleOracle", + "set_state_oracle", + "get_state_oracle", +] diff --git a/src/ethereum/state_oracle/interface.py b/src/ethereum/state_oracle/interface.py new file mode 100644 index 0000000000..bcc03a2803 --- /dev/null +++ b/src/ethereum/state_oracle/interface.py @@ -0,0 +1,213 @@ +""" +Abstract interface for state oracle implementations. +""" + +from typing import Any, Callable, Optional, Protocol + +from ethereum_types.bytes import Bytes20, Bytes32 +from ethereum_types.numeric import U256 + +# Use generic types for compatibility across forks +Account = Any +Address = Bytes20 + + +class MerkleOracle(Protocol): + """ + Oracle interface for Merkle Patricia Trie based state. + """ + + def get_account( + self, address: Address # noqa: U100 + ) -> Account: # noqa: U100 + """ + Get account information for the given address. + + `address` is the account address to retrieve. + + Returns EMPTY_ACCOUNT if the account doesn't exist. + """ + + def get_account_optional( + self, address: Address # noqa: U100 + ) -> Optional[Account]: # noqa: U100 + """ + Get account information for the given address. + + `address` is the account address to retrieve. + + Returns None if the account doesn't exist. + Use this when you need to distinguish between non-existent and + empty accounts. + """ + + def get_storage( + self, address: Address, key: Bytes32 # noqa: U100 + ) -> Bytes32: # noqa: U100 + """Get storage value at `key` for the given `address`.""" + + def state_root(self) -> Bytes32: # noqa: U100 + """Compute and return the current state root.""" + + def get_storage_original( + self, address: Address, key: Bytes32 # noqa: U100 + ) -> Bytes32: # noqa: U100 + """ + Get original storage value before current transaction started. + + `address` is the contract address and `key` is the storage slot. + + This is required for SSTORE gas calculations per EIP-2200. + The implementation should use state snapshots/checkpoints to + track pre-transaction values. + TODO: The oracle does not have a `begin_transaction` method, + TODO: so it kind of breaks here. + + Parameters + ---------- + address : Bytes20 + Contract address + key : Bytes32 + Storage slot key + + Returns + ------- + Bytes32 + Original storage value as 32-byte value + """ + + def set_storage_value( + self, address: Address, key: Bytes32, value: Bytes32 # noqa: U100 + ) -> None: + """ + Set individual storage value. + + `address` is the contract address, `key` is the storage slot, + and `value` is the new storage value. + + Parameters + ---------- + address : Bytes20 + Contract address + key : Bytes32 + Storage slot key + value : Bytes32 + Storage value as 32-byte value + """ + + def account_has_code_or_nonce( + self, address: Address # noqa: U100 + ) -> bool: # noqa: U100 + """ + Check if account has non-zero code or nonce. + + Used during contract creation to check if address is available. + """ + + def account_has_storage(self, address: Address) -> bool: # noqa: U100 + """ + Check if account has any storage slots. + + Used during contract creation to check if address is available. + """ + + def is_account_alive(self, address: Address) -> bool: # noqa: U100 + """ + Check if account is alive (exists and not marked for deletion). + + Used in CALL instructions and SELFDESTRUCT. + """ + + def account_exists(self, address: Address) -> bool: # noqa: U100 + """ + Check if account exists in the state. + """ + + def increment_nonce(self, address: Address) -> None: # noqa: U100 + """ + Increment account nonce. + + Used during contract creation and transaction processing. + """ + + def set_code(self, address: Address, code: Any) -> None: # noqa: U100 + """ + Set account code. + + Used during contract creation and EOA delegation. + """ + + def set_account_balance( + self, address: Address, balance: U256 # noqa: U100 + ) -> None: # noqa: U100 + """ + Set account balance. + + Used in SELFDESTRUCT and other balance transfer operations. + """ + + def move_ether( + self, sender: Bytes20, recipient: Bytes20, amount: U256 # noqa: U100 + ) -> None: + """ + Transfer ether between accounts. + + Handles balance updates for both sender and recipient accounts. + Used in CALL instructions and contract transfers. + """ + + def add_created_account(self, address: Address) -> None: # noqa: U100 + """ + Mark account as created in current transaction. + + Used for tracking accounts created during transaction execution. + """ + + def is_created_account(self, address: Address) -> bool: # noqa: U100 + """ + Check if account was created in current transaction. + + Used in SELFDESTRUCT and other operations that need to know + if account was created in current transaction. + """ + + def account_exists_and_is_empty( + self, address: Address # noqa: U100 + ) -> bool: # noqa: U100 + """ + Check if account exists and is empty. + + Used for account cleanup logic. + """ + + def destroy_account(self, address: Address) -> None: # noqa: U100 + """ + Mark account for destruction. + + Used in SELFDESTRUCT and account cleanup. + """ + + def destroy_storage(self, address: Address) -> None: # noqa: U100 + """ + Completely remove the storage at address. + """ + + def modify_state( + self, + address: Address, # noqa: U100 + modifier_function: Callable[[Account], None], # noqa: U100 + ) -> None: + """ + Modify an account using a modifier function. + + Parameters + ---------- + address : Address + Address of account to modify + modifier_function : Callable[[Account], None] + Function that takes an account and modifies it in place + """ + + @property + def state(self) -> Any: # noqa: U100 + """Access to underlying state for compatibility.""" diff --git a/src/ethereum/state_oracle/memory_oracle.py b/src/ethereum/state_oracle/memory_oracle.py new file mode 100644 index 0000000000..61b498f843 --- /dev/null +++ b/src/ethereum/state_oracle/memory_oracle.py @@ -0,0 +1,179 @@ +""" +Memory-based state oracle implementation. + +This impl wraps the existing execution-specs `State` object +to provide the oracle interface. +This is mainly done as a first pass to make the diff small. +""" + +from typing import Any, Callable, Optional + +from ethereum_types.bytes import Bytes20, Bytes32 +from ethereum_types.numeric import U256 + +# TODO: This file is current Osaka specific -- we could move state.py +# into here to mitigate this. + +# Use generic types for compatibility across forks +Account = Any +Address = Bytes20 +State = Any + + +class MemoryMerkleOracle: + """ + Merkle oracle implementation that wraps existing execution-specs state. + """ + + def __init__(self, state: State) -> None: + """ + Initialize oracle with existing state. + + Parameters + ---------- + state : State + The existing execution-specs state object. + """ + self._state = state + + def get_account(self, address: Address) -> Account: + """ + Get account information for the given address. + + Returns EMPTY_ACCOUNT if not exists. + """ + from ethereum.osaka.state import get_account + + return get_account(self._state, address) + + def get_account_optional(self, address: Address) -> Optional[Account]: + """ + Get account information for the given address. + + Returns None if not exists. + """ + from ethereum.osaka.state import get_account_optional + + return get_account_optional(self._state, address) + + def get_storage(self, address: Address, key: Bytes32) -> Bytes32: + """Get storage value at key for the given address.""" + from ethereum.osaka.state import get_storage + + storage_value = get_storage(self._state, address, key) + return storage_value.to_be_bytes32() + + def state_root(self) -> Bytes32: + """Compute and return the current state root.""" + from ethereum.osaka.state import state_root + + return state_root(self._state) + + def get_storage_original(self, address: Address, key: Bytes32) -> Bytes32: + """Get original storage value (before transaction started).""" + from ethereum.osaka.state import get_storage_original + + storage_value = get_storage_original(self._state, address, key) + return storage_value.to_be_bytes32() + + def set_storage_value( + self, address: Address, key: Bytes32, value: Bytes32 + ) -> None: + """Set a single storage value.""" + from ethereum_types.numeric import U256 + + from ethereum.osaka.state import set_storage + + # Convert Bytes32 to U256 for storage + storage_value = U256.from_be_bytes(value) + set_storage(self._state, address, key, storage_value) + + def account_has_code_or_nonce(self, address: Address) -> bool: + """Check if account has non-zero code or nonce.""" + from ethereum.osaka.state import account_has_code_or_nonce + + return account_has_code_or_nonce(self._state, address) + + def account_has_storage(self, address: Address) -> bool: + """Check if account has any storage slots.""" + from ethereum.osaka.state import account_has_storage + + return account_has_storage(self._state, address) + + def is_account_alive(self, address: Address) -> bool: + """Check if account is alive (exists and not marked for deletion).""" + from ethereum.osaka.state import is_account_alive + + return is_account_alive(self._state, address) + + def account_exists(self, address: Address) -> bool: + """Check if account exists in the state.""" + from ethereum.osaka.state import account_exists + + return account_exists(self._state, address) + + def increment_nonce(self, address: Address) -> None: + """Increment account nonce.""" + from ethereum.osaka.state import increment_nonce + + increment_nonce(self._state, address) + + def set_code(self, address: Address, code: Any) -> None: + """Set account code.""" + from ethereum.osaka.state import set_code + + set_code(self._state, address, code) + + def set_account_balance(self, address: Address, balance: U256) -> None: + """Set account balance.""" + from ethereum.osaka.state import set_account_balance + + set_account_balance(self._state, address, balance) + + def move_ether( + self, sender: Address, recipient: Address, amount: U256 + ) -> None: + """Transfer ether between accounts.""" + from ethereum.osaka.state import move_ether + + move_ether(self._state, sender, recipient, amount) + + def add_created_account(self, address: Address) -> None: + """Mark account as created in current transaction.""" + # Add to the created_accounts set in state + self._state.created_accounts.add(address) + + def is_created_account(self, address: Address) -> bool: + """Check if account was created in current transaction.""" + return address in self._state.created_accounts + + def account_exists_and_is_empty(self, address: Address) -> bool: + """Check if account exists and is empty.""" + from ethereum.osaka.state import account_exists_and_is_empty + + return account_exists_and_is_empty(self._state, address) + + def destroy_account(self, address: Address) -> None: + """Mark account for destruction.""" + from ethereum.osaka.state import destroy_account + + destroy_account(self._state, address) + + def destroy_storage(self, address: Address) -> None: + """Completely remove the storage at address.""" + from ethereum.osaka.state import destroy_storage + + destroy_storage(self._state, address) + + def modify_state( + self, address: Address, modifier_function: Callable[[Account], None] + ) -> None: + """Modify an account using a modifier function.""" + from ethereum.osaka.state import modify_state + + modify_state(self._state, address, modifier_function) + + @property + def state(self) -> State: + """Access to underlying state for compatibility.""" + return self._state diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index f00e049357..edf8b54182 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -132,11 +132,21 @@ def block_environment(self) -> Any: Create the environment for the transaction. The keyword arguments are adjusted according to the fork. """ + # Set up global oracle for Osaka fork + if self.fork.is_after_fork("ethereum.osaka"): + from ethereum.state_oracle import ( + MemoryMerkleOracle, + set_state_oracle, + ) + + set_state_oracle(MemoryMerkleOracle(self.alloc.state)) + kw_arguments = { "block_hashes": self.env.block_hashes, "coinbase": self.env.coinbase, "number": self.env.block_number, "time": self.env.block_timestamp, + # TODO: Remove this, since its not being used. "state": self.alloc.state, "block_gas_limit": self.env.block_gas_limit, "chain_id": self.chain_id,