diff --git a/src/ethereum/arrow_glacier/fork.py b/src/ethereum/arrow_glacier/fork.py index c6fd9d4291..a6faa5429a 100644 --- a/src/ethereum/arrow_glacier/fork.py +++ b/src/ethereum/arrow_glacier/fork.py @@ -21,17 +21,22 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, @@ -44,10 +49,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -157,33 +163,42 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + difficulty=block.header.difficulty, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -255,7 +270,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -268,11 +283,27 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -384,24 +415,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - base_fee_per_gas: Uint, - gas_available: Uint, - chain_id: U64, ) -> Tuple[Address, Uint]: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - base_fee_per_gas : - The block base fee. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -415,32 +443,43 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, FeeMarketTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address, effective_gas_price def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -474,46 +513,11 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -526,98 +530,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. - """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address, effective_gas_price = check_transaction( - tx, base_fee_per_gas, gas_available, chain_id - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -656,10 +589,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -741,8 +671,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -757,103 +690,116 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - max_gas_fee: Uint - if isinstance(tx, FeeMarketTransaction): - max_gas_fee = Uint(tx.gas) * Uint(tx.max_fee_per_gas) - else: - max_gas_fee = Uint(tx.gas) * Uint(tx.gas_price) - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender_account = get_account(block_env.state, sender) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() + access_list_addresses = set() + access_list_storage_keys = set() if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/arrow_glacier/state.py b/src/ethereum/arrow_glacier/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/arrow_glacier/state.py +++ b/src/ethereum/arrow_glacier/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/arrow_glacier/transactions.py b/src/ethereum/arrow_glacier/transactions.py index 27cc083538..b853357506 100644 --- a/src/ethereum/arrow_glacier/transactions.py +++ b/src/ethereum/arrow_glacier/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -177,10 +177,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -191,15 +191,15 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -380,3 +380,22 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/arrow_glacier/utils/message.py b/src/ethereum/arrow_glacier/utils/message.py index c1268d4f6a..34461d7656 100644 --- a/src/ethereum/arrow_glacier/utils/message.py +++ b/src/ethereum/arrow_glacier/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this arrow_glacier version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/arrow_glacier/vm/__init__.py b/src/ethereum/arrow_glacier/vm/__init__.py index 0194e6ec10..245a05e454 100644 --- a/src/ethereum/arrow_glacier/vm/__init__.py +++ b/src/ethereum/arrow_glacier/vm/__init__.py @@ -13,40 +13,83 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -56,6 +99,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -81,7 +126,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -91,7 +135,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -113,7 +157,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -142,7 +186,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/arrow_glacier/vm/instructions/block.py b/src/ethereum/arrow_glacier/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/block.py +++ b/src/ethereum/arrow_glacier/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/arrow_glacier/vm/instructions/environment.py b/src/ethereum/arrow_glacier/vm/instructions/environment.py index dc59e168bd..172ce97d70 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/environment.py +++ b/src/ethereum/arrow_glacier/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -533,7 +535,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/arrow_glacier/vm/instructions/storage.py b/src/ethereum/arrow_glacier/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/storage.py +++ b/src/ethereum/arrow_glacier/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/arrow_glacier/vm/instructions/system.py b/src/ethereum/arrow_glacier/vm/instructions/system.py index 961b3e0c00..7a2f1efeb3 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/system.py +++ b/src/ethereum/arrow_glacier/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -80,7 +84,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -92,19 +96,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -120,7 +124,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -157,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -273,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -290,7 +298,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -346,7 +354,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -362,7 +370,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -436,7 +444,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -481,8 +489,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -491,23 +502,29 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/arrow_glacier/vm/interpreter.py b/src/ethereum/arrow_glacier/vm/interpreter.py index 1ca1087fad..7f48846c36 100644 --- a/src/ethereum/arrow_glacier/vm/interpreter.py +++ b/src/ethereum/arrow_glacier/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -83,13 +84,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -161,11 +160,12 @@ def process_create_message(message: Message, env: Environment) -> Evm: Returns ------- - evm: :py:class:`~ethereum.london.vm.Evm` + evm: :py:class:`~ethereum.arrow_glacier.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +174,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +194,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -219,33 +219,34 @@ def process_message(message: Message, env: Environment) -> Evm: Returns ------- - evm: :py:class:`~ethereum.london.vm.Evm` + evm: :py:class:`~ethereum.arrow_glacier.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +271,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/berlin/fork.py b/src/ethereum/berlin/fork.py index 653995ca92..662974467f 100644 --- a/src/ethereum/berlin/fork.py +++ b/src/ethereum/berlin/fork.py @@ -11,7 +11,6 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass from typing import List, Optional, Set, Tuple, Union @@ -21,17 +20,22 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, @@ -43,10 +47,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -154,32 +159,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -189,7 +203,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -202,11 +216,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -307,21 +340,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -333,16 +366,27 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -376,45 +420,11 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -427,93 +437,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. - """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs + process_transaction(block_env, block_output, tx, Uint(i)) - pay_rewards(state, block_number, coinbase, ommers) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -552,10 +496,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -637,8 +578,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -653,87 +597,108 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() + access_list_addresses = set() + access_list_storage_keys = set() if isinstance(tx, AccessListTransaction): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/berlin/state.py b/src/ethereum/berlin/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/berlin/state.py +++ b/src/ethereum/berlin/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/berlin/transactions.py b/src/ethereum/berlin/transactions.py index 73b00dae30..3a3b4bd749 100644 --- a/src/ethereum/berlin/transactions.py +++ b/src/ethereum/berlin/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -149,10 +149,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -163,15 +163,15 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, AccessListTransaction): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -316,3 +316,22 @@ def signing_hash_2930(tx: AccessListTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/berlin/utils/message.py b/src/ethereum/berlin/utils/message.py index 122d127a1c..caeecac1fc 100644 --- a/src/ethereum/berlin/utils/message.py +++ b/src/ethereum/berlin/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this berlin version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/berlin/vm/__init__.py b/src/ethereum/berlin/vm/__init__.py index 6c8afc96e6..76276e86fc 100644 --- a/src/ethereum/berlin/vm/__init__.py +++ b/src/ethereum/berlin/vm/__init__.py @@ -13,39 +13,82 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -55,6 +98,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -80,7 +125,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -90,7 +134,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -112,7 +156,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -141,7 +185,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/berlin/vm/instructions/block.py b/src/ethereum/berlin/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/berlin/vm/instructions/block.py +++ b/src/ethereum/berlin/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/berlin/vm/instructions/environment.py b/src/ethereum/berlin/vm/instructions/environment.py index 0c2e50ab2c..a3807aa28b 100644 --- a/src/ethereum/berlin/vm/instructions/environment.py +++ b/src/ethereum/berlin/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) diff --git a/src/ethereum/berlin/vm/instructions/storage.py b/src/ethereum/berlin/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/berlin/vm/instructions/storage.py +++ b/src/ethereum/berlin/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/berlin/vm/instructions/system.py b/src/ethereum/berlin/vm/instructions/system.py index 5f1be5a398..a561423a0f 100644 --- a/src/ethereum/berlin/vm/instructions/system.py +++ b/src/ethereum/berlin/vm/instructions/system.py @@ -72,6 +72,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -81,7 +85,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -93,19 +97,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -121,7 +125,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -158,7 +162,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -274,8 +280,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -291,7 +299,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -347,7 +355,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -363,7 +371,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -437,7 +445,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -482,8 +490,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -502,23 +513,30 @@ def selfdestruct(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + originator = evm.message.current_target + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/berlin/vm/interpreter.py b/src/ethereum/berlin/vm/interpreter.py index 1050727b5c..058eaabf83 100644 --- a/src/ethereum/berlin/vm/interpreter.py +++ b/src/ethereum/berlin/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -82,13 +83,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -98,28 +97,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -147,7 +146,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -163,8 +162,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.berlin.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -173,15 +173,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -190,19 +190,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -218,30 +218,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.berlin.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -266,7 +267,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/byzantium/fork.py b/src/ethereum/byzantium/fork.py index dff26a8483..bd924561d1 100644 --- a/src/ethereum/byzantium/fork.py +++ b/src/ethereum/byzantium/fork.py @@ -11,34 +11,42 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass from typing import List, Optional, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -146,32 +154,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -181,7 +198,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -194,11 +211,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -299,21 +335,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -325,16 +361,26 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Receipt: @@ -343,8 +389,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -368,45 +412,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -419,90 +429,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -541,10 +488,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -626,8 +570,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -642,77 +589,93 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/byzantium/state.py b/src/ethereum/byzantium/state.py index eefb7bff4e..1c14d581a8 100644 --- a/src/ethereum/byzantium/state.py +++ b/src/ethereum/byzantium/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -571,3 +571,20 @@ def increase_balance(account: Account) -> None: account.balance += amount modify_state(state, address, increase_balance) + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/byzantium/transactions.py b/src/ethereum/byzantium/transactions.py index 1c67eeaf02..21ffbc1b6f 100644 --- a/src/ethereum/byzantium/transactions.py +++ b/src/ethereum/byzantium/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -157,6 +157,7 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) ) + return Address(keccak256(public_key)[12:32]) @@ -219,3 +220,18 @@ def signing_hash_155(tx: Transaction, chain_id: U64) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/byzantium/utils/message.py b/src/ethereum/byzantium/utils/message.py index 7e79326319..b00501e453 100644 --- a/src/ethereum/byzantium/utils/message.py +++ b/src/ethereum/byzantium/utils/message.py @@ -12,87 +12,68 @@ Message specific functions used in this byzantium version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.byzantium.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, parent_evm=None, ) diff --git a/src/ethereum/byzantium/vm/__init__.py b/src/ethereum/byzantium/vm/__init__.py index 4dcea68be8..81df1e3ad4 100644 --- a/src/ethereum/byzantium/vm/__init__.py +++ b/src/ethereum/byzantium/vm/__init__.py @@ -13,38 +13,80 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import Transaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -54,6 +96,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -77,7 +121,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -87,7 +130,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: @@ -107,7 +150,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) @@ -134,7 +177,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/byzantium/vm/instructions/block.py b/src/ethereum/byzantium/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/byzantium/vm/instructions/block.py +++ b/src/ethereum/byzantium/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/byzantium/vm/instructions/environment.py b/src/ethereum/byzantium/vm/instructions/environment.py index 5a18a1c86d..561efd63da 100644 --- a/src/ethereum/byzantium/vm/instructions/environment.py +++ b/src/ethereum/byzantium/vm/instructions/environment.py @@ -75,7 +75,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -101,7 +101,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -312,7 +312,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -335,8 +335,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -371,7 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/byzantium/vm/instructions/storage.py b/src/ethereum/byzantium/vm/instructions/storage.py index bb8596bbd7..bc1e9b5a2c 100644 --- a/src/ethereum/byzantium/vm/instructions/storage.py +++ b/src/ethereum/byzantium/vm/instructions/storage.py @@ -45,7 +45,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -68,7 +70,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -80,7 +83,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/byzantium/vm/instructions/system.py b/src/ethereum/byzantium/vm/instructions/system.py index 52388abb2b..827346bc97 100644 --- a/src/ethereum/byzantium/vm/instructions/system.py +++ b/src/ethereum/byzantium/vm/instructions/system.py @@ -83,11 +83,13 @@ def create(evm: Evm) -> None: evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -98,18 +100,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -123,7 +131,7 @@ def create(evm: Evm) -> None: is_static=False, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -201,8 +209,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -216,7 +226,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -266,7 +276,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if value == 0 or is_account_alive(evm.env.state, to) + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -282,7 +292,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -349,7 +359,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -390,8 +400,11 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -410,23 +423,29 @@ def selfdestruct(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/byzantium/vm/interpreter.py b/src/ethereum/byzantium/vm/interpreter.py index 70576c537d..7a6433beaf 100644 --- a/src/ethereum/byzantium/vm/interpreter.py +++ b/src/ethereum/byzantium/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -46,7 +47,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -81,13 +82,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -97,28 +96,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -146,7 +145,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -162,8 +161,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.byzantium.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -171,10 +171,10 @@ def process_create_message(message: Message, env: Environment) -> Evm: # * The address created by two `CREATE` calls collide. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -183,19 +183,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -211,30 +211,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.byzantium.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -259,7 +260,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 22d7d5a532..7932db33d3 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -16,16 +16,20 @@ from typing import List, Optional, Tuple, Union from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root, VersionedHash +from .fork_types import Account, Address, VersionedHash from .state import ( State, TransientStorage, @@ -34,7 +38,7 @@ destroy_touched_empty_accounts, get_account, increment_nonce, - process_withdrawal, + modify_state, set_account_balance, state_root, ) @@ -46,10 +50,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.hexadecimal import hex_to_address from .utils.message import prepare_message from .vm import Message @@ -170,44 +175,50 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - excess_blob_gas = calculate_excess_blob_gas(parent_header) - if block.header.excess_blob_gas != excess_blob_gas: - raise InvalidBlock - - validate_header(block.header, parent_header) + validate_header(chain, block.header) if block.ommers != (): raise InvalidBlock - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.prev_randao, - block.transactions, - chain.chain_id, - block.withdrawals, - block.header.parent_beacon_block_root, - excess_blob_gas, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + excess_blob_gas=block.header.excess_blob_gas, + parent_beacon_block_root=block.header.parent_beacon_block_root, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock - if apply_body_output.withdrawals_root != block.header.withdrawals_root: + if withdrawals_root != block.header.withdrawals_root: raise InvalidBlock - if apply_body_output.blob_gas_used != block.header.blob_gas_used: + if block_output.blob_gas_used != block.header.blob_gas_used: raise InvalidBlock chain.blocks.append(block) @@ -279,7 +290,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -292,11 +303,31 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + excess_blob_gas = calculate_excess_blob_gas(parent_header) + if header.excess_blob_gas != excess_blob_gas: + raise InvalidBlock + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -327,30 +358,21 @@ def validate_header(header: Header, parent_header: Header) -> None: def check_transaction( - state: State, + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, - base_fee_per_gas: Uint, - excess_blob_gas: U64, -) -> Tuple[Address, Uint, Tuple[VersionedHash, ...]]: +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], Uint]: """ Check if the transaction is includable in the block. Parameters ---------- - state : - Current state. + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. - base_fee_per_gas : - The block base fee. - excess_blob_gas : - The excess blob gas. Returns ------- @@ -360,31 +382,41 @@ def check_transaction( The price to charge for gas when the transaction is executed. blob_versioned_hashes : The blob versioned hashes of the transaction. + tx_blob_gas_used: + The blob gas used by the transaction. Raises ------ InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) - sender_account = get_account(state, sender_address) + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise InvalidBlock + + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price max_gas_fee = tx.gas * tx.gas_price @@ -398,7 +430,7 @@ def check_transaction( if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: raise InvalidBlock - blob_gas_price = calculate_blob_gas_price(excess_blob_gas) + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: raise InvalidBlock @@ -412,15 +444,20 @@ def check_transaction( raise InvalidBlock if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): raise InvalidBlock - if sender_account.code != bytearray(): + if sender_account.code: raise InvalidSenderError("not EOA") - return sender_address, effective_gas_price, blob_versioned_hashes + return ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -454,91 +491,46 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - withdrawals_root : `ethereum.fork_types.Root` - Trie root of all the withdrawals in the block. - blob_gas_used : `ethereum.base_types.Uint` - Total blob gas used in the block. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - withdrawals_root: Root - blob_gas_used: Uint - - def process_system_transaction( + block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, - state: State, - chain_id: U64, - excess_blob_gas: U64, ) -> MessageCallOutput: """ Process a system transaction. Parameters ---------- + block_env : + The block scoped environment. target_address : Address of the contract to call. data : Data to pass to the contract. - block_hashes : - List of hashes of the previous 256 blocks. - coinbase : - Address of the block's coinbase. - block_number : - Block number. - base_fee_per_gas : - Base fee per gas. - block_gas_limit : - Gas limit of the block. - block_time : - Time the block was produced. - prev_randao : - Previous randao value. - state : - Current state. - chain_id : - ID of the chain. - excess_blob_gas : - Excess blob gas. Returns ------- system_tx_output : `MessageCallOutput` Output of processing the system transaction. """ - system_contract_code = get_account(state, target_address).code + system_contract_code = get_account(block_env.state, target_address).code + + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + access_list_addresses=set(), + access_list_storage_keys=set(), + transient_storage=TransientStorage(), + blob_versioned_hashes=(), + index_in_block=None, + tx_hash=None, + traces=[], + ) system_tx_message = Message( + block_env=block_env, + tx_env=tx_env, caller=SYSTEM_ADDRESS, target=target_address, gas=SYSTEM_TRANSACTION_GAS, @@ -555,26 +547,7 @@ def process_system_transaction( parent_evm=None, ) - system_tx_env = vm.Environment( - caller=SYSTEM_ADDRESS, - block_hashes=block_hashes, - origin=SYSTEM_ADDRESS, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=base_fee_per_gas, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - excess_blob_gas=excess_blob_gas, - blob_versioned_hashes=(), - transient_storage=TransientStorage(), - ) - - system_tx_output = process_message_call(system_tx_message, system_tx_env) + system_tx_output = process_message_call(system_tx_message) # TODO: Empty accounts in post-merge forks are impossible # see Ethereum Improvement Proposal 7523. @@ -582,27 +555,17 @@ def process_system_transaction( # and will have to be removed in the future. # See https://github.com/ethereum/execution-specs/issues/955 destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts + block_env.state, system_tx_output.touched_accounts ) return system_tx_output def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], - chain_id: U64, withdrawals: Tuple[Withdrawal, ...], - parent_beacon_block_root: Root, - excess_blob_gas: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -615,150 +578,40 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - prev_randao : - The previous randao from the beacon chain. + block_env : + The block scoped environment. transactions : Transactions included in the block. - ommers : - Headers of ancestor blocks which are not direct parents (formerly - uncles.) - chain_id : - ID of the executing chain. withdrawals : Withdrawals to be processed in the current block. - parent_beacon_block_root : - The root of the beacon block from the parent block. - excess_blob_gas : - Excess blob gas calculated from the previous block. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - blob_gas_used = Uint(0) - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() process_system_transaction( - BEACON_ROOTS_ADDRESS, - parent_beacon_block_root, - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - state, - chain_id, - excess_blob_gas, + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, ) for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - ( - sender_address, - effective_gas_price, - blob_versioned_hashes, - ) = check_transaction( - state, - tx, - gas_available, - chain_id, - base_fee_per_gas, - excess_blob_gas, - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - excess_blob_gas=excess_blob_gas, - blob_versioned_hashes=blob_versioned_hashes, - transient_storage=TransientStorage(), - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs - blob_gas_used += calculate_total_blob_gas(tx) - if blob_gas_used > MAX_BLOB_GAS_PER_BLOCK: - raise InvalidBlock - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) + process_transaction(block_env, block_output, tx, Uint(i)) - for i, wd in enumerate(withdrawals): - trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd)) + process_withdrawals(block_env, block_output, withdrawals) - process_withdrawal(state, wd) - - if account_exists_and_is_empty(state, wd.address): - destroy_account(state, wd.address) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - root(withdrawals_trie), - blob_gas_used, - ) + return block_output def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -773,97 +626,154 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) if isinstance(tx, BlobTransaction): - blob_gas_fee = calculate_data_fee(env.excess_blob_gas, tx) + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) else: blob_gas_fee = Uint(0) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() - preaccessed_addresses.add(env.coinbase) + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) if isinstance( tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) ): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + transient_storage=TransientStorage(), + blob_versioned_hashes=blob_versioned_hashes, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) + + block_output.block_gas_used += tx_gas_used + block_output.blob_gas_used += tx_blob_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) + + block_output.block_logs += tx_output.logs + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * U256(10**9) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) - destroy_touched_empty_accounts(env.state, output.touched_accounts) + modify_state(block_env.state, wd.address, increase_recipient_balance) - return total_gas_used, output.logs, output.error + if account_exists_and_is_empty(block_env.state, wd.address): + destroy_account(block_env.state, wd.address) def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/cancun/state.py b/src/ethereum/cancun/state.py index 1cb9cdcdc7..54c54593c3 100644 --- a/src/ethereum/cancun/state.py +++ b/src/ethereum/cancun/state.py @@ -23,7 +23,6 @@ from ethereum_types.frozen import modify from ethereum_types.numeric import U256, Uint -from .blocks import Withdrawal from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set @@ -533,20 +532,6 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, recipient_address, increase_recipient_balance) -def process_withdrawal( - state: State, - wd: Withdrawal, -) -> None: - """ - Increase the balance of the withdrawing account. - """ - - def increase_recipient_balance(recipient: Account) -> None: - recipient.balance += wd.amount * U256(10**9) - - modify_state(state, wd.address, increase_recipient_balance) - - def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. diff --git a/src/ethereum/cancun/transactions.py b/src/ethereum/cancun/transactions.py index bd9167e9df..b2e5c47ffd 100644 --- a/src/ethereum/cancun/transactions.py +++ b/src/ethereum/cancun/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address, VersionedHash -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -212,12 +212,12 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ from .vm.gas import init_code_cost - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -226,19 +226,19 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: data_cost += TX_DATA_COST_PER_NON_ZERO if tx.to == Bytes0(b""): - create_cost = TX_CREATE_COST + int(init_code_cost(Uint(len(tx.data)))) + create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance( tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) ): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -457,3 +457,22 @@ def signing_hash_4844(tx: BlobTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/cancun/utils/message.py b/src/ethereum/cancun/utils/message.py index 68deb1a62f..37aff9a59a 100644 --- a/src/ethereum/cancun/utils/message.py +++ b/src/ethereum/cancun/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this cancun version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/cancun/vm/__init__.py b/src/ethereum/cancun/vm/__init__.py index 04bb7a353a..be19e8081b 100644 --- a/src/ethereum/cancun/vm/__init__.py +++ b/src/ethereum/cancun/vm/__init__.py @@ -13,44 +13,96 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, VersionedHash from ..state import State, TransientStorage, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 prev_randao: Bytes32 - state: State - chain_id: U64 - traces: List[dict] excess_blob_gas: U64 - blob_versioned_hashes: Tuple[VersionedHash, ...] + parent_beacon_block_root: Hash32 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + blob_gas_used : `ethereum.base_types.Uint` + Total blob gas used in the block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + blob_gas_used: Uint = Uint(0) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] transient_storage: TransientStorage + blob_versioned_hashes: Tuple[VersionedHash, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + traces: List[dict] @dataclass @@ -59,6 +111,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -84,7 +138,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -94,7 +147,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -116,7 +169,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -145,7 +198,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/cancun/vm/instructions/block.py b/src/ethereum/cancun/vm/instructions/block.py index 2914c802c9..42c95480cf 100644 --- a/src/ethereum/cancun/vm/instructions/block.py +++ b/src/ethereum/cancun/vm/instructions/block.py @@ -44,13 +44,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -85,7 +91,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -118,7 +124,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -150,7 +156,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -182,7 +188,7 @@ def prev_randao(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -214,7 +220,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -243,7 +249,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/cancun/vm/instructions/environment.py b/src/ethereum/cancun/vm/instructions/environment.py index 6a524f1496..5ddd12dac8 100644 --- a/src/ethereum/cancun/vm/instructions/environment.py +++ b/src/ethereum/cancun/vm/instructions/environment.py @@ -85,7 +85,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -111,7 +111,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -322,7 +322,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -351,7 +351,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -393,7 +393,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -479,7 +479,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -511,7 +511,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -536,7 +538,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -559,8 +561,8 @@ def blob_hash(evm: Evm) -> None: charge_gas(evm, GAS_BLOBHASH_OPCODE) # OPERATION - if int(index) < len(evm.env.blob_versioned_hashes): - blob_hash = evm.env.blob_versioned_hashes[index] + if int(index) < len(evm.message.tx_env.blob_versioned_hashes): + blob_hash = evm.message.tx_env.blob_versioned_hashes[index] else: blob_hash = Bytes32(b"\x00" * 32) push(evm.stack, U256.from_be_bytes(blob_hash)) @@ -586,7 +588,9 @@ def blob_base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - blob_base_fee = calculate_blob_gas_price(evm.env.excess_blob_gas) + blob_base_fee = calculate_blob_gas_price( + evm.message.block_env.excess_blob_gas + ) push(evm.stack, U256(blob_base_fee)) # PROGRAM COUNTER diff --git a/src/ethereum/cancun/vm/instructions/storage.py b/src/ethereum/cancun/vm/instructions/storage.py index f88e295736..65a0d5a9b6 100644 --- a/src/ethereum/cancun/vm/instructions/storage.py +++ b/src/ethereum/cancun/vm/instructions/storage.py @@ -56,7 +56,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -80,10 +82,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -123,7 +126,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) @@ -146,7 +149,7 @@ def tload(evm: Evm) -> None: # OPERATION value = get_transient_storage( - evm.env.transient_storage, evm.message.current_target, key + evm.message.tx_env.transient_storage, evm.message.current_target, key ) push(evm.stack, value) @@ -171,7 +174,10 @@ def tstore(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext set_transient_storage( - evm.env.transient_storage, evm.message.current_target, key, new_value + evm.message.tx_env.transient_storage, + evm.message.current_target, + key, + new_value, ) # PROGRAM COUNTER diff --git a/src/ethereum/cancun/vm/instructions/system.py b/src/ethereum/cancun/vm/instructions/system.py index 58cec71f07..ff473fc285 100644 --- a/src/ethereum/cancun/vm/instructions/system.py +++ b/src/ethereum/cancun/vm/instructions/system.py @@ -93,7 +93,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -105,15 +105,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -129,7 +133,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -167,7 +171,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -297,8 +303,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -314,7 +322,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -370,7 +378,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -386,7 +394,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -460,7 +468,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -505,8 +513,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -515,10 +526,12 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - originator_balance = get_account(evm.env.state, originator).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance move_ether( - evm.env.state, + evm.message.block_env.state, originator, beneficiary, originator_balance, @@ -526,14 +539,14 @@ def selfdestruct(evm: Evm) -> None: # register account for deletion only if it was created # in the same transaction - if originator in evm.env.state.created_accounts: + if originator in evm.message.block_env.state.created_accounts: # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/cancun/vm/interpreter.py b/src/ethereum/cancun/vm/interpreter.py index c743ade3c5..3994bc693f 100644 --- a/src/ethereum/cancun/vm/interpreter.py +++ b/src/ethereum/cancun/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple, Union +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -81,15 +82,13 @@ class MessageCallOutput: gas_left: Uint refund_counter: U256 - logs: Union[Tuple[()], Tuple[Log, ...]] + logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -164,8 +163,10 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.cancun.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage # take snapshot of state before processing the message - begin_transaction(env.state, env.transient_storage) + begin_transaction(state, transient_storage) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +175,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +195,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state, env.transient_storage) + set_code(state, message.current_target, contract_code) + commit_transaction(state, transient_storage) else: - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -222,30 +223,32 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.cancun.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state, env.transient_storage) + begin_transaction(state, transient_storage) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) else: - commit_transaction(env.state, env.transient_storage) + commit_transaction(state, transient_storage) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +273,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/constantinople/fork.py b/src/ethereum/constantinople/fork.py index fb57e0f5ba..02d1e3f75b 100644 --- a/src/ethereum/constantinople/fork.py +++ b/src/ethereum/constantinople/fork.py @@ -11,34 +11,42 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass from typing import List, Optional, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -146,32 +154,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -181,7 +198,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -194,11 +211,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -299,21 +335,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -325,16 +361,26 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Receipt: @@ -343,8 +389,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -368,45 +412,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -419,90 +429,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -541,10 +488,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -626,8 +570,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -642,77 +589,93 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/constantinople/state.py b/src/ethereum/constantinople/state.py index eefb7bff4e..1c14d581a8 100644 --- a/src/ethereum/constantinople/state.py +++ b/src/ethereum/constantinople/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -571,3 +571,20 @@ def increase_balance(account: Account) -> None: account.balance += amount modify_state(state, address, increase_balance) + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/constantinople/transactions.py b/src/ethereum/constantinople/transactions.py index 1c67eeaf02..21ffbc1b6f 100644 --- a/src/ethereum/constantinople/transactions.py +++ b/src/ethereum/constantinople/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -157,6 +157,7 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) ) + return Address(keccak256(public_key)[12:32]) @@ -219,3 +220,18 @@ def signing_hash_155(tx: Transaction, chain_id: U64) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/constantinople/utils/message.py b/src/ethereum/constantinople/utils/message.py index 147654e802..a47395dbf4 100644 --- a/src/ethereum/constantinople/utils/message.py +++ b/src/ethereum/constantinople/utils/message.py @@ -12,87 +12,68 @@ Message specific functions used in this constantinople version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.constantinople.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, parent_evm=None, ) diff --git a/src/ethereum/constantinople/vm/__init__.py b/src/ethereum/constantinople/vm/__init__.py index 4dcea68be8..81df1e3ad4 100644 --- a/src/ethereum/constantinople/vm/__init__.py +++ b/src/ethereum/constantinople/vm/__init__.py @@ -13,38 +13,80 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import Transaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -54,6 +96,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -77,7 +121,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -87,7 +130,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: @@ -107,7 +150,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) @@ -134,7 +177,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/constantinople/vm/instructions/block.py b/src/ethereum/constantinople/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/constantinople/vm/instructions/block.py +++ b/src/ethereum/constantinople/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/constantinople/vm/instructions/environment.py b/src/ethereum/constantinople/vm/instructions/environment.py index af92af6891..77ebb8c5fb 100644 --- a/src/ethereum/constantinople/vm/instructions/environment.py +++ b/src/ethereum/constantinople/vm/instructions/environment.py @@ -78,7 +78,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -104,7 +104,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -315,7 +315,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -338,7 +338,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -373,7 +373,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -453,7 +453,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, GAS_CODE_HASH) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) diff --git a/src/ethereum/constantinople/vm/instructions/storage.py b/src/ethereum/constantinople/vm/instructions/storage.py index bb8596bbd7..bc1e9b5a2c 100644 --- a/src/ethereum/constantinople/vm/instructions/storage.py +++ b/src/ethereum/constantinople/vm/instructions/storage.py @@ -45,7 +45,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -68,7 +70,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -80,7 +83,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/constantinople/vm/instructions/system.py b/src/ethereum/constantinople/vm/instructions/system.py index 0c7608d40d..eaa8a98601 100644 --- a/src/ethereum/constantinople/vm/instructions/system.py +++ b/src/ethereum/constantinople/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) evm.gas_left -= create_message_gas if evm.message.is_static: @@ -78,7 +82,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -90,9 +94,11 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return @@ -100,9 +106,11 @@ def generic_create( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -116,7 +124,7 @@ def generic_create( is_static=False, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -153,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -269,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -284,7 +296,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -334,7 +346,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if value == 0 or is_account_alive(evm.env.state, to) + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -350,7 +362,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -417,7 +429,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -458,8 +470,11 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -478,23 +493,30 @@ def selfdestruct(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + originator = evm.message.current_target + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/constantinople/vm/interpreter.py b/src/ethereum/constantinople/vm/interpreter.py index 72679f5de1..fe3bb59cbb 100644 --- a/src/ethereum/constantinople/vm/interpreter.py +++ b/src/ethereum/constantinople/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -46,7 +47,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -81,13 +82,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -97,28 +96,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -146,7 +145,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -162,8 +161,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.constantinople.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -172,10 +172,10 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -184,19 +184,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -212,30 +212,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.constantinople.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -260,7 +261,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/dao_fork/fork.py b/src/ethereum/dao_fork/fork.py index 7b653c5bc2..20bc645ef9 100644 --- a/src/ethereum/dao_fork/fork.py +++ b/src/ethereum/dao_fork/fork.py @@ -13,12 +13,11 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -29,7 +28,7 @@ from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom from .dao import apply_dao -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, create_ether, @@ -39,8 +38,13 @@ set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -154,31 +158,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -188,7 +202,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -201,11 +215,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -311,18 +344,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. Returns ------- @@ -334,15 +370,25 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock sender_address = recover_sender(tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, post_state: Bytes32, cumulative_gas_used: Uint, logs: Tuple[Log, ...], @@ -352,8 +398,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -377,44 +421,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -427,21 +438,8 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : @@ -450,65 +448,17 @@ def apply_body( Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, state_root(state), (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs - - pay_rewards(state, block_number, coinbase, ommers) + process_transaction(block_env, block_output, tx, Uint(i)) - block_gas_used = block_gas_limit - gas_available + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -547,10 +497,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -632,8 +579,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -648,70 +598,88 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee ) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + state_root(block_env.state), + block_output.block_gas_used, + tx_output.logs, + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/dao_fork/transactions.py b/src/ethereum/dao_fork/transactions.py index 96d825e2ba..6db00e736d 100644 --- a/src/ethereum/dao_fork/transactions.py +++ b/src/ethereum/dao_fork/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(tx: Transaction) -> Address: @@ -180,3 +180,18 @@ def signing_hash(tx: Transaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/dao_fork/utils/message.py b/src/ethereum/dao_fork/utils/message.py index 43792fed6d..8ab3cd0ffa 100644 --- a/src/ethereum/dao_fork/utils/message.py +++ b/src/ethereum/dao_fork/utils/message.py @@ -11,82 +11,67 @@ Message specific functions used in this Dao Fork version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.dao_fork.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, + should_transfer_value=True, parent_evm=None, ) diff --git a/src/ethereum/dao_fork/vm/__init__.py b/src/ethereum/dao_fork/vm/__init__.py index f2945ad891..d351223871 100644 --- a/src/ethereum/dao_fork/vm/__init__.py +++ b/src/ethereum/dao_fork/vm/__init__.py @@ -13,37 +13,79 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State +from ..transactions import Transaction +from ..trie import Trie __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -53,6 +95,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -75,7 +119,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -83,7 +126,7 @@ class Evm: message: Message output: Bytes accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: diff --git a/src/ethereum/dao_fork/vm/instructions/block.py b/src/ethereum/dao_fork/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/dao_fork/vm/instructions/block.py +++ b/src/ethereum/dao_fork/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/dao_fork/vm/instructions/environment.py b/src/ethereum/dao_fork/vm/instructions/environment.py index 1bce21fda7..36215ecb1a 100644 --- a/src/ethereum/dao_fork/vm/instructions/environment.py +++ b/src/ethereum/dao_fork/vm/instructions/environment.py @@ -73,7 +73,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -99,7 +99,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -310,7 +310,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -333,8 +333,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -369,7 +368,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/dao_fork/vm/instructions/storage.py b/src/ethereum/dao_fork/vm/instructions/storage.py index 7b299e07d0..76c11bcfe4 100644 --- a/src/ethereum/dao_fork/vm/instructions/storage.py +++ b/src/ethereum/dao_fork/vm/instructions/storage.py @@ -44,7 +44,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -67,7 +69,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -79,7 +82,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/dao_fork/vm/instructions/system.py b/src/ethereum/dao_fork/vm/instructions/system.py index 1e87e93319..4637a10c1d 100644 --- a/src/ethereum/dao_fork/vm/instructions/system.py +++ b/src/ethereum/dao_fork/vm/instructions/system.py @@ -73,11 +73,13 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -88,18 +90,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -112,7 +120,7 @@ def create(evm: Evm) -> None: should_transfer_value=True, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -185,8 +193,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -199,7 +209,7 @@ def generic_call( should_transfer_value=should_transfer_value, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -246,14 +256,14 @@ def call(evm: Evm) -> None: code_address = to message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -306,14 +316,14 @@ def callcode(evm: Evm) -> None: ], ) message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -366,17 +376,23 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) diff --git a/src/ethereum/dao_fork/vm/interpreter.py b/src/ethereum/dao_fork/vm/interpreter.py index a1a29f231d..f2241d0459 100644 --- a/src/ethereum/dao_fork/vm/interpreter.py +++ b/src/ethereum/dao_fork/vm/interpreter.py @@ -17,6 +17,7 @@ from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -44,7 +45,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -75,12 +76,10 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -90,27 +89,25 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) + evm = process_message(message) if evm.error: logs: Tuple[Log, ...] = () @@ -134,7 +131,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -150,35 +147,36 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.dao_fork.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely # circumstances: # * The address created by two `CREATE` calls collide. # * The first `CREATE` left empty code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - evm = process_message(message, env) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT try: charge_gas(evm, contract_code_gas) except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -194,30 +192,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.dao_fork.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -242,7 +241,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/frontier/fork.py b/src/ethereum/frontier/fork.py index 3e96a9aa73..3127970d36 100644 --- a/src/ethereum/frontier/fork.py +++ b/src/ethereum/frontier/fork.py @@ -13,10 +13,10 @@ """ from dataclasses import dataclass -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -26,7 +26,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, create_ether, @@ -36,8 +36,13 @@ set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -143,31 +148,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -177,7 +192,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -190,11 +205,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -293,18 +327,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. Returns ------- @@ -316,15 +353,25 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock sender_address = recover_sender(tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, post_state: Bytes32, cumulative_gas_used: Uint, logs: Tuple[Log, ...], @@ -334,8 +381,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -359,44 +404,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -409,21 +421,8 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : @@ -432,65 +431,17 @@ def apply_body( Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, state_root(state), (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs + process_transaction(block_env, block_output, tx, Uint(i)) - pay_rewards(state, block_number, coinbase, ommers) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -529,10 +480,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -614,8 +562,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -630,70 +581,88 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee ) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + state_root(block_env.state), + block_output.block_gas_used, + tx_output.logs, + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/frontier/transactions.py b/src/ethereum/frontier/transactions.py index 396765e22c..e9801dfafe 100644 --- a/src/ethereum/frontier/transactions.py +++ b/src/ethereum/frontier/transactions.py @@ -17,9 +17,9 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) @slotted_freezable @@ -98,10 +98,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -109,7 +109,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: else: data_cost += TX_DATA_COST_PER_NON_ZERO - return Uint(TX_BASE_COST + data_cost) + return TX_BASE_COST + data_cost def recover_sender(tx: Transaction) -> Address: @@ -174,3 +174,18 @@ def signing_hash(tx: Transaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/frontier/utils/message.py b/src/ethereum/frontier/utils/message.py index d7bf7cc180..07af3bda16 100644 --- a/src/ethereum/frontier/utils/message.py +++ b/src/ethereum/frontier/utils/message.py @@ -11,74 +11,62 @@ Message specific functions used in this frontier version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.frontier.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), diff --git a/src/ethereum/frontier/vm/__init__.py b/src/ethereum/frontier/vm/__init__.py index c8ff6f284a..5ea270ab77 100644 --- a/src/ethereum/frontier/vm/__init__.py +++ b/src/ethereum/frontier/vm/__init__.py @@ -13,37 +13,79 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State +from ..transactions import Transaction +from ..trie import Trie __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -53,6 +95,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -74,7 +118,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -82,7 +125,7 @@ class Evm: message: Message output: Bytes accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: diff --git a/src/ethereum/frontier/vm/instructions/block.py b/src/ethereum/frontier/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/frontier/vm/instructions/block.py +++ b/src/ethereum/frontier/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/frontier/vm/instructions/environment.py b/src/ethereum/frontier/vm/instructions/environment.py index 1bce21fda7..36215ecb1a 100644 --- a/src/ethereum/frontier/vm/instructions/environment.py +++ b/src/ethereum/frontier/vm/instructions/environment.py @@ -73,7 +73,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -99,7 +99,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -310,7 +310,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -333,8 +333,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -369,7 +368,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/frontier/vm/instructions/storage.py b/src/ethereum/frontier/vm/instructions/storage.py index 7b299e07d0..76c11bcfe4 100644 --- a/src/ethereum/frontier/vm/instructions/storage.py +++ b/src/ethereum/frontier/vm/instructions/storage.py @@ -44,7 +44,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -67,7 +69,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -79,7 +82,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/frontier/vm/instructions/system.py b/src/ethereum/frontier/vm/instructions/system.py index a86174de7e..0625f34a07 100644 --- a/src/ethereum/frontier/vm/instructions/system.py +++ b/src/ethereum/frontier/vm/instructions/system.py @@ -72,11 +72,13 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -87,18 +89,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -110,7 +118,7 @@ def create(evm: Evm) -> None: code_address=None, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -182,8 +190,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -195,7 +205,7 @@ def generic_call( code_address=code_address, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -242,14 +252,14 @@ def call(evm: Evm) -> None: code_address = to message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -301,14 +311,14 @@ def callcode(evm: Evm) -> None: ], ) message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -360,17 +370,23 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) diff --git a/src/ethereum/frontier/vm/interpreter.py b/src/ethereum/frontier/vm/interpreter.py index a4b8399a78..9fde7ed588 100644 --- a/src/ethereum/frontier/vm/interpreter.py +++ b/src/ethereum/frontier/vm/interpreter.py @@ -17,6 +17,7 @@ from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -44,7 +45,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -75,12 +76,10 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -90,27 +89,25 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) + evm = process_message(message) if evm.error: logs: Tuple[Log, ...] = () @@ -134,7 +131,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -150,17 +147,18 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.frontier.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely # circumstances: # * The address created by two `CREATE` calls collide. # * The first `CREATE` left empty code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - evm = process_message(message, env) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -169,14 +167,14 @@ def process_create_message(message: Message, env: Environment) -> Evm: except ExceptionalHalt: evm.output = b"" else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -192,30 +190,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.frontier.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -240,7 +239,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/gray_glacier/fork.py b/src/ethereum/gray_glacier/fork.py index 7889c1aa70..0dee77a920 100644 --- a/src/ethereum/gray_glacier/fork.py +++ b/src/ethereum/gray_glacier/fork.py @@ -21,17 +21,22 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, @@ -44,10 +49,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -157,33 +163,42 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + difficulty=block.header.difficulty, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -255,7 +270,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -268,11 +283,27 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -384,24 +415,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - base_fee_per_gas: Uint, - gas_available: Uint, - chain_id: U64, ) -> Tuple[Address, Uint]: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - base_fee_per_gas : - The block base fee. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -415,32 +443,43 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, FeeMarketTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address, effective_gas_price def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -474,46 +513,11 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -526,98 +530,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. - """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address, effective_gas_price = check_transaction( - tx, base_fee_per_gas, gas_available, chain_id - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -656,10 +589,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -741,8 +671,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -757,103 +690,116 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - max_gas_fee: Uint - if isinstance(tx, FeeMarketTransaction): - max_gas_fee = Uint(tx.gas) * Uint(tx.max_fee_per_gas) - else: - max_gas_fee = Uint(tx.gas) * Uint(tx.gas_price) - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender_account = get_account(block_env.state, sender) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() + access_list_addresses = set() + access_list_storage_keys = set() if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/gray_glacier/state.py b/src/ethereum/gray_glacier/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/gray_glacier/state.py +++ b/src/ethereum/gray_glacier/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/gray_glacier/transactions.py b/src/ethereum/gray_glacier/transactions.py index 27cc083538..b853357506 100644 --- a/src/ethereum/gray_glacier/transactions.py +++ b/src/ethereum/gray_glacier/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -177,10 +177,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -191,15 +191,15 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -380,3 +380,22 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/gray_glacier/utils/message.py b/src/ethereum/gray_glacier/utils/message.py index 73e4bc320f..ffd49e9790 100644 --- a/src/ethereum/gray_glacier/utils/message.py +++ b/src/ethereum/gray_glacier/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this gray_glacier version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/gray_glacier/vm/__init__.py b/src/ethereum/gray_glacier/vm/__init__.py index 0194e6ec10..245a05e454 100644 --- a/src/ethereum/gray_glacier/vm/__init__.py +++ b/src/ethereum/gray_glacier/vm/__init__.py @@ -13,40 +13,83 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -56,6 +99,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -81,7 +126,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -91,7 +135,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -113,7 +157,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -142,7 +186,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/gray_glacier/vm/instructions/block.py b/src/ethereum/gray_glacier/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/gray_glacier/vm/instructions/block.py +++ b/src/ethereum/gray_glacier/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/gray_glacier/vm/instructions/environment.py b/src/ethereum/gray_glacier/vm/instructions/environment.py index dc59e168bd..172ce97d70 100644 --- a/src/ethereum/gray_glacier/vm/instructions/environment.py +++ b/src/ethereum/gray_glacier/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -533,7 +535,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/gray_glacier/vm/instructions/storage.py b/src/ethereum/gray_glacier/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/gray_glacier/vm/instructions/storage.py +++ b/src/ethereum/gray_glacier/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/gray_glacier/vm/instructions/system.py b/src/ethereum/gray_glacier/vm/instructions/system.py index 961b3e0c00..7a2f1efeb3 100644 --- a/src/ethereum/gray_glacier/vm/instructions/system.py +++ b/src/ethereum/gray_glacier/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -80,7 +84,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -92,19 +96,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -120,7 +124,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -157,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -273,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -290,7 +298,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -346,7 +354,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -362,7 +370,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -436,7 +444,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -481,8 +489,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -491,23 +502,29 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/gray_glacier/vm/interpreter.py b/src/ethereum/gray_glacier/vm/interpreter.py index 1ca1087fad..50a532f9df 100644 --- a/src/ethereum/gray_glacier/vm/interpreter.py +++ b/src/ethereum/gray_glacier/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -83,13 +84,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -161,11 +160,12 @@ def process_create_message(message: Message, env: Environment) -> Evm: Returns ------- - evm: :py:class:`~ethereum.london.vm.Evm` + evm: :py:class:`~ethereum.gray_glacier.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +174,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +194,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -219,33 +219,34 @@ def process_message(message: Message, env: Environment) -> Evm: Returns ------- - evm: :py:class:`~ethereum.london.vm.Evm` + evm: :py:class:`~ethereum.gray_glacier.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +271,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/homestead/fork.py b/src/ethereum/homestead/fork.py index cf39821f14..813f5766f0 100644 --- a/src/ethereum/homestead/fork.py +++ b/src/ethereum/homestead/fork.py @@ -13,10 +13,10 @@ """ from dataclasses import dataclass -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -26,7 +26,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, create_ether, @@ -36,8 +36,13 @@ set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -143,31 +148,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -177,7 +192,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -190,11 +205,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -293,18 +327,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. Returns ------- @@ -316,15 +353,25 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock sender_address = recover_sender(tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, post_state: Bytes32, cumulative_gas_used: Uint, logs: Tuple[Log, ...], @@ -334,8 +381,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -359,44 +404,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -409,21 +421,8 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : @@ -432,65 +431,17 @@ def apply_body( Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, state_root(state), (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs + process_transaction(block_env, block_output, tx, Uint(i)) - pay_rewards(state, block_number, coinbase, ommers) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -529,10 +480,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -614,8 +562,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -630,70 +581,88 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee ) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + state_root(block_env.state), + block_output.block_gas_used, + tx_output.logs, + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/homestead/transactions.py b/src/ethereum/homestead/transactions.py index 96d825e2ba..6db00e736d 100644 --- a/src/ethereum/homestead/transactions.py +++ b/src/ethereum/homestead/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(tx: Transaction) -> Address: @@ -180,3 +180,18 @@ def signing_hash(tx: Transaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/homestead/utils/message.py b/src/ethereum/homestead/utils/message.py index 436a35840b..8eb830f2d7 100644 --- a/src/ethereum/homestead/utils/message.py +++ b/src/ethereum/homestead/utils/message.py @@ -11,82 +11,67 @@ Message specific functions used in this homestead version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.homestead.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, + should_transfer_value=True, parent_evm=None, ) diff --git a/src/ethereum/homestead/vm/__init__.py b/src/ethereum/homestead/vm/__init__.py index f2945ad891..d351223871 100644 --- a/src/ethereum/homestead/vm/__init__.py +++ b/src/ethereum/homestead/vm/__init__.py @@ -13,37 +13,79 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State +from ..transactions import Transaction +from ..trie import Trie __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -53,6 +95,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -75,7 +119,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -83,7 +126,7 @@ class Evm: message: Message output: Bytes accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: diff --git a/src/ethereum/homestead/vm/instructions/block.py b/src/ethereum/homestead/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/homestead/vm/instructions/block.py +++ b/src/ethereum/homestead/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/homestead/vm/instructions/environment.py b/src/ethereum/homestead/vm/instructions/environment.py index 1bce21fda7..36215ecb1a 100644 --- a/src/ethereum/homestead/vm/instructions/environment.py +++ b/src/ethereum/homestead/vm/instructions/environment.py @@ -73,7 +73,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -99,7 +99,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -310,7 +310,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -333,8 +333,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -369,7 +368,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/homestead/vm/instructions/storage.py b/src/ethereum/homestead/vm/instructions/storage.py index 7b299e07d0..76c11bcfe4 100644 --- a/src/ethereum/homestead/vm/instructions/storage.py +++ b/src/ethereum/homestead/vm/instructions/storage.py @@ -44,7 +44,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -67,7 +69,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -79,7 +82,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/homestead/vm/instructions/system.py b/src/ethereum/homestead/vm/instructions/system.py index 1e87e93319..4637a10c1d 100644 --- a/src/ethereum/homestead/vm/instructions/system.py +++ b/src/ethereum/homestead/vm/instructions/system.py @@ -73,11 +73,13 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -88,18 +90,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -112,7 +120,7 @@ def create(evm: Evm) -> None: should_transfer_value=True, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -185,8 +193,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -199,7 +209,7 @@ def generic_call( should_transfer_value=should_transfer_value, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -246,14 +256,14 @@ def call(evm: Evm) -> None: code_address = to message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -306,14 +316,14 @@ def callcode(evm: Evm) -> None: ], ) message_call_gas = calculate_message_call_gas( - evm.env.state, gas, to, value + evm.message.block_env.state, gas, to, value ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -366,17 +376,23 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) diff --git a/src/ethereum/homestead/vm/interpreter.py b/src/ethereum/homestead/vm/interpreter.py index 6f9839d729..ec8f4bd8e5 100644 --- a/src/ethereum/homestead/vm/interpreter.py +++ b/src/ethereum/homestead/vm/interpreter.py @@ -17,6 +17,7 @@ from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -44,7 +45,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -75,12 +76,10 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -90,27 +89,25 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) + evm = process_message(message) if evm.error: logs: Tuple[Log, ...] = () @@ -134,7 +131,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -150,35 +147,36 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.homestead.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely # circumstances: # * The address created by two `CREATE` calls collide. # * The first `CREATE` left empty code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - evm = process_message(message, env) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT try: charge_gas(evm, contract_code_gas) except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -194,30 +192,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.homestead.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -242,7 +241,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/istanbul/fork.py b/src/ethereum/istanbul/fork.py index ac55974300..02d1e3f75b 100644 --- a/src/ethereum/istanbul/fork.py +++ b/src/ethereum/istanbul/fork.py @@ -11,34 +11,42 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass from typing import List, Optional, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -146,32 +154,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -181,7 +198,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -194,11 +211,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -299,21 +335,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -325,16 +361,26 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Receipt: @@ -343,8 +389,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -368,45 +412,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -419,91 +429,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -542,10 +488,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -627,8 +570,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -643,77 +589,93 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/istanbul/state.py b/src/ethereum/istanbul/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/istanbul/state.py +++ b/src/ethereum/istanbul/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/istanbul/transactions.py b/src/ethereum/istanbul/transactions.py index e455e2549a..28115976e8 100644 --- a/src/ethereum/istanbul/transactions.py +++ b/src/ethereum/istanbul/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -157,6 +157,7 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) ) + return Address(keccak256(public_key)[12:32]) @@ -219,3 +220,18 @@ def signing_hash_155(tx: Transaction, chain_id: U64) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/istanbul/utils/message.py b/src/ethereum/istanbul/utils/message.py index 0eeafed649..b84475427b 100644 --- a/src/ethereum/istanbul/utils/message.py +++ b/src/ethereum/istanbul/utils/message.py @@ -12,87 +12,68 @@ Message specific functions used in this istanbul version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.istanbul.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, parent_evm=None, ) diff --git a/src/ethereum/istanbul/vm/__init__.py b/src/ethereum/istanbul/vm/__init__.py index 2cb23ad8a7..81df1e3ad4 100644 --- a/src/ethereum/istanbul/vm/__init__.py +++ b/src/ethereum/istanbul/vm/__init__.py @@ -13,39 +13,80 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import Transaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -55,6 +96,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -78,7 +121,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -88,7 +130,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: @@ -108,7 +150,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) @@ -135,7 +177,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/istanbul/vm/instructions/block.py b/src/ethereum/istanbul/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/istanbul/vm/instructions/block.py +++ b/src/ethereum/istanbul/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/istanbul/vm/instructions/environment.py b/src/ethereum/istanbul/vm/instructions/environment.py index fc300ca74a..9f26185b8f 100644 --- a/src/ethereum/istanbul/vm/instructions/environment.py +++ b/src/ethereum/istanbul/vm/instructions/environment.py @@ -79,7 +79,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -105,7 +105,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -316,7 +316,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -339,7 +339,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -374,7 +374,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -454,7 +454,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, GAS_CODE_HASH) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -486,7 +486,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) diff --git a/src/ethereum/istanbul/vm/instructions/storage.py b/src/ethereum/istanbul/vm/instructions/storage.py index b962c5fe4e..4bcc9aef2c 100644 --- a/src/ethereum/istanbul/vm/instructions/storage.py +++ b/src/ethereum/istanbul/vm/instructions/storage.py @@ -46,7 +46,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -70,10 +72,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) if original_value == current_value and current_value != new_value: if original_value == 0: @@ -105,7 +108,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/istanbul/vm/instructions/system.py b/src/ethereum/istanbul/vm/instructions/system.py index 0c7608d40d..eaa8a98601 100644 --- a/src/ethereum/istanbul/vm/instructions/system.py +++ b/src/ethereum/istanbul/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) evm.gas_left -= create_message_gas if evm.message.is_static: @@ -78,7 +82,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -90,9 +94,11 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return @@ -100,9 +106,11 @@ def generic_create( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -116,7 +124,7 @@ def generic_create( is_static=False, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -153,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -269,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -284,7 +296,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -334,7 +346,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if value == 0 or is_account_alive(evm.env.state, to) + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -350,7 +362,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -417,7 +429,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -458,8 +470,11 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -478,23 +493,30 @@ def selfdestruct(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + originator = evm.message.current_target + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/istanbul/vm/interpreter.py b/src/ethereum/istanbul/vm/interpreter.py index 0805a095da..d5fae4defa 100644 --- a/src/ethereum/istanbul/vm/interpreter.py +++ b/src/ethereum/istanbul/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -82,13 +83,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -98,28 +97,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -147,7 +146,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -163,8 +162,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.istanbul.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -173,15 +173,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -190,19 +190,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -218,30 +218,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.istanbul.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -266,7 +267,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/london/fork.py b/src/ethereum/london/fork.py index 4bd5dda7bf..fc8ea7adbc 100644 --- a/src/ethereum/london/fork.py +++ b/src/ethereum/london/fork.py @@ -21,17 +21,22 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import FORK_CRITERIA, vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, @@ -44,10 +49,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -158,33 +164,42 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + difficulty=block.header.difficulty, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -256,7 +271,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -269,11 +284,27 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -390,24 +421,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - base_fee_per_gas: Uint, - gas_available: Uint, - chain_id: U64, ) -> Tuple[Address, Uint]: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - base_fee_per_gas : - The block base fee. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -421,32 +449,43 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, FeeMarketTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address, effective_gas_price def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -480,46 +519,11 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -532,98 +536,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. - """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address, effective_gas_price = check_transaction( - tx, base_fee_per_gas, gas_available, chain_id - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -662,10 +595,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -747,8 +677,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -763,103 +696,116 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - max_gas_fee: Uint - if isinstance(tx, FeeMarketTransaction): - max_gas_fee = Uint(tx.gas) * Uint(tx.max_fee_per_gas) - else: - max_gas_fee = Uint(tx.gas) * Uint(tx.gas_price) - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender_account = get_account(block_env.state, sender) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() + access_list_addresses = set() + access_list_storage_keys = set() if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/london/state.py b/src/ethereum/london/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/london/state.py +++ b/src/ethereum/london/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/london/transactions.py b/src/ethereum/london/transactions.py index 27cc083538..b853357506 100644 --- a/src/ethereum/london/transactions.py +++ b/src/ethereum/london/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -177,10 +177,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -191,15 +191,15 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -380,3 +380,22 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/london/utils/message.py b/src/ethereum/london/utils/message.py index fcd8b1dc59..54caa37e91 100644 --- a/src/ethereum/london/utils/message.py +++ b/src/ethereum/london/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this london version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/london/vm/__init__.py b/src/ethereum/london/vm/__init__.py index 0194e6ec10..245a05e454 100644 --- a/src/ethereum/london/vm/__init__.py +++ b/src/ethereum/london/vm/__init__.py @@ -13,40 +13,83 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -56,6 +99,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -81,7 +126,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -91,7 +135,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -113,7 +157,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -142,7 +186,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/london/vm/instructions/block.py b/src/ethereum/london/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/london/vm/instructions/block.py +++ b/src/ethereum/london/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/london/vm/instructions/environment.py b/src/ethereum/london/vm/instructions/environment.py index dc59e168bd..172ce97d70 100644 --- a/src/ethereum/london/vm/instructions/environment.py +++ b/src/ethereum/london/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -533,7 +535,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/london/vm/instructions/storage.py b/src/ethereum/london/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/london/vm/instructions/storage.py +++ b/src/ethereum/london/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/london/vm/instructions/system.py b/src/ethereum/london/vm/instructions/system.py index 961b3e0c00..7a2f1efeb3 100644 --- a/src/ethereum/london/vm/instructions/system.py +++ b/src/ethereum/london/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -80,7 +84,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -92,19 +96,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -120,7 +124,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -157,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -273,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -290,7 +298,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -346,7 +354,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -362,7 +370,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -436,7 +444,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -481,8 +489,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -491,23 +502,29 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/london/vm/interpreter.py b/src/ethereum/london/vm/interpreter.py index 1ca1087fad..e02ab84549 100644 --- a/src/ethereum/london/vm/interpreter.py +++ b/src/ethereum/london/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -83,13 +84,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -164,8 +163,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.london.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +174,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +194,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -222,30 +222,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.london.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +271,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/muir_glacier/fork.py b/src/ethereum/muir_glacier/fork.py index cfd3b01dea..d8e65346dc 100644 --- a/src/ethereum/muir_glacier/fork.py +++ b/src/ethereum/muir_glacier/fork.py @@ -11,34 +11,42 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass from typing import List, Optional, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -146,32 +154,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -181,7 +198,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -194,11 +211,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -299,21 +335,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -325,16 +361,26 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Receipt: @@ -368,45 +414,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -419,91 +431,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -542,10 +490,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -627,8 +572,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -643,77 +591,93 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/muir_glacier/state.py b/src/ethereum/muir_glacier/state.py index 032610f7dd..9cafc1b168 100644 --- a/src/ethereum/muir_glacier/state.py +++ b/src/ethereum/muir_glacier/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -630,3 +630,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/muir_glacier/transactions.py b/src/ethereum/muir_glacier/transactions.py index e455e2549a..28115976e8 100644 --- a/src/ethereum/muir_glacier/transactions.py +++ b/src/ethereum/muir_glacier/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -157,6 +157,7 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) ) + return Address(keccak256(public_key)[12:32]) @@ -219,3 +220,18 @@ def signing_hash_155(tx: Transaction, chain_id: U64) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/muir_glacier/utils/message.py b/src/ethereum/muir_glacier/utils/message.py index 2c7508621d..9d4c30605e 100644 --- a/src/ethereum/muir_glacier/utils/message.py +++ b/src/ethereum/muir_glacier/utils/message.py @@ -12,87 +12,68 @@ Message specific functions used in this muir_glacier version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.muir_glacier.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, parent_evm=None, ) diff --git a/src/ethereum/muir_glacier/vm/__init__.py b/src/ethereum/muir_glacier/vm/__init__.py index 2cb23ad8a7..81df1e3ad4 100644 --- a/src/ethereum/muir_glacier/vm/__init__.py +++ b/src/ethereum/muir_glacier/vm/__init__.py @@ -13,39 +13,80 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import Transaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -55,6 +96,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -78,7 +121,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -88,7 +130,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: @@ -108,7 +150,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) @@ -135,7 +177,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/muir_glacier/vm/instructions/block.py b/src/ethereum/muir_glacier/vm/instructions/block.py index e94b8c69ed..2abe8928f2 100644 --- a/src/ethereum/muir_glacier/vm/instructions/block.py +++ b/src/ethereum/muir_glacier/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -201,7 +207,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/muir_glacier/vm/instructions/environment.py b/src/ethereum/muir_glacier/vm/instructions/environment.py index fc300ca74a..9f26185b8f 100644 --- a/src/ethereum/muir_glacier/vm/instructions/environment.py +++ b/src/ethereum/muir_glacier/vm/instructions/environment.py @@ -79,7 +79,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -105,7 +105,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -316,7 +316,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -339,7 +339,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -374,7 +374,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -454,7 +454,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, GAS_CODE_HASH) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -486,7 +486,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) diff --git a/src/ethereum/muir_glacier/vm/instructions/storage.py b/src/ethereum/muir_glacier/vm/instructions/storage.py index b962c5fe4e..4bcc9aef2c 100644 --- a/src/ethereum/muir_glacier/vm/instructions/storage.py +++ b/src/ethereum/muir_glacier/vm/instructions/storage.py @@ -46,7 +46,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -70,10 +72,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) if original_value == current_value and current_value != new_value: if original_value == 0: @@ -105,7 +108,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/muir_glacier/vm/instructions/system.py b/src/ethereum/muir_glacier/vm/instructions/system.py index 0c7608d40d..eaa8a98601 100644 --- a/src/ethereum/muir_glacier/vm/instructions/system.py +++ b/src/ethereum/muir_glacier/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) evm.gas_left -= create_message_gas if evm.message.is_static: @@ -78,7 +82,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -90,9 +94,11 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return @@ -100,9 +106,11 @@ def generic_create( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -116,7 +124,7 @@ def generic_create( is_static=False, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -153,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -269,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -284,7 +296,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -334,7 +346,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if value == 0 or is_account_alive(evm.env.state, to) + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -350,7 +362,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -417,7 +429,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -458,8 +470,11 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -478,23 +493,30 @@ def selfdestruct(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + originator = evm.message.current_target + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/muir_glacier/vm/interpreter.py b/src/ethereum/muir_glacier/vm/interpreter.py index 6b1d1d4fff..3b9c30ff19 100644 --- a/src/ethereum/muir_glacier/vm/interpreter.py +++ b/src/ethereum/muir_glacier/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -82,13 +83,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -98,28 +97,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -147,7 +146,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -163,8 +162,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.muir_glacier.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -173,15 +173,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -190,19 +190,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -218,30 +218,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.muir_glacier.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -266,7 +267,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index 67feb05538..ac9a916675 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -16,20 +16,25 @@ from typing import List, Optional, Tuple, Union from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, @@ -42,10 +47,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -151,33 +157,42 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) if block.ommers != (): raise InvalidBlock - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.prev_randao, - block.transactions, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -249,7 +264,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -262,11 +277,27 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -297,24 +328,21 @@ def validate_header(header: Header, parent_header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - base_fee_per_gas: Uint, - gas_available: Uint, - chain_id: U64, ) -> Tuple[Address, Uint]: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - base_fee_per_gas : - The block base fee. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -328,32 +356,43 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, FeeMarketTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address, effective_gas_price def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -365,7 +404,7 @@ def make_receipt( tx : The executed transaction. error : - The error from the execution if any. + Error in the top level frame of the transaction, if any. cumulative_gas_used : The total gas used so far in the block after the transaction was executed. @@ -387,45 +426,10 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -438,101 +442,30 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - prev_randao : - The previous randao from the beacon chain. + block_env : + The block scoped environment. transactions : Transactions included in the block. - ommers : - Headers of ancestor blocks which are not direct parents (formerly - uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address, effective_gas_price = check_transaction( - tx, base_fee_per_gas, gas_available, chain_id - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - ) + process_transaction(block_env, block_output, tx, Uint(i)) - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -547,102 +480,116 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - if isinstance(tx, FeeMarketTransaction): - max_gas_fee = tx.gas * tx.max_fee_per_gas - else: - max_gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender_account = get_account(block_env.state, sender) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() + access_list_addresses = set() + access_list_storage_keys = set() if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs, output.error + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/paris/state.py b/src/ethereum/paris/state.py index 44ce37b285..9af890fb2f 100644 --- a/src/ethereum/paris/state.py +++ b/src/ethereum/paris/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -610,3 +610,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/paris/transactions.py b/src/ethereum/paris/transactions.py index 27cc083538..b853357506 100644 --- a/src/ethereum/paris/transactions.py +++ b/src/ethereum/paris/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -177,10 +177,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -191,15 +191,15 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -380,3 +380,22 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/paris/utils/message.py b/src/ethereum/paris/utils/message.py index d0a74169b0..38554c73a8 100644 --- a/src/ethereum/paris/utils/message.py +++ b/src/ethereum/paris/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this paris version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/paris/vm/__init__.py b/src/ethereum/paris/vm/__init__.py index 09c7667789..fe4d472966 100644 --- a/src/ethereum/paris/vm/__init__.py +++ b/src/ethereum/paris/vm/__init__.py @@ -13,40 +13,83 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 prev_randao: Bytes32 - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -56,6 +99,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -81,7 +126,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -91,7 +135,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -113,7 +157,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -142,7 +186,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/paris/vm/instructions/block.py b/src/ethereum/paris/vm/instructions/block.py index 3e2b1aa3e8..f361a6ee39 100644 --- a/src/ethereum/paris/vm/instructions/block.py +++ b/src/ethereum/paris/vm/instructions/block.py @@ -44,13 +44,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -85,7 +91,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -118,7 +124,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -150,7 +156,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -182,7 +188,7 @@ def prev_randao(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -214,7 +220,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -243,7 +249,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/paris/vm/instructions/environment.py b/src/ethereum/paris/vm/instructions/environment.py index dc59e168bd..172ce97d70 100644 --- a/src/ethereum/paris/vm/instructions/environment.py +++ b/src/ethereum/paris/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -533,7 +535,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/paris/vm/instructions/storage.py b/src/ethereum/paris/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/paris/vm/instructions/storage.py +++ b/src/ethereum/paris/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/paris/vm/instructions/system.py b/src/ethereum/paris/vm/instructions/system.py index 961b3e0c00..7a2f1efeb3 100644 --- a/src/ethereum/paris/vm/instructions/system.py +++ b/src/ethereum/paris/vm/instructions/system.py @@ -71,6 +71,10 @@ def generic_create( # if it's not moved inside this method from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -80,7 +84,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -92,19 +96,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -120,7 +124,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -157,7 +161,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -273,8 +279,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -290,7 +298,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -346,7 +354,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -362,7 +370,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -436,7 +444,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -481,8 +489,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -491,23 +502,29 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/paris/vm/interpreter.py b/src/ethereum/paris/vm/interpreter.py index 8fc8d1ad73..950196d7cc 100644 --- a/src/ethereum/paris/vm/interpreter.py +++ b/src/ethereum/paris/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple, Union +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -81,15 +82,13 @@ class MessageCallOutput: gas_left: Uint refund_counter: U256 - logs: Union[Tuple[()], Tuple[Log, ...]] + logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -164,8 +163,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.paris.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +174,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +194,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -222,30 +222,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.paris.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +271,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 9c149f65d7..50b5033e53 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -16,7 +16,7 @@ from typing import List, Optional, Tuple, Union from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -29,7 +29,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Authorization, Bloom, Root, VersionedHash +from .fork_types import Account, Address, Authorization, VersionedHash from .requests import ( CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, @@ -45,7 +45,7 @@ destroy_touched_empty_accounts, get_account, increment_nonce, - process_withdrawal, + modify_state, set_account_balance, state_root, ) @@ -58,10 +58,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.hexadecimal import hex_to_address from .utils.message import prepare_message from .vm import Message @@ -194,46 +195,53 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - excess_blob_gas = calculate_excess_blob_gas(parent_header) - if block.header.excess_blob_gas != excess_blob_gas: - raise InvalidBlock - - validate_header(block.header, parent_header) + validate_header(chain, block.header) if block.ommers != (): raise InvalidBlock - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.prev_randao, - block.transactions, - chain.chain_id, - block.withdrawals, - block.header.parent_beacon_block_root, - excess_blob_gas, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + excess_blob_gas=block.header.excess_blob_gas, + parent_beacon_block_root=block.header.parent_beacon_block_root, ) - if apply_body_output.block_gas_used != block.header.gas_used: + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + requests_hash = compute_requests_hash(block_output.requests) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock - if apply_body_output.withdrawals_root != block.header.withdrawals_root: + if withdrawals_root != block.header.withdrawals_root: raise InvalidBlock - if apply_body_output.blob_gas_used != block.header.blob_gas_used: + if block_output.blob_gas_used != block.header.blob_gas_used: raise InvalidBlock - if apply_body_output.requests_hash != block.header.requests_hash: + if requests_hash != block.header.requests_hash: raise InvalidBlock chain.blocks.append(block) @@ -305,7 +313,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -318,11 +326,31 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + excess_blob_gas = calculate_excess_blob_gas(parent_header) + if header.excess_blob_gas != excess_blob_gas: + raise InvalidBlock + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -353,30 +381,21 @@ def validate_header(header: Header, parent_header: Header) -> None: def check_transaction( - state: State, + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, - base_fee_per_gas: Uint, - excess_blob_gas: U64, -) -> Tuple[Address, Uint, Tuple[VersionedHash, ...]]: +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], Uint]: """ Check if the transaction is includable in the block. Parameters ---------- - state : - Current state. + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. - base_fee_per_gas : - The block base fee. - excess_blob_gas : - The excess blob gas. Returns ------- @@ -386,33 +405,43 @@ def check_transaction( The price to charge for gas when the transaction is executed. blob_versioned_hashes : The blob versioned hashes of the transaction. + tx_blob_gas_used: + The blob gas used by the transaction. Raises ------ InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) - sender_account = get_account(state, sender_address) + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise InvalidBlock + + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance( tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) ): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price max_gas_fee = tx.gas * tx.gas_price @@ -424,7 +453,7 @@ def check_transaction( if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: raise InvalidBlock - blob_gas_price = calculate_blob_gas_price(excess_blob_gas) + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: raise InvalidBlock @@ -447,12 +476,15 @@ def check_transaction( raise InvalidBlock if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): raise InvalidBlock - if sender_account.code != bytearray() and not is_valid_delegation( - sender_account.code - ): + if sender_account.code and not is_valid_delegation(sender_account.code): raise InvalidSenderError("not EOA") - return sender_address, effective_gas_price, blob_versioned_hashes + return ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) def make_receipt( @@ -491,94 +523,47 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - withdrawals_root : `ethereum.fork_types.Root` - Trie root of all the withdrawals in the block. - blob_gas_used : `ethereum.base_types.Uint` - Total blob gas used in the block. - requests_hash : `Bytes` - Hash of all the requests in the block. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - withdrawals_root: Root - blob_gas_used: Uint - requests_hash: Bytes - - def process_system_transaction( + block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, - state: State, - chain_id: U64, - excess_blob_gas: U64, ) -> MessageCallOutput: """ Process a system transaction. Parameters ---------- + block_env : + The block scoped environment. target_address : Address of the contract to call. data : Data to pass to the contract. - block_hashes : - List of hashes of the previous 256 blocks. - coinbase : - Address of the block's coinbase. - block_number : - Block number. - base_fee_per_gas : - Base fee per gas. - block_gas_limit : - Gas limit of the block. - block_time : - Time the block was produced. - prev_randao : - Previous randao value. - state : - Current state. - chain_id : - ID of the chain. - excess_blob_gas : - Excess blob gas. Returns ------- system_tx_output : `MessageCallOutput` Output of processing the system transaction. """ - system_contract_code = get_account(state, target_address).code + system_contract_code = get_account(block_env.state, target_address).code + + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + access_list_addresses=set(), + access_list_storage_keys=set(), + transient_storage=TransientStorage(), + blob_versioned_hashes=(), + authorizations=(), + index_in_block=None, + tx_hash=None, + traces=[], + ) system_tx_message = Message( + block_env=block_env, + tx_env=tx_env, caller=SYSTEM_ADDRESS, target=target_address, gas=SYSTEM_TRANSACTION_GAS, @@ -593,29 +578,9 @@ def process_system_transaction( accessed_addresses=set(), accessed_storage_keys=set(), parent_evm=None, - authorizations=(), ) - system_tx_env = vm.Environment( - caller=SYSTEM_ADDRESS, - block_hashes=block_hashes, - origin=SYSTEM_ADDRESS, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=base_fee_per_gas, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - excess_blob_gas=excess_blob_gas, - blob_versioned_hashes=(), - transient_storage=TransientStorage(), - ) - - system_tx_output = process_message_call(system_tx_message, system_tx_env) + system_tx_output = process_message_call(system_tx_message) # TODO: Empty accounts in post-merge forks are impossible # see Ethereum Improvement Proposal 7523. @@ -623,27 +588,17 @@ def process_system_transaction( # and will have to be removed in the future. # See https://github.com/ethereum/execution-specs/issues/955 destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts + block_env.state, system_tx_output.touched_accounts ) return system_tx_output def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], - chain_id: U64, withdrawals: Tuple[Withdrawal, ...], - parent_beacon_block_root: Root, - excess_blob_gas: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -656,246 +611,69 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - prev_randao : - The previous randao from the beacon chain. + block_env : + The block scoped environment. transactions : Transactions included in the block. - ommers : - Headers of ancestor blocks which are not direct parents (formerly - uncles.) - chain_id : - ID of the executing chain. withdrawals : Withdrawals to be processed in the current block. - parent_beacon_block_root : - The root of the beacon block from the parent block. - excess_blob_gas : - Excess blob gas calculated from the previous block. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - blob_gas_used = Uint(0) - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () - deposit_requests: Bytes = b"" + block_output = vm.BlockOutput() process_system_transaction( - BEACON_ROOTS_ADDRESS, - parent_beacon_block_root, - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - state, - chain_id, - excess_blob_gas, + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, ) process_system_transaction( - HISTORY_STORAGE_ADDRESS, - block_hashes[-1], # The parent hash - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - state, - chain_id, - excess_blob_gas, + block_env=block_env, + target_address=HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash ) for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - ( - sender_address, - effective_gas_price, - blob_versioned_hashes, - ) = check_transaction( - state, - tx, - gas_available, - chain_id, - base_fee_per_gas, - excess_blob_gas, - ) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - excess_blob_gas=excess_blob_gas, - blob_versioned_hashes=blob_versioned_hashes, - transient_storage=TransientStorage(), - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - deposit_requests += parse_deposit_requests_from_receipt(receipt) + process_withdrawals(block_env, block_output, withdrawals) - block_logs += logs - blob_gas_used += calculate_total_blob_gas(tx) - if blob_gas_used > MAX_BLOB_GAS_PER_BLOCK: - raise InvalidBlock - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - for i, wd in enumerate(withdrawals): - trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd)) - - process_withdrawal(state, wd) - - if account_exists_and_is_empty(state, wd.address): - destroy_account(state, wd.address) - - requests_from_execution = process_general_purpose_requests( - deposit_requests, - state, - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - chain_id, - excess_blob_gas, + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, ) - requests_hash = compute_requests_hash(requests_from_execution) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - root(withdrawals_trie), - blob_gas_used, - requests_hash, - ) + return block_output def process_general_purpose_requests( - deposit_requests: Bytes, - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, - chain_id: U64, - excess_blob_gas: U64, -) -> List[Bytes]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, +) -> None: """ Process all the requests in the block. Parameters ---------- - deposit_requests : - The deposit requests. - state : - Current state. - block_hashes : - List of hashes of the previous 256 blocks. - coinbase : - Address of the block's coinbase. - block_number : - Block number. - base_fee_per_gas : - Base fee per gas. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced. - prev_randao : - The previous randao from the beacon chain. - chain_id : - ID of the executing chain. - excess_blob_gas : - Excess blob gas. - - Returns - ------- - requests_from_execution : `List[Bytes]` - The requests from the execution + block_env : + The execution environment for the Block. + block_output : + The block output for the current block. """ # Requests are to be in ascending order of request type - requests_from_execution: List[Bytes] = [] + deposit_requests = block_output.deposit_requests + requests_from_execution = block_output.requests if len(deposit_requests) > 0: requests_from_execution.append(DEPOSIT_REQUEST_TYPE + deposit_requests) system_withdrawal_tx_output = process_system_transaction( - WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, - b"", - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - state, - chain_id, - excess_blob_gas, + block_env=block_env, + target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + data=b"", ) if len(system_withdrawal_tx_output.return_data) > 0: @@ -904,18 +682,9 @@ def process_general_purpose_requests( ) system_consolidation_tx_output = process_system_transaction( - CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, - b"", - block_hashes, - coinbase, - block_number, - base_fee_per_gas, - block_gas_limit, - block_time, - prev_randao, - state, - chain_id, - excess_blob_gas, + block_env=block_env, + target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + data=b"", ) if len(system_consolidation_tx_output.return_data) > 0: @@ -924,12 +693,13 @@ def process_general_purpose_requests( + system_consolidation_tx_output.return_data ) - return requests_from_execution - def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[EthereumException]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -944,41 +714,56 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) if isinstance(tx, BlobTransaction): - blob_gas_fee = calculate_data_fee(env.excess_blob_gas, tx) + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) else: blob_gas_fee = Uint(0) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() - preaccessed_addresses.add(env.coinbase) + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) if isinstance( tx, ( @@ -989,70 +774,118 @@ def process_transaction( ), ): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) + access_list_storage_keys.add((address, key)) authorizations: Tuple[Authorization, ...] = () if isinstance(tx, SetCodeTransaction): authorizations = tx.authorizations - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + transient_storage=TransientStorage(), + blob_versioned_hashes=blob_versioned_hashes, authorizations=authorizations, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) # For EIP-7623 we first calculate the execution_gas_used, which includes # the execution gas refund. - execution_gas_used = tx.gas - output.gas_left + execution_gas_used = tx.gas - tx_output.gas_left gas_refund = min( - execution_gas_used // Uint(5), Uint(output.refund_counter) + execution_gas_used // Uint(5), Uint(tx_output.refund_counter) ) execution_gas_used -= gas_refund # Transactions with less execution_gas_used than the floor pay at the # floor cost. - total_gas_used = max(execution_gas_used, calldata_floor_gas_cost) + tx_gas_used = max(execution_gas_used, calldata_floor_gas_cost) - output.gas_left = tx.gas - total_gas_used - gas_refund_amount = output.gas_left * env.gas_price + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = total_gas_used * priority_fee_per_gas + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) + + block_output.block_gas_used += tx_gas_used + block_output.blob_gas_used += tx_blob_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + block_output.block_logs += tx_output.logs + + block_output.deposit_requests += parse_deposit_requests_from_receipt( + receipt + ) + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * U256(10**9) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) - destroy_touched_empty_accounts(env.state, output.touched_accounts) + modify_state(block_env.state, wd.address, increase_recipient_balance) - return total_gas_used, output.logs, output.error + if account_exists_and_is_empty(block_env.state, wd.address): + destroy_account(block_env.state, wd.address) def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/prague/state.py b/src/ethereum/prague/state.py index 920ee6dd49..8a0e14728e 100644 --- a/src/ethereum/prague/state.py +++ b/src/ethereum/prague/state.py @@ -23,7 +23,6 @@ from ethereum_types.frozen import modify from ethereum_types.numeric import U256, Uint -from .blocks import Withdrawal from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set @@ -534,20 +533,6 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, recipient_address, increase_recipient_balance) -def process_withdrawal( - state: State, - wd: Withdrawal, -) -> None: - """ - Increase the balance of the withdrawing account. - """ - - def increase_recipient_balance(recipient: Account) -> None: - recipient.balance += wd.amount * U256(10**9) - - modify_state(state, wd.address, increase_recipient_balance) - - def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index f10f954fab..9b934b64c2 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -552,3 +552,22 @@ def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/prague/utils/message.py b/src/ethereum/prague/utils/message.py index 3d1420b835..9d3f615739 100644 --- a/src/ethereum/prague/utils/message.py +++ b/src/ethereum/prague/utils/message.py @@ -12,68 +12,34 @@ Message specific functions used in this prague version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint - -from ..fork_types import Address, Authorization +from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.eoa_delegation import get_delegated_code_address from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), - authorizations: Tuple[Authorization, ...] = (), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call - authorizations: - Authorizations that should be applied to the message call. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -81,48 +47,49 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) + accessed_addresses.update(tx_env.access_list_addresses) - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code delegated_address = get_delegated_code_address(code) if delegated_address is not None: accessed_addresses.add(delegated_address) - code = get_account(env.state, delegated_address).code + code = get_account(block_env.state, delegated_address).code - if code_address is None: - code_address = target + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, - authorizations=authorizations, ) diff --git a/src/ethereum/prague/vm/__init__.py b/src/ethereum/prague/vm/__init__.py index e2abe20d98..55f1b92499 100644 --- a/src/ethereum/prague/vm/__init__.py +++ b/src/ethereum/prague/vm/__init__.py @@ -13,7 +13,7 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 @@ -22,36 +22,92 @@ from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, Authorization, VersionedHash from ..state import State, TransientStorage, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 prev_randao: Bytes32 - state: State - chain_id: U64 - traces: List[dict] excess_blob_gas: U64 - blob_versioned_hashes: Tuple[VersionedHash, ...] + parent_beacon_block_root: Hash32 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + blob_gas_used : `ethereum.base_types.Uint` + Total blob gas used in the block. + requests : `Bytes` + Hash of all the requests in the block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + blob_gas_used: Uint = Uint(0) + deposit_requests: Bytes = Bytes(b"") + requests: List[Bytes] = field(default_factory=list) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] transient_storage: TransientStorage + blob_versioned_hashes: Tuple[VersionedHash, ...] + authorizations: Tuple[Authorization, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + traces: List[dict] @dataclass @@ -60,6 +116,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -74,7 +132,6 @@ class Message: accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] parent_evm: Optional["Evm"] - authorizations: Tuple[Authorization, ...] @dataclass @@ -86,7 +143,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -118,7 +174,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -147,7 +203,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/prague/vm/eoa_delegation.py b/src/ethereum/prague/vm/eoa_delegation.py index 8728375a25..bb6a7e33d9 100644 --- a/src/ethereum/prague/vm/eoa_delegation.py +++ b/src/ethereum/prague/vm/eoa_delegation.py @@ -17,7 +17,7 @@ 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 Environment, Evm, Message +from . import Evm, Message SET_CODE_TX_MAGIC = b"\x05" EOA_DELEGATION_MARKER = b"\xEF\x01\x00" @@ -130,7 +130,8 @@ def access_delegation( delegation : `Tuple[bool, Address, Bytes, Uint]` The delegation address, code, and access gas cost. """ - code = get_account(evm.env.state, address).code + state = evm.message.block_env.state + code = get_account(state, address).code if not is_valid_delegation(code): return False, address, code, Uint(0) @@ -140,12 +141,12 @@ def access_delegation( else: evm.accessed_addresses.add(address) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS - code = get_account(evm.env.state, address).code + code = get_account(state, address).code return True, address, code, access_gas_cost -def set_delegation(message: Message, env: Environment) -> U256: +def set_delegation(message: Message) -> U256: """ Set the delegation code for the authorities in the message. @@ -161,9 +162,10 @@ def set_delegation(message: Message, env: Environment) -> U256: refund_counter: `U256` Refund from authority which already exists in state. """ + state = message.block_env.state refund_counter = U256(0) - for auth in message.authorizations: - if auth.chain_id not in (env.chain_id, U256(0)): + for auth in message.tx_env.authorizations: + if auth.chain_id not in (message.block_env.chain_id, U256(0)): continue if auth.nonce >= U64.MAX_VALUE: @@ -176,32 +178,30 @@ def set_delegation(message: Message, env: Environment) -> U256: message.accessed_addresses.add(authority) - authority_account = get_account(env.state, authority) + authority_account = get_account(state, authority) authority_code = authority_account.code - if authority_code != bytearray() and not is_valid_delegation( - authority_code - ): + if authority_code and not is_valid_delegation(authority_code): continue authority_nonce = authority_account.nonce if authority_nonce != auth.nonce: continue - if account_exists(env.state, authority): + if account_exists(state, 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(env.state, authority, code_to_set) + set_code(state, authority, code_to_set) - increment_nonce(env.state, authority) + increment_nonce(state, authority) if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") - message.code = get_account(env.state, message.code_address).code + message.code = get_account(state, message.code_address).code if is_valid_delegation(message.code): message.code_address = Address( @@ -209,6 +209,6 @@ def set_delegation(message: Message, env: Environment) -> U256: ) message.accessed_addresses.add(message.code_address) - message.code = get_account(env.state, message.code_address).code + message.code = get_account(state, message.code_address).code return refund_counter diff --git a/src/ethereum/prague/vm/instructions/block.py b/src/ethereum/prague/vm/instructions/block.py index b4f7b556d7..e47a99de85 100644 --- a/src/ethereum/prague/vm/instructions/block.py +++ b/src/ethereum/prague/vm/instructions/block.py @@ -44,13 +44,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -85,7 +91,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -118,7 +124,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -150,7 +156,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -182,7 +188,7 @@ def prev_randao(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -214,7 +220,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -243,7 +249,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/environment.py b/src/ethereum/prague/vm/instructions/environment.py index 6a524f1496..5ddd12dac8 100644 --- a/src/ethereum/prague/vm/instructions/environment.py +++ b/src/ethereum/prague/vm/instructions/environment.py @@ -85,7 +85,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -111,7 +111,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -322,7 +322,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -351,7 +351,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -393,7 +393,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -479,7 +479,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -511,7 +511,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -536,7 +538,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -559,8 +561,8 @@ def blob_hash(evm: Evm) -> None: charge_gas(evm, GAS_BLOBHASH_OPCODE) # OPERATION - if int(index) < len(evm.env.blob_versioned_hashes): - blob_hash = evm.env.blob_versioned_hashes[index] + if int(index) < len(evm.message.tx_env.blob_versioned_hashes): + blob_hash = evm.message.tx_env.blob_versioned_hashes[index] else: blob_hash = Bytes32(b"\x00" * 32) push(evm.stack, U256.from_be_bytes(blob_hash)) @@ -586,7 +588,9 @@ def blob_base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - blob_base_fee = calculate_blob_gas_price(evm.env.excess_blob_gas) + blob_base_fee = calculate_blob_gas_price( + evm.message.block_env.excess_blob_gas + ) push(evm.stack, U256(blob_base_fee)) # PROGRAM COUNTER diff --git a/src/ethereum/prague/vm/instructions/storage.py b/src/ethereum/prague/vm/instructions/storage.py index f88e295736..65a0d5a9b6 100644 --- a/src/ethereum/prague/vm/instructions/storage.py +++ b/src/ethereum/prague/vm/instructions/storage.py @@ -56,7 +56,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -80,10 +82,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -123,7 +126,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) @@ -146,7 +149,7 @@ def tload(evm: Evm) -> None: # OPERATION value = get_transient_storage( - evm.env.transient_storage, evm.message.current_target, key + evm.message.tx_env.transient_storage, evm.message.current_target, key ) push(evm.stack, value) @@ -171,7 +174,10 @@ def tstore(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext set_transient_storage( - evm.env.transient_storage, evm.message.current_target, key, new_value + evm.message.tx_env.transient_storage, + evm.message.current_target, + key, + new_value, ) # PROGRAM COUNTER diff --git a/src/ethereum/prague/vm/instructions/system.py b/src/ethereum/prague/vm/instructions/system.py index a75e036f06..88ba466e8e 100644 --- a/src/ethereum/prague/vm/instructions/system.py +++ b/src/ethereum/prague/vm/instructions/system.py @@ -94,7 +94,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -106,15 +106,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -129,9 +133,8 @@ def generic_create( accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, - authorizations=(), ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -169,7 +172,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -301,7 +306,7 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code if is_delegated_code and len(code) == 0: evm.gas_left += gas @@ -309,6 +314,8 @@ def generic_call( return child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -323,9 +330,8 @@ def generic_call( accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, - authorizations=(), ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -388,7 +394,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -404,7 +410,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -488,7 +494,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -535,8 +541,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -545,10 +554,12 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - originator_balance = get_account(evm.env.state, originator).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance move_ether( - evm.env.state, + evm.message.block_env.state, originator, beneficiary, originator_balance, @@ -556,14 +567,14 @@ def selfdestruct(evm: Evm) -> None: # register account for deletion only if it was created # in the same transaction - if originator in evm.env.state.created_accounts: + if originator in evm.message.block_env.state.created_accounts: # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/prague/vm/interpreter.py b/src/ethereum/prague/vm/interpreter.py index d105142452..5856c5af9c 100644 --- a/src/ethereum/prague/vm/interpreter.py +++ b/src/ethereum/prague/vm/interpreter.py @@ -49,7 +49,7 @@ from ..vm.eoa_delegation import set_delegation from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -91,9 +91,7 @@ class MessageCallOutput: return_data: Bytes -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -103,19 +101,17 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), @@ -127,12 +123,14 @@ def process_message_call( Bytes(b""), ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - if message.authorizations != (): - refund_counter += set_delegation(message, env) - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + if message.tx_env.authorizations != (): + refund_counter += set_delegation(message) + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -161,7 +159,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -177,8 +175,10 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.prague.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage # take snapshot of state before processing the message - begin_transaction(env.state, env.transient_storage) + begin_transaction(state, transient_storage) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -187,15 +187,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -207,19 +207,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state, env.transient_storage) + set_code(state, message.current_target, contract_code) + commit_transaction(state, transient_storage) else: - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -235,30 +235,32 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.prague.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state, env.transient_storage) + begin_transaction(state, transient_storage) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state, env.transient_storage) + rollback_transaction(state, transient_storage) else: - commit_transaction(env.state, env.transient_storage) + commit_transaction(state, transient_storage) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -283,7 +285,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index f0fb77eb66..46e39874c1 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -16,23 +16,28 @@ from typing import List, Optional, Tuple, Union from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Account, Address from .state import ( State, account_exists_and_is_empty, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, - process_withdrawal, + modify_state, set_account_balance, state_root, ) @@ -43,10 +48,11 @@ Transaction, decode_transaction, encode_transaction, + get_transaction_hash, recover_sender, validate_transaction, ) -from .trie import Trie, root, trie_set +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -152,36 +158,46 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) if block.ommers != (): raise InvalidBlock - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.base_fee_per_gas, - block.header.gas_limit, - block.header.timestamp, - block.header.prev_randao, - block.transactions, - chain.chain_id, - block.withdrawals, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock - if apply_body_output.withdrawals_root != block.header.withdrawals_root: + if withdrawals_root != block.header.withdrawals_root: raise InvalidBlock chain.blocks.append(block) @@ -253,7 +269,7 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -266,11 +282,27 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + if header.gas_used > header.gas_limit: raise InvalidBlock @@ -301,24 +333,21 @@ def validate_header(header: Header, parent_header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - base_fee_per_gas: Uint, - gas_available: Uint, - chain_id: U64, ) -> Tuple[Address, Uint]: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - base_fee_per_gas : - The block base fee. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -332,32 +361,43 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) if isinstance(tx, FeeMarketTransaction): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock - if tx.max_fee_per_gas < base_fee_per_gas: + if tx.max_fee_per_gas < block_env.base_fee_per_gas: raise InvalidBlock priority_fee_per_gas = min( tx.max_priority_fee_per_gas, - tx.max_fee_per_gas - base_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, ) - effective_gas_price = priority_fee_per_gas + base_fee_per_gas + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - if tx.gas_price < base_fee_per_gas: + if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address, effective_gas_price def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -391,49 +431,11 @@ def make_receipt( return encode_receipt(tx, receipt) -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - withdrawals_root : `ethereum.fork_types.Root` - Trie root of all the withdrawals in the block. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - withdrawals_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - base_fee_per_gas: Uint, - block_gas_limit: Uint, - block_time: U256, - prev_randao: Bytes32, + block_env: vm.BlockEnvironment, transactions: Tuple[Union[LegacyTransaction, Bytes], ...], - chain_id: U64, withdrawals: Tuple[Withdrawal, ...], -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -446,115 +448,36 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - base_fee_per_gas : - Base fee per gas of within the block. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - prev_randao : - The previous randao from the beacon chain. + block_env : + The block scoped environment. + block_output : + The block output for the current block. transactions : Transactions included in the block. - ommers : - Headers of ancestor blocks which are not direct parents (formerly - uncles.) - chain_id : - ID of the executing chain. withdrawals : Withdrawals to be processed in the current block. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[ - Bytes, Optional[Union[Bytes, LegacyTransaction]] - ] = Trie(secured=False, default=None) - receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( - secured=False, default=None - ) - withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(map(decode_transaction, transactions)): - trie_set( - transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) - ) - - sender_address, effective_gas_price = check_transaction( - tx, base_fee_per_gas, gas_available, chain_id - ) + process_transaction(block_env, block_output, tx, Uint(i)) - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - base_fee_per_gas=base_fee_per_gas, - gas_price=effective_gas_price, - time=block_time, - prev_randao=prev_randao, - state=state, - chain_id=chain_id, - traces=[], - ) - - gas_used, logs, error = process_transaction(env, tx) - gas_available -= gas_used + process_withdrawals(block_env, block_output, withdrawals) - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - for i, wd in enumerate(withdrawals): - trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd)) - - process_withdrawal(state, wd) - - if account_exists_and_is_empty(state, wd.address): - destroy_account(state, wd.address) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - root(withdrawals_trie), - ) + return block_output def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -569,103 +492,142 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) + ( + sender, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - if isinstance(tx, FeeMarketTransaction): - max_gas_fee = tx.gas * tx.max_fee_per_gas - else: - max_gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender_account = get_account(block_env.state, sender) - effective_gas_fee = tx.gas * env.gas_price + effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee ) - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) - preaccessed_addresses = set() - preaccessed_storage_keys = set() - preaccessed_addresses.add(env.coinbase) + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for address, keys in tx.access_list: - preaccessed_addresses.add(address) + access_list_addresses.add(address) for key in keys: - preaccessed_storage_keys.add((address, key)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, - preaccessed_addresses=frozenset(preaccessed_addresses), - preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + access_list_storage_keys.add((address, key)) + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + traces=[], ) - output = process_message_call(message, env) + message = prepare_message(block_env, tx_env, tx) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(5), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + tx_output = process_message_call(message) - # For non-1559 transactions env.gas_price == tx.gas_price - priority_fee_per_gas = env.gas_price - env.base_fee_per_gas - transaction_fee = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(5), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price - total_gas_used = gas_used - gas_refund + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) + + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) + + block_output.block_logs += tx_output.logs - for address in output.accounts_to_delete: - destroy_account(env.state, address) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * U256(10**9) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) + + modify_state(block_env.state, wd.address, increase_recipient_balance) - return total_gas_used, output.logs, output.error + if account_exists_and_is_empty(block_env.state, wd.address): + destroy_account(block_env.state, wd.address) def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/shanghai/state.py b/src/ethereum/shanghai/state.py index e28e755d9d..9af890fb2f 100644 --- a/src/ethereum/shanghai/state.py +++ b/src/ethereum/shanghai/state.py @@ -17,13 +17,12 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify from ethereum_types.numeric import U256, Uint -from .blocks import Withdrawal from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set @@ -501,20 +500,6 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, recipient_address, increase_recipient_balance) -def process_withdrawal( - state: State, - wd: Withdrawal, -) -> None: - """ - Increase the balance of the withdrawing account. - """ - - def increase_recipient_balance(recipient: Account) -> None: - recipient.balance += wd.amount * U256(10**9) - - modify_state(state, wd.address, increase_recipient_balance) - - def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. @@ -625,3 +610,20 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: assert isinstance(original_value, U256) return original_value + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/shanghai/transactions.py b/src/ethereum/shanghai/transactions.py index 5323b3bd09..f78f3af49e 100644 --- a/src/ethereum/shanghai/transactions.py +++ b/src/ethereum/shanghai/transactions.py @@ -9,7 +9,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 16 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 -TX_ACCESS_LIST_ADDRESS_COST = 2400 -TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(16) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -182,12 +182,12 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ from .vm.gas import init_code_cost - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -196,17 +196,17 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: data_cost += TX_DATA_COST_PER_NON_ZERO if tx.to == Bytes0(b""): - create_cost = TX_CREATE_COST + int(init_code_cost(Uint(len(tx.data)))) + create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) else: - create_cost = 0 + create_cost = Uint(0) - access_list_cost = 0 + access_list_cost = Uint(0) if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST - access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + return TX_BASE_COST + data_cost + create_cost + access_list_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -387,3 +387,22 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/shanghai/utils/message.py b/src/ethereum/shanghai/utils/message.py index 2d5c3b50ef..07832dd672 100644 --- a/src/ethereum/shanghai/utils/message.py +++ b/src/ethereum/shanghai/utils/message.py @@ -12,64 +12,33 @@ Message specific functions used in this shanghai version of specification. """ -from typing import FrozenSet, Optional, Tuple, Union - -from ethereum_types.bytes import Bytes, Bytes0, Bytes32 -from ethereum_types.numeric import U256, Uint +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, - is_static: bool = False, - preaccessed_addresses: FrozenSet[Address] = frozenset(), - preaccessed_storage_keys: FrozenSet[ - Tuple[(Address, Bytes32)] - ] = frozenset(), + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. - is_static: - if True then it prevents all state-changing operations from being - executed. - preaccessed_addresses: - Addresses that should be marked as accessed prior to the message call - preaccessed_storage_keys: - Storage keys that should be marked as accessed prior to the message - call + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- @@ -77,40 +46,44 @@ def prepare_message( Items containing contract creation or message call specific data. """ accessed_addresses = set() - accessed_addresses.add(caller) + accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) - if isinstance(target, Bytes0): + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") accessed_addresses.add(current_target) return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, - is_static=is_static, + should_transfer_value=True, + is_static=False, accessed_addresses=accessed_addresses, - accessed_storage_keys=set(preaccessed_storage_keys), + accessed_storage_keys=set(tx_env.access_list_storage_keys), parent_evm=None, ) diff --git a/src/ethereum/shanghai/vm/__init__.py b/src/ethereum/shanghai/vm/__init__.py index 09c7667789..3d48ccaafe 100644 --- a/src/ethereum/shanghai/vm/__init__.py +++ b/src/ethereum/shanghai/vm/__init__.py @@ -13,40 +13,88 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import LegacyTransaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint base_fee_per_gas: Uint - gas_limit: Uint - gas_price: Uint time: U256 prev_randao: Bytes32 - state: State - chain_id: U64 + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = field(default_factory=lambda: Trie(secured=False, default=None)) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] traces: List[dict] @@ -56,6 +104,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -81,7 +131,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -91,7 +140,7 @@ class Evm: accounts_to_delete: Set[Address] touched_accounts: Set[Address] return_data: Bytes - error: Optional[Exception] + error: Optional[EthereumException] accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] @@ -113,7 +162,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) evm.accessed_addresses.update(child_evm.accessed_addresses) @@ -142,7 +191,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/shanghai/vm/instructions/block.py b/src/ethereum/shanghai/vm/instructions/block.py index ca4fc42958..4eaee1b02d 100644 --- a/src/ethereum/shanghai/vm/instructions/block.py +++ b/src/ethereum/shanghai/vm/instructions/block.py @@ -44,13 +44,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -85,7 +91,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -118,7 +124,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -150,7 +156,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -182,7 +188,7 @@ def prev_randao(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -214,7 +220,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -243,7 +249,7 @@ def chain_id(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.chain_id)) + push(evm.stack, U256(evm.message.block_env.chain_id)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/shanghai/vm/instructions/environment.py b/src/ethereum/shanghai/vm/instructions/environment.py index dc59e168bd..172ce97d70 100644 --- a/src/ethereum/shanghai/vm/instructions/environment.py +++ b/src/ethereum/shanghai/vm/instructions/environment.py @@ -82,7 +82,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -108,7 +108,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -319,7 +319,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -348,7 +348,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -390,7 +390,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -476,7 +476,7 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.env.state, address) + account = get_account(evm.message.block_env.state, address) if account == EMPTY_ACCOUNT: codehash = U256(0) @@ -508,7 +508,9 @@ def self_balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, evm.message.current_target).balance + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance push(evm.stack, balance) @@ -533,7 +535,7 @@ def base_fee(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.base_fee_per_gas)) + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/shanghai/vm/instructions/storage.py b/src/ethereum/shanghai/vm/instructions/storage.py index c1c84399d9..319162b381 100644 --- a/src/ethereum/shanghai/vm/instructions/storage.py +++ b/src/ethereum/shanghai/vm/instructions/storage.py @@ -50,7 +50,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -74,10 +76,11 @@ 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( - evm.env.state, evm.message.current_target, key + state, evm.message.current_target, key ) - current_value = get_storage(evm.env.state, evm.message.current_target, key) + current_value = get_storage(state, evm.message.current_target, key) gas_cost = Uint(0) @@ -117,7 +120,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/shanghai/vm/instructions/system.py b/src/ethereum/shanghai/vm/instructions/system.py index 2498d11792..f214469225 100644 --- a/src/ethereum/shanghai/vm/instructions/system.py +++ b/src/ethereum/shanghai/vm/instructions/system.py @@ -92,7 +92,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) if ( sender.balance < endowment @@ -104,15 +104,19 @@ def generic_create( return if account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) return - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -128,7 +132,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -166,7 +170,9 @@ def create(evm: Evm) -> None: evm.memory += b"\x00" * extend_memory.expand_by contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) generic_create( @@ -296,8 +302,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -313,7 +321,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -369,7 +377,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if is_account_alive(evm.env.state, to) or value == 0 + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -385,7 +393,7 @@ def call(evm: Evm) -> None: raise WriteInStaticContext evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -459,7 +467,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -504,8 +512,11 @@ def selfdestruct(evm: Evm) -> None: gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -514,23 +525,29 @@ def selfdestruct(evm: Evm) -> None: raise WriteInStaticContext originator = evm.message.current_target - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/shanghai/vm/interpreter.py b/src/ethereum/shanghai/vm/interpreter.py index c82dc5e2c6..3ef20b10db 100644 --- a/src/ethereum/shanghai/vm/interpreter.py +++ b/src/ethereum/shanghai/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple, Union +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -47,7 +48,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -81,15 +82,13 @@ class MessageCallOutput: gas_left: Uint refund_counter: U256 - logs: Union[Tuple[()], Tuple[Log, ...]] + logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -99,28 +98,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -148,7 +147,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -164,8 +163,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.shanghai.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -174,15 +174,15 @@ def process_create_message(message: Message, env: Environment) -> Evm: # `CREATE` or `CREATE2` call. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, 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. - mark_account_created(env.state, message.current_target) + mark_account_created(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -194,19 +194,19 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.output = b"" evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -222,30 +222,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.shanghai.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -270,7 +271,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/spurious_dragon/fork.py b/src/ethereum/spurious_dragon/fork.py index 420fce593e..77c4f74360 100644 --- a/src/ethereum/spurious_dragon/fork.py +++ b/src/ethereum/spurious_dragon/fork.py @@ -11,12 +11,11 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -26,19 +25,25 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, account_exists_and_is_empty, create_ether, destroy_account, + destroy_touched_empty_accounts, get_account, increment_nonce, set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -144,32 +149,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, - chain.chain_id, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -179,7 +193,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -192,11 +206,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -295,21 +328,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, - chain_id: U64, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. - chain_id : - The ID of the current chain. Returns ------- @@ -321,15 +354,25 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock - sender_address = recover_sender(chain_id, tx) + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, post_state: Bytes32, cumulative_gas_used: Uint, logs: Tuple[Log, ...], @@ -339,8 +382,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -364,45 +405,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], - chain_id: U64, -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -415,90 +422,27 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : Headers of ancestor blocks which are not direct parents (formerly uncles.) - chain_id : - ID of the executing chain. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available, chain_id) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, state_root(state), (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + process_transaction(block_env, block_output, tx, Uint(i)) - block_logs += logs + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - pay_rewards(state, block_number, coinbase, ommers) - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -537,10 +481,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -622,8 +563,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -638,77 +582,95 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) if coinbase_balance_after_mining_fee != 0: set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, ) - elif account_exists_and_is_empty(env.state, env.coinbase): - destroy_account(env.state, env.coinbase) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + state_root(block_env.state), + block_output.block_gas_used, + tx_output.logs, + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/spurious_dragon/state.py b/src/ethereum/spurious_dragon/state.py index eefb7bff4e..1c14d581a8 100644 --- a/src/ethereum/spurious_dragon/state.py +++ b/src/ethereum/spurious_dragon/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Tuple from ethereum_types.bytes import Bytes from ethereum_types.frozen import modify @@ -571,3 +571,20 @@ def increase_balance(account: Account) -> None: account.balance += amount modify_state(state, address, increase_balance) + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/spurious_dragon/transactions.py b/src/ethereum/spurious_dragon/transactions.py index 1c67eeaf02..21ffbc1b6f 100644 --- a/src/ethereum/spurious_dragon/transactions.py +++ b/src/ethereum/spurious_dragon/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(chain_id: U64, tx: Transaction) -> Address: @@ -157,6 +157,7 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) ) + return Address(keccak256(public_key)[12:32]) @@ -219,3 +220,18 @@ def signing_hash_155(tx: Transaction, chain_id: U64) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/spurious_dragon/utils/message.py b/src/ethereum/spurious_dragon/utils/message.py index de8b274d5d..c513ddfc6d 100644 --- a/src/ethereum/spurious_dragon/utils/message.py +++ b/src/ethereum/spurious_dragon/utils/message.py @@ -12,82 +12,67 @@ Message specific functions used in this spurious dragon version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.spurious_dragon.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, + should_transfer_value=True, parent_evm=None, ) diff --git a/src/ethereum/spurious_dragon/vm/__init__.py b/src/ethereum/spurious_dragon/vm/__init__.py index 808dd0f48a..b63a25edab 100644 --- a/src/ethereum/spurious_dragon/vm/__init__.py +++ b/src/ethereum/spurious_dragon/vm/__init__.py @@ -13,38 +13,80 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State, account_exists_and_is_empty +from ..transactions import Transaction +from ..trie import Trie from .precompiled_contracts import RIPEMD160_ADDRESS __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -54,6 +96,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -76,7 +120,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -85,7 +128,7 @@ class Evm: output: Bytes accounts_to_delete: Set[Address] touched_accounts: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: @@ -105,7 +148,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accounts_to_delete.update(child_evm.accounts_to_delete) evm.touched_accounts.update(child_evm.touched_accounts) if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(child_evm.message.current_target) @@ -132,7 +175,7 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: evm.touched_accounts.add(RIPEMD160_ADDRESS) if child_evm.message.current_target == RIPEMD160_ADDRESS: if account_exists_and_is_empty( - evm.env.state, child_evm.message.current_target + evm.message.block_env.state, child_evm.message.current_target ): evm.touched_accounts.add(RIPEMD160_ADDRESS) evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/spurious_dragon/vm/instructions/block.py b/src/ethereum/spurious_dragon/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/block.py +++ b/src/ethereum/spurious_dragon/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/spurious_dragon/vm/instructions/environment.py b/src/ethereum/spurious_dragon/vm/instructions/environment.py index 1bce21fda7..36215ecb1a 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/environment.py +++ b/src/ethereum/spurious_dragon/vm/instructions/environment.py @@ -73,7 +73,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -99,7 +99,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -310,7 +310,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -333,8 +333,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -369,7 +368,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/spurious_dragon/vm/instructions/storage.py b/src/ethereum/spurious_dragon/vm/instructions/storage.py index 7b299e07d0..76c11bcfe4 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/storage.py +++ b/src/ethereum/spurious_dragon/vm/instructions/storage.py @@ -44,7 +44,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -67,7 +69,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -79,7 +82,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/spurious_dragon/vm/instructions/system.py b/src/ethereum/spurious_dragon/vm/instructions/system.py index 3ce0780272..a4d809e110 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/system.py +++ b/src/ethereum/spurious_dragon/vm/instructions/system.py @@ -80,11 +80,13 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -95,18 +97,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -119,7 +127,7 @@ def create(evm: Evm) -> None: should_transfer_value=True, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -192,8 +200,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -206,7 +216,7 @@ def generic_call( should_transfer_value=should_transfer_value, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -254,7 +264,7 @@ def call(evm: Evm) -> None: create_gas_cost = ( Uint(0) - if value == 0 or is_account_alive(evm.env.state, to) + if is_account_alive(evm.message.block_env.state, to) or value == 0 else GAS_NEW_ACCOUNT ) transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE @@ -270,7 +280,7 @@ def call(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -335,7 +345,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -374,8 +384,11 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT if ( - not is_account_alive(evm.env.state, beneficiary) - and get_account(evm.env.state, evm.message.current_target).balance != 0 + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 ): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT @@ -392,24 +405,29 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) - # OPERATION - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) # mark beneficiary as touched - if account_exists_and_is_empty(evm.env.state, beneficiary): + if account_exists_and_is_empty(evm.message.block_env.state, beneficiary): evm.touched_accounts.add(beneficiary) # HALT the execution diff --git a/src/ethereum/spurious_dragon/vm/interpreter.py b/src/ethereum/spurious_dragon/vm/interpreter.py index a219901806..e3c1fee6d2 100644 --- a/src/ethereum/spurious_dragon/vm/interpreter.py +++ b/src/ethereum/spurious_dragon/vm/interpreter.py @@ -12,11 +12,12 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from typing import Iterable, Optional, Set, Tuple +from typing import Optional, Set, Tuple from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -46,7 +47,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -80,13 +81,11 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - touched_accounts: Iterable[Address] - error: Optional[Exception] + touched_accounts: Set[Address] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -96,28 +95,28 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) - if account_exists_and_is_empty(env.state, Address(message.target)): + evm = process_message(message) + if account_exists_and_is_empty( + block_env.state, Address(message.target) + ): evm.touched_accounts.add(Address(message.target)) if evm.error: @@ -145,7 +144,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -161,8 +160,9 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.spurious_dragon.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely @@ -170,10 +170,10 @@ def process_create_message(message: Message, env: Environment) -> Evm: # * The address created by two `CREATE` calls collide. # * The first `CREATE` happened before Spurious Dragon and left empty # code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - increment_nonce(env.state, message.current_target) - evm = process_message(message, env) + increment_nonce(state, message.current_target) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT @@ -182,18 +182,18 @@ def process_create_message(message: Message, env: Environment) -> Evm: if len(contract_code) > MAX_CODE_SIZE: raise OutOfGasError except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -209,30 +209,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.spurious_dragon.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -257,7 +258,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum/tangerine_whistle/fork.py b/src/ethereum/tangerine_whistle/fork.py index cf39821f14..9a13f28c3b 100644 --- a/src/ethereum/tangerine_whistle/fork.py +++ b/src/ethereum/tangerine_whistle/fork.py @@ -11,12 +11,11 @@ Entry point for the Ethereum specification. """ - from dataclasses import dataclass -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 @@ -26,7 +25,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom -from .fork_types import Address, Bloom, Root +from .fork_types import Address from .state import ( State, create_ether, @@ -36,8 +35,13 @@ set_account_balance, state_root, ) -from .transactions import Transaction, recover_sender, validate_transaction -from .trie import Trie, root, trie_set +from .transactions import ( + Transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -143,31 +147,41 @@ def state_transition(chain: BlockChain, block: Block) -> None: block : Block to apply to `chain`. """ - parent_header = chain.blocks[-1].header - validate_header(block.header, parent_header) + validate_header(chain, block.header) validate_ommers(block.ommers, block.header, chain) - apply_body_output = apply_body( - chain.state, - get_last_256_block_hashes(chain), - block.header.coinbase, - block.header.number, - block.header.gas_limit, - block.header.timestamp, - block.header.difficulty, - block.transactions, - block.ommers, + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + time=block.header.timestamp, + difficulty=block.header.difficulty, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + ommers=block.ommers, ) - if apply_body_output.block_gas_used != block.header.gas_used: + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( - f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + f"{block_output.block_gas_used} != {block.header.gas_used}" ) - if apply_body_output.transactions_root != block.header.transactions_root: + if transactions_root != block.header.transactions_root: raise InvalidBlock - if apply_body_output.state_root != block.header.state_root: + if block_state_root != block.header.state_root: raise InvalidBlock - if apply_body_output.receipt_root != block.header.receipt_root: + if receipt_root != block.header.receipt_root: raise InvalidBlock - if apply_body_output.block_logs_bloom != block.header.bloom: + if block_logs_bloom != block.header.bloom: raise InvalidBlock chain.blocks.append(block) @@ -177,7 +191,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def validate_header(chain: BlockChain, header: Header) -> None: """ Verifies a block header. @@ -190,11 +204,30 @@ def validate_header(header: Header, parent_header: Header) -> None: Parameters ---------- + chain : + History and current state. header : Header to check for correctness. - parent_header : - Parent Header of the header to check for correctness """ + if header.number < Uint(1): + raise InvalidBlock + parent_header_number = header.number - Uint(1) + first_block_number = chain.blocks[0].header.number + last_block_number = chain.blocks[-1].header.number + + if ( + parent_header_number < first_block_number + or parent_header_number > last_block_number + ): + raise InvalidBlock + + parent_header = chain.blocks[ + parent_header_number - first_block_number + ].header + + if header.gas_used > header.gas_limit: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -293,18 +326,21 @@ def validate_proof_of_work(header: Header) -> None: def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, tx: Transaction, - gas_available: Uint, ) -> Address: """ Check if the transaction is includable in the block. Parameters ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. tx : The transaction. - gas_available : - The gas remaining in the block. Returns ------- @@ -316,15 +352,25 @@ def check_transaction( InvalidBlock : If the transaction is not includable. """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise InvalidBlock sender_address = recover_sender(tx) + sender_account = get_account(block_env.state, sender_address) + + max_gas_fee = tx.gas * tx.gas_price + + if sender_account.nonce != tx.nonce: + raise InvalidBlock + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InvalidBlock + if sender_account.code: + raise InvalidSenderError("not EOA") return sender_address def make_receipt( - tx: Transaction, post_state: Bytes32, cumulative_gas_used: Uint, logs: Tuple[Log, ...], @@ -334,8 +380,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -359,44 +403,11 @@ def make_receipt( return receipt -@dataclass -class ApplyBodyOutput: - """ - Output from applying the block body to the present state. - - Contains the following: - - block_gas_used : `ethereum.base_types.Uint` - Gas used for executing all transactions. - transactions_root : `ethereum.fork_types.Root` - Trie root of all the transactions in the block. - receipt_root : `ethereum.fork_types.Root` - Trie root of all the receipts in the block. - block_logs_bloom : `Bloom` - Logs bloom of all the logs included in all the transactions of the - block. - state_root : `ethereum.fork_types.Root` - State root after all transactions have been executed. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root - - def apply_body( - state: State, - block_hashes: List[Hash32], - coinbase: Address, - block_number: Uint, - block_gas_limit: Uint, - block_time: U256, - block_difficulty: Uint, + block_env: vm.BlockEnvironment, transactions: Tuple[Transaction, ...], ommers: Tuple[Header, ...], -) -> ApplyBodyOutput: +) -> vm.BlockOutput: """ Executes a block. @@ -409,21 +420,8 @@ def apply_body( Parameters ---------- - state : - Current account state. - block_hashes : - List of hashes of the previous 256 blocks in the order of - increasing block number. - coinbase : - Address of account which receives block reward and transaction fees. - block_number : - Position of the block within the chain. - block_gas_limit : - Initial amount of gas available for execution in this block. - block_time : - Time the block was produced, measured in seconds since the epoch. - block_difficulty : - Difficulty of the block. + block_env : + The block scoped environment. transactions : Transactions included in the block. ommers : @@ -432,65 +430,17 @@ def apply_body( Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + block_output : + The block output for the current block. """ - gas_available = block_gas_limit - transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( - secured=False, default=None - ) - receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( - secured=False, default=None - ) - block_logs: Tuple[Log, ...] = () + block_output = vm.BlockOutput() for i, tx in enumerate(transactions): - trie_set(transactions_trie, rlp.encode(Uint(i)), tx) - - sender_address = check_transaction(tx, gas_available) - - env = vm.Environment( - caller=sender_address, - origin=sender_address, - block_hashes=block_hashes, - coinbase=coinbase, - number=block_number, - gas_limit=block_gas_limit, - gas_price=tx.gas_price, - time=block_time, - difficulty=block_difficulty, - state=state, - traces=[], - ) - - gas_used, logs = process_transaction(env, tx) - gas_available -= gas_used - - receipt = make_receipt( - tx, state_root(state), (block_gas_limit - gas_available), logs - ) - - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - - block_logs += logs + process_transaction(block_env, block_output, tx, Uint(i)) - pay_rewards(state, block_number, coinbase, ommers) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = logs_bloom(block_logs) - - return ApplyBodyOutput( - block_gas_used, - root(transactions_trie), - root(receipts_trie), - block_logs_bloom, - state_root(state), - ) + return block_output def validate_ommers( @@ -529,10 +479,7 @@ def validate_ommers( for ommer in ommers: if Uint(1) > ommer.number or ommer.number >= block_header.number: raise InvalidBlock - ommer_parent_header = chain.blocks[ - -(block_header.number - ommer.number) - 1 - ].header - validate_header(ommer, ommer_parent_header) + validate_header(chain, ommer) if len(ommers) > 2: raise InvalidBlock @@ -614,8 +561,11 @@ def pay_rewards( def process_transaction( - env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...]]: + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: """ Execute a transaction against the provided environment. @@ -630,70 +580,88 @@ def process_transaction( Parameters ---------- - env : + block_env : Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. tx : Transaction to execute. - - Returns - ------- - gas_left : `ethereum.base_types.U256` - Remaining gas after execution. - logs : `Tuple[ethereum.blocks.Log, ...]` - Logs generated during execution. + index: + Index of the transaction in the block. """ + trie_set(block_output.transactions_trie, rlp.encode(Uint(index)), tx) intrinsic_gas = validate_transaction(tx) - sender = env.origin - sender_account = get_account(env.state, sender) - gas_fee = tx.gas * tx.gas_price - if sender_account.nonce != tx.nonce: - raise InvalidBlock - if Uint(sender_account.balance) < gas_fee + Uint(tx.value): - raise InvalidBlock - if sender_account.code != bytearray(): - raise InvalidSenderError("not EOA") + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) gas = tx.gas - intrinsic_gas - increment_nonce(env.state, sender) + increment_nonce(block_env.state, sender) + + gas_fee = tx.gas * tx.gas_price sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee - set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) - - message = prepare_message( - sender, - tx.to, - tx.value, - tx.data, - gas, - env, + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) ) - output = process_message_call(message, env) + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=tx.gas_price, + gas=gas, + index_in_block=index, + tx_hash=get_transaction_hash(tx), + traces=[], + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) - gas_used = tx.gas - output.gas_left - gas_refund = min(gas_used // Uint(2), Uint(output.refund_counter)) - gas_refund_amount = (output.gas_left + gas_refund) * tx.gas_price - transaction_fee = (tx.gas - output.gas_left - gas_refund) * tx.gas_price - total_gas_used = gas_used - gas_refund + gas_used = tx.gas - tx_output.gas_left + gas_refund = min(gas_used // Uint(2), Uint(tx_output.refund_counter)) + tx_gas_used = gas_used - gas_refund + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * tx.gas_price + + transaction_fee = tx_gas_used * tx.gas_price # refund gas sender_balance_after_refund = get_account( - env.state, sender + block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( - env.state, env.coinbase + block_env.state, block_env.coinbase ).balance + U256(transaction_fee) set_account_balance( - env.state, env.coinbase, coinbase_balance_after_mining_fee + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee ) - for address in output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + block_output.block_gas_used += tx_gas_used + + receipt = make_receipt( + state_root(block_env.state), + block_output.block_gas_used, + tx_output.logs, + ) + + trie_set( + block_output.receipts_trie, + rlp.encode(Uint(index)), + receipt, + ) - return total_gas_used, output.logs + block_output.block_logs += tx_output.logs def compute_header_hash(header: Header) -> Hash32: diff --git a/src/ethereum/tangerine_whistle/transactions.py b/src/ethereum/tangerine_whistle/transactions.py index 96d825e2ba..6db00e736d 100644 --- a/src/ethereum/tangerine_whistle/transactions.py +++ b/src/ethereum/tangerine_whistle/transactions.py @@ -17,10 +17,10 @@ from .fork_types import Address -TX_BASE_COST = 21000 -TX_DATA_COST_PER_NON_ZERO = 68 -TX_DATA_COST_PER_ZERO = 4 -TX_CREATE_COST = 32000 +TX_BASE_COST = Uint(21000) +TX_DATA_COST_PER_NON_ZERO = Uint(68) +TX_DATA_COST_PER_ZERO = Uint(4) +TX_CREATE_COST = Uint(32000) @slotted_freezable @@ -99,10 +99,10 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ - data_cost = 0 + data_cost = Uint(0) for byte in tx.data: if byte == 0: @@ -113,9 +113,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST else: - create_cost = 0 + create_cost = Uint(0) - return Uint(TX_BASE_COST + data_cost + create_cost) + return TX_BASE_COST + data_cost + create_cost def recover_sender(tx: Transaction) -> Address: @@ -180,3 +180,18 @@ def signing_hash(tx: Transaction) -> Hash32: ) ) ) + + +def get_transaction_hash(tx: Transaction) -> Hash32: + """ + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256(rlp.encode(tx)) diff --git a/src/ethereum/tangerine_whistle/utils/message.py b/src/ethereum/tangerine_whistle/utils/message.py index b8821422ce..47b84c1af2 100644 --- a/src/ethereum/tangerine_whistle/utils/message.py +++ b/src/ethereum/tangerine_whistle/utils/message.py @@ -12,82 +12,67 @@ Message specific functions used in this tangerine whistle version of specification. """ -from typing import Optional, Union - from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import Uint from ..fork_types import Address from ..state import get_account -from ..vm import Environment, Message +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment from .address import compute_contract_address def prepare_message( - caller: Address, - target: Union[Bytes0, Address], - value: U256, - data: Bytes, - gas: Uint, - env: Environment, - code_address: Optional[Address] = None, - should_transfer_value: bool = True, + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. Parameters ---------- - caller : - Address which initiated the transaction - target : - Address whose code will be executed - value : - Value to be transferred. - data : - Array of bytes provided to the code in `target`. - gas : - Gas provided for the code in `target`. - env : + block_env : Environment for the Ethereum Virtual Machine. - code_address : - This is usually same as the `target` address except when an alternative - accounts code needs to be executed. - eg. `CALLCODE` calling a precompile. - should_transfer_value : - if True ETH should be transferred while executing a message call. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. Returns ------- message: `ethereum.tangerine_whistle.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + if isinstance(tx.to, Bytes0): current_target = compute_contract_address( - caller, - get_account(env.state, caller).nonce - Uint(1), + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), ) msg_data = Bytes(b"") - code = data - elif isinstance(target, Address): - current_target = target - msg_data = data - code = get_account(env.state, target).code - if code_address is None: - code_address = target + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + + code_address = tx.to else: raise AssertionError("Target must be address or empty bytes") return Message( - caller=caller, - target=target, - gas=gas, - value=value, + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, data=msg_data, code=code, depth=Uint(0), current_target=current_target, code_address=code_address, - should_transfer_value=should_transfer_value, + should_transfer_value=True, parent_evm=None, ) diff --git a/src/ethereum/tangerine_whistle/vm/__init__.py b/src/ethereum/tangerine_whistle/vm/__init__.py index f2945ad891..d351223871 100644 --- a/src/ethereum/tangerine_whistle/vm/__init__.py +++ b/src/ethereum/tangerine_whistle/vm/__init__.py @@ -13,37 +13,79 @@ `.fork_types.Account`. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional, Set, Tuple, Union from ethereum_types.bytes import Bytes, Bytes0 -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException -from ..blocks import Log +from ..blocks import Log, Receipt from ..fork_types import Address from ..state import State +from ..transactions import Transaction +from ..trie import Trie __all__ = ("Environment", "Evm", "Message") @dataclass -class Environment: +class BlockEnvironment: """ Items external to the virtual machine itself, provided by the environment. """ - caller: Address + chain_id: U64 + state: State + block_gas_limit: Uint block_hashes: List[Hash32] - origin: Address coinbase: Address number: Uint - gas_limit: Uint - gas_price: Uint time: U256 difficulty: Uint - state: State + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Transaction]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + index_in_block: Uint + tx_hash: Optional[Hash32] traces: List[dict] @@ -53,6 +95,8 @@ class Message: Items that are used by contract creation or message call. """ + block_env: BlockEnvironment + tx_env: TransactionEnvironment caller: Address target: Union[Bytes0, Address] current_target: Address @@ -75,7 +119,6 @@ class Evm: memory: bytearray code: Bytes gas_left: Uint - env: Environment valid_jump_destinations: Set[Uint] logs: Tuple[Log, ...] refund_counter: int @@ -83,7 +126,7 @@ class Evm: message: Message output: Bytes accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: diff --git a/src/ethereum/tangerine_whistle/vm/instructions/block.py b/src/ethereum/tangerine_whistle/vm/instructions/block.py index bec65654b1..fc9bd51a23 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/block.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/block.py @@ -38,13 +38,19 @@ def block_hash(evm: Evm) -> None: # OPERATION max_block_number = block_number + Uint(256) - if evm.env.number <= block_number or evm.env.number > max_block_number: + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): # Default hash to 0, if the block of interest is not yet on the chain # (including the block which has the current executing transaction), # or if the block's age is more than 256. hash = b"\x00" else: - hash = evm.env.block_hashes[-(evm.env.number - block_number)] + hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] push(evm.stack, U256.from_be_bytes(hash)) @@ -73,7 +79,7 @@ def coinbase(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -100,7 +106,7 @@ def timestamp(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, evm.env.time) + push(evm.stack, evm.message.block_env.time) # PROGRAM COUNTER evm.pc += Uint(1) @@ -126,7 +132,7 @@ def number(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.number)) + push(evm.stack, U256(evm.message.block_env.number)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -152,7 +158,7 @@ def difficulty(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.difficulty)) + push(evm.stack, U256(evm.message.block_env.difficulty)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -178,7 +184,7 @@ def gas_limit(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_limit)) + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/tangerine_whistle/vm/instructions/environment.py b/src/ethereum/tangerine_whistle/vm/instructions/environment.py index 1bce21fda7..36215ecb1a 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/environment.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/environment.py @@ -73,7 +73,7 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.env.state, address).balance + balance = get_account(evm.message.block_env.state, address).balance push(evm.stack, balance) @@ -99,7 +99,7 @@ def origin(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256.from_be_bytes(evm.env.origin)) + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -310,7 +310,7 @@ def gasprice(evm: Evm) -> None: charge_gas(evm, GAS_BASE) # OPERATION - push(evm.stack, U256(evm.env.gas_price)) + push(evm.stack, U256(evm.message.tx_env.gas_price)) # PROGRAM COUNTER evm.pc += Uint(1) @@ -333,8 +333,7 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code codesize = U256(len(code)) push(evm.stack, codesize) @@ -369,7 +368,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.env.state, address).code + code = get_account(evm.message.block_env.state, address).code value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/tangerine_whistle/vm/instructions/storage.py b/src/ethereum/tangerine_whistle/vm/instructions/storage.py index 7b299e07d0..76c11bcfe4 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/storage.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/storage.py @@ -44,7 +44,9 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_SLOAD) # OPERATION - value = get_storage(evm.env.state, evm.message.current_target, key) + value = get_storage( + evm.message.block_env.state, evm.message.current_target, key + ) push(evm.stack, value) @@ -67,7 +69,8 @@ def sstore(evm: Evm) -> None: new_value = pop(evm.stack) # GAS - current_value = get_storage(evm.env.state, evm.message.current_target, key) + state = evm.message.block_env.state + current_value = get_storage(state, evm.message.current_target, key) if new_value != 0 and current_value == 0: gas_cost = GAS_STORAGE_SET else: @@ -79,7 +82,7 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - set_storage(evm.env.state, evm.message.current_target, key, new_value) + set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/tangerine_whistle/vm/instructions/system.py b/src/ethereum/tangerine_whistle/vm/instructions/system.py index b931d8e8eb..1d5b2fdd81 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/system.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/system.py @@ -79,11 +79,13 @@ def create(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_address = evm.message.current_target - sender = get_account(evm.env.state, sender_address) + sender = get_account(evm.message.block_env.state, sender_address) contract_address = compute_contract_address( evm.message.current_target, - get_account(evm.env.state, evm.message.current_target).nonce, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, ) if ( @@ -94,18 +96,24 @@ def create(evm: Evm) -> None: push(evm.stack, U256(0)) evm.gas_left += create_message_gas elif account_has_code_or_nonce( - evm.env.state, contract_address - ) or account_has_storage(evm.env.state, contract_address): - increment_nonce(evm.env.state, evm.message.current_target) + 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 + ) push(evm.stack, U256(0)) else: call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size ) - increment_nonce(evm.env.state, evm.message.current_target) + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=evm.message.current_target, target=Bytes0(), gas=create_message_gas, @@ -118,7 +126,7 @@ def create(evm: Evm) -> None: should_transfer_value=True, parent_evm=evm, ) - child_evm = process_create_message(child_message, evm.env) + child_evm = process_create_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -191,8 +199,10 @@ def generic_call( call_data = memory_read_bytes( evm.memory, memory_input_start_position, memory_input_size ) - code = get_account(evm.env.state, code_address).code + code = get_account(evm.message.block_env.state, code_address).code child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, caller=caller, target=to, gas=gas, @@ -205,7 +215,7 @@ def generic_call( should_transfer_value=should_transfer_value, parent_evm=evm, ) - child_evm = process_message(child_message, evm.env) + child_evm = process_message(child_message) if child_evm.error: incorporate_child_on_error(evm, child_evm) @@ -251,7 +261,7 @@ def call(evm: Evm) -> None: code_address = to - _account_exists = account_exists(evm.env.state, to) + _account_exists = account_exists(evm.message.block_env.state, to) create_gas_cost = Uint(0) if _account_exists else GAS_NEW_ACCOUNT transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE message_call_gas = calculate_message_call_gas( @@ -266,7 +276,7 @@ def call(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -331,7 +341,7 @@ def callcode(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( - evm.env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target ).balance if sender_balance < value: push(evm.stack, U256(0)) @@ -369,7 +379,7 @@ def selfdestruct(evm: Evm) -> None: # GAS gas_cost = GAS_SELF_DESTRUCT - if not account_exists(evm.env.state, beneficiary): + if not account_exists(evm.message.block_env.state, beneficiary): gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT originator = evm.message.current_target @@ -386,17 +396,23 @@ def selfdestruct(evm: Evm) -> None: charge_gas(evm, gas_cost) # OPERATION - beneficiary_balance = get_account(evm.env.state, beneficiary).balance - originator_balance = get_account(evm.env.state, originator).balance + beneficiary_balance = get_account( + evm.message.block_env.state, beneficiary + ).balance + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance # First Transfer to beneficiary set_account_balance( - evm.env.state, beneficiary, beneficiary_balance + originator_balance + evm.message.block_env.state, + beneficiary, + beneficiary_balance + originator_balance, ) # Next, Zero the balance of the address being deleted (must come after # sending to beneficiary in case the contract named itself as the # beneficiary). - set_account_balance(evm.env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0)) # register account for deletion evm.accounts_to_delete.add(originator) diff --git a/src/ethereum/tangerine_whistle/vm/interpreter.py b/src/ethereum/tangerine_whistle/vm/interpreter.py index a02a0673ba..216d6a349f 100644 --- a/src/ethereum/tangerine_whistle/vm/interpreter.py +++ b/src/ethereum/tangerine_whistle/vm/interpreter.py @@ -17,6 +17,7 @@ from ethereum_types.bytes import Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -44,7 +45,7 @@ from ..vm import Message from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Environment, Evm +from . import Evm from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -75,12 +76,10 @@ class MessageCallOutput: refund_counter: U256 logs: Tuple[Log, ...] accounts_to_delete: Set[Address] - error: Optional[Exception] + error: Optional[EthereumException] -def process_message_call( - message: Message, env: Environment -) -> MessageCallOutput: +def process_message_call(message: Message) -> MessageCallOutput: """ If `message.current` is empty then it creates a smart contract else it executes a call from the `message.caller` to the `message.target`. @@ -90,27 +89,25 @@ def process_message_call( message : Transaction specific items. - env : - External items required for EVM execution. - Returns ------- output : `MessageCallOutput` Output of the message call """ + block_env = message.block_env refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( - env.state, message.current_target - ) or account_has_storage(env.state, message.current_target) + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) if is_collision: return MessageCallOutput( Uint(0), U256(0), tuple(), set(), AddressCollision() ) else: - evm = process_create_message(message, env) + evm = process_create_message(message) else: - evm = process_message(message, env) + evm = process_message(message) if evm.error: logs: Tuple[Log, ...] = () @@ -134,7 +131,7 @@ def process_message_call( ) -def process_create_message(message: Message, env: Environment) -> Evm: +def process_create_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -150,35 +147,36 @@ def process_create_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.tangerine_whistle.vm.Evm` Items containing execution specific objects. """ + state = message.block_env.state # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) # If the address where the account is being created has storage, it is # destroyed. This can only happen in the following highly unlikely # circumstances: # * The address created by two `CREATE` calls collide. # * The first `CREATE` left empty code. - destroy_storage(env.state, message.current_target) + destroy_storage(state, message.current_target) - evm = process_message(message, env) + evm = process_message(message) if not evm.error: contract_code = evm.output contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT try: charge_gas(evm, contract_code_gas) except ExceptionalHalt as error: - rollback_transaction(env.state) + rollback_transaction(state) evm.gas_left = Uint(0) evm.error = error else: - set_code(env.state, message.current_target, contract_code) - commit_transaction(env.state) + set_code(state, message.current_target, contract_code) + commit_transaction(state) else: - rollback_transaction(env.state) + rollback_transaction(state) return evm -def process_message(message: Message, env: Environment) -> Evm: +def process_message(message: Message) -> Evm: """ Executes a call to create a smart contract. @@ -194,30 +192,31 @@ def process_message(message: Message, env: Environment) -> Evm: evm: :py:class:`~ethereum.tangerine_whistle.vm.Evm` Items containing execution specific objects """ + state = message.block_env.state if message.depth > STACK_DEPTH_LIMIT: raise StackDepthLimitError("Stack depth limit reached") # take snapshot of state before processing the message - begin_transaction(env.state) + begin_transaction(state) - touch_account(env.state, message.current_target) + touch_account(state, message.current_target) if message.should_transfer_value and message.value != 0: move_ether( - env.state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value ) - evm = execute_code(message, env) + evm = execute_code(message) if evm.error: # revert state to the last saved checkpoint # since the message call resulted in an error - rollback_transaction(env.state) + rollback_transaction(state) else: - commit_transaction(env.state) + commit_transaction(state) return evm -def execute_code(message: Message, env: Environment) -> Evm: +def execute_code(message: Message) -> Evm: """ Executes bytecode present in the `message`. @@ -242,7 +241,6 @@ def execute_code(message: Message, env: Environment) -> Evm: memory=bytearray(), code=code, gas_left=message.gas, - env=env, valid_jump_destinations=valid_jump_destinations, logs=(), refund_counter=0, diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index 2409e5a883..1a7af024a8 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -47,16 +47,6 @@ def is_after_fork(self, target_fork_name: str) -> bool: break return return_value - @property - def SYSTEM_TRANSACTION_GAS(self) -> Any: - """SYSTEM_TRANSACTION_GAS of the given fork.""" - return self._module("fork").SYSTEM_TRANSACTION_GAS - - @property - def SYSTEM_ADDRESS(self) -> Any: - """SYSTEM_ADDRESS of the given fork.""" - return self._module("fork").SYSTEM_ADDRESS - @property def BEACON_ROOTS_ADDRESS(self) -> Any: """BEACON_ROOTS_ADDRESS of the given fork.""" @@ -67,21 +57,6 @@ def HISTORY_STORAGE_ADDRESS(self) -> Any: """HISTORY_STORAGE_ADDRESS of the given fork.""" return self._module("fork").HISTORY_STORAGE_ADDRESS - @property - def WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS(self) -> Any: - """WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS of the given fork.""" - return self._module("fork").WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS - - @property - def CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS(self) -> Any: - """CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS of the given fork.""" - return self._module("fork").CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS - - @property - def HISTORY_SERVE_WINDOW(self) -> Any: - """HISTORY_SERVE_WINDOW of the given fork.""" - return self._module("fork").HISTORY_SERVE_WINDOW - @property def process_general_purpose_requests(self) -> Any: """process_general_purpose_requests function of the given fork.""" @@ -92,6 +67,11 @@ def process_system_transaction(self) -> Any: """process_system_transaction function of the given fork.""" return self._module("fork").process_system_transaction + @property + def process_withdrawals(self) -> Any: + """process_withdrawals function of the given fork.""" + return self._module("fork").process_withdrawals + @property def calculate_block_difficulty(self) -> Any: """calculate_block_difficulty function of the given fork.""" @@ -118,9 +98,9 @@ def state_transition(self) -> Any: return self._module("fork").state_transition @property - def make_receipt(self) -> Any: - """make_receipt function of the fork""" - return self._module("fork").make_receipt + def pay_rewards(self) -> Any: + """pay_rewards function of the fork""" + return self._module("fork").pay_rewards @property def signing_hash(self) -> Any: @@ -158,9 +138,9 @@ def signing_hash_4844(self) -> Any: return self._module("transactions").signing_hash_4844 @property - def check_transaction(self) -> Any: - """check_transaction function of the fork""" - return self._module("fork").check_transaction + def get_transaction_hash(self) -> Any: + """get_transaction_hash function of the fork""" + return self._module("transactions").get_transaction_hash @property def process_transaction(self) -> Any: @@ -178,9 +158,9 @@ def Block(self) -> Any: return self._module("blocks").Block @property - def parse_deposit_requests_from_receipt(self) -> Any: - """parse_deposit_requests_from_receipt function of the fork""" - return self._module("requests").parse_deposit_requests_from_receipt + def decode_receipt(self) -> Any: + """decode_receipt function of the fork""" + return self._module("blocks").decode_receipt @property def compute_requests_hash(self) -> Any: @@ -252,51 +232,16 @@ def State(self) -> Any: """State class of the fork""" return self._module("state").State - @property - def TransientStorage(self) -> Any: - """Transient storage class of the fork""" - return self._module("state").TransientStorage - - @property - def get_account(self) -> Any: - """get_account function of the fork""" - return self._module("state").get_account - @property def set_account(self) -> Any: """set_account function of the fork""" return self._module("state").set_account - @property - def create_ether(self) -> Any: - """create_ether function of the fork""" - return self._module("state").create_ether - @property def set_storage(self) -> Any: """set_storage function of the fork""" return self._module("state").set_storage - @property - def account_exists_and_is_empty(self) -> Any: - """account_exists_and_is_empty function of the fork""" - return self._module("state").account_exists_and_is_empty - - @property - def destroy_touched_empty_accounts(self) -> Any: - """destroy_account function of the fork""" - return self._module("state").destroy_touched_empty_accounts - - @property - def destroy_account(self) -> Any: - """destroy_account function of the fork""" - return self._module("state").destroy_account - - @property - def process_withdrawal(self) -> Any: - """process_withdrawal function of the fork""" - return self._module("state").process_withdrawal - @property def state_root(self) -> Any: """state_root function of the fork""" @@ -307,11 +252,6 @@ def close_state(self) -> Any: """close_state function of the fork""" return self._module("state").close_state - @property - def Trie(self) -> Any: - """Trie class of the fork""" - return self._module("trie").Trie - @property def root(self) -> Any: """Root function of the fork""" @@ -322,11 +262,6 @@ def copy_trie(self) -> Any: """copy_trie function of the fork""" return self._module("trie").copy_trie - @property - def trie_set(self) -> Any: - """trie_set function of the fork""" - return self._module("trie").trie_set - @property def hex_to_address(self) -> Any: """hex_to_address function of the fork""" @@ -338,35 +273,25 @@ def hex_to_root(self) -> Any: return self._module("utils.hexadecimal").hex_to_root @property - def Environment(self) -> Any: - """Environment class of the fork""" - return self._module("vm").Environment + def BlockEnvironment(self) -> Any: + """Block environment class of the fork""" + return self._module("vm").BlockEnvironment @property - def Message(self) -> Any: - """Message class of the fork""" - return self._module("vm").Message + def BlockOutput(self) -> Any: + """Block output class of the fork""" + return self._module("vm").BlockOutput @property def Authorization(self) -> Any: """Authorization class of the fork""" - return self._module("vm.eoa_delegation").Authorization + return self._module("fork_types").Authorization @property def TARGET_BLOB_GAS_PER_BLOCK(self) -> Any: """TARGET_BLOB_GAS_PER_BLOCK of the fork""" return self._module("vm.gas").TARGET_BLOB_GAS_PER_BLOCK - @property - def calculate_total_blob_gas(self) -> Any: - """calculate_total_blob_gas function of the fork""" - return self._module("vm.gas").calculate_total_blob_gas - - @property - def process_message_call(self) -> Any: - """process_message_call function of the fork""" - return self._module("vm.interpreter").process_message_call - @property def apply_dao(self) -> Any: """apply_dao function of the fork""" diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py index 196352655d..1e1ac04bcb 100644 --- a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -146,7 +146,7 @@ def run_test_case( t8n_options.output_basedir = output_basedir t8n = T8N(t8n_options, out_stream, in_stream) - t8n.apply_body() + t8n.run_state_test() return t8n.result diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index c835147d4d..4d9de304fc 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -9,12 +9,10 @@ from typing import Any, TextIO from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, Uint from ethereum import trace -from ethereum.crypto.hash import keccak256 -from ethereum.exceptions import EthereumException, InvalidBlock +from ethereum.exceptions import EthereumException from ethereum_spec_tools.forks import Hardfork from ..loaders.fixture_loader import Load @@ -26,7 +24,7 @@ parse_hex_or_int, ) from .env import Env -from .evm_trace import evm_trace, output_traces +from .evm_trace import evm_trace from .t8n_types import Alloc, Result, Txs @@ -127,50 +125,7 @@ def __init__( self.env.block_difficulty, self.env.base_fee_per_gas ) - @property - def BLOCK_REWARD(self) -> Any: - """ - For the t8n tool, the block reward is - provided as a command line option - """ - if self.options.state_reward < 0 or self.fork.is_after_fork( - "ethereum.paris" - ): - return None - else: - return U256(self.options.state_reward) - - def check_transaction(self, tx: Any, gas_available: Any) -> Any: - """ - Implements the check_transaction function of the fork. - The arguments to be passed are adjusted according to the fork. - """ - # TODO: The current PR changes the signature of the check_transaction - # in cancun only. Once this is approved and ported over to the - # the other forks in PR #890, this function has to be updated. - # This is a temporary change to make the tool work for cancun. - if self.fork.is_after_fork("ethereum.cancun"): - return self.fork.check_transaction( - self.alloc.state, - tx, - gas_available, - self.chain_id, - self.env.base_fee_per_gas, - self.env.excess_blob_gas, - ) - arguments = [tx] - - if self.fork.is_after_fork("ethereum.london"): - arguments.append(self.env.base_fee_per_gas) - - arguments.append(gas_available) - - if self.fork.is_after_fork("ethereum.spurious_dragon"): - arguments.append(self.chain_id) - - return self.fork.check_transaction(*arguments) - - def environment(self, tx: Any, gas_available: Any) -> Any: + def block_environment(self) -> Any: """ Create the environment for the transaction. The keyword arguments are adjusted according to the fork. @@ -179,102 +134,27 @@ def environment(self, tx: Any, gas_available: Any) -> Any: "block_hashes": self.env.block_hashes, "coinbase": self.env.coinbase, "number": self.env.block_number, - "gas_limit": self.env.block_gas_limit, "time": self.env.block_timestamp, "state": self.alloc.state, + "block_gas_limit": self.env.block_gas_limit, + "chain_id": self.chain_id, } + if self.fork.is_after_fork("ethereum.london"): + kw_arguments["base_fee_per_gas"] = self.env.base_fee_per_gas + if self.fork.is_after_fork("ethereum.paris"): kw_arguments["prev_randao"] = self.env.prev_randao else: kw_arguments["difficulty"] = self.env.block_difficulty - if self.fork.is_after_fork("ethereum.istanbul"): - kw_arguments["chain_id"] = self.chain_id - - check_tx_return = self.check_transaction(tx, gas_available) - if self.fork.is_after_fork("ethereum.cancun"): - ( - sender_address, - effective_gas_price, - blob_versioned_hashes, - ) = check_tx_return - kw_arguments["base_fee_per_gas"] = self.env.base_fee_per_gas - kw_arguments["caller"] = kw_arguments["origin"] = sender_address - kw_arguments["gas_price"] = effective_gas_price - kw_arguments["blob_versioned_hashes"] = blob_versioned_hashes + kw_arguments[ + "parent_beacon_block_root" + ] = self.env.parent_beacon_block_root kw_arguments["excess_blob_gas"] = self.env.excess_blob_gas - kw_arguments["transient_storage"] = self.fork.TransientStorage() - elif self.fork.is_after_fork("ethereum.london"): - sender_address, effective_gas_price = check_tx_return - kw_arguments["base_fee_per_gas"] = self.env.base_fee_per_gas - kw_arguments["caller"] = kw_arguments["origin"] = sender_address - kw_arguments["gas_price"] = effective_gas_price - else: - sender_address = check_tx_return - kw_arguments["caller"] = kw_arguments["origin"] = sender_address - kw_arguments["gas_price"] = tx.gas_price - - kw_arguments["traces"] = [] - - return self.fork.Environment(**kw_arguments) - - def tx_trie_set(self, trie: Any, index: Any, tx: Any) -> Any: - """Add a transaction to the trie.""" - arguments = [trie, rlp.encode(Uint(index))] - if self.fork.is_after_fork("ethereum.berlin"): - arguments.append(self.fork.encode_transaction(tx)) - else: - arguments.append(tx) - - self.fork.trie_set(*arguments) - - def make_receipt( - self, tx: Any, process_transaction_return: Any, gas_available: Any - ) -> Any: - """Create a transaction receipt.""" - arguments = [tx] - - if self.fork.is_after_fork("ethereum.byzantium"): - arguments.append(process_transaction_return[2]) - else: - arguments.append(self.fork.state_root(self.alloc.state)) - arguments.append((self.env.block_gas_limit - gas_available)) - arguments.append(process_transaction_return[1]) - - return self.fork.make_receipt(*arguments) - - def pay_rewards(self) -> None: - """ - Pay the miner and the ommers. - This function is re-implemented since the uncle header - might not be available in the t8n tool. - """ - coinbase = self.env.coinbase - ommers = self.env.ommers - state = self.alloc.state - - miner_reward = self.BLOCK_REWARD + ( - U256(len(ommers)) * (self.BLOCK_REWARD // U256(32)) - ) - self.fork.create_ether(state, coinbase, miner_reward) - touched_accounts = [coinbase] - - for ommer in ommers: - # Ommer age with respect to the current block. - ommer_miner_reward = ((8 - ommer.delta) * self.BLOCK_REWARD) // 8 - self.fork.create_ether(state, ommer.address, ommer_miner_reward) - touched_accounts.append(ommer.address) - - if self.fork.is_after_fork("ethereum.spurious_dragon"): - # Destroy empty accounts that were touched by - # paying the rewards. This is only important if - # the block rewards were zero. - for account in touched_accounts: - if self.fork.account_exists_and_is_empty(state, account): - self.fork.destroy_account(state, account) + return self.fork.BlockEnvironment(**kw_arguments) def backup_state(self) -> None: """Back up the state in order to restore in case of an error.""" @@ -292,195 +172,94 @@ def restore_state(self) -> None: state = self.alloc.state state._main_trie, state._storage_tries = self.alloc.state_backup - def apply_body(self) -> None: + def run_state_test(self) -> Any: """ - The apply body function is seen as the entry point of - the t8n tool into the designated fork. The function has been - re-implemented here to account for the differences in the - transaction processing between the forks. However, the general - structure of the function is the same. + Apply a single transaction on pre-state. No system operations + are performed. """ - block_gas_limit = self.env.block_gas_limit - - gas_available = block_gas_limit - transactions_trie = self.fork.Trie(secured=False, default=None) - receipts_trie = self.fork.Trie(secured=False, default=None) - block_logs = () - blob_gas_used = Uint(0) - if ( - self.fork.is_after_fork("ethereum.prague") - and not self.options.state_test - ): - deposit_requests: Bytes = b"" + block_env = self.block_environment() + block_output = self.fork.BlockOutput() + self.backup_state() + if len(self.txs.transactions) > 0: + tx = self.txs.transactions[0] + try: + self.fork.process_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + index=Uint(0), + ) + except EthereumException as e: + self.txs.rejected_txs[0] = f"Failed transaction: {e!r}" + self.restore_state() + self.logger.warning(f"Transaction {0} failed: {str(e)}") + + self.result.update(self, block_env, block_output) + self.result.rejected = self.txs.rejected_txs + def run_blockchain_test(self) -> None: + """ + Apply a block on the pre-state. Also includes system operations. + """ + block_env = self.block_environment() + block_output = self.fork.BlockOutput() + + if self.fork.is_after_fork("ethereum.prague"): self.fork.process_system_transaction( - self.fork.HISTORY_STORAGE_ADDRESS, - self.env.parent_hash, - self.env.block_hashes, - self.env.coinbase, - self.env.block_number, - self.env.base_fee_per_gas, - self.env.block_gas_limit, - self.env.block_timestamp, - self.env.prev_randao, - self.alloc.state, - self.chain_id, - self.env.excess_blob_gas, + block_env=block_env, + target_address=self.fork.HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash ) - if ( - self.fork.is_after_fork("ethereum.cancun") - and not self.options.state_test - ): + if self.fork.is_after_fork("ethereum.cancun"): self.fork.process_system_transaction( - self.fork.BEACON_ROOTS_ADDRESS, - self.env.parent_beacon_block_root, - self.env.block_hashes, - self.env.coinbase, - self.env.block_number, - self.env.base_fee_per_gas, - self.env.block_gas_limit, - self.env.block_timestamp, - self.env.prev_randao, - self.alloc.state, - self.chain_id, - self.env.excess_blob_gas, + block_env=block_env, + target_address=self.fork.BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, ) - for i, (tx_idx, tx) in enumerate(self.txs.transactions): - # i is the index among valid transactions - # tx_idx is the index among all transactions. tx_idx is only used - # to identify the transaction in the rejected_txs dictionary. + for i, tx in zip(self.txs.successfully_parsed, self.txs.transactions): self.backup_state() - try: - env = self.environment(tx, gas_available) - - process_transaction_return = self.fork.process_transaction( - env, tx + self.fork.process_transaction( + block_env, block_output, tx, Uint(i) ) - - if self.fork.is_after_fork("ethereum.cancun"): - blob_gas_used += self.fork.calculate_total_blob_gas(tx) - if blob_gas_used > self.fork.MAX_BLOB_GAS_PER_BLOCK: - raise InvalidBlock except EthereumException as e: - # The tf tools expects some non-blank error message - # even in case e is blank. - self.txs.rejected_txs[tx_idx] = f"Failed transaction: {e!r}" + self.txs.rejected_txs[i] = f"Failed transaction: {e!r}" self.restore_state() - self.logger.warning(f"Transaction {tx_idx} failed: {e!r}") - else: - self.txs.add_transaction(tx) - gas_consumed = process_transaction_return[0] - gas_available -= gas_consumed - - if self.options.trace: - tx_hash = self.txs.get_tx_hash(tx) - output_traces( - env.traces, i, tx_hash, self.options.output_basedir - ) - self.tx_trie_set(transactions_trie, i, tx) - - receipt = self.make_receipt( - tx, process_transaction_return, gas_available - ) - - self.fork.trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) - if ( - self.fork.is_after_fork("ethereum.prague") - and not self.options.state_test - ): - deposit_requests += ( - self.fork.parse_deposit_requests_from_receipt(receipt) - ) - - self.txs.add_receipt(tx, gas_consumed) - - block_logs += process_transaction_return[1] - - self.alloc.state._snapshots = [] - - if self.BLOCK_REWARD is not None: - self.pay_rewards() - - block_gas_used = block_gas_limit - gas_available - - block_logs_bloom = self.fork.logs_bloom(block_logs) - - logs_hash = keccak256(rlp.encode(block_logs)) - - if ( - self.fork.is_after_fork("ethereum.shanghai") - and not self.options.state_test - ): - withdrawals_trie = self.fork.Trie(secured=False, default=None) - - for i, wd in enumerate(self.env.withdrawals): - self.fork.trie_set( - withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd) - ) - - self.fork.process_withdrawal(self.alloc.state, wd) - - if self.fork.account_exists_and_is_empty( - self.alloc.state, wd.address - ): - self.fork.destroy_account(self.alloc.state, wd.address) - - self.result.withdrawals_root = self.fork.root(withdrawals_trie) - - if self.fork.is_after_fork("ethereum.cancun"): - self.result.blob_gas_used = blob_gas_used - self.result.excess_blob_gas = self.env.excess_blob_gas - - if ( - self.fork.is_after_fork("ethereum.prague") - and not self.options.state_test - ): - requests_from_execution = ( - self.fork.process_general_purpose_requests( - deposit_requests, - self.alloc.state, - self.env.block_hashes, - self.env.coinbase, - self.env.block_number, - self.env.base_fee_per_gas, - self.env.block_gas_limit, - self.env.block_timestamp, - self.env.prev_randao, - self.chain_id, - self.env.excess_blob_gas, - ) + self.logger.warning(f"Transaction {i} failed: {e!r}") + + if not self.fork.is_after_fork("ethereum.paris"): + self.fork.pay_rewards( + block_env.state, + block_env.number, + block_env.coinbase, + self.env.ommers, ) - requests_hash = self.fork.compute_requests_hash( - requests_from_execution + + if self.fork.is_after_fork("ethereum.shanghai"): + self.fork.process_withdrawals( + block_env, block_output, self.env.withdrawals ) - self.result.state_root = self.fork.state_root(self.alloc.state) - self.result.tx_root = self.fork.root(transactions_trie) - self.result.receipt_root = self.fork.root(receipts_trie) - self.result.bloom = block_logs_bloom - self.result.logs_hash = logs_hash - self.result.rejected = self.txs.rejected_txs - self.result.receipts = self.txs.successful_receipts - self.result.gas_used = block_gas_used + if self.fork.is_after_fork("ethereum.prague"): + self.fork.process_general_purpose_requests(block_env, block_output) - if ( - self.fork.is_after_fork("ethereum.prague") - and not self.options.state_test - ): - self.result.requests_hash = requests_hash - self.result.requests = requests_from_execution + self.result.update(self, block_env, block_output) + self.result.rejected = self.txs.rejected_txs def run(self) -> int: """Run the transition and provide the relevant outputs""" + # Clean out files from the output directory + for file in os.listdir(self.options.output_basedir): + if file.endswith(".json") or file.endswith(".jsonl"): + os.remove(os.path.join(self.options.output_basedir, file)) + try: - self.apply_body() + if self.options.state_test: + self.run_state_test() + else: + self.run_blockchain_test() except FatalException as e: self.logger.error(str(e)) return 1 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py index 06094f2cba..3802d36209 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py @@ -5,7 +5,15 @@ import os from contextlib import ExitStack from dataclasses import asdict, dataclass, is_dataclass -from typing import List, Optional, Protocol, TextIO, Union, runtime_checkable +from typing import ( + Any, + List, + Optional, + Protocol, + TextIO, + Union, + runtime_checkable, +) from ethereum_types.bytes import Bytes from ethereum_types.numeric import U256, Uint @@ -24,6 +32,7 @@ ) EXCLUDE_FROM_OUTPUT = ["gasCostTraced", "errorTraced", "precompile"] +OUTPUT_DIR = "." @dataclass @@ -69,11 +78,13 @@ def __init__( @runtime_checkable -class Environment(Protocol): +class TransactionEnvironment(Protocol): """ - The class implements the environment interface for trace. + The class implements the tx_env interface for trace. """ + index_in_block: Optional[Uint] + tx_hash: Optional[Bytes] traces: List[Union["Trace", "FinalTrace"]] @@ -84,6 +95,7 @@ class Message(Protocol): """ depth: int + tx_env: TransactionEnvironment parent_evm: Optional["Evm"] @@ -98,7 +110,6 @@ class EvmWithoutReturnData(Protocol): memory: bytearray code: Bytes gas_left: Uint - env: Environment refund_counter: int running: bool message: Message @@ -115,7 +126,6 @@ class EvmWithReturnData(Protocol): memory: bytearray code: Bytes gas_left: Uint - env: Environment refund_counter: int running: bool message: Message @@ -126,7 +136,7 @@ class EvmWithReturnData(Protocol): def evm_trace( - evm: object, + evm: Any, event: TraceEvent, trace_memory: bool = False, trace_stack: bool = True, @@ -135,11 +145,19 @@ def evm_trace( """ Create a trace of the event. """ + # System Transaction do not have a tx_hash or index + if ( + evm.message.tx_env.index_in_block is None + or evm.message.tx_env.tx_hash is None + ): + return + assert isinstance(evm, (EvmWithoutReturnData, EvmWithReturnData)) + traces = evm.message.tx_env.traces last_trace = None - if evm.env.traces: - last_trace = evm.env.traces[-1] + if traces: + last_trace = traces[-1] refund_counter = evm.refund_counter parent_evm = evm.message.parent_evm @@ -165,7 +183,13 @@ def evm_trace( pass elif isinstance(event, TransactionEnd): final_trace = FinalTrace(event.gas_used, event.output, event.error) - evm.env.traces.append(final_trace) + traces.append(final_trace) + + output_traces( + traces, + evm.message.tx_env.index_in_block, + evm.message.tx_env.tx_hash, + ) elif isinstance(event, PrecompileStart): new_trace = Trace( pc=int(evm.pc), @@ -182,7 +206,7 @@ def evm_trace( precompile=True, ) - evm.env.traces.append(new_trace) + traces.append(new_trace) elif isinstance(event, PrecompileEnd): assert isinstance(last_trace, Trace) @@ -206,7 +230,7 @@ def evm_trace( opName=str(event.op).split(".")[-1], ) - evm.env.traces.append(new_trace) + traces.append(new_trace) elif isinstance(event, OpEnd): assert isinstance(last_trace, Trace) @@ -251,7 +275,7 @@ def evm_trace( error=type(event.error).__name__, ) - evm.env.traces.append(new_trace) + traces.append(new_trace) elif not last_trace.errorTraced: # If the error for the last trace is not covered # the exception is attributed to the last trace. @@ -271,7 +295,7 @@ def evm_trace( trace_return_data, ) elif isinstance(event, GasAndRefund): - if not evm.env.traces: + if len(traces) == 0: # In contract creation transactions, there may not be any traces return @@ -323,9 +347,8 @@ def output_op_trace( def output_traces( traces: List[Union[Trace, FinalTrace]], - tx_index: int, + index_in_block: int, tx_hash: bytes, - output_basedir: str | TextIO = ".", ) -> None: """ Output the traces to a json file. @@ -333,15 +356,15 @@ def output_traces( with ExitStack() as stack: json_file: TextIO - if isinstance(output_basedir, str): + if isinstance(OUTPUT_DIR, str): tx_hash_str = "0x" + tx_hash.hex() output_path = os.path.join( - output_basedir, f"trace-{tx_index}-{tx_hash_str}.jsonl" + OUTPUT_DIR, f"trace-{index_in_block}-{tx_hash_str}.jsonl" ) json_file = open(output_path, "w") stack.push(json_file) else: - json_file = output_basedir + json_file = OUTPUT_DIR for trace in traces: if getattr(trace, "precompile", False): diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index d5f0f67284..108e41d09c 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -3,9 +3,9 @@ """ import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple -from ethereum_rlp import rlp +from ethereum_rlp import Simple, rlp from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint @@ -85,19 +85,11 @@ class Txs: return a list of transactions. """ - rejected_txs: Dict[int, str] - successful_txs: List[Any] - successful_receipts: List[Any] - all_txs: List[Any] - t8n: "T8N" - data: Any - rlp_input: bool - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.t8n = t8n + self.successfully_parsed: List[int] = [] + self.transactions: List[Tuple[Uint, Any]] = [] self.rejected_txs = {} - self.successful_txs = [] - self.successful_receipts = [] self.rlp_input = False self.all_txs = [] @@ -109,25 +101,21 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): data = json.load(f) if data is None: - self.data = [] + self.data: Simple = [] elif isinstance(data, str): self.rlp_input = True self.data = rlp.decode(hex_to_bytes(data)) else: self.data = data - @property - def transactions(self) -> Iterator[Tuple[int, Any]]: - """ - Read the transactions file and return a list of transactions. - Can read from JSON or RLP. - """ for idx, raw_tx in enumerate(self.data): try: if self.rlp_input: - yield idx, self.parse_rlp_tx(raw_tx) + self.transactions.append(self.parse_rlp_tx(raw_tx)) + self.successfully_parsed.append(idx) else: - yield idx, self.parse_json_tx(raw_tx) + self.transactions.append(self.parse_json_tx(raw_tx)) + self.successfully_parsed.append(idx) except UnsupportedTx as e: self.t8n.logger.warning( f"Unsupported transaction type {idx}: " @@ -199,38 +187,6 @@ def parse_json_tx(self, raw_tx: Any) -> Any: return transaction - def add_transaction(self, tx: Any) -> None: - """ - Add a transaction to the list of successful transactions. - """ - if self.t8n.fork.is_after_fork("ethereum.berlin"): - self.successful_txs.append(self.t8n.fork.encode_transaction(tx)) - else: - self.successful_txs.append(tx) - - def get_tx_hash(self, tx: Any) -> bytes: - """ - Get the transaction hash of a transaction. - """ - if self.t8n.fork.is_after_fork("ethereum.berlin") and not isinstance( - tx, self.t8n.fork.LegacyTransaction - ): - return keccak256(self.t8n.fork.encode_transaction(tx)) - else: - return keccak256(rlp.encode(tx)) - - def add_receipt(self, tx: Any, gas_consumed: Uint) -> None: - """ - Add t8n receipt info for valid tx - """ - tx_hash = self.get_tx_hash(tx) - - data = { - "transactionHash": "0x" + tx_hash.hex(), - "gasUsed": hex(gas_consumed), - } - self.successful_receipts.append(data) - def sign_transaction(self, json_tx: Any) -> None: """ Sign a transaction. This function will be invoked if a `secretKey` @@ -312,6 +268,68 @@ class Result: requests_hash: Optional[Hash32] = None requests: Optional[List[Bytes]] = None + def get_receipts_from_tries( + self, t8n: Any, tx_trie: Any, receipts_trie: Any + ) -> List[Any]: + """ + Get receipts from the transaction and receipts tries. + """ + receipts: List[Any] = [] + for index in tx_trie._data: + if index not in receipts_trie._data: + # Meaning the transaction has somehow failed + return receipts + + tx = tx_trie._data.get(index) + tx_hash = t8n.fork.get_transaction_hash(tx) + + receipt = receipts_trie._data.get(index) + + if hasattr(t8n.fork, "decode_receipt"): + decoded_receipt = t8n.fork.decode_receipt(receipt) + else: + decoded_receipt = receipt + + gas_consumed = decoded_receipt.cumulative_gas_used + + receipts.append( + { + "transactionHash": "0x" + tx_hash.hex(), + "gasUsed": hex(gas_consumed), + } + ) + + return receipts + + def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: + """ + Update the result after processing the inputs. + """ + self.gas_used = block_output.block_gas_used + self.tx_root = t8n.fork.root(block_output.transactions_trie) + self.receipt_root = t8n.fork.root(block_output.receipts_trie) + self.bloom = t8n.fork.logs_bloom(block_output.block_logs) + self.logs_hash = keccak256(rlp.encode(block_output.block_logs)) + self.state_root = t8n.fork.state_root(block_env.state) + self.receipts = self.get_receipts_from_tries( + t8n, block_output.transactions_trie, block_output.receipts_trie + ) + + if hasattr(block_env, "base_fee_per_gas"): + self.base_fee = block_env.base_fee_per_gas + + if hasattr(block_output, "withdrawals_trie"): + self.withdrawals_root = t8n.fork.root( + block_output.withdrawals_trie + ) + + if hasattr(block_env, "excess_blob_gas"): + self.excess_blob_gas = block_env.excess_blob_gas + + if hasattr(block_output, "requests"): + self.requests = block_output.requests + self.requests_hash = t8n.fork.compute_requests_hash(self.requests) + def to_json(self) -> Any: """Encode the result to JSON""" data = {} @@ -345,13 +363,7 @@ def to_json(self) -> Any: for idx, error in self.rejected.items() ] - data["receipts"] = [ - { - "transactionHash": item["transactionHash"], - "gasUsed": item["gasUsed"], - } - for item in self.receipts - ] + data["receipts"] = self.receipts if self.requests_hash is not None: assert self.requests is not None diff --git a/tests/berlin/test_evm_tools.py b/tests/berlin/test_evm_tools.py index 8589da292f..8735219cde 100644 --- a/tests/berlin/test_evm_tools.py +++ b/tests/berlin/test_evm_tools.py @@ -3,21 +3,18 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" -FORK_NAME = "Berlin" - -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, +ETHEREUM_STATE_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "Berlin" SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -28,15 +25,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/berlin/test_state_transition.py b/tests/berlin/test_state_transition.py index 48f947aec5..b5d935463f 100644 --- a/tests/berlin/test_state_transition.py +++ b/tests/berlin/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,18 +11,12 @@ run_blockchain_st_test, ) -fetch_berlin_tests = partial(fetch_state_test_files, network="Berlin") - -FIXTURES_LOADER = Load("Berlin", "berlin") - -run_berlin_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Berlin" +PACKAGE = "berlin" # Every test below takes more than 60s to run and # hence they've been marked as slow @@ -73,107 +62,35 @@ "stTimeConsuming/", ) -fetch_state_tests = partial( - fetch_berlin_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_general_state_tests(test_case: Dict) -> None: - run_berlin_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.LegacyTransaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - chain_id=Uint(1), - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +# Run EEST test fixtures +@pytest.mark.parametrize( + "test_case", + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), + ids=idfn, +) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/byzantium/test_evm_tools.py b/tests/byzantium/test_evm_tools.py index f14fe49df4..cdf5ea6a22 100644 --- a/tests/byzantium/test_evm_tools.py +++ b/tests/byzantium/test_evm_tools.py @@ -3,33 +3,51 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "Byzantium" -run_evm_tools_test = partial( +SLOW_TESTS = () + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/byzantium/test_state_transition.py b/tests/byzantium/test_state_transition.py index c392873f78..dd15c234ab 100644 --- a/tests/byzantium/test_state_transition.py +++ b/tests/byzantium/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,18 +11,12 @@ run_blockchain_st_test, ) -fetch_byzantium_tests = partial(fetch_state_test_files, network="Byzantium") - -FIXTURES_LOADER = Load("Byzantium", "byzantium") - -run_byzantium_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Byzantium" +PACKAGE = "byzantium" # These are tests that are considered to be incorrect, # Please provide an explanation when adding entries @@ -67,124 +56,35 @@ "bcUncleHeaderValidity/wrongMixHash.json", ) -fetch_legacy_state_tests = partial( - fetch_byzantium_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=LEGACY_SLOW_TESTS, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_byzantium_blockchain_st_tests(test_case) - - -# Run Non-Legacy StateTests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_byzantium_tests(test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_state_tests(test_case: Dict) -> None: - run_byzantium_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/cancun/test_evm_tools.py b/tests/cancun/test_evm_tools.py index fe78ba037f..3bd6c91a6f 100644 --- a/tests/cancun/test_evm_tools.py +++ b/tests/cancun/test_evm_tools.py @@ -3,22 +3,17 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +ETHEREUM_STATE_TESTS_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "Cancun" -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, -) - SLOW_TESTS = ( "CALLBlake2f_MaxRounds", "CALLCODEBlake2f", @@ -28,15 +23,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/cancun/test_state_transition.py b/tests/cancun/test_state_transition.py index e8544bd186..6c72238bb4 100644 --- a/tests/cancun/test_state_transition.py +++ b/tests/cancun/test_state_transition.py @@ -3,7 +3,7 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -11,22 +11,10 @@ run_blockchain_st_test, ) -fetch_cancun_tests = partial(fetch_state_test_files, network="Cancun") - -FIXTURES_LOADER = Load("Cancun", "cancun") - -run_cancun_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER -) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -ETHEREUM_SPEC_TESTS_PATH = TEST_FIXTURES["execution_spec_tests"][ - "fixture_path" -] - - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/" +ETHEREUM_BLOCKCHAIN_TESTS_DIR = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Cancun" +PACKAGE = "cancun" SLOW_TESTS = ( # GeneralStateTests @@ -74,32 +62,35 @@ "stStaticCall/", ) -fetch_state_tests = partial( - fetch_cancun_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_general_state_tests(test_case: Dict) -> None: - run_cancun_blockchain_st_tests(test_case) - - -# Run execution-spec-generated-tests -test_dir = f"{ETHEREUM_SPEC_TESTS_PATH}/fixtures/withdrawals" +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_cancun_tests(test_dir), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_execution_specs_generated_tests(test_case: Dict) -> None: - run_cancun_blockchain_st_tests(test_case) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/conftest.py b/tests/conftest.py index 0f64dc1704..d61805f4b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,15 @@ def pytest_addoption(parser: Parser) -> None: help="Use optimized state and ethash", ) + parser.addoption( + "--evm_trace", + dest="evm_trace", + default=False, + action="store_const", + const=True, + help="Create an evm trace", + ) + def pytest_configure(config: Config) -> None: """ @@ -37,6 +46,19 @@ def pytest_configure(config: Config) -> None: ethereum_optimized.monkey_patch(None) + if config.getoption("evm_trace"): + path = config.getoption("evm_trace") + import ethereum.trace + import ethereum_spec_tools.evm_tools.t8n.evm_trace as evm_trace_module + from ethereum_spec_tools.evm_tools.t8n.evm_trace import ( + evm_trace as new_trace_function, + ) + + # Replace the function in the module + ethereum.trace.evm_trace = new_trace_function + # Set the output directory for traces + evm_trace_module.OUTPUT_DIR = path + def download_fixtures(url: str, location: str) -> None: # xdist processes will all try to download the fixtures. diff --git a/tests/constantinople/test_evm_tools.py b/tests/constantinople/test_evm_tools.py index 01536adfdc..0cafe1c7db 100644 --- a/tests/constantinople/test_evm_tools.py +++ b/tests/constantinople/test_evm_tools.py @@ -3,33 +3,50 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "ConstantinopleFix" -run_evm_tools_test = partial( +SLOW_TESTS = () +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/constantinople/test_state_transition.py b/tests/constantinople/test_state_transition.py index 1eda69979c..a6e8ae089e 100644 --- a/tests/constantinople/test_state_transition.py +++ b/tests/constantinople/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,20 +11,12 @@ run_blockchain_st_test, ) -fetch_constantinople_tests = partial( - fetch_state_test_files, network="ConstantinopleFix" +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) - -FIXTURES_LOADER = Load("ConstantinopleFix", "constantinople") - -run_constantinople_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER -) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "ConstantinopleFix" +PACKAGE = "constantinople" # These are tests that are considered to be incorrect, @@ -70,124 +57,35 @@ "bcUncleHeaderValidity/wrongMixHash.json", ) -fetch_legacy_state_tests = partial( - fetch_constantinople_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=LEGACY_SLOW_TESTS, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_constantinople_blockchain_st_tests(test_case) - - -# Run Non-Legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_constantinople_tests(test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_state_tests(test_case: Dict) -> None: - run_constantinople_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/frontier/test_evm_tools.py b/tests/frontier/test_evm_tools.py index fb6635dea0..7ef3de2ac3 100644 --- a/tests/frontier/test_evm_tools.py +++ b/tests/frontier/test_evm_tools.py @@ -3,33 +3,51 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "Frontier" -run_evm_tools_test = partial( +SLOW_TESTS = () + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/frontier/test_state_transition.py b/tests/frontier/test_state_transition.py index 840fe3a9ca..1f27ff15f9 100644 --- a/tests/frontier/test_state_transition.py +++ b/tests/frontier/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,21 +11,12 @@ run_blockchain_st_test, ) -fetch_frontier_tests = partial(fetch_state_test_files, network="Frontier") - -FIXTURES_LOADER = Load("Frontier", "frontier") - -run_frontier_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER -) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - - -# Run legacy general state tests -legacy_test_dir = ( +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Frontier" +PACKAGE = "frontier" LEGACY_IGNORE_LIST = ( # Valid block tests to be ignored @@ -59,126 +45,35 @@ "bcUncleHeaderValidity/wrongMixHash.json", ) -fetch_legacy_state_tests = partial( - fetch_frontier_tests, - legacy_test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=SLOW_LIST, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_frontier_blockchain_st_tests(test_case) - - -# Run Non-Legacy Tests -non_legacy_test_dir = ( - f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" -) - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_frontier_tests(non_legacy_test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_tests(test_case: Dict) -> None: - run_frontier_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index b551825535..b25e2049ff 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,10 +1,6 @@ # Update the links and commit has in order to consume # newer/other tests TEST_FIXTURES = { - "execution_spec_tests": { - "url": "https://github.com/ethereum/execution-spec-tests/releases/download/v0.2.5/fixtures.tar.gz", - "fixture_path": "tests/fixtures/execution_spec_tests", - }, "evm_tools_testdata": { "url": "https://github.com/gurukamath/evm-tools-testdata.git", "commit_hash": "792422d", @@ -17,7 +13,11 @@ }, "latest_fork_tests": { "url": "https://github.com/gurukamath/latest_fork_tests.git", - "commit_hash": "e15efcb", + "commit_hash": "bc74af5", "fixture_path": "tests/fixtures/latest_fork_tests", }, } + + +ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] +EEST_TESTS_PATH = TEST_FIXTURES["latest_fork_tests"]["fixture_path"] diff --git a/tests/helpers/load_evm_tools_tests.py b/tests/helpers/load_evm_tools_tests.py index 82ff9abe37..38af0eafff 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -127,6 +127,6 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: t8n_options = parser.parse_args(t8n_args) t8n = T8N(t8n_options, sys.stdout, in_stream) - t8n.apply_body() + t8n.run_state_test() assert hex_to_bytes(post_hash) == t8n.result.state_root diff --git a/tests/helpers/load_vm_tests.py b/tests/helpers/load_vm_tests.py index 6469f58e3d..361c82bd83 100644 --- a/tests/helpers/load_vm_tests.py +++ b/tests/helpers/load_vm_tests.py @@ -40,6 +40,9 @@ def __init__(self, network: str, fork_name: str): self.Account = self.fork_types.Account self.Address = self.fork_types.Address + self.transactions = self._module("transactions") + self.Transaction = self.transactions.Transaction + self.hexadecimal = self._module("utils.hexadecimal") self.hex_to_address = self.hexadecimal.hex_to_address @@ -47,7 +50,8 @@ def __init__(self, network: str, fork_name: str): self.prepare_message = self.message.prepare_message self.vm = self._module("vm") - self.Environment = self.vm.Environment + self.BlockEnvironment = self.vm.BlockEnvironment + self.TransactionEnvironment = self.vm.TransactionEnvironment self.interpreter = self._module("vm.interpreter") self.process_message_call = self.interpreter.process_message_call @@ -62,18 +66,17 @@ def run_test( Execute a test case and check its post state. """ test_data = self.load_test(test_dir, test_file) - target = test_data["target"] - env = test_data["env"] + block_env = test_data["block_env"] + tx_env = test_data["tx_env"] + tx = test_data["tx"] + message = self.prepare_message( - caller=test_data["caller"], - target=target, - value=test_data["value"], - data=test_data["data"], - gas=test_data["gas"], - env=env, + block_env=block_env, + tx_env=tx_env, + tx=tx, ) - output = self.process_message_call(message, env) + output = self.process_message_call(message) if test_data["has_post_state"]: if check_gas_left: @@ -89,10 +92,10 @@ def run_test( for addr in test_data["post_state_addresses"]: assert self.storage_root( test_data["expected_post_state"], addr - ) == self.storage_root(env.state, addr) + ) == self.storage_root(block_env.state, addr) else: assert output.error is not None - self.close_state(env.state) + self.close_state(block_env.state) self.close_state(test_data["expected_post_state"]) def load_test(self, test_dir: str, test_file: str) -> Any: @@ -104,16 +107,33 @@ def load_test(self, test_dir: str, test_file: str) -> Any: with open(path, "r") as fp: json_data = json.load(fp)[test_name] - env = self.json_to_env(json_data) + block_env = self.json_to_block_env(json_data) + + tx = self.Transaction( + nonce=U256(0), + gas_price=hex_to_u256(json_data["exec"]["gasPrice"]), + gas=hex_to_uint(json_data["exec"]["gas"]), + to=self.hex_to_address(json_data["exec"]["address"]), + value=hex_to_u256(json_data["exec"]["value"]), + data=hex_to_bytes(json_data["exec"]["data"]), + v=U256(0), + r=U256(0), + s=U256(0), + ) + + tx_env = self.TransactionEnvironment( + origin=self.hex_to_address(json_data["exec"]["caller"]), + gas_price=tx.gas_price, + gas=tx.gas, + index_in_block=Uint(0), + tx_hash=b"56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + traces=[], + ) return { - "caller": self.hex_to_address(json_data["exec"]["caller"]), - "target": self.hex_to_address(json_data["exec"]["address"]), - "data": hex_to_bytes(json_data["exec"]["data"]), - "value": hex_to_u256(json_data["exec"]["value"]), - "gas": hex_to_uint(json_data["exec"]["gas"]), - "depth": Uint(0), - "env": env, + "block_env": block_env, + "tx_env": tx_env, + "tx": tx, "expected_gas_left": hex_to_u256(json_data.get("gas", "0x64")), "expected_logs_hash": hex_to_bytes(json_data.get("logs", "0x00")), "expected_post_state": self.json_to_state( @@ -125,9 +145,9 @@ def load_test(self, test_dir: str, test_file: str) -> Any: "has_post_state": bool(json_data.get("post", {})), } - def json_to_env(self, json_data: Any) -> Any: + def json_to_block_env(self, json_data: Any) -> Any: """ - Deserialize an `Environment` instance from JSON. + Deserialize a `BlockEnvironment` instance from JSON. """ caller_hex_address = json_data["exec"]["caller"] # Some tests don't have the caller state defined in the test case. Hence @@ -146,18 +166,15 @@ def json_to_env(self, json_data: Any) -> Any: chain_id=U64(1), ) - return self.Environment( - caller=self.hex_to_address(json_data["exec"]["caller"]), - origin=self.hex_to_address(json_data["exec"]["origin"]), + return self.BlockEnvironment( + chain_id=chain.chain_id, + state=current_state, block_hashes=self.get_last_256_block_hashes(chain), coinbase=self.hex_to_address(json_data["env"]["currentCoinbase"]), number=hex_to_uint(json_data["env"]["currentNumber"]), - gas_limit=hex_to_uint(json_data["env"]["currentGasLimit"]), - gas_price=hex_to_u256(json_data["exec"]["gasPrice"]), + block_gas_limit=hex_to_uint(json_data["env"]["currentGasLimit"]), time=hex_to_u256(json_data["env"]["currentTimestamp"]), difficulty=hex_to_uint(json_data["env"]["currentDifficulty"]), - state=current_state, - traces=[], ) def json_to_state(self, raw: Any) -> Any: diff --git a/tests/homestead/test_evm_tools.py b/tests/homestead/test_evm_tools.py index 7bb7e32bdc..e8e2e24f52 100644 --- a/tests/homestead/test_evm_tools.py +++ b/tests/homestead/test_evm_tools.py @@ -3,33 +3,52 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "Homestead" -run_evm_tools_test = partial( + +SLOW_TESTS = () + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/homestead/test_state_transition.py b/tests/homestead/test_state_transition.py index 978ee95512..4fd6613646 100644 --- a/tests/homestead/test_state_transition.py +++ b/tests/homestead/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,19 +11,12 @@ run_blockchain_st_test, ) -fetch_homestead_tests = partial(fetch_state_test_files, network="Homestead") - -FIXTURES_LOADER = Load("Homestead", "homestead") - -run_homestead_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - - -# Run legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Homestead" +PACKAGE = "homestead" # Every test below takes more than 60s to run and # hence they've been marked as slow @@ -143,124 +131,35 @@ "randomStatetest94_", ) -fetch_legacy_state_tests = partial( - fetch_homestead_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=LEGACY_SLOW_TESTS, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_homestead_blockchain_st_tests(test_case) - - -# Run Non-Legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_homestead_tests(test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_tests(test_case: Dict) -> None: - run_homestead_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/istanbul/test_evm_tools.py b/tests/istanbul/test_evm_tools.py index 279083cbb3..80c169a440 100644 --- a/tests/istanbul/test_evm_tools.py +++ b/tests/istanbul/test_evm_tools.py @@ -3,21 +3,18 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" -FORK_NAME = "Istanbul" - -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, +ETHEREUM_STATE_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "Istanbul" SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -28,15 +25,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/istanbul/test_state_transition.py b/tests/istanbul/test_state_transition.py index b778d25c14..50fada9659 100644 --- a/tests/istanbul/test_state_transition.py +++ b/tests/istanbul/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,18 +11,12 @@ run_blockchain_st_test, ) -fetch_istanbul_tests = partial(fetch_state_test_files, network="Istanbul") - -FIXTURES_LOADER = Load("Istanbul", "istanbul") - -run_istanbul_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Istanbul" +PACKAGE = "istanbul" # Every test below takes more than 60s to run and # hence they've been marked as slow @@ -75,107 +64,35 @@ "stTimeConsuming/", ) -fetch_state_tests = partial( - fetch_istanbul_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_state_tests(test_case: Dict) -> None: - run_istanbul_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - chain_id=Uint(1), - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +# Run EEST test fixtures +@pytest.mark.parametrize( + "test_case", + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), + ids=idfn, +) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/london/test_evm_tools.py b/tests/london/test_evm_tools.py index 0e0b8aa1c5..0347da6cec 100644 --- a/tests/london/test_evm_tools.py +++ b/tests/london/test_evm_tools.py @@ -3,21 +3,18 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" -FORK_NAME = "London" - -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, +ETHEREUM_STATE_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "London" SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -28,15 +25,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/london/test_state_transition.py b/tests/london/test_state_transition.py index 87723c8130..764ee942de 100644 --- a/tests/london/test_state_transition.py +++ b/tests/london/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,18 +11,12 @@ run_blockchain_st_test, ) -fetch_london_tests = partial(fetch_state_test_files, network="London") - -FIXTURES_LOADER = Load("London", "london") - -run_london_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "London" +PACKAGE = "london" # Every test below takes more than 60s to run and # hence they've been marked as slow @@ -75,109 +64,35 @@ "stTimeConsuming/", ) -fetch_state_tests = partial( - fetch_london_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_state_tests(test_case: Dict) -> None: - run_london_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - base_fee_per_gas=Uint(16), - ) - - genesis_header_hash = bytes.fromhex( - "4a62c29ca7f3a61e5519eabbf57a40bb28ee1f164839b3160281c30d2443a69e" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.LegacyTransaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - chain_id=Uint(1), - base_fee_per_gas=Uint(16), - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +# Run EEST test fixtures +@pytest.mark.parametrize( + "test_case", + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), + ids=idfn, +) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/paris/test_evm_tools.py b/tests/paris/test_evm_tools.py index 18313a7584..1598e44b4b 100644 --- a/tests/paris/test_evm_tools.py +++ b/tests/paris/test_evm_tools.py @@ -3,21 +3,18 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" -FORK_NAME = "Paris" - -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, +ETHEREUM_STATE_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "Paris" SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -28,15 +25,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/paris/test_state_transition.py b/tests/paris/test_state_transition.py index 46c47ae95c..5478389ca6 100644 --- a/tests/paris/test_state_transition.py +++ b/tests/paris/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,18 +11,12 @@ run_blockchain_st_test, ) -fetch_paris_tests = partial(fetch_state_test_files, network="Paris") - -FIXTURES_LOADER = Load("Paris", "paris") - -run_paris_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Paris" +PACKAGE = "paris" # Every test below takes more than 60s to run and # hence they've been marked as slow @@ -75,109 +64,35 @@ "stTimeConsuming/", ) -fetch_state_tests = partial( - fetch_paris_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) + +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_state_tests(test_case: Dict) -> None: - run_paris_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - prev_randao=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - base_fee_per_gas=Uint(16), - ) - - genesis_header_hash = bytes.fromhex( - "4a62c29ca7f3a61e5519eabbf57a40bb28ee1f164839b3160281c30d2443a69e" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.LegacyTransaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - prev_randao=U256.from_be_bytes(genesis_block.header.prev_randao), - state=state, - chain_id=Uint(1), - base_fee_per_gas=Uint(16), - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +# Run EEST test fixtures +@pytest.mark.parametrize( + "test_case", + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), + ids=idfn, +) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/prague/test_evm_tools.py b/tests/prague/test_evm_tools.py index 3879ee1d65..93770bf686 100644 --- a/tests/prague/test_evm_tools.py +++ b/tests/prague/test_evm_tools.py @@ -1,23 +1,19 @@ from functools import partial -from typing import Dict, Generator, Tuple +from typing import Dict import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +ETHEREUM_STATE_TESTS_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "Prague" -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, -) SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -34,27 +30,37 @@ "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-multi_inf_pair-]", ) -test_dirs = ( - "tests/fixtures/latest_fork_tests/state_tests/prague/eip2537_bls_12_381_precompiles", - "tests/fixtures/latest_fork_tests/state_tests/prague/eip7702_set_code_tx", - "tests/fixtures/latest_fork_tests/state_tests/prague/eip7623_increase_calldata_cost", + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, ) -def fetch_temporary_tests(test_dirs: Tuple[str, ...]) -> Generator: - for test_dir in test_dirs: - yield from fetch_evm_tools_tests( - test_dir, - FORK_NAME, - SLOW_TESTS, - ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_temporary_tests(test_dirs), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/prague/test_state_transition.py b/tests/prague/test_state_transition.py index f47d9f6a66..dd2bd52b43 100644 --- a/tests/prague/test_state_transition.py +++ b/tests/prague/test_state_transition.py @@ -1,9 +1,9 @@ from functools import partial -from typing import Dict, Generator, Tuple +from typing import Dict import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -11,22 +11,10 @@ run_blockchain_st_test, ) -fetch_prague_tests = partial(fetch_state_test_files, network="Prague") - -FIXTURES_LOADER = Load("Prague", "prague") - -run_prague_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER -) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -ETHEREUM_SPEC_TESTS_PATH = TEST_FIXTURES["execution_spec_tests"][ - "fixture_path" -] - - -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/" +ETHEREUM_BLOCKCHAIN_TESTS_DIR = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Prague" +PACKAGE = "prague" SLOW_TESTS = ( # GeneralStateTests @@ -82,40 +70,35 @@ "stStaticCall/", ) -fetch_state_tests = partial( - fetch_prague_tests, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) -# Run temporary test fixtures for Prague -test_dirs = ( - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip7002_el_triggerable_withdrawals", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip6110_deposits", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip7251_consolidations", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip7685_general_purpose_el_requests", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip2537_bls_12_381_precompiles", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip2935_historical_block_hashes_from_state", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip7702_set_code_tx", - "tests/fixtures/latest_fork_tests/blockchain_tests/prague/eip7623_increase_calldata_cost", -) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) -def fetch_temporary_tests(test_dirs: Tuple[str, ...]) -> Generator: - """ - Fetch the relevant tests for a particular EIP-Implementation - from among the temporary fixtures from ethereum-spec-tests. - """ - for test_dir in test_dirs: - yield from fetch_state_tests(test_dir) +# Run tests from ethereum/tests +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_temporary_tests(test_dirs), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_execution_specs_generated_tests(test_case: Dict) -> None: - run_prague_blockchain_st_tests(test_case) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/shanghai/test_evm_tools.py b/tests/shanghai/test_evm_tools.py index 071261e39f..4f7497ee7b 100644 --- a/tests/shanghai/test_evm_tools.py +++ b/tests/shanghai/test_evm_tools.py @@ -3,21 +3,18 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" -FORK_NAME = "Shanghai" - -run_evm_tools_test = partial( - load_evm_tools_test, - fork_name=FORK_NAME, +ETHEREUM_STATE_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "Shanghai" SLOW_TESTS = ( "CALLBlake2f_MaxRounds", @@ -28,15 +25,36 @@ ) +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + + +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/shanghai/test_state_transition.py b/tests/shanghai/test_state_transition.py index 62a51f7228..700ad49756 100644 --- a/tests/shanghai/test_state_transition.py +++ b/tests/shanghai/test_state_transition.py @@ -3,7 +3,7 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -11,23 +11,14 @@ run_blockchain_st_test, ) -fetch_shanghai_tests = partial(fetch_state_test_files, network="Shanghai") - -FIXTURES_LOADER = Load("Shanghai", "shanghai") - -run_shanghai_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -ETHEREUM_SPEC_TESTS_PATH = TEST_FIXTURES["execution_spec_tests"][ - "fixture_path" -] +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "Shanghai" +PACKAGE = "shanghai" -# Run state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Cancun/BlockchainTests/" - SLOW_TESTS = ( # GeneralStateTests "stTimeConsuming/CALLBlake2f_MaxRounds.json", @@ -74,32 +65,35 @@ "stStaticCall/", ) -fetch_state_tests = partial( - fetch_shanghai_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_general_state_tests(test_case: Dict) -> None: - run_shanghai_blockchain_st_tests(test_case) - - -# Run execution-spec-generated-tests -test_dir = f"{ETHEREUM_SPEC_TESTS_PATH}/fixtures/withdrawals" +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_shanghai_tests(test_dir), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_execution_specs_generated_tests(test_case: Dict) -> None: - run_shanghai_blockchain_st_tests(test_case) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/spurious_dragon/test_evm_tools.py b/tests/spurious_dragon/test_evm_tools.py index 66a76de0f4..5394bd5c1c 100644 --- a/tests/spurious_dragon/test_evm_tools.py +++ b/tests/spurious_dragon/test_evm_tools.py @@ -3,33 +3,51 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "EIP158" -run_evm_tools_test = partial( +SLOW_TESTS = () + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/spurious_dragon/test_state_transition.py b/tests/spurious_dragon/test_state_transition.py index d3af5e4788..dc2091b56a 100644 --- a/tests/spurious_dragon/test_state_transition.py +++ b/tests/spurious_dragon/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,19 +11,12 @@ run_blockchain_st_test, ) -fetch_spurious_dragon_tests = partial(fetch_state_test_files, network="EIP158") - -FIXTURES_LOADER = Load("EIP158", "spurious_dragon") - -run_spurious_dragon_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - - -# Run legacy general state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "EIP158" +PACKAGE = "spurious_dragon" LEGACY_SLOW_TESTS = ( # GeneralStateTests @@ -61,124 +49,35 @@ "GasLimitHigherThan2p63m1_EIP158", ) -fetch_legacy_state_tests = partial( - fetch_spurious_dragon_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=LEGACY_SLOW_TESTS, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_spurious_dragon_blockchain_st_tests(test_case) - - -# Run Non-Legacy State tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_spurious_dragon_tests(test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_state_tests(test_case: Dict) -> None: - run_spurious_dragon_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/tangerine_whistle/test_evm_tools.py b/tests/tangerine_whistle/test_evm_tools.py index 63b33108b7..94a6b8ab8a 100644 --- a/tests/tangerine_whistle/test_evm_tools.py +++ b/tests/tangerine_whistle/test_evm_tools.py @@ -3,33 +3,52 @@ import pytest -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_evm_tools_tests import ( fetch_evm_tools_tests, idfn, load_evm_tools_test, ) -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] -TEST_DIR = ( +ETHEREUM_STATE_TESTS_DIR = ( f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/GeneralStateTests/" ) +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" FORK_NAME = "EIP150" -run_evm_tools_test = partial( + +SLOW_TESTS = () + +# Define tests +fetch_tests = partial( + fetch_evm_tools_tests, + fork_name=FORK_NAME, + slow_tests=SLOW_TESTS, +) + +run_tests = partial( load_evm_tools_test, fork_name=FORK_NAME, ) +# Run tests from ethereum/tests +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_tests(ETHEREUM_STATE_TESTS_DIR), + ids=idfn, +) +def test_ethereum_tests_evm_tools(test_case: Dict) -> None: + run_tests(test_case) + + +# Run EEST test fixtures @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - ), + fetch_tests(EEST_STATE_TESTS_DIR), ids=idfn, ) -def test_evm_tools(test_case: Dict) -> None: - run_evm_tools_test(test_case) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/tangerine_whistle/test_state_transition.py b/tests/tangerine_whistle/test_state_transition.py index c56202145b..ba376d6aa7 100644 --- a/tests/tangerine_whistle/test_state_transition.py +++ b/tests/tangerine_whistle/test_state_transition.py @@ -2,13 +2,8 @@ from typing import Dict import pytest -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes8, Bytes32 -from ethereum_types.numeric import U256, Uint -from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock -from tests.helpers import TEST_FIXTURES +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH from tests.helpers.load_state_tests import ( Load, fetch_state_test_files, @@ -16,21 +11,12 @@ run_blockchain_st_test, ) -fetch_tangerine_whistle_tests = partial( - fetch_state_test_files, network="EIP150" +ETHEREUM_BLOCKCHAIN_TESTS_DIR = ( + f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" ) - -FIXTURES_LOADER = Load("EIP150", "tangerine_whistle") - -run_tangerine_whistle_blockchain_st_tests = partial( - run_blockchain_st_test, load=FIXTURES_LOADER -) - -ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] - - -# Run legacy state tests -test_dir = f"{ETHEREUM_TESTS_PATH}/LegacyTests/Constantinople/BlockchainTests/" +EEST_BLOCKCHAIN_TESTS_DIR = f"{EEST_TESTS_PATH}/blockchain_tests/" +NETWORK = "EIP150" +PACKAGE = "tangerine_whistle" LEGACY_SLOW_TESTS = ( "stRandom/randomStatetest177.json", @@ -63,124 +49,35 @@ "GasLimitHigherThan2p63m1_EIP150", ) -fetch_legacy_state_tests = partial( - fetch_tangerine_whistle_tests, - test_dir, +# Define Tests +fetch_tests = partial( + fetch_state_test_files, + network=NETWORK, ignore_list=LEGACY_IGNORE_LIST, slow_list=LEGACY_SLOW_TESTS, big_memory_list=LEGACY_BIG_MEMORY_TESTS, ) +FIXTURES_LOADER = Load(NETWORK, PACKAGE) +run_tests = partial(run_blockchain_st_test, load=FIXTURES_LOADER) + + +# Run tests from ethereum/tests @pytest.mark.parametrize( "test_case", - fetch_legacy_state_tests(), + fetch_tests(ETHEREUM_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_legacy_state_tests(test_case: Dict) -> None: - run_tangerine_whistle_blockchain_st_tests(test_case) - - -# Run Non-Legacy State Tests -test_dir = f"{ETHEREUM_TESTS_PATH}/BlockchainTests/GeneralStateTests/" - -non_legacy_only_in = ( - "stCreateTest/CREATE_HighNonce.json", - "stCreateTest/CREATE_HighNonceMinus1.json", -) +def test_ethereum_tests(test_case: Dict) -> None: + run_tests(test_case) +# Run EEST test fixtures @pytest.mark.parametrize( "test_case", - fetch_tangerine_whistle_tests(test_dir, only_in=non_legacy_only_in), + fetch_tests(EEST_BLOCKCHAIN_TESTS_DIR), ids=idfn, ) -def test_non_legacy_state_tests(test_case: Dict) -> None: - run_tangerine_whistle_blockchain_st_tests(test_case) - - -def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.fork.Header( - parent_hash=Hash32([0] * 32), - ommers_hash=Hash32.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ), - coinbase=FIXTURES_LOADER.fork.hex_to_address( - "8888f1f195afa192cfee860698584c030f4c9db1" - ), - state_root=FIXTURES_LOADER.fork.hex_to_root( - "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" - ), - transactions_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - receipt_root=FIXTURES_LOADER.fork.hex_to_root( - "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - ), - bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), - difficulty=Uint(0x020000), - number=Uint(0x00), - gas_limit=Uint(0x2FEFD8), - gas_used=Uint(0x00), - timestamp=U256(0x54C98C81), - extra_data=Bytes([0x42]), - mix_digest=Bytes32([0] * 32), - nonce=Bytes8([0] * 8), - ) - - genesis_header_hash = bytes.fromhex( - "0b22b0d49035cb4f8a969d584f36126e0ac6996b9db7264ac5a192b8698177eb" - ) - - assert keccak256(rlp.encode(genesis_header)) == genesis_header_hash - - genesis_block = FIXTURES_LOADER.fork.Block( - genesis_header, - (), - (), - ) - - state = FIXTURES_LOADER.fork.State() - - address = FIXTURES_LOADER.fork.hex_to_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ) - - account = FIXTURES_LOADER.fork.Account( - nonce=Uint(0), - balance=U256(0x056BC75E2D63100000), - code=Bytes(), - ) - - FIXTURES_LOADER.fork.set_account(state, address, account) - - tx = FIXTURES_LOADER.fork.Transaction( - nonce=U256(0x00), - gas_price=Uint(1000), - gas=Uint(150000), - to=FIXTURES_LOADER.fork.hex_to_address( - "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" - ), - value=U256(1000000000000000000000), - data=Bytes(), - v=U256(0), - r=U256(0), - s=U256(0), - ) - - env = FIXTURES_LOADER.fork.Environment( - caller=address, - origin=address, - block_hashes=[genesis_header_hash], - coinbase=genesis_block.header.coinbase, - number=genesis_block.header.number + Uint(1), - gas_limit=genesis_block.header.gas_limit, - gas_price=tx.gas_price, - time=genesis_block.header.timestamp, - difficulty=genesis_block.header.difficulty, - state=state, - traces=[], - ) - - with pytest.raises(InvalidBlock): - FIXTURES_LOADER.fork.process_transaction(env, tx) +def test_eest_tests(test_case: Dict) -> None: + run_tests(test_case) diff --git a/whitelist.txt b/whitelist.txt index 8281e2ff3b..74cbb64d59 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -464,3 +464,6 @@ impl x2 eoa + +blockchain +listdir \ No newline at end of file