From 0a00660446ae69a90ad58c4bc7aaa32cc5a59136 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Tue, 21 Jan 2025 16:33:15 +0100 Subject: [PATCH 01/38] prague initial commit --- src/ethereum/prague/__init__.py | 10 + src/ethereum/prague/blocks.py | 100 ++ src/ethereum/prague/bloom.py | 85 ++ src/ethereum/prague/exceptions.py | 24 + src/ethereum/prague/fork.py | 879 ++++++++++++++++++ src/ethereum/prague/fork_types.py | 64 ++ src/ethereum/prague/state.py | 734 +++++++++++++++ src/ethereum/prague/transactions.py | 453 +++++++++ src/ethereum/prague/trie.py | 470 ++++++++++ src/ethereum/prague/utils/__init__.py | 3 + src/ethereum/prague/utils/address.py | 93 ++ src/ethereum/prague/utils/hexadecimal.py | 68 ++ src/ethereum/prague/utils/message.py | 116 +++ src/ethereum/prague/vm/__init__.py | 151 +++ src/ethereum/prague/vm/exceptions.py | 140 +++ src/ethereum/prague/vm/gas.py | 362 ++++++++ .../prague/vm/instructions/__init__.py | 366 ++++++++ .../prague/vm/instructions/arithmetic.py | 373 ++++++++ .../prague/vm/instructions/bitwise.py | 240 +++++ src/ethereum/prague/vm/instructions/block.py | 249 +++++ .../prague/vm/instructions/comparison.py | 178 ++++ .../prague/vm/instructions/control_flow.py | 171 ++++ .../prague/vm/instructions/environment.py | 587 ++++++++++++ src/ethereum/prague/vm/instructions/keccak.py | 64 ++ src/ethereum/prague/vm/instructions/log.py | 88 ++ src/ethereum/prague/vm/instructions/memory.py | 177 ++++ src/ethereum/prague/vm/instructions/stack.py | 209 +++++ .../prague/vm/instructions/storage.py | 178 ++++ src/ethereum/prague/vm/instructions/system.py | 691 ++++++++++++++ src/ethereum/prague/vm/interpreter.py | 314 +++++++ src/ethereum/prague/vm/memory.py | 81 ++ .../vm/precompiled_contracts/__init__.py | 40 + .../vm/precompiled_contracts/alt_bn128.py | 154 +++ .../vm/precompiled_contracts/blake2f.py | 41 + .../vm/precompiled_contracts/ecrecover.py | 62 ++ .../vm/precompiled_contracts/identity.py | 38 + .../vm/precompiled_contracts/mapping.py | 49 + .../prague/vm/precompiled_contracts/modexp.py | 169 ++++ .../precompiled_contracts/point_evaluation.py | 72 ++ .../vm/precompiled_contracts/ripemd160.py | 43 + .../prague/vm/precompiled_contracts/sha256.py | 40 + src/ethereum/prague/vm/runtime.py | 67 ++ src/ethereum/prague/vm/stack.py | 59 ++ tests/prague/__init__.py | 0 tests/prague/test_evm_tools.py | 42 + tests/prague/test_rlp.py | 165 ++++ tests/prague/test_state_transition.py | 105 +++ tests/prague/test_trie.py | 89 ++ 48 files changed, 8953 insertions(+) create mode 100644 src/ethereum/prague/__init__.py create mode 100644 src/ethereum/prague/blocks.py create mode 100644 src/ethereum/prague/bloom.py create mode 100644 src/ethereum/prague/exceptions.py create mode 100644 src/ethereum/prague/fork.py create mode 100644 src/ethereum/prague/fork_types.py create mode 100644 src/ethereum/prague/state.py create mode 100644 src/ethereum/prague/transactions.py create mode 100644 src/ethereum/prague/trie.py create mode 100644 src/ethereum/prague/utils/__init__.py create mode 100644 src/ethereum/prague/utils/address.py create mode 100644 src/ethereum/prague/utils/hexadecimal.py create mode 100644 src/ethereum/prague/utils/message.py create mode 100644 src/ethereum/prague/vm/__init__.py create mode 100644 src/ethereum/prague/vm/exceptions.py create mode 100644 src/ethereum/prague/vm/gas.py create mode 100644 src/ethereum/prague/vm/instructions/__init__.py create mode 100644 src/ethereum/prague/vm/instructions/arithmetic.py create mode 100644 src/ethereum/prague/vm/instructions/bitwise.py create mode 100644 src/ethereum/prague/vm/instructions/block.py create mode 100644 src/ethereum/prague/vm/instructions/comparison.py create mode 100644 src/ethereum/prague/vm/instructions/control_flow.py create mode 100644 src/ethereum/prague/vm/instructions/environment.py create mode 100644 src/ethereum/prague/vm/instructions/keccak.py create mode 100644 src/ethereum/prague/vm/instructions/log.py create mode 100644 src/ethereum/prague/vm/instructions/memory.py create mode 100644 src/ethereum/prague/vm/instructions/stack.py create mode 100644 src/ethereum/prague/vm/instructions/storage.py create mode 100644 src/ethereum/prague/vm/instructions/system.py create mode 100644 src/ethereum/prague/vm/interpreter.py create mode 100644 src/ethereum/prague/vm/memory.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/__init__.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/alt_bn128.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/blake2f.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/ecrecover.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/identity.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/mapping.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/modexp.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/point_evaluation.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/ripemd160.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/sha256.py create mode 100644 src/ethereum/prague/vm/runtime.py create mode 100644 src/ethereum/prague/vm/stack.py create mode 100644 tests/prague/__init__.py create mode 100644 tests/prague/test_evm_tools.py create mode 100644 tests/prague/test_rlp.py create mode 100644 tests/prague/test_state_transition.py create mode 100644 tests/prague/test_trie.py diff --git a/src/ethereum/prague/__init__.py b/src/ethereum/prague/__init__.py new file mode 100644 index 0000000000..04774a7578 --- /dev/null +++ b/src/ethereum/prague/__init__.py @@ -0,0 +1,10 @@ +""" +The Prague fork introduces transient storage, exposes beacon chain roots, +introduces a new blob-carrying transaction type, adds a memory copying +instruction, limits self-destruct to only work for contracts created in the +same transaction, and adds an instruction to read the blob base fee. +""" + +from ethereum.fork_criteria import ByTimestamp + +FORK_CRITERIA = ByTimestamp(1710338135) diff --git a/src/ethereum/prague/blocks.py b/src/ethereum/prague/blocks.py new file mode 100644 index 0000000000..54ef88e15f --- /dev/null +++ b/src/ethereum/prague/blocks.py @@ -0,0 +1,100 @@ +""" +A `Block` is a single link in the chain that is Ethereum. Each `Block` contains +a `Header` and zero or more transactions. Each `Header` contains associated +metadata like the block number, parent block hash, and how much gas was +consumed by its transactions. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" +from dataclasses import dataclass +from typing import Tuple, Union + +from ethereum_types.bytes import Bytes, Bytes8, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +from ..crypto.hash import Hash32 +from .fork_types import Address, Bloom, Root +from .transactions import LegacyTransaction + + +@slotted_freezable +@dataclass +class Withdrawal: + """ + Withdrawals that have been validated on the consensus layer. + """ + + index: U64 + validator_index: U64 + address: Address + amount: U256 + + +@slotted_freezable +@dataclass +class Header: + """ + Header portion of a block on the chain. + """ + + parent_hash: Hash32 + ommers_hash: Hash32 + coinbase: Address + state_root: Root + transactions_root: Root + receipt_root: Root + bloom: Bloom + difficulty: Uint + number: Uint + gas_limit: Uint + gas_used: Uint + timestamp: U256 + extra_data: Bytes + prev_randao: Bytes32 + nonce: Bytes8 + base_fee_per_gas: Uint + withdrawals_root: Root + blob_gas_used: U64 + excess_blob_gas: U64 + parent_beacon_block_root: Root + + +@slotted_freezable +@dataclass +class Block: + """ + A complete block. + """ + + header: Header + transactions: Tuple[Union[Bytes, LegacyTransaction], ...] + ommers: Tuple[Header, ...] + withdrawals: Tuple[Withdrawal, ...] + + +@slotted_freezable +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. + """ + + address: Address + topics: Tuple[Hash32, ...] + data: bytes + + +@slotted_freezable +@dataclass +class Receipt: + """ + Result of a transaction. + """ + + succeeded: bool + cumulative_gas_used: Uint + bloom: Bloom + logs: Tuple[Log, ...] diff --git a/src/ethereum/prague/bloom.py b/src/ethereum/prague/bloom.py new file mode 100644 index 0000000000..0ba6e431ab --- /dev/null +++ b/src/ethereum/prague/bloom.py @@ -0,0 +1,85 @@ +""" +Ethereum Logs Bloom +^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This modules defines functions for calculating bloom filters of logs. For the +general theory of bloom filters see e.g. `Wikipedia +`_. Bloom filters are used to allow +for efficient searching of logs by address and/or topic, by rapidly +eliminating blocks and receipts from their search. +""" + +from typing import Tuple + +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import keccak256 + +from .blocks import Log +from .fork_types import Bloom + + +def add_to_bloom(bloom: bytearray, bloom_entry: bytes) -> None: + """ + Add a bloom entry to the bloom filter (`bloom`). + + The number of hash functions used is 3. They are calculated by taking the + least significant 11 bits from the first 3 16-bit words of the + `keccak_256()` hash of `bloom_entry`. + + Parameters + ---------- + bloom : + The bloom filter. + bloom_entry : + An entry which is to be added to bloom filter. + """ + hash = keccak256(bloom_entry) + + for idx in (0, 2, 4): + # Obtain the least significant 11 bits from the pair of bytes + # (16 bits), and set this bit in bloom bytearray. + # The obtained bit is 0-indexed in the bloom filter from the least + # significant bit to the most significant bit. + bit_to_set = Uint.from_be_bytes(hash[idx : idx + 2]) & Uint(0x07FF) + # Below is the index of the bit in the bytearray (where 0-indexed + # byte is the most significant byte) + bit_index = 0x07FF - int(bit_to_set) + + byte_index = bit_index // 8 + bit_value = 1 << (7 - (bit_index % 8)) + bloom[byte_index] = bloom[byte_index] | bit_value + + +def logs_bloom(logs: Tuple[Log, ...]) -> Bloom: + """ + Obtain the logs bloom from a list of log entries. + + The address and each topic of a log are added to the bloom filter. + + Parameters + ---------- + logs : + List of logs for which the logs bloom is to be obtained. + + Returns + ------- + logs_bloom : `Bloom` + The logs bloom obtained which is 256 bytes with some bits set as per + the caller address and the log topics. + """ + bloom: bytearray = bytearray(b"\x00" * 256) + + for log in logs: + add_to_bloom(bloom, log.address) + for topic in log.topics: + add_to_bloom(bloom, topic) + + return Bloom(bloom) diff --git a/src/ethereum/prague/exceptions.py b/src/ethereum/prague/exceptions.py new file mode 100644 index 0000000000..5781a2c1c3 --- /dev/null +++ b/src/ethereum/prague/exceptions.py @@ -0,0 +1,24 @@ +""" +Exceptions specific to this fork. +""" + +from typing import Final + +from ethereum.exceptions import InvalidTransaction + + +class TransactionTypeError(InvalidTransaction): + """ + Unknown [EIP-2718] transaction type byte. + + [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + """ + + transaction_type: Final[int] + """ + The type byte of the transaction that caused the error. + """ + + def __init__(self, transaction_type: int): + super().__init__(f"unknown transaction type `{transaction_type}`") + self.transaction_type = transaction_type diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py new file mode 100644 index 0000000000..5bbb517ed6 --- /dev/null +++ b/src/ethereum/prague/fork.py @@ -0,0 +1,879 @@ +""" +Ethereum Specification +^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSenderError + +from . import vm +from .blocks import Block, Header, Log, Receipt, Withdrawal +from .bloom import logs_bloom +from .fork_types import Address, Bloom, Root, VersionedHash +from .state import ( + State, + TransientStorage, + account_exists_and_is_empty, + destroy_account, + destroy_touched_empty_accounts, + get_account, + increment_nonce, + process_withdrawal, + set_account_balance, + state_root, +) +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, + calculate_intrinsic_cost, + decode_transaction, + encode_transaction, + recover_sender, + validate_transaction, +) +from .trie import Trie, root, trie_set +from .utils.hexadecimal import hex_to_address +from .utils.message import prepare_message +from .vm import Message +from .vm.gas import ( + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, +) +from .vm.interpreter import process_message_call + +BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) +ELASTICITY_MULTIPLIER = Uint(2) +GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) +GAS_LIMIT_MINIMUM = Uint(5000) +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK = Uint(786432) +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + chain_id: U64 + + +def apply_fork(old: BlockChain) -> BlockChain: + """ + Transforms the state from the previous hard fork (`old`) into the block + chain object for this hard fork and returns it. + + When forks need to implement an irregular state transition, this function + is used to handle the irregularity. See the :ref:`DAO Fork ` for + an example. + + Parameters + ---------- + old : + Previous block chain object. + + Returns + ------- + new : `BlockChain` + Upgraded block chain object for this hard fork. + """ + return old + + +def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: + """ + Obtain the list of hashes of the previous 256 blocks in order of + increasing block number. + + This function will return less hashes for the first 256 blocks. + + The ``BLOCKHASH`` opcode needs to access the latest hashes on the chain, + therefore this function retrieves them. + + Parameters + ---------- + chain : + History and current state. + + Returns + ------- + recent_block_hashes : `List[Hash32]` + Hashes of the recent 256 blocks in order of increasing block number. + """ + recent_blocks = chain.blocks[-255:] + # TODO: This function has not been tested rigorously + if len(recent_blocks) == 0: + return [] + + recent_block_hashes = [] + + for block in recent_blocks: + prev_block_hash = block.header.parent_hash + recent_block_hashes.append(prev_block_hash) + + # We are computing the hash only for the most recent block and not for + # the rest of the blocks as they have successors which have the hash of + # the current block as parent hash. + most_recent_block_hash = keccak256(rlp.encode(recent_blocks[-1].header)) + recent_block_hashes.append(most_recent_block_hash) + + return recent_block_hashes + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + All parts of the block's contents need to be verified before being added + to the chain. Blocks are verified by ensuring that the contents of the + block make logical sense with the contents of the parent block. The + information in the block's header must also match the corresponding + information in the block. + + To implement Ethereum, in theory clients are only required to store the + most recent 255 blocks of the chain since as far as execution is + concerned, only those blocks are accessed. Practically, however, clients + should store more blocks to handle reorgs. + + Parameters + ---------- + chain : + History and current state. + 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) + 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, + ) + if apply_body_output.block_gas_used != block.header.gas_used: + raise InvalidBlock( + f"{apply_body_output.block_gas_used} != {block.header.gas_used}" + ) + if apply_body_output.transactions_root != block.header.transactions_root: + raise InvalidBlock + if apply_body_output.state_root != block.header.state_root: + raise InvalidBlock + if apply_body_output.receipt_root != block.header.receipt_root: + raise InvalidBlock + if apply_body_output.block_logs_bloom != block.header.bloom: + raise InvalidBlock + if apply_body_output.withdrawals_root != block.header.withdrawals_root: + raise InvalidBlock + if apply_body_output.blob_gas_used != block.header.blob_gas_used: + raise InvalidBlock + + chain.blocks.append(block) + if len(chain.blocks) > 255: + # Real clients have to store more blocks to deal with reorgs, but the + # protocol only requires the last 255 + chain.blocks = chain.blocks[-255:] + + +def calculate_base_fee_per_gas( + block_gas_limit: Uint, + parent_gas_limit: Uint, + parent_gas_used: Uint, + parent_base_fee_per_gas: Uint, +) -> Uint: + """ + Calculates the base fee per gas for the block. + + Parameters + ---------- + block_gas_limit : + Gas limit of the block for which the base fee is being calculated. + parent_gas_limit : + Gas limit of the parent block. + parent_gas_used : + Gas used in the parent block. + parent_base_fee_per_gas : + Base fee per gas of the parent block. + + Returns + ------- + base_fee_per_gas : `Uint` + Base fee per gas for the block. + """ + parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER + if not check_gas_limit(block_gas_limit, parent_gas_limit): + raise InvalidBlock + + if parent_gas_used == parent_gas_target: + expected_base_fee_per_gas = parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = max( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR, + Uint(1), + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas + base_fee_per_gas_delta + ) + else: + gas_used_delta = parent_gas_target - parent_gas_used + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = ( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas - base_fee_per_gas_delta + ) + + return Uint(expected_base_fee_per_gas) + + +def validate_header(header: Header, parent_header: Header) -> None: + """ + Verifies a block header. + + In order to consider a block's header valid, the logic for the + quantities in the header should match the logic for the block itself. + For example the header timestamp should be greater than the block's parent + timestamp because the block was created *after* the parent block. + Additionally, the block's number should be directly following the parent + block's number since it is the next block in the sequence. + + Parameters + ---------- + header : + Header to check for correctness. + parent_header : + Parent Header of the header to check for correctness + """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_header.base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: + raise InvalidBlock + if header.number != parent_header.number + Uint(1): + raise InvalidBlock + if len(header.extra_data) > 32: + raise InvalidBlock + if header.difficulty != 0: + raise InvalidBlock + if header.nonce != b"\x00\x00\x00\x00\x00\x00\x00\x00": + raise InvalidBlock + if header.ommers_hash != EMPTY_OMMER_HASH: + raise InvalidBlock + + block_parent_hash = keccak256(rlp.encode(parent_header)) + if header.parent_hash != block_parent_hash: + raise InvalidBlock + + +def check_transaction( + state: State, + tx: Transaction, + gas_available: Uint, + chain_id: U64, + base_fee_per_gas: Uint, + excess_blob_gas: U64, +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...]]: + """ + Check if the transaction is includable in the block. + + Parameters + ---------- + state : + Current state. + 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 + ------- + sender_address : + The sender of the transaction. + effective_gas_price : + The price to charge for gas when the transaction is executed. + blob_versioned_hashes : + The blob versioned hashes of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not includable. + """ + if tx.gas > gas_available: + raise InvalidBlock + sender_address = recover_sender(chain_id, tx) + sender_account = get_account(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: + raise InvalidBlock + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas + else: + if tx.gas_price < base_fee_per_gas: + raise InvalidBlock + effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + if not isinstance(tx.to, Address): + raise InvalidBlock + if len(tx.blob_versioned_hashes) == 0: + raise InvalidBlock + for blob_versioned_hash in tx.blob_versioned_hashes: + if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: + raise InvalidBlock + + blob_gas_price = calculate_blob_gas_price(excess_blob_gas) + if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: + raise InvalidBlock + + max_gas_fee += calculate_total_blob_gas(tx) * Uint( + tx.max_fee_per_blob_gas + ) + blob_versioned_hashes = tx.blob_versioned_hashes + else: + blob_versioned_hashes = () + 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") + + return sender_address, effective_gas_price, blob_versioned_hashes + + +def make_receipt( + tx: Transaction, + error: Optional[Exception], + cumulative_gas_used: Uint, + logs: Tuple[Log, ...], +) -> Union[Bytes, Receipt]: + """ + Make the receipt for a transaction that was executed. + + Parameters + ---------- + tx : + The executed transaction. + error : + 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. + logs : + The logs produced by the transaction. + + Returns + ------- + receipt : + The receipt for the transaction. + """ + receipt = Receipt( + succeeded=error is None, + cumulative_gas_used=cumulative_gas_used, + bloom=logs_bloom(logs), + logs=logs, + ) + + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + else: + 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. + 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 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, + transactions: Tuple[Union[LegacyTransaction, Bytes], ...], + chain_id: U64, + withdrawals: Tuple[Withdrawal, ...], + parent_beacon_block_root: Root, + excess_blob_gas: U64, +) -> ApplyBodyOutput: + """ + Executes a block. + + Many of the contents of a block are stored in data structures called + tries. There is a transactions trie which is similar to a ledger of the + transactions stored in the current block. There is also a receipts trie + which stores the results of executing a transaction, like the post state + and gas used. This function creates and executes the block that is to be + added to the chain. + + 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. + 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. + """ + 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, ...] = () + + beacon_block_roots_contract_code = get_account( + state, BEACON_ROOTS_ADDRESS + ).code + + system_tx_message = Message( + caller=SYSTEM_ADDRESS, + target=BEACON_ROOTS_ADDRESS, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=parent_beacon_block_root, + code=beacon_block_roots_contract_code, + depth=Uint(0), + current_target=BEACON_ROOTS_ADDRESS, + code_address=BEACON_ROOTS_ADDRESS, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + parent_evm=None, + ) + + system_tx_env = vm.Environment( + caller=SYSTEM_ADDRESS, + origin=SYSTEM_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=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) + + destroy_touched_empty_accounts( + system_tx_env.state, system_tx_output.touched_accounts + ) + + 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) + + 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), + blob_gas_used, + ) + + +def process_transaction( + env: vm.Environment, tx: Transaction +) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + """ + Execute a transaction against the provided environment. + + This function processes the actions needed to execute a transaction. + It decrements the sender's account after calculating the gas fee and + refunds them the proper amount after execution. Calling contracts, + deploying code, and incrementing nonces are all examples of actions that + happen within this function or from a call made within this function. + + Accounts that are marked for deletion are processed and destroyed after + execution. + + Parameters + ---------- + env : + Environment for the Ethereum Virtual Machine. + tx : + Transaction to execute. + + Returns + ------- + gas_left : `ethereum.base_types.U256` + Remaining gas after execution. + logs : `Tuple[ethereum.blocks.Log, ...]` + Logs generated during execution. + """ + if not validate_transaction(tx): + raise InvalidBlock + + sender = env.origin + sender_account = get_account(env.state, sender) + + if isinstance(tx, BlobTransaction): + blob_gas_fee = calculate_data_fee(env.excess_blob_gas, tx) + else: + blob_gas_fee = Uint(0) + + effective_gas_fee = tx.gas * env.gas_price + + gas = tx.gas - calculate_intrinsic_cost(tx) + increment_nonce(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)) + + preaccessed_addresses = set() + preaccessed_storage_keys = set() + preaccessed_addresses.add(env.coinbase) + if isinstance( + tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + ): + for address, keys in tx.access_list: + preaccessed_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), + ) + + output = process_message_call(message, env) + + 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 + + # 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 + + total_gas_used = gas_used - gas_refund + + # refund gas + sender_balance_after_refund = get_account( + env.state, sender + ).balance + U256(gas_refund_amount) + set_account_balance(env.state, sender, sender_balance_after_refund) + + # transfer miner fees + coinbase_balance_after_mining_fee = get_account( + env.state, 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 + ) + elif account_exists_and_is_empty(env.state, env.coinbase): + destroy_account(env.state, env.coinbase) + + for address in output.accounts_to_delete: + destroy_account(env.state, address) + + destroy_touched_empty_accounts(env.state, output.touched_accounts) + + return total_gas_used, output.logs, output.error + + +def compute_header_hash(header: Header) -> Hash32: + """ + Computes the hash of a block header. + + The header hash of a block is the canonical hash that is used to refer + to a specific block and completely distinguishes a block from another. + + ``keccak256`` is a function that produces a 256 bit hash of any input. + It also takes in any number of bytes as an input and produces a single + hash for them. A hash is a completely unique output for a single input. + So an input corresponds to one unique hash that can be used to identify + the input exactly. + + Prior to using the ``keccak256`` hash function, the header must be + encoded using the Recursive-Length Prefix. See :ref:`rlp`. + RLP encoding the header converts it into a space-efficient format that + allows for easy transfer of data between nodes. The purpose of RLP is to + encode arbitrarily nested arrays of binary data, and RLP is the primary + encoding method used to serialize objects in Ethereum's execution layer. + The only purpose of RLP is to encode structure; encoding specific data + types (e.g. strings, floats) is left up to higher-order protocols. + + Parameters + ---------- + header : + Header of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the header. + """ + return keccak256(rlp.encode(header)) + + +def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: + """ + Validates the gas limit for a block. + + The bounds of the gas limit, ``max_adjustment_delta``, is set as the + quotient of the parent block's gas limit and the + ``GAS_LIMIT_ADJUSTMENT_FACTOR``. Therefore, if the gas limit that is + passed through as a parameter is greater than or equal to the *sum* of + the parent's gas and the adjustment delta then the limit for gas is too + high and fails this function's check. Similarly, if the limit is less + than or equal to the *difference* of the parent's gas and the adjustment + delta *or* the predefined ``GAS_LIMIT_MINIMUM`` then this function's + check fails because the gas limit doesn't allow for a sufficient or + reasonable amount of gas to be used on a block. + + Parameters + ---------- + gas_limit : + Gas limit to validate. + + parent_gas_limit : + Gas limit of the parent block. + + Returns + ------- + check : `bool` + True if gas limit constraints are satisfied, False otherwise. + """ + max_adjustment_delta = parent_gas_limit // GAS_LIMIT_ADJUSTMENT_FACTOR + if gas_limit >= parent_gas_limit + max_adjustment_delta: + return False + if gas_limit <= parent_gas_limit - max_adjustment_delta: + return False + if gas_limit < GAS_LIMIT_MINIMUM: + return False + + return True diff --git a/src/ethereum/prague/fork_types.py b/src/ethereum/prague/fork_types.py new file mode 100644 index 0000000000..dc287c84f1 --- /dev/null +++ b/src/ethereum/prague/fork_types.py @@ -0,0 +1,64 @@ +""" +Ethereum Types +^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Types re-used throughout the specification, which are specific to Ethereum. +""" + +from dataclasses import dataclass + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes20, Bytes256 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U256, Uint + +from ..crypto.hash import Hash32, keccak256 + +Address = Bytes20 +Root = Hash32 +VersionedHash = Hash32 + +Bloom = Bytes256 + + +@slotted_freezable +@dataclass +class Account: + """ + State associated with an address. + """ + + nonce: Uint + balance: U256 + code: bytes + + +EMPTY_ACCOUNT = Account( + nonce=Uint(0), + balance=U256(0), + code=bytearray(), +) + + +def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes: + """ + Encode `Account` dataclass. + + Storage is not stored in the `Account` dataclass, so `Accounts` cannot be + encoded without providing a storage root. + """ + return rlp.encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + storage_root, + keccak256(raw_account_data.code), + ) + ) diff --git a/src/ethereum/prague/state.py b/src/ethereum/prague/state.py new file mode 100644 index 0000000000..1cb9cdcdc7 --- /dev/null +++ b/src/ethereum/prague/state.py @@ -0,0 +1,734 @@ +""" +State +^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state contains all information that is preserved between transactions. + +It consists of a main account trie and storage tries for each contract. + +There is a distinction between an account that does not exist and +`EMPTY_ACCOUNT`. +""" +from dataclasses import dataclass, field +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 + + +@dataclass +class State: + """ + Contains all information that is preserved between transactions. + """ + + _main_trie: Trie[Address, Optional[Account]] = field( + default_factory=lambda: Trie(secured=True, default=None) + ) + _storage_tries: Dict[Address, Trie[Bytes, U256]] = field( + default_factory=dict + ) + _snapshots: List[ + Tuple[ + Trie[Address, Optional[Account]], Dict[Address, Trie[Bytes, U256]] + ] + ] = field(default_factory=list) + created_accounts: Set[Address] = field(default_factory=set) + + +@dataclass +class TransientStorage: + """ + Contains all information that is preserved between message calls + within a transaction. + """ + + _tries: Dict[Address, Trie[Bytes, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes, U256]]] = field( + default_factory=list + ) + + +def close_state(state: State) -> None: + """ + Free resources held by the state. Used by optimized implementations to + release file descriptors. + """ + del state._main_trie + del state._storage_tries + del state._snapshots + del state.created_accounts + + +def begin_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Start a state transaction. + + Transactions are entirely implicit and can be nested. It is not possible to + calculate the state root during a transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.append( + ( + copy_trie(state._main_trie), + {k: copy_trie(t) for (k, t) in state._storage_tries.items()}, + ) + ) + transient_storage._snapshots.append( + {k: copy_trie(t) for (k, t) in transient_storage._tries.items()} + ) + + +def commit_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Commit a state transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._snapshots.pop() + + +def rollback_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Rollback a state transaction, resetting the state to the point when the + corresponding `start_transaction()` call was made. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._main_trie, state._storage_tries = state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._tries = transient_storage._snapshots.pop() + + +def get_account(state: State, address: Address) -> Account: + """ + Get the `Account` object at an address. Returns `EMPTY_ACCOUNT` if there + is no account at the address. + + Use `get_account_optional()` if you care about the difference between a + non-existent account and `EMPTY_ACCOUNT`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = get_account_optional(state, address) + if isinstance(account, Account): + return account + else: + return EMPTY_ACCOUNT + + +def get_account_optional(state: State, address: Address) -> Optional[Account]: + """ + Get the `Account` object at an address. Returns `None` (rather than + `EMPTY_ACCOUNT`) if there is no account at the address. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = trie_get(state._main_trie, address) + return account + + +def set_account( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. Setting to `None` deletes + the account (but not its storage, see `destroy_account()`). + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + trie_set(state._main_trie, address, account) + + +def destroy_account(state: State, address: Address) -> None: + """ + Completely remove the account at `address` and all of its storage. + + This function is made available exclusively for the `SELFDESTRUCT` + opcode. It is expected that `SELFDESTRUCT` will be disabled in a future + hardfork and this function will be removed. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account to destroy. + """ + destroy_storage(state, address) + set_account(state, address, None) + + +def destroy_storage(state: State, address: Address) -> None: + """ + Completely remove the storage at `address`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account whose storage is to be deleted. + """ + if address in state._storage_tries: + del state._storage_tries[address] + + +def mark_account_created(state: State, address: Address) -> None: + """ + Mark an account as having been created in the current transaction. + This information is used by `get_storage_original()` to handle an obscure + edgecase. + + The marker is not removed even if the account creation reverts. Since the + account cannot have had code prior to its creation and can't call + `get_storage_original()`, this is harmless. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account that has been created. + """ + state.created_accounts.add(address) + + +def get_storage(state: State, address: Address, key: Bytes) -> U256: + """ + Get a value at a storage key on an account. Returns `U256(0)` if the + storage key has not been set previously. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + + Returns + ------- + value : `U256` + Value at the key. + """ + trie = state._storage_tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_storage( + state: State, address: Address, key: Bytes, value: U256 +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + assert trie_get(state._main_trie, address) is not None + + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + +def storage_root(state: State, address: Address) -> Root: + """ + Calculate the storage root of an account. + + Parameters + ---------- + state: + The state + address : + Address of the account. + + Returns + ------- + root : `Root` + Storage root of the account. + """ + assert not state._snapshots + if address in state._storage_tries: + return root(state._storage_tries[address]) + else: + return EMPTY_TRIE_ROOT + + +def state_root(state: State) -> Root: + """ + Calculate the state root. + + Parameters + ---------- + state: + The current state. + + Returns + ------- + root : `Root` + The state root. + """ + assert not state._snapshots + + def get_storage_root(address: Address) -> Root: + return storage_root(state, address) + + return root(state._main_trie, get_storage_root=get_storage_root) + + +def account_exists(state: State, address: Address) -> bool: + """ + Checks if an account exists in the state trie + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + account_exists : `bool` + True if account exists in the state trie, False otherwise + """ + return get_account_optional(state, address) is not None + + +def account_has_code_or_nonce(state: State, address: Address) -> bool: + """ + Checks if an account has non zero nonce or non empty code + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_code_or_nonce : `bool` + True if the account has non zero nonce or non empty code, + False otherwise. + """ + account = get_account(state, address) + return account.nonce != Uint(0) or account.code != b"" + + +def account_has_storage(state: State, address: Address) -> bool: + """ + Checks if an account has storage. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_storage : `bool` + True if the account has storage, False otherwise. + """ + return address in state._storage_tries + + +def is_account_empty(state: State, address: Address) -> bool: + """ + Checks if an account has zero nonce, empty code and zero balance. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + is_empty : `bool` + True if if an account has zero nonce, empty code and zero balance, + False otherwise. + """ + account = get_account(state, address) + return ( + account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + +def account_exists_and_is_empty(state: State, address: Address) -> bool: + """ + Checks if an account exists and has zero nonce, empty code and zero + balance. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + exists_and_is_empty : `bool` + True if an account exists and has zero nonce, empty code and zero + balance, False otherwise. + """ + account = get_account_optional(state, address) + return ( + account is not None + and account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + +def is_account_alive(state: State, address: Address) -> bool: + """ + Check whether is an account is both in the state and non empty. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + is_alive : `bool` + True if the account is alive. + """ + account = get_account_optional(state, address) + if account is None: + return False + else: + return not ( + account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + +def modify_state( + state: State, address: Address, f: Callable[[Account], None] +) -> None: + """ + Modify an `Account` in the `State`. + """ + set_account(state, address, modify(get_account(state, address), f)) + + +def move_ether( + state: State, + sender_address: Address, + recipient_address: Address, + amount: U256, +) -> None: + """ + Move funds between accounts. + """ + + def reduce_sender_balance(sender: Account) -> None: + if sender.balance < amount: + raise AssertionError + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(state, sender_address, reduce_sender_balance) + 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. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + + amount: + The amount that needs to set in balance. + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(state, address, set_balance) + + +def touch_account(state: State, address: Address) -> None: + """ + Initializes an account to state. + + Parameters + ---------- + state: + The current state. + + address: + The address of the account that need to initialised. + """ + if not account_exists(state, address): + set_account(state, address, EMPTY_ACCOUNT) + + +def increment_nonce(state: State, address: Address) -> None: + """ + Increments the nonce of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += Uint(1) + + modify_state(state, address, increase_nonce) + + +def set_code(state: State, address: Address, code: Bytes) -> None: + """ + Sets Account code. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose code needs to be update. + + code: + The bytecode that needs to be set. + """ + + def write_code(sender: Account) -> None: + sender.code = code + + modify_state(state, address, write_code) + + +def get_storage_original(state: State, address: Address, key: Bytes) -> U256: + """ + Get the original value in a storage slot i.e. the value before the current + transaction began. This function reads the value from the snapshots taken + before executing the transaction. + + Parameters + ---------- + state: + The current state. + address: + Address of the account to read the value from. + key: + Key of the storage slot. + """ + # In the transaction where an account is created, its preexisting storage + # is ignored. + if address in state.created_accounts: + return U256(0) + + _, original_trie = state._snapshots[0] + original_account_trie = original_trie.get(address) + + if original_account_trie is None: + original_value = U256(0) + else: + original_value = trie_get(original_account_trie, key) + + assert isinstance(original_value, U256) + + return original_value + + +def get_transient_storage( + transient_storage: TransientStorage, address: Address, key: Bytes +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Returns `U256(0)` if the storage key has not been set previously. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + Returns + ------- + value : `U256` + Value at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_transient_storage( + transient_storage: TransientStorage, + address: Address, + key: Bytes, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + transient_storage._tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del transient_storage._tries[address] + + +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/prague/transactions.py b/src/ethereum/prague/transactions.py new file mode 100644 index 0000000000..deb594c17c --- /dev/null +++ b/src/ethereum/prague/transactions.py @@ -0,0 +1,453 @@ +""" +Transactions are atomic units of work created externally to Ethereum and +submitted to be executed. If Ethereum is viewed as a state machine, +transactions are the events that move between states. +""" +from dataclasses import dataclass +from typing import Tuple, Union + +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.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError + +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 + + +@slotted_freezable +@dataclass +class LegacyTransaction: + """ + Atomic operation performed on the block chain. + """ + + nonce: U256 + gas_price: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + v: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class AccessListTransaction: + """ + The transaction type added in EIP-2930 to support access lists. + """ + + chain_id: U64 + nonce: U256 + gas_price: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + y_parity: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class FeeMarketTransaction: + """ + The transaction type added in EIP-1559. + """ + + chain_id: U64 + nonce: U256 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + y_parity: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in EIP-4844. + """ + + chain_id: U64 + nonce: U256 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Address + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + max_fee_per_blob_gas: U256 + blob_versioned_hashes: Tuple[VersionedHash, ...] + y_parity: U256 + r: U256 + s: U256 + + +Transaction = Union[ + LegacyTransaction, + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, +] + + +def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]: + """ + Encode a transaction. Needed because non-legacy transactions aren't RLP. + """ + if isinstance(tx, LegacyTransaction): + return tx + elif isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(tx) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(tx) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(tx) + else: + raise Exception(f"Unable to encode transaction of type {type(tx)}") + + +def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: + """ + Decode a transaction. Needed because non-legacy transactions aren't RLP. + """ + if isinstance(tx, Bytes): + if tx[0] == 1: + return rlp.decode_to(AccessListTransaction, tx[1:]) + elif tx[0] == 2: + return rlp.decode_to(FeeMarketTransaction, tx[1:]) + elif tx[0] == 3: + return rlp.decode_to(BlobTransaction, tx[1:]) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction) -> bool: + """ + Verifies a transaction. + + The gas in a transaction gets used to pay for the intrinsic cost of + operations, therefore if there is insufficient gas then it would not + be possible to execute a transaction and it will be declared invalid. + + Additionally, the nonce of a transaction must not equal or exceed the + limit defined in `EIP-2681 `_. + In practice, defining the limit as ``2**64-1`` has no impact because + sending ``2**64-1`` transactions is improbable. It's not strictly + impossible though, ``2**64-1`` transactions is the entire capacity of the + Ethereum blockchain at 2022 gas limits for a little over 22 years. + + Parameters + ---------- + tx : + Transaction to validate. + + Returns + ------- + verified : `bool` + True if the transaction can be executed, or False otherwise. + """ + from .vm.interpreter import MAX_CODE_SIZE + + if calculate_intrinsic_cost(tx) > tx.gas: + return False + if tx.nonce >= U256(U64.MAX_VALUE): + return False + if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: + return False + + return True + + +def calculate_intrinsic_cost(tx: Transaction) -> Uint: + """ + Calculates the gas that is charged before execution is started. + + The intrinsic cost of the transaction is charged before execution has + begun. Functions/operations in the EVM cost money to execute so this + intrinsic cost is for the operations that need to be paid for as part of + the transaction. Data transfer, for example, is part of this intrinsic + cost. It costs ether to send data over the wire and that ether is + accounted for in the intrinsic cost calculated in this function. This + intrinsic cost must be calculated and paid for before execution in order + for all operations to be implemented. + + Parameters + ---------- + tx : + Transaction to compute the intrinsic cost of. + + Returns + ------- + verified : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + """ + from .vm.gas import init_code_cost + + data_cost = 0 + + for byte in tx.data: + if byte == 0: + data_cost += TX_DATA_COST_PER_ZERO + else: + 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)))) + else: + create_cost = 0 + + access_list_cost = 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 + + return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + + +def recover_sender(chain_id: U64, tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + The v, r, and s values are the three parts that make up the signature + of a transaction. In order to recover the sender of a transaction the two + components needed are the signature (``v``, ``r``, and ``s``) and the + signing hash of the transaction. The sender's public key can be obtained + with these two values and therefore the sender address can be retrieved. + + Parameters + ---------- + tx : + Transaction of interest. + chain_id : + ID of the executing chain. + + Returns + ------- + sender : `ethereum.fork_types.Address` + The address of the account that signed the transaction. + """ + r, s = tx.r, tx.s + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("bad r") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("bad s") + + if isinstance(tx, LegacyTransaction): + v = tx.v + if v == 27 or v == 28: + public_key = secp256k1_recover( + r, s, v - U256(27), signing_hash_pre155(tx) + ) + else: + chain_id_x2 = U256(chain_id) * U256(2) + if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: + raise InvalidSignatureError("bad v") + public_key = secp256k1_recover( + r, + s, + v - U256(35) - chain_id_x2, + signing_hash_155(tx, chain_id), + ) + elif isinstance(tx, AccessListTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_2930(tx) + ) + elif isinstance(tx, FeeMarketTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_1559(tx) + ) + elif isinstance(tx, BlobTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) + + return Address(keccak256(public_key)[12:32]) + + +def signing_hash_pre155(tx: LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre EIP 155) signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: LegacyTransaction, chain_id: U64) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 155 signature. + + Parameters + ---------- + tx : + Transaction of interest. + chain_id : + The id of the current chain. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + chain_id, + Uint(0), + Uint(0), + ) + ) + ) + + +def signing_hash_2930(tx: AccessListTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 2930 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x01" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 1559 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x02" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP-4844 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x03" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.max_fee_per_blob_gas, + tx.blob_versioned_hashes, + ) + ) + ) diff --git a/src/ethereum/prague/trie.py b/src/ethereum/prague/trie.py new file mode 100644 index 0000000000..082cbb5495 --- /dev/null +++ b/src/ethereum/prague/trie.py @@ -0,0 +1,470 @@ +""" +State Trie +^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`.fork_types.Account` objects. +""" + +import copy +from dataclasses import dataclass, field +from typing import ( + Callable, + Dict, + Generic, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + TypeVar, + Union, +) + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.shanghai import trie as previous_trie +from ethereum.utils.hexadecimal import hex_to_bytes + +from .blocks import Receipt, Withdrawal +from .fork_types import Account, Address, Root, encode_account +from .transactions import LegacyTransaction + +# note: an empty trie (regardless of whether it is secured) has root: +# +# keccak256(RLP(b'')) +# == +# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 # noqa: E501,SC10 +# +# also: +# +# keccak256(RLP(())) +# == +# 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 # noqa: E501,SC10 +# +# which is the sha3Uncles hash in block header with no uncles +EMPTY_TRIE_ROOT = Root( + hex_to_bytes( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + ) +) + +Node = Union[ + Account, Bytes, LegacyTransaction, Receipt, Uint, U256, Withdrawal, None +] +K = TypeVar("K", bound=Bytes) +V = TypeVar( + "V", + Optional[Account], + Optional[Bytes], + Bytes, + Optional[Union[LegacyTransaction, Bytes]], + Optional[Union[Receipt, Bytes]], + Optional[Union[Withdrawal, Bytes]], + Uint, + U256, +) + + +@slotted_freezable +@dataclass +class LeafNode: + """Leaf node in the Merkle Trie""" + + rest_of_key: Bytes + value: rlp.Extended + + +@slotted_freezable +@dataclass +class ExtensionNode: + """Extension node in the Merkle Trie""" + + key_segment: Bytes + subnode: rlp.Extended + + +@slotted_freezable +@dataclass +class BranchNode: + """Branch node in the Merkle Trie""" + + subnodes: List[rlp.Extended] + value: rlp.Extended + + +InternalNode = Union[LeafNode, ExtensionNode, BranchNode] + + +def encode_internal_node(node: Optional[InternalNode]) -> rlp.Extended: + """ + Encodes a Merkle Trie node into its RLP form. The RLP will then be + serialized into a `Bytes` and hashed unless it is less that 32 bytes + when serialized. + + This function also accepts `None`, representing the absence of a node, + which is encoded to `b""`. + + Parameters + ---------- + node : Optional[InternalNode] + The node to encode. + + Returns + ------- + encoded : `rlp.Extended` + The node encoded as RLP. + """ + unencoded: rlp.Extended + if node is None: + unencoded = b"" + elif isinstance(node, LeafNode): + unencoded = ( + nibble_list_to_compact(node.rest_of_key, True), + node.value, + ) + elif isinstance(node, ExtensionNode): + unencoded = ( + nibble_list_to_compact(node.key_segment, False), + node.subnode, + ) + elif isinstance(node, BranchNode): + unencoded = node.subnodes + [node.value] + else: + raise AssertionError(f"Invalid internal node type {type(node)}!") + + encoded = rlp.encode(unencoded) + if len(encoded) < 32: + return unencoded + else: + return keccak256(encoded) + + +def encode_node(node: Node, storage_root: Optional[Bytes] = None) -> Bytes: + """ + Encode a Node for storage in the Merkle Trie. + + Currently mostly an unimplemented stub. + """ + if isinstance(node, Account): + assert storage_root is not None + return encode_account(node, storage_root) + elif isinstance(node, (LegacyTransaction, Receipt, Withdrawal, U256)): + return rlp.encode(node) + elif isinstance(node, Bytes): + return node + else: + return previous_trie.encode_node(node, storage_root) + + +@dataclass +class Trie(Generic[K, V]): + """ + The Merkle Trie. + """ + + secured: bool + default: V + _data: Dict[K, V] = field(default_factory=dict) + + +def copy_trie(trie: Trie[K, V]) -> Trie[K, V]: + """ + Create a copy of `trie`. Since only frozen objects may be stored in tries, + the contents are reused. + + Parameters + ---------- + trie: `Trie` + Trie to copy. + + Returns + ------- + new_trie : `Trie[K, V]` + A copy of the trie. + """ + return Trie(trie.secured, trie.default, copy.copy(trie._data)) + + +def trie_set(trie: Trie[K, V], key: K, value: V) -> None: + """ + Stores an item in a Merkle Trie. + + This method deletes the key if `value == trie.default`, because the Merkle + Trie represents the default value by omitting it from the trie. + + Parameters + ---------- + trie: `Trie` + Trie to store in. + key : `Bytes` + Key to lookup. + value : `V` + Node to insert at `key`. + """ + if value == trie.default: + if key in trie._data: + del trie._data[key] + else: + trie._data[key] = value + + +def trie_get(trie: Trie[K, V], key: K) -> V: + """ + Gets an item from the Merkle Trie. + + This method returns `trie.default` if the key is missing. + + Parameters + ---------- + trie: + Trie to lookup in. + key : + Key to lookup. + + Returns + ------- + node : `V` + Node at `key` in the trie. + """ + return trie._data.get(key, trie.default) + + +def common_prefix_length(a: Sequence, b: Sequence) -> int: + """ + Find the longest common prefix of two sequences. + """ + for i in range(len(a)): + if i >= len(b) or a[i] != b[i]: + return i + return len(a) + + +def nibble_list_to_compact(x: Bytes, is_leaf: bool) -> Bytes: + """ + Compresses nibble-list into a standard byte array with a flag. + + A nibble-list is a list of byte values no greater than `15`. The flag is + encoded in high nibble of the highest byte. The flag nibble can be broken + down into two two-bit flags. + + Highest nibble:: + + +---+---+----------+--------+ + | _ | _ | is_leaf | parity | + +---+---+----------+--------+ + 3 2 1 0 + + + The lowest bit of the nibble encodes the parity of the length of the + remaining nibbles -- `0` when even and `1` when odd. The second lowest bit + is used to distinguish leaf and extension nodes. The other two bits are not + used. + + Parameters + ---------- + x : + Array of nibbles. + is_leaf : + True if this is part of a leaf node, or false if it is an extension + node. + + Returns + ------- + compressed : `bytearray` + Compact byte array. + """ + compact = bytearray() + + if len(x) % 2 == 0: # ie even length + compact.append(16 * (2 * is_leaf)) + for i in range(0, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + else: + compact.append(16 * ((2 * is_leaf) + 1) + x[0]) + for i in range(1, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + + return Bytes(compact) + + +def bytes_to_nibble_list(bytes_: Bytes) -> Bytes: + """ + Converts a `Bytes` into to a sequence of nibbles (bytes with value < 16). + + Parameters + ---------- + bytes_: + The `Bytes` to convert. + + Returns + ------- + nibble_list : `Bytes` + The `Bytes` in nibble-list format. + """ + nibble_list = bytearray(2 * len(bytes_)) + for byte_index, byte in enumerate(bytes_): + nibble_list[byte_index * 2] = (byte & 0xF0) >> 4 + nibble_list[byte_index * 2 + 1] = byte & 0x0F + return Bytes(nibble_list) + + +def _prepare_trie( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Mapping[Bytes, Bytes]: + """ + Prepares the trie for root calculation. Removes values that are empty, + hashes the keys (if `secured == True`) and encodes all the nodes. + + Parameters + ---------- + trie : + The `Trie` to prepare. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + Returns + ------- + out : `Mapping[ethereum.base_types.Bytes, Node]` + Object with keys mapped to nibble-byte form. + """ + mapped: MutableMapping[Bytes, Bytes] = {} + + for preimage, value in trie._data.items(): + if isinstance(value, Account): + assert get_storage_root is not None + address = Address(preimage) + encoded_value = encode_node(value, get_storage_root(address)) + else: + encoded_value = encode_node(value) + if encoded_value == b"": + raise AssertionError + key: Bytes + if trie.secured: + # "secure" tries hash keys once before construction + key = keccak256(preimage) + else: + key = preimage + mapped[bytes_to_nibble_list(key)] = encoded_value + + return mapped + + +def root( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Root: + """ + Computes the root of a modified merkle patricia trie (MPT). + + Parameters + ---------- + trie : + `Trie` to get the root of. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + + Returns + ------- + root : `.fork_types.Root` + MPT root of the underlying key-value pairs. + """ + obj = _prepare_trie(trie, get_storage_root) + + root_node = encode_internal_node(patricialize(obj, Uint(0))) + if len(rlp.encode(root_node)) < 32: + return keccak256(rlp.encode(root_node)) + else: + assert isinstance(root_node, Bytes) + return Root(root_node) + + +def patricialize( + obj: Mapping[Bytes, Bytes], level: Uint +) -> Optional[InternalNode]: + """ + Structural composition function. + + Used to recursively patricialize and merkleize a dictionary. Includes + memoization of the tree structure and hashes. + + Parameters + ---------- + obj : + Underlying trie key-value pairs, with keys in nibble-list format. + level : + Current trie level. + + Returns + ------- + node : `ethereum.base_types.Bytes` + Root node of `obj`. + """ + if len(obj) == 0: + return None + + arbitrary_key = next(iter(obj)) + + # if leaf node + if len(obj) == 1: + leaf = LeafNode(arbitrary_key[level:], obj[arbitrary_key]) + return leaf + + # prepare for extension node check by finding max j such that all keys in + # obj have the same key[i:j] + substring = arbitrary_key[level:] + prefix_length = len(substring) + for key in obj: + prefix_length = min( + prefix_length, common_prefix_length(substring, key[level:]) + ) + + # finished searching, found another key at the current level + if prefix_length == 0: + break + + # if extension node + if prefix_length > 0: + prefix = arbitrary_key[int(level) : int(level) + prefix_length] + return ExtensionNode( + prefix, + encode_internal_node( + patricialize(obj, level + Uint(prefix_length)) + ), + ) + + branches: List[MutableMapping[Bytes, Bytes]] = [] + for _ in range(16): + branches.append({}) + value = b"" + for key in obj: + if len(key) == level: + # shouldn't ever have an account or receipt in an internal node + if isinstance(obj[key], (Account, Receipt, Uint)): + raise AssertionError + value = obj[key] + else: + branches[key[level]][key] = obj[key] + + return BranchNode( + [ + encode_internal_node(patricialize(branches[k], level + Uint(1))) + for k in range(16) + ], + value, + ) diff --git a/src/ethereum/prague/utils/__init__.py b/src/ethereum/prague/utils/__init__.py new file mode 100644 index 0000000000..224a4d269b --- /dev/null +++ b/src/ethereum/prague/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/prague/utils/address.py b/src/ethereum/prague/utils/address.py new file mode 100644 index 0000000000..3120d7a2c2 --- /dev/null +++ b/src/ethereum/prague/utils/address.py @@ -0,0 +1,93 @@ +""" +Hardfork Utility Functions For Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this prague version of +specification. +""" +from typing import Union + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.byte import left_pad_zero_bytes + +from ..fork_types import Address + + +def to_address(data: Union[Uint, U256]) -> Address: + """ + Convert a Uint or U256 value to a valid address (20 bytes). + + Parameters + ---------- + data : + The string to be converted to bytes. + + Returns + ------- + address : `Address` + The obtained address. + """ + return Address(data.to_be_bytes32()[-20:]) + + +def compute_contract_address(address: Address, nonce: Uint) -> Address: + """ + Computes address of the new account that needs to be created. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + nonce : + The transaction count of the account that wants to create the new + account. + + Returns + ------- + address: `Address` + The computed address of the new account. + """ + computed_address = keccak256(rlp.encode([address, nonce])) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + return Address(padded_address) + + +def compute_create2_contract_address( + address: Address, salt: Bytes32, call_data: bytearray +) -> Address: + """ + Computes address of the new account that needs to be created, which is + based on the sender address, salt and the call data as well. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + salt : + Address generation salt. + call_data : + The code of the new account which is to be created. + + Returns + ------- + address: `ethereum.prague.fork_types.Address` + The computed address of the new account. + """ + preimage = b"\xff" + address + salt + keccak256(call_data) + computed_address = keccak256(preimage) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + + return Address(padded_address) diff --git a/src/ethereum/prague/utils/hexadecimal.py b/src/ethereum/prague/utils/hexadecimal.py new file mode 100644 index 0000000000..5d6090084c --- /dev/null +++ b/src/ethereum/prague/utils/hexadecimal.py @@ -0,0 +1,68 @@ +""" +Utility Functions For Hexadecimal Strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Hexadecimal utility functions used in this specification, specific to +Prague types. +""" +from ethereum.utils.hexadecimal import remove_hex_prefix + +from ..fork_types import Address, Bloom, Root + + +def hex_to_root(hex_string: str) -> Root: + """ + Convert hex string to trie root. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to trie root. + + Returns + ------- + root : `Root` + Trie root obtained from the given hexadecimal string. + """ + return Root(bytes.fromhex(remove_hex_prefix(hex_string))) + + +def hex_to_bloom(hex_string: str) -> Bloom: + """ + Convert hex string to bloom. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to bloom. + + Returns + ------- + bloom : `Bloom` + Bloom obtained from the given hexadecimal string. + """ + return Bloom(bytes.fromhex(remove_hex_prefix(hex_string))) + + +def hex_to_address(hex_string: str) -> Address: + """ + Convert hex string to Address (20 bytes). + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to Address. + + Returns + ------- + address : `Address` + The address obtained from the given hexadecimal string. + """ + return Address(bytes.fromhex(remove_hex_prefix(hex_string).rjust(40, "0"))) diff --git a/src/ethereum/prague/utils/message.py b/src/ethereum/prague/utils/message.py new file mode 100644 index 0000000000..7e91a7872c --- /dev/null +++ b/src/ethereum/prague/utils/message.py @@ -0,0 +1,116 @@ +""" +Hardfork Utility Functions For The Message Data-structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this prague 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 ..fork_types import Address +from ..state import get_account +from ..vm import Environment, Message +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(), +) -> 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 : + 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 + + Returns + ------- + message: `ethereum.prague.vm.Message` + Items containing contract creation or message call specific data. + """ + if isinstance(target, Bytes0): + current_target = compute_contract_address( + caller, + get_account(env.state, caller).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 + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses = set() + accessed_addresses.add(current_target) + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) + + return Message( + caller=caller, + target=target, + gas=gas, + value=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, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, + ) diff --git a/src/ethereum/prague/vm/__init__.py b/src/ethereum/prague/vm/__init__.py new file mode 100644 index 0000000000..04bb7a353a --- /dev/null +++ b/src/ethereum/prague/vm/__init__.py @@ -0,0 +1,151 @@ +""" +Ethereum Virtual Machine (EVM) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`.fork_types.Account`. +""" + +from dataclasses import dataclass +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 ..blocks import Log +from ..fork_types import Address, VersionedHash +from ..state import State, TransientStorage, account_exists_and_is_empty +from .precompiled_contracts import RIPEMD160_ADDRESS + +__all__ = ("Environment", "Evm", "Message") + + +@dataclass +class Environment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + caller: Address + 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, ...] + transient_storage: TransientStorage + + +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + caller: Address + target: Union[Bytes0, Address] + current_target: Address + gas: Uint + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + is_static: bool + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: Uint + env: Environment + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: int + running: bool + message: Message + output: Bytes + accounts_to_delete: Set[Address] + touched_accounts: Set[Address] + return_data: Bytes + error: Optional[Exception] + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + evm.gas_left += child_evm.gas_left + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + 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.touched_accounts.add(child_evm.message.current_target) + evm.accessed_addresses.update(child_evm.accessed_addresses) + evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + # In block 2675119, the empty account at 0x3 (the RIPEMD160 precompile) was + # cleared despite running out of gas. This is an obscure edge case that can + # only happen to a precompile. + # According to the general rules governing clearing of empty accounts, the + # touch should have been reverted. Due to client bugs, this event went + # unnoticed and 0x3 has been exempted from the rule that touches are + # reverted in order to preserve this historical behaviour. + if RIPEMD160_ADDRESS in child_evm.touched_accounts: + 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.touched_accounts.add(RIPEMD160_ADDRESS) + evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/prague/vm/exceptions.py b/src/ethereum/prague/vm/exceptions.py new file mode 100644 index 0000000000..2a4f2d2f65 --- /dev/null +++ b/src/ethereum/prague/vm/exceptions.py @@ -0,0 +1,140 @@ +""" +Ethereum Virtual Machine (EVM) Exceptions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Exceptions which cause the EVM to halt exceptionally. +""" + +from ethereum.exceptions import EthereumException + + +class ExceptionalHalt(EthereumException): + """ + Indicates that the EVM has experienced an exceptional halt. This causes + execution to immediately end with all gas being consumed. + """ + + +class Revert(EthereumException): + """ + Raised by the `REVERT` opcode. + + Unlike other EVM exceptions this does not result in the consumption of all + gas. + """ + + pass + + +class StackUnderflowError(ExceptionalHalt): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(ExceptionalHalt): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(ExceptionalHalt): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(ExceptionalHalt): + """ + Raised when an invalid opcode is encountered. + """ + + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code + + +class InvalidJumpDestError(ExceptionalHalt): + """ + Occurs when the destination of a jump operation doesn't meet any of the + following criteria: + + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + """ + + +class StackDepthLimitError(ExceptionalHalt): + """ + Raised when the message depth is greater than `1024` + """ + + pass + + +class WriteInStaticContext(ExceptionalHalt): + """ + Raised when an attempt is made to modify the state while operating inside + of a STATICCALL context. + """ + + pass + + +class OutOfBoundsRead(ExceptionalHalt): + """ + Raised when an attempt was made to read data beyond the + boundaries of the buffer. + """ + + pass + + +class InvalidParameter(ExceptionalHalt): + """ + Raised when invalid parameters are passed. + """ + + pass + + +class InvalidContractPrefix(ExceptionalHalt): + """ + Raised when the new contract code starts with 0xEF. + """ + + pass + + +class AddressCollision(ExceptionalHalt): + """ + Raised when the new contract address has a collision. + """ + + pass + + +class KZGProofError(ExceptionalHalt): + """ + Raised when the point evaluation precompile can't verify a proof. + """ + + pass diff --git a/src/ethereum/prague/vm/gas.py b/src/ethereum/prague/vm/gas.py new file mode 100644 index 0000000000..cc3ccc7c6b --- /dev/null +++ b/src/ethereum/prague/vm/gas.py @@ -0,0 +1,362 @@ +""" +Ethereum Virtual Machine (EVM) Gas +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" +from dataclasses import dataclass +from typing import List, Tuple + +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..transactions import BlobTransaction, Transaction +from . import Evm +from .exceptions import OutOfGasError + +GAS_JUMPDEST = Uint(1) +GAS_BASE = Uint(2) +GAS_VERY_LOW = Uint(3) +GAS_STORAGE_SET = Uint(20000) +GAS_STORAGE_UPDATE = Uint(5000) +GAS_STORAGE_CLEAR_REFUND = Uint(4800) +GAS_LOW = Uint(5) +GAS_MID = Uint(8) +GAS_HIGH = Uint(10) +GAS_EXPONENTIATION = Uint(10) +GAS_EXPONENTIATION_PER_BYTE = Uint(50) +GAS_MEMORY = Uint(3) +GAS_KECCAK256 = Uint(30) +GAS_KECCAK256_WORD = Uint(6) +GAS_COPY = Uint(3) +GAS_BLOCK_HASH = Uint(20) +GAS_LOG = Uint(375) +GAS_LOG_DATA = Uint(8) +GAS_LOG_TOPIC = Uint(375) +GAS_CREATE = Uint(32000) +GAS_CODE_DEPOSIT = Uint(200) +GAS_ZERO = Uint(0) +GAS_NEW_ACCOUNT = Uint(25000) +GAS_CALL_VALUE = Uint(9000) +GAS_CALL_STIPEND = Uint(2300) +GAS_SELF_DESTRUCT = Uint(5000) +GAS_SELF_DESTRUCT_NEW_ACCOUNT = Uint(25000) +GAS_ECRECOVER = Uint(3000) +GAS_SHA256 = Uint(60) +GAS_SHA256_WORD = Uint(12) +GAS_RIPEMD160 = Uint(600) +GAS_RIPEMD160_WORD = Uint(120) +GAS_IDENTITY = Uint(15) +GAS_IDENTITY_WORD = Uint(3) +GAS_RETURN_DATA_COPY = Uint(3) +GAS_FAST_STEP = Uint(5) +GAS_BLAKE2_PER_ROUND = Uint(1) +GAS_COLD_SLOAD = Uint(2100) +GAS_COLD_ACCOUNT_ACCESS = Uint(2600) +GAS_WARM_ACCESS = Uint(100) +GAS_INIT_CODE_WORD_COST = Uint(2) +GAS_BLOBHASH_OPCODE = Uint(3) +GAS_POINT_EVALUATION = Uint(50000) + +TARGET_BLOB_GAS_PER_BLOCK = U64(393216) +GAS_PER_BLOB = Uint(2**17) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) + + +@dataclass +class ExtendMemory: + """ + Define the parameters for memory extension in opcodes + + `cost`: `ethereum.base_types.Uint` + The gas required to perform the extension + `expand_by`: `ethereum.base_types.Uint` + The size by which the memory will be extended + """ + + cost: Uint + expand_by: Uint + + +@dataclass +class MessageCallGas: + """ + Define the gas cost and stipend for executing the call opcodes. + + `cost`: `ethereum.base_types.Uint` + The non-refundable portion of gas reserved for executing the + call opcode. + `stipend`: `ethereum.base_types.Uint` + The portion of gas available to sub-calls that is refundable + if not consumed + """ + + cost: Uint + stipend: Uint + + +def charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left`. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(int(amount))) + + if evm.gas_left < amount: + raise OutOfGasError + else: + evm.gas_left -= amount + + +def calculate_memory_gas_cost(size_in_bytes: Uint) -> Uint: + """ + Calculates the gas cost for allocating memory + to the smallest multiple of 32 bytes, + such that the allocated size is at least as big as the given size. + + Parameters + ---------- + size_in_bytes : + The size of the data in bytes. + + Returns + ------- + total_gas_cost : `ethereum.base_types.Uint` + The gas cost for storing data in memory. + """ + size_in_words = ceil32(size_in_bytes) // Uint(32) + linear_cost = size_in_words * GAS_MEMORY + quadratic_cost = size_in_words ** Uint(2) // Uint(512) + total_gas_cost = linear_cost + quadratic_cost + try: + return total_gas_cost + except ValueError: + raise OutOfGasError + + +def calculate_gas_extend_memory( + memory: bytearray, extensions: List[Tuple[U256, U256]] +) -> ExtendMemory: + """ + Calculates the gas amount to extend memory + + Parameters + ---------- + memory : + Memory contents of the EVM. + extensions: + List of extensions to be made to the memory. + Consists of a tuple of start position and size. + + Returns + ------- + extend_memory: `ExtendMemory` + """ + size_to_extend = Uint(0) + to_be_paid = Uint(0) + current_size = Uint(len(memory)) + for start_position, size in extensions: + if size == 0: + continue + before_size = ceil32(current_size) + after_size = ceil32(Uint(start_position) + Uint(size)) + if after_size <= before_size: + continue + + size_to_extend += after_size - before_size + already_paid = calculate_memory_gas_cost(before_size) + total_cost = calculate_memory_gas_cost(after_size) + to_be_paid += total_cost - already_paid + + current_size = after_size + + return ExtendMemory(to_be_paid, size_to_extend) + + +def calculate_message_call_gas( + value: U256, + gas: Uint, + gas_left: Uint, + memory_cost: Uint, + extra_gas: Uint, + call_stipend: Uint = GAS_CALL_STIPEND, +) -> MessageCallGas: + """ + Calculates the MessageCallGas (cost and stipend) for + executing call Opcodes. + + Parameters + ---------- + value: + The amount of `ETH` that needs to be transferred. + gas : + The amount of gas provided to the message-call. + gas_left : + The amount of gas left in the current frame. + memory_cost : + The amount needed to extend the memory in the current frame. + extra_gas : + The amount of gas needed for transferring value + creating a new + account inside a message call. + call_stipend : + The amount of stipend provided to a message call to execute code while + transferring value(ETH). + + Returns + ------- + message_call_gas: `MessageCallGas` + """ + call_stipend = Uint(0) if value == 0 else call_stipend + if gas_left < extra_gas + memory_cost: + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + gas = min(gas, max_message_call_gas(gas_left - memory_cost - extra_gas)) + + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + +def max_message_call_gas(gas: Uint) -> Uint: + """ + Calculates the maximum gas that is allowed for making a message call + + Parameters + ---------- + gas : + The amount of gas provided to the message-call. + + Returns + ------- + max_allowed_message_call_gas: `ethereum.base_types.Uint` + The maximum gas allowed for making the message-call. + """ + return gas - (gas // Uint(64)) + + +def init_code_cost(init_code_length: Uint) -> Uint: + """ + Calculates the gas to be charged for the init code in CREAT* + opcodes as well as create transactions. + + Parameters + ---------- + init_code_length : + The length of the init code provided to the opcode + or a create transaction + + Returns + ------- + init_code_gas: `ethereum.base_types.Uint` + The gas to be charged for the init code. + """ + return GAS_INIT_CODE_WORD_COST * ceil32(init_code_length) // Uint(32) + + +def calculate_excess_blob_gas(parent_header: Header) -> U64: + """ + Calculated the excess blob gas for the current block based + on the gas used in the parent block. + + Parameters + ---------- + parent_header : + The parent block of the current block. + + Returns + ------- + excess_blob_gas: `ethereum.base_types.U64` + The excess blob gas for the current block. + """ + # At the fork block, these are defined as zero. + excess_blob_gas = U64(0) + blob_gas_used = U64(0) + + if isinstance(parent_header, Header): + # After the fork block, read them from the parent header. + excess_blob_gas = parent_header.excess_blob_gas + blob_gas_used = parent_header.blob_gas_used + + parent_blob_gas = excess_blob_gas + blob_gas_used + if parent_blob_gas < TARGET_BLOB_GAS_PER_BLOCK: + return U64(0) + else: + return parent_blob_gas - TARGET_BLOB_GAS_PER_BLOCK + + +def calculate_total_blob_gas(tx: Transaction) -> Uint: + """ + Calculate the total blob gas for a transaction. + + Parameters + ---------- + tx : + The transaction for which the blob gas is to be calculated. + + Returns + ------- + total_blob_gas: `ethereum.base_types.Uint` + The total blob gas for the transaction. + """ + if isinstance(tx, BlobTransaction): + return GAS_PER_BLOB * Uint(len(tx.blob_versioned_hashes)) + else: + return Uint(0) + + +def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + excess_blob_gas : + The excess blob gas for the block. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + """ + return taylor_exponential( + MIN_BLOB_GASPRICE, + Uint(excess_blob_gas), + BLOB_GASPRICE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + excess_blob_gas : + The excess_blob_gas for the execution. + tx : + The transaction for which the blob data fee is to be calculated. + + Returns + ------- + data_fee: `Uint` + The blob data fee. + """ + return calculate_total_blob_gas(tx) * calculate_blob_gas_price( + excess_blob_gas + ) diff --git a/src/ethereum/prague/vm/instructions/__init__.py b/src/ethereum/prague/vm/instructions/__init__.py new file mode 100644 index 0000000000..b220581c72 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/__init__.py @@ -0,0 +1,366 @@ +""" +EVM Instruction Encoding (Opcodes) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +import enum +from typing import Callable, Dict + +from . import arithmetic as arithmetic_instructions +from . import bitwise as bitwise_instructions +from . import block as block_instructions +from . import comparison as comparison_instructions +from . import control_flow as control_flow_instructions +from . import environment as environment_instructions +from . import keccak as keccak_instructions +from . import log as log_instructions +from . import memory as memory_instructions +from . import stack as stack_instructions +from . import storage as storage_instructions +from . import system as system_instructions + + +class Ops(enum.Enum): + """ + Enum for EVM Opcodes + """ + + # Arithmetic Ops + ADD = 0x01 + MUL = 0x02 + SUB = 0x03 + DIV = 0x04 + SDIV = 0x05 + MOD = 0x06 + SMOD = 0x07 + ADDMOD = 0x08 + MULMOD = 0x09 + EXP = 0x0A + SIGNEXTEND = 0x0B + + # Comparison Ops + LT = 0x10 + GT = 0x11 + SLT = 0x12 + SGT = 0x13 + EQ = 0x14 + ISZERO = 0x15 + + # Bitwise Ops + AND = 0x16 + OR = 0x17 + XOR = 0x18 + NOT = 0x19 + BYTE = 0x1A + SHL = 0x1B + SHR = 0x1C + SAR = 0x1D + + # Keccak Op + KECCAK = 0x20 + + # Environmental Ops + ADDRESS = 0x30 + BALANCE = 0x31 + ORIGIN = 0x32 + CALLER = 0x33 + CALLVALUE = 0x34 + CALLDATALOAD = 0x35 + CALLDATASIZE = 0x36 + CALLDATACOPY = 0x37 + CODESIZE = 0x38 + CODECOPY = 0x39 + GASPRICE = 0x3A + EXTCODESIZE = 0x3B + EXTCODECOPY = 0x3C + RETURNDATASIZE = 0x3D + RETURNDATACOPY = 0x3E + EXTCODEHASH = 0x3F + + # Block Ops + BLOCKHASH = 0x40 + COINBASE = 0x41 + TIMESTAMP = 0x42 + NUMBER = 0x43 + PREVRANDAO = 0x44 + GASLIMIT = 0x45 + CHAINID = 0x46 + SELFBALANCE = 0x47 + BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A + + # Control Flow Ops + STOP = 0x00 + JUMP = 0x56 + JUMPI = 0x57 + PC = 0x58 + GAS = 0x5A + JUMPDEST = 0x5B + + # Storage Ops + SLOAD = 0x54 + SSTORE = 0x55 + TLOAD = 0x5C + TSTORE = 0x5D + + # Pop Operation + POP = 0x50 + + # Push Operations + PUSH0 = 0x5F + PUSH1 = 0x60 + PUSH2 = 0x61 + PUSH3 = 0x62 + PUSH4 = 0x63 + PUSH5 = 0x64 + PUSH6 = 0x65 + PUSH7 = 0x66 + PUSH8 = 0x67 + PUSH9 = 0x68 + PUSH10 = 0x69 + PUSH11 = 0x6A + PUSH12 = 0x6B + PUSH13 = 0x6C + PUSH14 = 0x6D + PUSH15 = 0x6E + PUSH16 = 0x6F + PUSH17 = 0x70 + PUSH18 = 0x71 + PUSH19 = 0x72 + PUSH20 = 0x73 + PUSH21 = 0x74 + PUSH22 = 0x75 + PUSH23 = 0x76 + PUSH24 = 0x77 + PUSH25 = 0x78 + PUSH26 = 0x79 + PUSH27 = 0x7A + PUSH28 = 0x7B + PUSH29 = 0x7C + PUSH30 = 0x7D + PUSH31 = 0x7E + PUSH32 = 0x7F + + # Dup operations + DUP1 = 0x80 + DUP2 = 0x81 + DUP3 = 0x82 + DUP4 = 0x83 + DUP5 = 0x84 + DUP6 = 0x85 + DUP7 = 0x86 + DUP8 = 0x87 + DUP9 = 0x88 + DUP10 = 0x89 + DUP11 = 0x8A + DUP12 = 0x8B + DUP13 = 0x8C + DUP14 = 0x8D + DUP15 = 0x8E + DUP16 = 0x8F + + # Swap operations + SWAP1 = 0x90 + SWAP2 = 0x91 + SWAP3 = 0x92 + SWAP4 = 0x93 + SWAP5 = 0x94 + SWAP6 = 0x95 + SWAP7 = 0x96 + SWAP8 = 0x97 + SWAP9 = 0x98 + SWAP10 = 0x99 + SWAP11 = 0x9A + SWAP12 = 0x9B + SWAP13 = 0x9C + SWAP14 = 0x9D + SWAP15 = 0x9E + SWAP16 = 0x9F + + # Memory Operations + MLOAD = 0x51 + MSTORE = 0x52 + MSTORE8 = 0x53 + MSIZE = 0x59 + MCOPY = 0x5E + + # Log Operations + LOG0 = 0xA0 + LOG1 = 0xA1 + LOG2 = 0xA2 + LOG3 = 0xA3 + LOG4 = 0xA4 + + # System Operations + CREATE = 0xF0 + CALL = 0xF1 + CALLCODE = 0xF2 + RETURN = 0xF3 + DELEGATECALL = 0xF4 + CREATE2 = 0xF5 + STATICCALL = 0xFA + REVERT = 0xFD + SELFDESTRUCT = 0xFF + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: control_flow_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.LT: comparison_instructions.less_than, + Ops.GT: comparison_instructions.greater_than, + Ops.SLT: comparison_instructions.signed_less_than, + Ops.SGT: comparison_instructions.signed_greater_than, + Ops.EQ: comparison_instructions.equal, + Ops.ISZERO: comparison_instructions.is_zero, + Ops.AND: bitwise_instructions.bitwise_and, + Ops.OR: bitwise_instructions.bitwise_or, + Ops.XOR: bitwise_instructions.bitwise_xor, + Ops.NOT: bitwise_instructions.bitwise_not, + Ops.BYTE: bitwise_instructions.get_byte, + Ops.SHL: bitwise_instructions.bitwise_shl, + Ops.SHR: bitwise_instructions.bitwise_shr, + Ops.SAR: bitwise_instructions.bitwise_sar, + Ops.KECCAK: keccak_instructions.keccak, + Ops.SLOAD: storage_instructions.sload, + Ops.BLOCKHASH: block_instructions.block_hash, + Ops.COINBASE: block_instructions.coinbase, + Ops.TIMESTAMP: block_instructions.timestamp, + Ops.NUMBER: block_instructions.number, + Ops.PREVRANDAO: block_instructions.prev_randao, + Ops.GASLIMIT: block_instructions.gas_limit, + Ops.CHAINID: block_instructions.chain_id, + Ops.MLOAD: memory_instructions.mload, + Ops.MSTORE: memory_instructions.mstore, + Ops.MSTORE8: memory_instructions.mstore8, + Ops.MSIZE: memory_instructions.msize, + Ops.MCOPY: memory_instructions.mcopy, + Ops.ADDRESS: environment_instructions.address, + Ops.BALANCE: environment_instructions.balance, + Ops.ORIGIN: environment_instructions.origin, + Ops.CALLER: environment_instructions.caller, + Ops.CALLVALUE: environment_instructions.callvalue, + Ops.CALLDATALOAD: environment_instructions.calldataload, + Ops.CALLDATASIZE: environment_instructions.calldatasize, + Ops.CALLDATACOPY: environment_instructions.calldatacopy, + Ops.CODESIZE: environment_instructions.codesize, + Ops.CODECOPY: environment_instructions.codecopy, + Ops.GASPRICE: environment_instructions.gasprice, + Ops.EXTCODESIZE: environment_instructions.extcodesize, + Ops.EXTCODECOPY: environment_instructions.extcodecopy, + Ops.RETURNDATASIZE: environment_instructions.returndatasize, + Ops.RETURNDATACOPY: environment_instructions.returndatacopy, + Ops.EXTCODEHASH: environment_instructions.extcodehash, + Ops.SELFBALANCE: environment_instructions.self_balance, + Ops.BASEFEE: environment_instructions.base_fee, + Ops.BLOBHASH: environment_instructions.blob_hash, + Ops.BLOBBASEFEE: environment_instructions.blob_base_fee, + Ops.SSTORE: storage_instructions.sstore, + Ops.TLOAD: storage_instructions.tload, + Ops.TSTORE: storage_instructions.tstore, + Ops.JUMP: control_flow_instructions.jump, + Ops.JUMPI: control_flow_instructions.jumpi, + Ops.PC: control_flow_instructions.pc, + Ops.GAS: control_flow_instructions.gas_left, + Ops.JUMPDEST: control_flow_instructions.jumpdest, + Ops.POP: stack_instructions.pop, + Ops.PUSH0: stack_instructions.push0, + Ops.PUSH1: stack_instructions.push1, + Ops.PUSH2: stack_instructions.push2, + Ops.PUSH3: stack_instructions.push3, + Ops.PUSH4: stack_instructions.push4, + Ops.PUSH5: stack_instructions.push5, + Ops.PUSH6: stack_instructions.push6, + Ops.PUSH7: stack_instructions.push7, + Ops.PUSH8: stack_instructions.push8, + Ops.PUSH9: stack_instructions.push9, + Ops.PUSH10: stack_instructions.push10, + Ops.PUSH11: stack_instructions.push11, + Ops.PUSH12: stack_instructions.push12, + Ops.PUSH13: stack_instructions.push13, + Ops.PUSH14: stack_instructions.push14, + Ops.PUSH15: stack_instructions.push15, + Ops.PUSH16: stack_instructions.push16, + Ops.PUSH17: stack_instructions.push17, + Ops.PUSH18: stack_instructions.push18, + Ops.PUSH19: stack_instructions.push19, + Ops.PUSH20: stack_instructions.push20, + Ops.PUSH21: stack_instructions.push21, + Ops.PUSH22: stack_instructions.push22, + Ops.PUSH23: stack_instructions.push23, + Ops.PUSH24: stack_instructions.push24, + Ops.PUSH25: stack_instructions.push25, + Ops.PUSH26: stack_instructions.push26, + Ops.PUSH27: stack_instructions.push27, + Ops.PUSH28: stack_instructions.push28, + Ops.PUSH29: stack_instructions.push29, + Ops.PUSH30: stack_instructions.push30, + Ops.PUSH31: stack_instructions.push31, + Ops.PUSH32: stack_instructions.push32, + Ops.DUP1: stack_instructions.dup1, + Ops.DUP2: stack_instructions.dup2, + Ops.DUP3: stack_instructions.dup3, + Ops.DUP4: stack_instructions.dup4, + Ops.DUP5: stack_instructions.dup5, + Ops.DUP6: stack_instructions.dup6, + Ops.DUP7: stack_instructions.dup7, + Ops.DUP8: stack_instructions.dup8, + Ops.DUP9: stack_instructions.dup9, + Ops.DUP10: stack_instructions.dup10, + Ops.DUP11: stack_instructions.dup11, + Ops.DUP12: stack_instructions.dup12, + Ops.DUP13: stack_instructions.dup13, + Ops.DUP14: stack_instructions.dup14, + Ops.DUP15: stack_instructions.dup15, + Ops.DUP16: stack_instructions.dup16, + Ops.SWAP1: stack_instructions.swap1, + Ops.SWAP2: stack_instructions.swap2, + Ops.SWAP3: stack_instructions.swap3, + Ops.SWAP4: stack_instructions.swap4, + Ops.SWAP5: stack_instructions.swap5, + Ops.SWAP6: stack_instructions.swap6, + Ops.SWAP7: stack_instructions.swap7, + Ops.SWAP8: stack_instructions.swap8, + Ops.SWAP9: stack_instructions.swap9, + Ops.SWAP10: stack_instructions.swap10, + Ops.SWAP11: stack_instructions.swap11, + Ops.SWAP12: stack_instructions.swap12, + Ops.SWAP13: stack_instructions.swap13, + Ops.SWAP14: stack_instructions.swap14, + Ops.SWAP15: stack_instructions.swap15, + Ops.SWAP16: stack_instructions.swap16, + Ops.LOG0: log_instructions.log0, + Ops.LOG1: log_instructions.log1, + Ops.LOG2: log_instructions.log2, + Ops.LOG3: log_instructions.log3, + Ops.LOG4: log_instructions.log4, + Ops.CREATE: system_instructions.create, + Ops.RETURN: system_instructions.return_, + Ops.CALL: system_instructions.call, + Ops.CALLCODE: system_instructions.callcode, + Ops.DELEGATECALL: system_instructions.delegatecall, + Ops.SELFDESTRUCT: system_instructions.selfdestruct, + Ops.STATICCALL: system_instructions.staticcall, + Ops.REVERT: system_instructions.revert, + Ops.CREATE2: system_instructions.create2, +} diff --git a/src/ethereum/prague/vm/instructions/arithmetic.py b/src/ethereum/prague/vm/instructions/arithmetic.py new file mode 100644 index 0000000000..0b8df99543 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/arithmetic.py @@ -0,0 +1,373 @@ +""" +Ethereum Virtual Machine (EVM) Arithmetic Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import get_sign + +from .. import Evm +from ..gas import ( + GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, + GAS_LOW, + GAS_MID, + GAS_VERY_LOW, + charge_gas, +) +from ..stack import pop, push + + +def add(evm: Evm) -> None: + """ + Adds the top two elements of the stack together, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = x.wrapping_add(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sub(evm: Evm) -> None: + """ + Subtracts the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = x.wrapping_sub(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mul(evm: Evm) -> None: + """ + Multiply the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + result = x.wrapping_mul(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def div(evm: Evm) -> None: + """ + Integer division of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack) + divisor = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +U255_CEIL_VALUE = 2**255 + + +def sdiv(evm: Evm) -> None: + """ + Signed integer division of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if divisor == 0: + quotient = 0 + elif dividend == -U255_CEIL_VALUE and divisor == -1: + quotient = -U255_CEIL_VALUE + else: + sign = get_sign(dividend * divisor) + quotient = sign * (abs(dividend) // abs(divisor)) + + push(evm.stack, U256.from_signed(quotient)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mod(evm: Evm) -> None: + """ + Modulo remainder of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def smod(evm: Evm) -> None: + """ + Signed modulo remainder of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def addmod(evm: Evm) -> None: + """ + Modulo addition of the top 2 elements with the 3rd element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x + y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mulmod(evm: Evm) -> None: + """ + Modulo multiplication of the top 2 elements with the 3rd element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x * y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def exp(evm: Evm) -> None: + """ + Exponential operation of the top 2 elements. Pushes the result back on + the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + base = Uint(pop(evm.stack)) + exponent = Uint(pop(evm.stack)) + + # GAS + # This is equivalent to 1 + floor(log(y, 256)). But in python the log + # function is inaccurate leading to wrong results. + exponent_bits = exponent.bit_length() + exponent_bytes = (exponent_bits + Uint(7)) // Uint(8) + charge_gas( + evm, GAS_EXPONENTIATION + GAS_EXPONENTIATION_PER_BYTE * exponent_bytes + ) + + # OPERATION + result = U256(pow(base, exponent, Uint(U256.MAX_VALUE) + Uint(1))) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signextend(evm: Evm) -> None: + """ + Sign extend operation. In other words, extend a signed number which + fits in N bytes to 32 bytes. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_num = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if byte_num > U256(31): + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead b'\x00'. + value_bytes = bytes(value.to_be_bytes32()) + # Now among the obtained value bytes, consider only + # N `least significant bytes`, where N is `byte_num + 1`. + value_bytes = value_bytes[31 - int(byte_num) :] + sign_bit = value_bytes[0] >> 7 + if sign_bit == 0: + result = U256.from_be_bytes(value_bytes) + else: + num_bytes_prepend = U256(32) - (byte_num + U256(1)) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/bitwise.py b/src/ethereum/prague/vm/instructions/bitwise.py new file mode 100644 index 0000000000..3abb58be48 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/bitwise.py @@ -0,0 +1,240 @@ +""" +Ethereum Virtual Machine (EVM) Bitwise Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM bitwise instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_VERY_LOW, charge_gas +from ..stack import pop, push + + +def bitwise_and(evm: Evm) -> None: + """ + Bitwise AND operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x & y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_or(evm: Evm) -> None: + """ + Bitwise OR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x | y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_xor(evm: Evm) -> None: + """ + Bitwise XOR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x ^ y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_not(evm: Evm) -> None: + """ + Bitwise NOT operation of the top element of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, ~x) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def get_byte(evm: Evm) -> None: + """ + For a word (defined by next top element of the stack), retrieve the + Nth byte (0-indexed and defined by top element of stack) from the + left (most significant) to right (least significant). + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_index = pop(evm.stack) + word = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if byte_index >= U256(32): + result = U256(0) + else: + extra_bytes_to_right = U256(31) - byte_index + # Remove the extra bytes in the right + word = word >> (extra_bytes_to_right * U256(8)) + # Remove the extra bytes in the left + word = word & U256(0xFF) + result = word + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shl(evm: Evm) -> None: + """ + Logical shift left (SHL) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + shift = Uint(pop(evm.stack)) + value = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < Uint(256): + result = U256((value << shift) & Uint(U256.MAX_VALUE)) + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shr(evm: Evm) -> None: + """ + Logical shift right (SHR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + shift = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < U256(256): + result = value >> shift + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_sar(evm: Evm) -> None: + """ + Arithmetic shift right (SAR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + shift = int(pop(evm.stack)) + signed_value = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < 256: + result = U256.from_signed(signed_value >> shift) + elif signed_value >= 0: + result = U256(0) + else: + result = U256.MAX_VALUE + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/block.py b/src/ethereum/prague/vm/instructions/block.py new file mode 100644 index 0000000000..b4f7b556d7 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/block.py @@ -0,0 +1,249 @@ +""" +Ethereum Virtual Machine (EVM) Block Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM block instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_BASE, GAS_BLOCK_HASH, charge_gas +from ..stack import pop, push + + +def block_hash(evm: Evm) -> None: + """ + Push the hash of one of the 256 most recent complete blocks onto the + stack. The block number to hash is present at the top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + """ + # STACK + block_number = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_BLOCK_HASH) + + # OPERATION + max_block_number = block_number + Uint(256) + if evm.env.number <= block_number or evm.env.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)] + + push(evm.stack, U256.from_be_bytes(hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def coinbase(evm: Evm) -> None: + """ + Push the current block's beneficiary address (address of the block miner) + onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def timestamp(evm: Evm) -> None: + """ + Push the current block's timestamp onto the stack. Here the timestamp + being referred is actually the unix timestamp in seconds. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, evm.env.time) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def number(evm: Evm) -> None: + """ + Push the current block's number onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def prev_randao(evm: Evm) -> None: + """ + Push the `prev_randao` value onto the stack. + + The `prev_randao` value is the random output of the beacon chain's + randomness oracle for the previous block. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_limit(evm: Evm) -> None: + """ + Push the current block's gas limit onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.gas_limit)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def chain_id(evm: Evm) -> None: + """ + Push the chain id onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.prague.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.prague.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.chain_id)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/comparison.py b/src/ethereum/prague/vm/instructions/comparison.py new file mode 100644 index 0000000000..275455ba53 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/comparison.py @@ -0,0 +1,178 @@ +""" +Ethereum Virtual Machine (EVM) Comparison Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Comparison instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_VERY_LOW, charge_gas +from ..stack import pop, push + + +def less_than(evm: Evm) -> None: + """ + Checks if the top element is less than the next top element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_less_than(evm: Evm) -> None: + """ + Signed less-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def greater_than(evm: Evm) -> None: + """ + Checks if the top element is greater than the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_greater_than(evm: Evm) -> None: + """ + Signed greater-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def equal(evm: Evm) -> None: + """ + Checks if the top element is equal to the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left == right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def is_zero(evm: Evm) -> None: + """ + Checks if the top element is equal to 0. Pushes the result back on the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(x == 0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/control_flow.py b/src/ethereum/prague/vm/instructions/control_flow.py new file mode 100644 index 0000000000..7722661f79 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/control_flow.py @@ -0,0 +1,171 @@ +""" +Ethereum Virtual Machine (EVM) Control Flow Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM control flow instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ...vm.gas import GAS_BASE, GAS_HIGH, GAS_JUMPDEST, GAS_MID, charge_gas +from .. import Evm +from ..exceptions import InvalidJumpDestError +from ..stack import pop, push + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + pass + + # GAS + pass + + # OPERATION + evm.running = False + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jump(evm: Evm) -> None: + """ + Alter the program counter to the location specified by the top of the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + # PROGRAM COUNTER + evm.pc = Uint(jump_dest) + + +def jumpi(evm: Evm) -> None: + """ + Alter the program counter to the specified location if and only if a + condition is true. If the condition is not true, then the program counter + would increase only by 1. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + conditional_value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_HIGH) + + # OPERATION + if conditional_value == 0: + destination = evm.pc + Uint(1) + elif jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + else: + destination = jump_dest + + # PROGRAM COUNTER + evm.pc = destination + + +def pc(evm: Evm) -> None: + """ + Push onto the stack the value of the program counter after reaching the + current instruction and without increasing it for the next instruction. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.pc)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_left(evm: Evm) -> None: + """ + Push the amount of available gas (including the corresponding reduction + for the cost of this instruction) onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.gas_left)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jumpdest(evm: Evm) -> None: + """ + Mark a valid destination for jumps. This is a noop, present only + to be used by `JUMP` and `JUMPI` opcodes to verify that their jump is + valid. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_JUMPDEST) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/environment.py b/src/ethereum/prague/vm/instructions/environment.py new file mode 100644 index 0000000000..4ba384f9ba --- /dev/null +++ b/src/ethereum/prague/vm/instructions/environment.py @@ -0,0 +1,587 @@ +""" +Ethereum Virtual Machine (EVM) Environmental Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM environment related instructions. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from ...fork_types import EMPTY_ACCOUNT +from ...state import get_account +from ...utils.address import to_address +from ...vm.memory import buffer_read, memory_write +from .. import Evm +from ..exceptions import OutOfBoundsRead +from ..gas import ( + GAS_BASE, + GAS_BLOBHASH_OPCODE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_COPY, + GAS_FAST_STEP, + GAS_RETURN_DATA_COPY, + GAS_VERY_LOW, + GAS_WARM_ACCESS, + calculate_blob_gas_price, + calculate_gas_extend_memory, + charge_gas, +) +from ..stack import pop, push + + +def address(evm: Evm) -> None: + """ + Pushes the address of the current executing account to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.env.state, address).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def origin(evm: Evm) -> None: + """ + Pushes the address of the original transaction sender to the stack. + The origin address can only be an EOA. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.origin)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def caller(evm: Evm) -> None: + """ + Pushes the address of the caller onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.caller)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callvalue(evm: Evm) -> None: + """ + Push the value (in wei) sent with the call onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, evm.message.value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldataload(evm: Evm) -> None: + """ + Push a word (32 bytes) of the input data belonging to the current + environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_index = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + value = buffer_read(evm.message.data, start_index, U256(32)) + + push(evm.stack, U256.from_be_bytes(value)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatasize(evm: Evm) -> None: + """ + Push the size of input data in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.message.data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatacopy(evm: Evm) -> None: + """ + Copy a portion of the input data in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + data_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.message.data, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codesize(evm: Evm) -> None: + """ + Push the size of code running in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.code))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codecopy(evm: Evm) -> None: + """ + Copy a portion of the code in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.gas_price)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodesize(evm: Evm) -> None: + """ + Push the code size of a given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. + codesize = U256(len(get_account(evm.env.state, address).code)) + + push(evm.stack, codesize) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodecopy(evm: Evm) -> None: + """ + Copy a portion of an account's code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address(pop(evm.stack)) + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + else: + evm.accessed_addresses.add(address) + charge_gas( + evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatasize(evm: Evm) -> None: + """ + Pushes the size of the return data buffer onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.return_data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatacopy(evm: Evm) -> None: + """ + Copies data from the return data buffer code to memory + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + memory_start_index = pop(evm.stack) + return_data_start_position = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_RETURN_DATA_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + if Uint(return_data_start_position) + Uint(size) > ulen(evm.return_data): + raise OutOfBoundsRead + + evm.memory += b"\x00" * extend_memory.expand_by + value = evm.return_data[ + return_data_start_position : return_data_start_position + size + ] + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodehash(evm: Evm) -> None: + """ + Returns the keccak256 hash of a contract’s bytecode + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + account = get_account(evm.env.state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + codehash = U256.from_be_bytes(keccak256(account.code)) + + push(evm.stack, codehash) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def self_balance(evm: Evm) -> None: + """ + Pushes the balance of the current address to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.env.state, evm.message.current_target).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def base_fee(evm: Evm) -> None: + """ + Pushes the base fee of the current block on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.base_fee_per_gas)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_hash(evm: Evm) -> None: + """ + Pushes the versioned hash at a particular index on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + index = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BLOBHASH_OPCODE) + + # OPERATION + if int(index) < len(evm.env.blob_versioned_hashes): + blob_hash = evm.env.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_base_fee(evm: Evm) -> None: + """ + Pushes the blob base fee on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + blob_base_fee = calculate_blob_gas_price(evm.env.excess_blob_gas) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/keccak.py b/src/ethereum/prague/vm/instructions/keccak.py new file mode 100644 index 0000000000..830d368277 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/keccak.py @@ -0,0 +1,64 @@ +""" +Ethereum Virtual Machine (EVM) Keccak Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM keccak instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GAS_KECCAK256, + GAS_KECCAK256_WORD, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop, push + + +def keccak(evm: Evm) -> None: + """ + Pushes to the stack the Keccak-256 hash of a region of memory. + + This also expands the memory, in case the memory is insufficient to + access the data's memory location. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + word_gas_cost = GAS_KECCAK256_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_KECCAK256 + word_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + data = memory_read_bytes(evm.memory, memory_start_index, size) + hash = keccak256(data) + + push(evm.stack, U256.from_be_bytes(hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/log.py b/src/ethereum/prague/vm/instructions/log.py new file mode 100644 index 0000000000..87c06ed6be --- /dev/null +++ b/src/ethereum/prague/vm/instructions/log.py @@ -0,0 +1,88 @@ +""" +Ethereum Virtual Machine (EVM) Logging Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM logging instructions. +""" +from functools import partial + +from ethereum_types.numeric import Uint + +from ...blocks import Log +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GAS_LOG, + GAS_LOG_DATA, + GAS_LOG_TOPIC, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop + + +def log_n(evm: Evm, num_topics: int) -> None: + """ + Appends a log entry, having `num_topics` topics, to the evm logs. + + This will also expand the memory if the data (required by the log entry) + corresponding to the memory is not accessible. + + Parameters + ---------- + evm : + The current EVM frame. + num_topics : + The number of topics to be included in the log entry. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + topics = [] + for _ in range(num_topics): + topic = pop(evm.stack).to_be_bytes32() + topics.append(topic) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GAS_LOG + + GAS_LOG_DATA * Uint(size) + + GAS_LOG_TOPIC * Uint(num_topics) + + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + if evm.message.is_static: + raise WriteInStaticContext + log_entry = Log( + address=evm.message.current_target, + topics=tuple(topics), + data=memory_read_bytes(evm.memory, memory_start_index, size), + ) + + evm.logs = evm.logs + (log_entry,) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +log0 = partial(log_n, num_topics=0) +log1 = partial(log_n, num_topics=1) +log2 = partial(log_n, num_topics=2) +log3 = partial(log_n, num_topics=3) +log4 = partial(log_n, num_topics=4) diff --git a/src/ethereum/prague/vm/instructions/memory.py b/src/ethereum/prague/vm/instructions/memory.py new file mode 100644 index 0000000000..89533af37e --- /dev/null +++ b/src/ethereum/prague/vm/instructions/memory.py @@ -0,0 +1,177 @@ +""" +Ethereum Virtual Machine (EVM) Memory Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Memory instructions. +""" +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GAS_BASE, + GAS_COPY, + GAS_VERY_LOW, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def mstore(evm: Evm) -> None: + """ + Stores a word to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(len(value)))] + ) + + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + memory_write(evm.memory, start_position, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mstore8(evm: Evm) -> None: + """ + Stores a byte to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(1))] + ) + + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + normalized_bytes_value = Bytes([value & U256(0xFF)]) + memory_write(evm.memory, start_position, normalized_bytes_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mload(evm: Evm) -> None: + """ + Load word from memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(32))] + ) + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = U256.from_be_bytes( + memory_read_bytes(evm.memory, start_position, U256(32)) + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def msize(evm: Evm) -> None: + """ + Push the size of active memory in bytes onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.memory))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mcopy(evm: Evm) -> None: + """ + Copy the bytes in memory from one location to another. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + destination = pop(evm.stack) + source = pop(evm.stack) + length = pop(evm.stack) + + # GAS + words = ceil32(Uint(length)) // Uint(32) + copy_gas_cost = GAS_COPY * words + + extend_memory = calculate_gas_extend_memory( + evm.memory, [(source, length), (destination, length)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = memory_read_bytes(evm.memory, source, length) + memory_write(evm.memory, destination, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/stack.py b/src/ethereum/prague/vm/instructions/stack.py new file mode 100644 index 0000000000..2e8a492412 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/stack.py @@ -0,0 +1,209 @@ +""" +Ethereum Virtual Machine (EVM) Stack Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM stack related instructions. +""" + +from functools import partial + +from ethereum_types.numeric import U256, Uint + +from .. import Evm, stack +from ..exceptions import StackUnderflowError +from ..gas import GAS_BASE, GAS_VERY_LOW, charge_gas +from ..memory import buffer_read + + +def pop(evm: Evm) -> None: + """ + Remove item from stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + stack.pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes a N-byte immediate onto the stack. Push zero if num_bytes is zero. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. Push zero if num_bytes is zero. + + """ + # STACK + pass + + # GAS + if num_bytes == 0: + charge_gas(evm, GAS_BASE) + else: + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + data_to_push = U256.from_be_bytes( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(num_bytes)) + ) + stack.push(evm.stack, data_to_push) + + # PROGRAM COUNTER + evm.pc += Uint(1) + Uint(num_bytes) + + +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_VERY_LOW) + if item_number >= len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swap the top and the `item_number` element of the stack, where + the top of the stack is position zero. + + If `item_number` is zero, this function does nothing (which should not be + possible, since there is no `SWAP0` instruction). + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_VERY_LOW) + if item_number >= len(evm.stack): + raise StackUnderflowError + evm.stack[-1], evm.stack[-1 - item_number] = ( + evm.stack[-1 - item_number], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +push0 = partial(push_n, num_bytes=0) +push1 = partial(push_n, num_bytes=1) +push2 = partial(push_n, num_bytes=2) +push3 = partial(push_n, num_bytes=3) +push4 = partial(push_n, num_bytes=4) +push5 = partial(push_n, num_bytes=5) +push6 = partial(push_n, num_bytes=6) +push7 = partial(push_n, num_bytes=7) +push8 = partial(push_n, num_bytes=8) +push9 = partial(push_n, num_bytes=9) +push10 = partial(push_n, num_bytes=10) +push11 = partial(push_n, num_bytes=11) +push12 = partial(push_n, num_bytes=12) +push13 = partial(push_n, num_bytes=13) +push14 = partial(push_n, num_bytes=14) +push15 = partial(push_n, num_bytes=15) +push16 = partial(push_n, num_bytes=16) +push17 = partial(push_n, num_bytes=17) +push18 = partial(push_n, num_bytes=18) +push19 = partial(push_n, num_bytes=19) +push20 = partial(push_n, num_bytes=20) +push21 = partial(push_n, num_bytes=21) +push22 = partial(push_n, num_bytes=22) +push23 = partial(push_n, num_bytes=23) +push24 = partial(push_n, num_bytes=24) +push25 = partial(push_n, num_bytes=25) +push26 = partial(push_n, num_bytes=26) +push27 = partial(push_n, num_bytes=27) +push28 = partial(push_n, num_bytes=28) +push29 = partial(push_n, num_bytes=29) +push30 = partial(push_n, num_bytes=30) +push31 = partial(push_n, num_bytes=31) +push32 = partial(push_n, num_bytes=32) + +dup1 = partial(dup_n, item_number=0) +dup2 = partial(dup_n, item_number=1) +dup3 = partial(dup_n, item_number=2) +dup4 = partial(dup_n, item_number=3) +dup5 = partial(dup_n, item_number=4) +dup6 = partial(dup_n, item_number=5) +dup7 = partial(dup_n, item_number=6) +dup8 = partial(dup_n, item_number=7) +dup9 = partial(dup_n, item_number=8) +dup10 = partial(dup_n, item_number=9) +dup11 = partial(dup_n, item_number=10) +dup12 = partial(dup_n, item_number=11) +dup13 = partial(dup_n, item_number=12) +dup14 = partial(dup_n, item_number=13) +dup15 = partial(dup_n, item_number=14) +dup16 = partial(dup_n, item_number=15) + +swap1 = partial(swap_n, item_number=1) +swap2 = partial(swap_n, item_number=2) +swap3 = partial(swap_n, item_number=3) +swap4 = partial(swap_n, item_number=4) +swap5 = partial(swap_n, item_number=5) +swap6 = partial(swap_n, item_number=6) +swap7 = partial(swap_n, item_number=7) +swap8 = partial(swap_n, item_number=8) +swap9 = partial(swap_n, item_number=9) +swap10 = partial(swap_n, item_number=10) +swap11 = partial(swap_n, item_number=11) +swap12 = partial(swap_n, item_number=12) +swap13 = partial(swap_n, item_number=13) +swap14 = partial(swap_n, item_number=14) +swap15 = partial(swap_n, item_number=15) +swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/prague/vm/instructions/storage.py b/src/ethereum/prague/vm/instructions/storage.py new file mode 100644 index 0000000000..f88e295736 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/storage.py @@ -0,0 +1,178 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" +from ethereum_types.numeric import Uint + +from ...state import ( + get_storage, + get_storage_original, + get_transient_storage, + set_storage, + set_transient_storage, +) +from .. import Evm +from ..exceptions import OutOfGasError, WriteInStaticContext +from ..gas import ( + GAS_CALL_STIPEND, + GAS_COLD_SLOAD, + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + GAS_WARM_ACCESS, + charge_gas, +) +from ..stack import pop, push + + +def sload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + if (evm.message.current_target, key) in evm.accessed_storage_keys: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + charge_gas(evm, GAS_COLD_SLOAD) + + # OPERATION + value = get_storage(evm.env.state, evm.message.current_target, key) + + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + if evm.gas_left <= GAS_CALL_STIPEND: + raise OutOfGasError + + original_value = get_storage_original( + evm.env.state, evm.message.current_target, key + ) + current_value = get_storage(evm.env.state, evm.message.current_target, key) + + gas_cost = Uint(0) + + if (evm.message.current_target, key) not in evm.accessed_storage_keys: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + gas_cost += GAS_COLD_SLOAD + + if original_value == current_value and current_value != new_value: + if original_value == 0: + gas_cost += GAS_STORAGE_SET + else: + gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD + else: + gas_cost += GAS_WARM_ACCESS + + # Refund Counter Calculation + if current_value != new_value: + if original_value != 0 and current_value != 0 and new_value == 0: + # Storage is cleared for the first time in the transaction + evm.refund_counter += int(GAS_STORAGE_CLEAR_REFUND) + + if original_value != 0 and current_value == 0: + # Gas refund issued earlier to be reversed + evm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND) + + if original_value == new_value: + # Storage slot being restored to its original value + if original_value == 0: + # Slot was originally empty and was SET earlier + evm.refund_counter += int(GAS_STORAGE_SET - GAS_WARM_ACCESS) + else: + # Slot was originally non-empty and was UPDATED earlier + evm.refund_counter += int( + GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS + ) + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + set_storage(evm.env.state, evm.message.current_target, key, new_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + transient storage of the current account. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + + # OPERATION + value = get_transient_storage( + evm.env.transient_storage, evm.message.current_target, key + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's transient storage. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + if evm.message.is_static: + raise WriteInStaticContext + set_transient_storage( + evm.env.transient_storage, evm.message.current_target, key, new_value + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/prague/vm/instructions/system.py b/src/ethereum/prague/vm/instructions/system.py new file mode 100644 index 0000000000..da47df38cf --- /dev/null +++ b/src/ethereum/prague/vm/instructions/system.py @@ -0,0 +1,691 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" +from ethereum_types.bytes import Bytes0 +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from ...fork_types import Address +from ...state import ( + account_exists_and_is_empty, + account_has_code_or_nonce, + account_has_storage, + get_account, + increment_nonce, + is_account_alive, + move_ether, + set_account_balance, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address, +) +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import OutOfGasError, Revert, WriteInStaticContext +from ..gas import ( + GAS_CALL_VALUE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_CREATE, + GAS_KECCAK256_WORD, + GAS_NEW_ACCOUNT, + GAS_SELF_DESTRUCT, + GAS_SELF_DESTRUCT_NEW_ACCOUNT, + GAS_WARM_ACCESS, + GAS_ZERO, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_gas, + init_code_cost, + max_message_call_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def generic_create( + evm: Evm, + endowment: U256, + contract_address: Address, + memory_start_position: U256, + memory_size: U256, + init_code_gas: Uint, +) -> None: + """ + Core logic used by the `CREATE*` family of opcodes. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import ( + MAX_CODE_SIZE, + STACK_DEPTH_LIMIT, + process_create_message, + ) + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + if len(call_data) > 2 * MAX_CODE_SIZE: + raise OutOfGasError + + evm.accessed_addresses.add(contract_address) + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + if evm.message.is_static: + raise WriteInStaticContext + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(evm.env.state, sender_address) + + if ( + sender.balance < endowment + or sender.nonce == Uint(2**64 - 1) + or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT + ): + evm.gas_left += create_message_gas + push(evm.stack, U256(0)) + 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) + push(evm.stack, U256(0)) + return + + increment_nonce(evm.env.state, evm.message.current_target) + + child_message = Message( + caller=evm.message.current_target, + target=Bytes0(), + gas=create_message_gas, + value=endowment, + data=b"", + code=call_data, + current_target=contract_address, + depth=evm.message.depth + Uint(1), + code_address=None, + should_transfer_value=True, + is_static=False, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, + ) + child_evm = process_create_message(child_message, evm.env) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = b"" + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + init_code_gas = init_code_cost(Uint(memory_size)) + + charge_gas(evm, GAS_CREATE + extend_memory.cost + init_code_gas) + + # OPERATION + 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, + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + init_code_gas, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def create2(evm: Evm) -> None: + """ + Creates a new account with associated code. + + It's similar to CREATE opcode except that the address of new account + depends on the init_code instead of the nonce of sender. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + salt = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + call_data_words = ceil32(Uint(memory_size)) // Uint(32) + init_code_gas = init_code_cost(Uint(memory_size)) + charge_gas( + evm, + GAS_CREATE + + GAS_KECCAK256_WORD * call_data_words + + extend_memory.cost + + init_code_gas, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_create2_contract_address( + evm.message.current_target, + salt, + memory_read_bytes(evm.memory, memory_start_position, memory_size), + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + init_code_gas, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + + charge_gas(evm, GAS_ZERO + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + +def generic_call( + evm: Evm, + gas: Uint, + value: U256, + caller: Address, + to: Address, + code_address: Address, + should_transfer_value: bool, + is_staticcall: bool, + memory_input_start_position: U256, + memory_input_size: U256, + memory_output_start_position: U256, + memory_output_size: U256, +) -> None: + """ + Perform the core logic of the `CALL*` family of opcodes. + """ + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_message + + evm.return_data = b"" + + if evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT: + evm.gas_left += gas + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + code = get_account(evm.env.state, code_address).code + child_message = Message( + caller=caller, + target=to, + gas=gas, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + Uint(1), + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=True if is_staticcall else evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, + ) + child_evm = process_message(child_message, evm.env) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(1)) + + actual_output_size = min(memory_output_size, U256(len(child_evm.output))) + memory_write( + evm.memory, + memory_output_start_position, + child_evm.output[:actual_output_size], + ) + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + create_gas_cost = ( + Uint(0) + if is_account_alive(evm.env.state, to) or value == 0 + else GAS_NEW_ACCOUNT + ) + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + create_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + if evm.message.is_static and value != U256(0): + raise WriteInStaticContext + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.stipend + else: + generic_call( + evm, + message_call_gas.stipend, + value, + evm.message.current_target, + to, + to, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account’s code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + to = evm.message.current_target + + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost, + ) + 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 + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.stipend + else: + generic_call( + evm, + message_call_gas.stipend, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + beneficiary = to_address(pop(evm.stack)) + + # GAS + gas_cost = GAS_SELF_DESTRUCT + if beneficiary not in evm.accessed_addresses: + evm.accessed_addresses.add(beneficiary) + 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 + ): + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + + originator = evm.message.current_target + originator_balance = get_account(evm.env.state, originator).balance + + move_ether( + evm.env.state, + originator, + beneficiary, + originator_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + if originator in evm.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)) + evm.accounts_to_delete.add(originator) + + # mark beneficiary as touched + if account_exists_and_is_empty(evm.env.state, beneficiary): + evm.touched_accounts.add(beneficiary) + + # HALT the execution + evm.running = False + + # PROGRAM COUNTER + pass + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + message_call_gas = calculate_message_call_gas( + U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.stipend, + evm.message.value, + evm.message.caller, + evm.message.current_target, + code_address, + False, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def staticcall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.stipend, + U256(0), + evm.message.current_target, + to, + to, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def revert(evm: Evm) -> None: + """ + Stop execution and revert state changes, without consuming all provided gas + and also has the ability to return a reason + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + charge_gas(evm, extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + output = memory_read_bytes(evm.memory, memory_start_index, size) + evm.output = bytes(output) + raise Revert + + # PROGRAM COUNTER + pass diff --git a/src/ethereum/prague/vm/interpreter.py b/src/ethereum/prague/vm/interpreter.py new file mode 100644 index 0000000000..4a7c25a779 --- /dev/null +++ b/src/ethereum/prague/vm/interpreter.py @@ -0,0 +1,314 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" +from dataclasses import dataclass +from typing import Iterable, Optional, Set, Tuple, Union + +from ethereum_types.bytes import Bytes0 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) + +from ..blocks import Log +from ..fork_types import Address +from ..state import ( + account_exists_and_is_empty, + account_has_code_or_nonce, + account_has_storage, + begin_transaction, + commit_transaction, + destroy_storage, + increment_nonce, + mark_account_created, + move_ether, + rollback_transaction, + set_code, + touch_account, +) +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 .exceptions import ( + AddressCollision, + ExceptionalHalt, + InvalidContractPrefix, + InvalidOpcode, + OutOfGasError, + Revert, + StackDepthLimitError, +) +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = Uint(1024) +MAX_CODE_SIZE = 0x6000 + + +@dataclass +class MessageCallOutput: + """ + Output of a particular message call + + Contains the following: + + 1. `gas_left`: remaining gas after execution. + 2. `refund_counter`: gas to refund after execution. + 3. `logs`: list of `Log` generated during execution. + 4. `accounts_to_delete`: Contracts which have self-destructed. + 5. `touched_accounts`: Accounts that have been touched. + 6. `error`: The error from the execution if any. + """ + + gas_left: Uint + refund_counter: U256 + logs: Union[Tuple[()], Tuple[Log, ...]] + accounts_to_delete: Set[Address] + touched_accounts: Iterable[Address] + error: Optional[Exception] + + +def process_message_call( + message: Message, env: Environment +) -> 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`. + + Parameters + ---------- + message : + Transaction specific items. + + env : + External items required for EVM execution. + + Returns + ------- + output : `MessageCallOutput` + Output of the message call + """ + 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) + if is_collision: + return MessageCallOutput( + Uint(0), U256(0), tuple(), set(), set(), AddressCollision() + ) + else: + evm = process_create_message(message, env) + else: + evm = process_message(message, env) + if account_exists_and_is_empty(env.state, Address(message.target)): + evm.touched_accounts.add(Address(message.target)) + + if evm.error: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + touched_accounts = set() + refund_counter = U256(0) + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + touched_accounts = evm.touched_accounts + refund_counter = U256(evm.refund_counter) + + tx_end = TransactionEnd( + int(message.gas) - int(evm.gas_left), evm.output, evm.error + ) + evm_trace(evm, tx_end) + + return MessageCallOutput( + gas_left=evm.gas_left, + refund_counter=refund_counter, + logs=logs, + accounts_to_delete=accounts_to_delete, + touched_accounts=touched_accounts, + error=evm.error, + ) + + +def process_create_message(message: Message, env: Environment) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: :py:class:`~ethereum.prague.vm.Evm` + Items containing execution specific objects. + """ + # take snapshot of state before processing the message + begin_transaction(env.state, env.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 + # circumstances: + # * The address created by a `CREATE` call collides with a subsequent + # `CREATE` or `CREATE2` call. + # * The first `CREATE` happened before Spurious Dragon and left empty + # code. + destroy_storage(env.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) + + increment_nonce(env.state, message.current_target) + evm = process_message(message, env) + if not evm.error: + contract_code = evm.output + contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT + try: + if len(contract_code) > 0: + if contract_code[0] == 0xEF: + raise InvalidContractPrefix + charge_gas(evm, contract_code_gas) + if len(contract_code) > MAX_CODE_SIZE: + raise OutOfGasError + except ExceptionalHalt as error: + rollback_transaction(env.state, env.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) + else: + rollback_transaction(env.state, env.transient_storage) + return evm + + +def process_message(message: Message, env: Environment) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: :py:class:`~ethereum.prague.vm.Evm` + Items containing execution specific objects + """ + 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) + + touch_account(env.state, message.current_target) + + if message.should_transfer_value and message.value != 0: + move_ether( + env.state, message.caller, message.current_target, message.value + ) + + evm = execute_code(message, env) + 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) + else: + commit_transaction(env.state, env.transient_storage) + return evm + + +def execute_code(message: Message, env: Environment) -> Evm: + """ + Executes bytecode present in the `message`. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: `ethereum.vm.EVM` + Items containing execution specific objects + """ + code = message.code + valid_jump_destinations = get_valid_jump_destinations(code) + + evm = Evm( + pc=Uint(0), + stack=[], + memory=bytearray(), + code=code, + gas_left=message.gas, + env=env, + valid_jump_destinations=valid_jump_destinations, + logs=(), + refund_counter=0, + running=True, + message=message, + output=b"", + accounts_to_delete=set(), + touched_accounts=set(), + return_data=b"", + error=None, + accessed_addresses=message.accessed_addresses, + accessed_storage_keys=message.accessed_storage_keys, + ) + try: + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + return evm + + while evm.running and evm.pc < ulen(evm.code): + try: + op = Ops(evm.code[evm.pc]) + except ValueError: + raise InvalidOpcode(evm.code[evm.pc]) + + evm_trace(evm, OpStart(op)) + op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) + + except ExceptionalHalt as error: + evm_trace(evm, OpException(error)) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + except Revert as error: + evm_trace(evm, OpException(error)) + evm.error = error + return evm diff --git a/src/ethereum/prague/vm/memory.py b/src/ethereum/prague/vm/memory.py new file mode 100644 index 0000000000..aa2e7fdd57 --- /dev/null +++ b/src/ethereum/prague/vm/memory.py @@ -0,0 +1,81 @@ +""" +Ethereum Virtual Machine (EVM) Memory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.byte import right_pad_zero_bytes + + +def memory_write( + memory: bytearray, start_position: U256, value: Bytes +) -> None: + """ + Writes to memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + value : + Data to write to memory. + """ + memory[start_position : int(start_position) + len(value)] = value + + +def memory_read_bytes( + memory: bytearray, start_position: U256, size: U256 +) -> bytearray: + """ + Read bytes from memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + """ + return memory[start_position : Uint(start_position) + Uint(size)] + + +def buffer_read(buffer: Bytes, start_position: U256, size: U256) -> Bytes: + """ + Read bytes from a buffer. Padding with zeros if necessary. + + Parameters + ---------- + buffer : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + """ + return right_pad_zero_bytes( + buffer[start_position : Uint(start_position) + Uint(size)], size + ) diff --git a/src/ethereum/prague/vm/precompiled_contracts/__init__.py b/src/ethereum/prague/vm/precompiled_contracts/__init__.py new file mode 100644 index 0000000000..7ec48ca69b --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/__init__.py @@ -0,0 +1,40 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" + +from ...utils.hexadecimal import hex_to_address + +__all__ = ( + "ECRECOVER_ADDRESS", + "SHA256_ADDRESS", + "RIPEMD160_ADDRESS", + "IDENTITY_ADDRESS", + "MODEXP_ADDRESS", + "ALT_BN128_ADD_ADDRESS", + "ALT_BN128_MUL_ADDRESS", + "ALT_BN128_PAIRING_CHECK_ADDRESS", + "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", +) + +ECRECOVER_ADDRESS = hex_to_address("0x01") +SHA256_ADDRESS = hex_to_address("0x02") +RIPEMD160_ADDRESS = hex_to_address("0x03") +IDENTITY_ADDRESS = hex_to_address("0x04") +MODEXP_ADDRESS = hex_to_address("0x05") +ALT_BN128_ADD_ADDRESS = hex_to_address("0x06") +ALT_BN128_MUL_ADDRESS = hex_to_address("0x07") +ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") +BLAKE2F_ADDRESS = hex_to_address("0x09") +POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") diff --git a/src/ethereum/prague/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/prague/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 0000000000..dc75b40ac6 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,154 @@ +""" +Ethereum Virtual Machine (EVM) ALT_BN128 CONTRACTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ALT_BN128 precompiled contracts. +""" +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.alt_bn128 import ( + ALT_BN128_CURVE_ORDER, + ALT_BN128_PRIME, + BNF, + BNF2, + BNF12, + BNP, + BNP2, + pairing, +) + +from ...vm import Evm +from ...vm.gas import charge_gas +from ...vm.memory import buffer_read +from ..exceptions import OutOfGasError + + +def alt_bn128_add(evm: Evm) -> None: + """ + The ALT_BN128 addition precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(150)) + + # OPERATION + x0_bytes = buffer_read(data, U256(0), U256(32)) + x0_value = int(U256.from_be_bytes(x0_bytes)) + y0_bytes = buffer_read(data, U256(32), U256(32)) + y0_value = int(U256.from_be_bytes(y0_bytes)) + x1_bytes = buffer_read(data, U256(64), U256(32)) + x1_value = int(U256.from_be_bytes(x1_bytes)) + y1_bytes = buffer_read(data, U256(96), U256(32)) + y1_value = int(U256.from_be_bytes(y1_bytes)) + + for i in (x0_value, y0_value, x1_value, y1_value): + if i >= ALT_BN128_PRIME: + raise OutOfGasError + + try: + p0 = BNP(BNF(x0_value), BNF(y0_value)) + p1 = BNP(BNF(x1_value), BNF(y1_value)) + except ValueError: + raise OutOfGasError + + p = p0 + p1 + + evm.output = p.x.to_be_bytes32() + p.y.to_be_bytes32() + + +def alt_bn128_mul(evm: Evm) -> None: + """ + The ALT_BN128 multiplication precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(6000)) + + # OPERATION + x0_bytes = buffer_read(data, U256(0), U256(32)) + x0_value = int(U256.from_be_bytes(x0_bytes)) + y0_bytes = buffer_read(data, U256(32), U256(32)) + y0_value = int(U256.from_be_bytes(y0_bytes)) + n = int(U256.from_be_bytes(buffer_read(data, U256(64), U256(32)))) + + for i in (x0_value, y0_value): + if i >= ALT_BN128_PRIME: + raise OutOfGasError + + try: + p0 = BNP(BNF(x0_value), BNF(y0_value)) + except ValueError: + raise OutOfGasError + + p = p0.mul_by(n) + + evm.output = p.x.to_be_bytes32() + p.y.to_be_bytes32() + + +def alt_bn128_pairing_check(evm: Evm) -> None: + """ + The ALT_BN128 pairing check precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(34000 * (len(data) // 192) + 45000)) + + # OPERATION + if len(data) % 192 != 0: + raise OutOfGasError + result = BNF12.from_int(1) + for i in range(len(data) // 192): + values = [] + for j in range(6): + value = int( + U256.from_be_bytes( + data[i * 192 + 32 * j : i * 192 + 32 * (j + 1)] + ) + ) + if value >= ALT_BN128_PRIME: + raise OutOfGasError + values.append(value) + + try: + p = BNP(BNF(values[0]), BNF(values[1])) + q = BNP2( + BNF2((values[3], values[2])), BNF2((values[5], values[4])) + ) + except ValueError: + raise OutOfGasError() + if p.mul_by(ALT_BN128_CURVE_ORDER) != BNP.point_at_infinity(): + raise OutOfGasError + if q.mul_by(ALT_BN128_CURVE_ORDER) != BNP2.point_at_infinity(): + raise OutOfGasError + if p != BNP.point_at_infinity() and q != BNP2.point_at_infinity(): + result = result * pairing(q, p) + + if result == BNF12.from_int(1): + evm.output = U256(1).to_be_bytes32() + else: + evm.output = U256(0).to_be_bytes32() diff --git a/src/ethereum/prague/vm/precompiled_contracts/blake2f.py b/src/ethereum/prague/vm/precompiled_contracts/blake2f.py new file mode 100644 index 0000000000..0d86ba6e85 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,41 @@ +""" +Ethereum Virtual Machine (EVM) Blake2 PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `Blake2` precompiled contract. +""" +from ethereum.crypto.blake2 import Blake2b + +from ...vm import Evm +from ...vm.gas import GAS_BLAKE2_PER_ROUND, charge_gas +from ..exceptions import InvalidParameter + + +def blake2f(evm: Evm) -> None: + """ + Writes the Blake2 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + if len(data) != 213: + raise InvalidParameter + + blake2b = Blake2b() + rounds, h, m, t_0, t_1, f = blake2b.get_blake2_parameters(data) + + charge_gas(evm, GAS_BLAKE2_PER_ROUND * rounds) + if f not in [0, 1]: + raise InvalidParameter + + evm.output = blake2b.compress(rounds, h, m, t_0, t_1, f) diff --git a/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..293e977575 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,62 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ECRECOVER precompiled contract. +""" +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GAS_ECRECOVER, charge_gas +from ...vm.memory import buffer_read + + +def ecrecover(evm: Evm) -> None: + """ + Decrypts the address using elliptic curve DSA recovery mechanism and writes + the address to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + charge_gas(evm, GAS_ECRECOVER) + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + v = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + r = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(96), U256(32))) + + if v != U256(27) and v != U256(28): + return + if U256(0) >= r or r >= SECP256K1N: + return + if U256(0) >= s or s >= SECP256K1N: + return + + try: + public_key = secp256k1_recover(r, s, v - U256(27), message_hash) + except ValueError: + # unable to extract public key + return + + address = keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/prague/vm/precompiled_contracts/identity.py b/src/ethereum/prague/vm/precompiled_contracts/identity.py new file mode 100644 index 0000000000..88729c96d7 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/identity.py @@ -0,0 +1,38 @@ +""" +Ethereum Virtual Machine (EVM) IDENTITY PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `IDENTITY` precompiled contract. +""" +from ethereum_types.numeric import Uint + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_IDENTITY, GAS_IDENTITY_WORD, charge_gas + + +def identity(evm: Evm) -> None: + """ + Writes the message data to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_IDENTITY + GAS_IDENTITY_WORD * word_count) + + # OPERATION + evm.output = data diff --git a/src/ethereum/prague/vm/precompiled_contracts/mapping.py b/src/ethereum/prague/vm/precompiled_contracts/mapping.py new file mode 100644 index 0000000000..7bd3416b6b --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/mapping.py @@ -0,0 +1,49 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts their implementations. +""" +from typing import Callable, Dict + +from ...fork_types import Address +from . import ( + ALT_BN128_ADD_ADDRESS, + ALT_BN128_MUL_ADDRESS, + ALT_BN128_PAIRING_CHECK_ADDRESS, + BLAKE2F_ADDRESS, + ECRECOVER_ADDRESS, + IDENTITY_ADDRESS, + MODEXP_ADDRESS, + POINT_EVALUATION_ADDRESS, + RIPEMD160_ADDRESS, + SHA256_ADDRESS, +) +from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check +from .blake2f import blake2f +from .ecrecover import ecrecover +from .identity import identity +from .modexp import modexp +from .point_evaluation import point_evaluation +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + ECRECOVER_ADDRESS: ecrecover, + SHA256_ADDRESS: sha256, + RIPEMD160_ADDRESS: ripemd160, + IDENTITY_ADDRESS: identity, + MODEXP_ADDRESS: modexp, + ALT_BN128_ADD_ADDRESS: alt_bn128_add, + ALT_BN128_MUL_ADDRESS: alt_bn128_mul, + ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, + BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, +} diff --git a/src/ethereum/prague/vm/precompiled_contracts/modexp.py b/src/ethereum/prague/vm/precompiled_contracts/modexp.py new file mode 100644 index 0000000000..403fe86b11 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/modexp.py @@ -0,0 +1,169 @@ +""" +Ethereum Virtual Machine (EVM) MODEXP PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `MODEXP` precompiled contract. +""" +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ...vm import Evm +from ...vm.gas import charge_gas +from ..memory import buffer_read + +GQUADDIVISOR = Uint(3) + + +def modexp(evm: Evm) -> None: + """ + Calculates `(base**exp) % modulus` for arbitrary sized `base`, `exp` and. + `modulus`. The return value is the same length as the modulus. + """ + data = evm.message.data + + # GAS + base_length = U256.from_be_bytes(buffer_read(data, U256(0), U256(32))) + exp_length = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + + exp_start = U256(96) + base_length + + exp_head = Uint.from_be_bytes( + buffer_read(data, exp_start, min(U256(32), exp_length)) + ) + + charge_gas( + evm, + gas_cost(base_length, modulus_length, exp_length, exp_head), + ) + + # OPERATION + if base_length == 0 and modulus_length == 0: + evm.output = Bytes() + return + + base = Uint.from_be_bytes(buffer_read(data, U256(96), base_length)) + exp = Uint.from_be_bytes(buffer_read(data, exp_start, exp_length)) + + modulus_start = exp_start + exp_length + modulus = Uint.from_be_bytes( + buffer_read(data, modulus_start, modulus_length) + ) + + if modulus == 0: + evm.output = Bytes(b"\x00") * modulus_length + else: + evm.output = pow(base, exp, modulus).to_bytes( + Uint(modulus_length), "big" + ) + + +def complexity(base_length: U256, modulus_length: U256) -> Uint: + """ + Estimate the complexity of performing a modular exponentiation. + + Parameters + ---------- + + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + Returns + ------- + + complexity : `Uint` + Complexity of performing the operation. + """ + max_length = max(Uint(base_length), Uint(modulus_length)) + words = (max_length + Uint(7)) // Uint(8) + return words ** Uint(2) + + +def iterations(exponent_length: U256, exponent_head: Uint) -> Uint: + """ + Calculate the number of iterations required to perform a modular + exponentiation. + + Parameters + ---------- + + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as an unsigned integer. + + Returns + ------- + + iterations : `Uint` + Number of iterations. + """ + if exponent_length <= U256(32) and exponent_head == U256(0): + count = Uint(0) + elif exponent_length <= U256(32): + bit_length = Uint(exponent_head.bit_length()) + + if bit_length > Uint(0): + bit_length -= Uint(1) + + count = bit_length + else: + length_part = Uint(8) * (Uint(exponent_length) - Uint(32)) + bits_part = Uint(exponent_head.bit_length()) + + if bits_part > Uint(0): + bits_part -= Uint(1) + + count = length_part + bits_part + + return max(count, Uint(1)) + + +def gas_cost( + base_length: U256, + modulus_length: U256, + exponent_length: U256, + exponent_head: Uint, +) -> Uint: + """ + Calculate the gas cost of performing a modular exponentiation. + + Parameters + ---------- + + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as an unsigned integer. + + Returns + ------- + + gas_cost : `Uint` + Gas required for performing the operation. + """ + multiplication_complexity = complexity(base_length, modulus_length) + iteration_count = iterations(exponent_length, exponent_head) + cost = multiplication_complexity * iteration_count + cost //= GQUADDIVISOR + return max(Uint(200), cost) diff --git a/src/ethereum/prague/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/prague/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 0000000000..188f90f83f --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,72 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the POINT EVALUATION precompiled contract. +""" +from ethereum_types.bytes import Bytes, Bytes32, Bytes48 +from ethereum_types.numeric import U256 + +from ethereum.crypto.kzg import ( + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ...vm import Evm +from ...vm.exceptions import KZGProofError +from ...vm.gas import GAS_POINT_EVALUATION, charge_gas + +FIELD_ELEMENTS_PER_BLOB = 4096 +BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513 # noqa: E501 +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +def point_evaluation(evm: Evm) -> None: + """ + A pre-compile that verifies a KZG proof which claims that a blob + (represented by a commitment) evaluates to a given value at a given point. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 192: + raise KZGProofError + + versioned_hash = data[:32] + z = Bytes32(data[32:64]) + y = Bytes32(data[64:96]) + commitment = KZGCommitment(data[96:144]) + proof = Bytes48(data[144:192]) + + # GAS + charge_gas(evm, GAS_POINT_EVALUATION) + if kzg_commitment_to_versioned_hash(commitment) != versioned_hash: + raise KZGProofError + + # Verify KZG proof with z and y in big endian format + try: + kzg_proof_verification = verify_kzg_proof(commitment, z, y, proof) + except Exception as e: + raise KZGProofError from e + + if not kzg_proof_verification: + raise KZGProofError + + # Return FIELD_ELEMENTS_PER_BLOB and BLS_MODULUS as padded + # 32 byte big endian values + evm.output = Bytes( + U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + + U256(BLS_MODULUS).to_be_bytes32() + ) diff --git a/src/ethereum/prague/vm/precompiled_contracts/ripemd160.py b/src/ethereum/prague/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 0000000000..6af1086a82 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,43 @@ +""" +Ethereum Virtual Machine (EVM) RIPEMD160 PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `RIPEMD160` precompiled contract. +""" +import hashlib + +from ethereum_types.numeric import Uint + +from ethereum.utils.byte import left_pad_zero_bytes +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_RIPEMD160, GAS_RIPEMD160_WORD, charge_gas + + +def ripemd160(evm: Evm) -> None: + """ + Writes the ripemd160 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_RIPEMD160 + GAS_RIPEMD160_WORD * word_count) + + # OPERATION + hash_bytes = hashlib.new("ripemd160", data).digest() + padded_hash = left_pad_zero_bytes(hash_bytes, 32) + evm.output = padded_hash diff --git a/src/ethereum/prague/vm/precompiled_contracts/sha256.py b/src/ethereum/prague/vm/precompiled_contracts/sha256.py new file mode 100644 index 0000000000..db33a37967 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/sha256.py @@ -0,0 +1,40 @@ +""" +Ethereum Virtual Machine (EVM) SHA256 PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `SHA256` precompiled contract. +""" +import hashlib + +from ethereum_types.numeric import Uint + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_SHA256, GAS_SHA256_WORD, charge_gas + + +def sha256(evm: Evm) -> None: + """ + Writes the sha256 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_SHA256 + GAS_SHA256_WORD * word_count) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/prague/vm/runtime.py b/src/ethereum/prague/vm/runtime.py new file mode 100644 index 0000000000..d6bf90a812 --- /dev/null +++ b/src/ethereum/prague/vm/runtime.py @@ -0,0 +1,67 @@ +""" +Ethereum Virtual Machine (EVM) Runtime Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Runtime related operations used while executing EVM code. +""" +from typing import Set + +from ethereum_types.numeric import Uint, ulen + +from .instructions import Ops + + +def get_valid_jump_destinations(code: bytes) -> Set[Uint]: + """ + Analyze the evm code to obtain the set of valid jump destinations. + + Valid jump destinations are defined as follows: + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + + Note - Jump destinations are 0-indexed. + + Parameters + ---------- + code : + The EVM code which is to be executed. + + Returns + ------- + valid_jump_destinations: `Set[Uint]` + The set of valid jump destinations in the code. + """ + valid_jump_destinations = set() + pc = Uint(0) + + while pc < ulen(code): + try: + current_opcode = Ops(code[pc]) + except ValueError: + # Skip invalid opcodes, as they don't affect the jumpdest + # analysis. Nevertheless, such invalid opcodes would be caught + # and raised when the interpreter runs. + pc += Uint(1) + continue + + if current_opcode == Ops.JUMPDEST: + valid_jump_destinations.add(pc) + elif Ops.PUSH1.value <= current_opcode.value <= Ops.PUSH32.value: + # If PUSH-N opcodes are encountered, skip the current opcode along + # with the trailing data segment corresponding to the PUSH-N + # opcodes. + push_data_size = current_opcode.value - Ops.PUSH1.value + 1 + pc += Uint(push_data_size) + + pc += Uint(1) + + return valid_jump_destinations diff --git a/src/ethereum/prague/vm/stack.py b/src/ethereum/prague/vm/stack.py new file mode 100644 index 0000000000..f28a5b3b88 --- /dev/null +++ b/src/ethereum/prague/vm/stack.py @@ -0,0 +1,59 @@ +""" +Ethereum Virtual Machine (EVM) Stack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List + +from ethereum_types.numeric import U256 + +from .exceptions import StackOverflowError, StackUnderflowError + + +def pop(stack: List[U256]) -> U256: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/tests/prague/__init__.py b/tests/prague/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/prague/test_evm_tools.py b/tests/prague/test_evm_tools.py new file mode 100644 index 0000000000..781f77bb2b --- /dev/null +++ b/tests/prague/test_evm_tools.py @@ -0,0 +1,42 @@ +from functools import partial +from typing import Dict + +import pytest + +from tests.helpers import TEST_FIXTURES +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/" +FORK_NAME = "Prague" + +run_evm_tools_test = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + +SLOW_TESTS = ( + "CALLBlake2f_MaxRounds", + "CALLCODEBlake2f", + "CALLBlake2f", + "loopExp", + "loopMul", +) + + +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_evm_tools_tests( + TEST_DIR, + FORK_NAME, + SLOW_TESTS, + ), + ids=idfn, +) +def test_evm_tools(test_case: Dict) -> None: + run_evm_tools_test(test_case) diff --git a/tests/prague/test_rlp.py b/tests/prague/test_rlp.py new file mode 100644 index 0000000000..b6f610ab2f --- /dev/null +++ b/tests/prague/test_rlp.py @@ -0,0 +1,165 @@ +import pytest +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0, Bytes8, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.prague.blocks import Block, Header, Log, Receipt, Withdrawal +from ethereum.prague.transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, + decode_transaction, + encode_transaction, +) +from ethereum.prague.utils.hexadecimal import hex_to_address +from ethereum.crypto.hash import keccak256 +from ethereum.utils.hexadecimal import hex_to_bytes256 + +hash1 = keccak256(b"foo") +hash2 = keccak256(b"bar") +hash3 = keccak256(b"baz") +hash4 = keccak256(b"foobar") +hash5 = keccak256(b"quux") +hash6 = keccak256(b"foobarbaz") + +address1 = hex_to_address("0x00000000219ab540356cbb839cbe05303d7705fa") +address2 = hex_to_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") +address3 = hex_to_address("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8") + +bloom = hex_to_bytes256( + "0x886480c00200620d84180d0470000c503081160044d05015808" + "0037401107060120040105281100100104500414203040a208003" + "4814200610da1208a638d16e440c024880800301e1004c2b02285" + "0602000084c3249a0c084569c90c2002001586241041e8004035a" + "4400a0100938001e041180083180b0340661372060401428c0200" + "87410402b9484028100049481900c08034864314688d001548c30" + "00828e542284180280006402a28a0264da00ac223004006209609" + "83206603200084040122a4739080501251542082020a4087c0002" + "81c08800898d0900024047380000127038098e090801080000429" + "0c84201661040200201c0004b8490ad588804" +) + +legacy_transaction = LegacyTransaction( + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"foo"), + U256(27), + U256(5), + U256(6), +) + +access_list_transaction = AccessListTransaction( + U64(1), + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ((address1, (hash1, hash2)), (address2, tuple())), + U256(27), + U256(5), + U256(6), +) + +transaction_1559 = FeeMarketTransaction( + U64(1), + U256(1), + Uint(7), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ((address1, (hash1, hash2)), (address2, tuple())), + U256(27), + U256(5), + U256(6), +) + +withdrawal = Withdrawal(U64(0), U64(1), address1, U256(2)) + + +header = Header( + parent_hash=hash1, + ommers_hash=hash2, + coinbase=address1, + state_root=hash3, + transactions_root=hash4, + receipt_root=hash5, + bloom=bloom, + difficulty=Uint(1), + number=Uint(2), + gas_limit=Uint(3), + gas_used=Uint(4), + timestamp=U256(5), + extra_data=Bytes(b"foobar"), + prev_randao=Bytes32(b"1234567890abcdef1234567890abcdef"), + nonce=Bytes8(b"12345678"), + base_fee_per_gas=Uint(6), + withdrawals_root=hash6, + parent_beacon_block_root=Bytes32(b"1234567890abcdef1234567890abcdef"), + blob_gas_used=U64(7), + excess_blob_gas=U64(8), +) + +block = Block( + header=header, + transactions=( + encode_transaction(legacy_transaction), + encode_transaction(access_list_transaction), + encode_transaction(transaction_1559), + ), + ommers=(), + withdrawals=(withdrawal,), +) + +log1 = Log( + address=address1, + topics=(hash1, hash2), + data=Bytes(b"foobar"), +) + +log2 = Log( + address=address1, + topics=(hash1,), + data=Bytes(b"quux"), +) + +receipt = Receipt( + succeeded=True, + cumulative_gas_used=Uint(1), + bloom=bloom, + logs=(log1, log2), +) + + +@pytest.mark.parametrize( + "rlp_object", + [ + legacy_transaction, + access_list_transaction, + transaction_1559, + header, + block, + log1, + log2, + receipt, + withdrawal, + ], +) +def test_cancun_rlp(rlp_object: rlp.Extended) -> None: + encoded = rlp.encode(rlp_object) + assert rlp.decode_to(type(rlp_object), encoded) == rlp_object + + +@pytest.mark.parametrize( + "tx", [legacy_transaction, access_list_transaction, transaction_1559] +) +def test_transaction_encoding(tx: Transaction) -> None: + encoded = encode_transaction(tx) + assert decode_transaction(encoded) == tx diff --git a/tests/prague/test_state_transition.py b/tests/prague/test_state_transition.py new file mode 100644 index 0000000000..60e34a24ed --- /dev/null +++ b/tests/prague/test_state_transition.py @@ -0,0 +1,105 @@ +from functools import partial +from typing import Dict + +import pytest + +from tests.helpers import TEST_FIXTURES +from tests.helpers.load_state_tests import ( + Load, + fetch_state_test_files, + idfn, + run_blockchain_st_test, +) + +fetch_cancun_tests = partial(fetch_state_test_files, network="Prague") + +FIXTURES_LOADER = Load("Prague", "prague") + +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/" + +SLOW_TESTS = ( + # GeneralStateTests + "stTimeConsuming/CALLBlake2f_MaxRounds.json", + "stTimeConsuming/static_Call50000_sha256.json", + "vmPerformance/loopExp.json", + "vmPerformance/loopMul.json", + "QuadraticComplexitySolidity_CallDataCopy_d0g1v0_Cancun", + "CALLBlake2f_d9g0v0_Cancun", + "CALLCODEBlake2f_d9g0v0", + # GeneralStateTests + "stRandom/randomStatetest177.json", + "stCreateTest/CreateOOGafterMaxCodesize.json", + # ValidBlockTest + "bcExploitTest/DelegateCallSpam.json", + # InvalidBlockTest + "bcUncleHeaderValidity/nonceWrong.json", + "bcUncleHeaderValidity/wrongMixHash.json", +) + +# These are tests that are considered to be incorrect, +# Please provide an explanation when adding entries +IGNORE_TESTS = ( + # ValidBlockTest + "bcForkStressTest/ForkStressTest.json", + "bcGasPricerTest/RPC_API_Test.json", + "bcMultiChainTest", + "bcTotalDifficultyTest", + # InvalidBlockTest + "bcForgedTest", + "bcMultiChainTest", + "GasLimitHigherThan2p63m1_Cancun", +) + +# All tests that recursively create a large number of frames (50000) +BIG_MEMORY_TESTS = ( + # GeneralStateTests + "50000_", + "/stQuadraticComplexityTest/", + "/stRandom2/", + "/stRandom/", + "/stSpecialTest/", + "stTimeConsuming/", + "stBadOpcode/", + "stStaticCall/", +) + +fetch_state_tests = partial( + fetch_cancun_tests, + test_dir, + ignore_list=IGNORE_TESTS, + slow_list=SLOW_TESTS, + big_memory_list=BIG_MEMORY_TESTS, +) + + +@pytest.mark.parametrize( + "test_case", + fetch_state_tests(), + 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" + + +@pytest.mark.parametrize( + "test_case", + fetch_cancun_tests(test_dir), + ids=idfn, +) +def test_execution_specs_generated_tests(test_case: Dict) -> None: + run_cancun_blockchain_st_tests(test_case) diff --git a/tests/prague/test_trie.py b/tests/prague/test_trie.py new file mode 100644 index 0000000000..1938c238a2 --- /dev/null +++ b/tests/prague/test_trie.py @@ -0,0 +1,89 @@ +import json +from typing import Any + +from ethereum.prague.fork_types import Bytes +from ethereum.prague.trie import Trie, root, trie_set +from ethereum.utils.hexadecimal import ( + has_hex_prefix, + hex_to_bytes, + remove_hex_prefix, +) +from tests.helpers import TEST_FIXTURES + +FIXTURE_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] + + +def to_bytes(data: str) -> Bytes: + if data is None: + return b"" + if has_hex_prefix(data): + return hex_to_bytes(data) + + return data.encode() + + +def test_trie_secure_hex() -> None: + tests = load_tests("hex_encoded_securetrie_test.json") + + for name, test in tests.items(): + st: Trie[Bytes, Bytes] = Trie(secured=True, default=b"") + for k, v in test.get("in").items(): + trie_set(st, to_bytes(k), to_bytes(v)) + result = root(st) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_secure() -> None: + tests = load_tests("trietest_secureTrie.json") + + for name, test in tests.items(): + st: Trie[Bytes, Bytes] = Trie(secured=True, default=b"") + for t in test.get("in"): + trie_set(st, to_bytes(t[0]), to_bytes(t[1])) + result = root(st) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_secure_any_order() -> None: + tests = load_tests("trieanyorder_secureTrie.json") + + for name, test in tests.items(): + st: Trie[Bytes, Bytes] = Trie(secured=True, default=b"") + for k, v in test.get("in").items(): + trie_set(st, to_bytes(k), to_bytes(v)) + result = root(st) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie() -> None: + tests = load_tests("trietest.json") + + for name, test in tests.items(): + st: Trie[Bytes, Bytes] = Trie(secured=False, default=b"") + for t in test.get("in"): + trie_set(st, to_bytes(t[0]), to_bytes(t[1])) + result = root(st) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_any_order() -> None: + tests = load_tests("trieanyorder.json") + + for name, test in tests.items(): + st: Trie[Bytes, Bytes] = Trie(secured=False, default=b"") + for k, v in test.get("in").items(): + trie_set(st, to_bytes(k), to_bytes(v)) + result = root(st) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def load_tests(path: str) -> Any: + with open(f"{FIXTURE_PATH}/TrieTests/" + path) as f: + tests = json.load(f) + + return tests From 50c17ac989aef90b5525f0e883cfb79c1987a6c7 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Tue, 23 Apr 2024 11:01:01 +0200 Subject: [PATCH 02/38] changes for prague --- setup.cfg | 5 +++++ src/ethereum/prague/__init__.py | 9 +++------ src/ethereum/prague/trie.py | 2 +- tests/prague/test_rlp.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3bd4401f5e..d3a418e3ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -104,6 +104,11 @@ packages = ethereum/cancun/vm ethereum/cancun/vm/instructions ethereum/cancun/vm/precompiled_contracts + ethereum/prague + ethereum/prague/utils + ethereum/prague/vm + ethereum/prague/vm/instructions + ethereum/prague/vm/precompiled_contracts package_dir = diff --git a/src/ethereum/prague/__init__.py b/src/ethereum/prague/__init__.py index 04774a7578..09a10407a3 100644 --- a/src/ethereum/prague/__init__.py +++ b/src/ethereum/prague/__init__.py @@ -1,10 +1,7 @@ """ -The Prague fork introduces transient storage, exposes beacon chain roots, -introduces a new blob-carrying transaction type, adds a memory copying -instruction, limits self-destruct to only work for contracts created in the -same transaction, and adds an instruction to read the blob base fee. +The Prague fork. """ -from ethereum.fork_criteria import ByTimestamp +from ethereum.fork_criteria import Unscheduled -FORK_CRITERIA = ByTimestamp(1710338135) +FORK_CRITERIA = Unscheduled() diff --git a/src/ethereum/prague/trie.py b/src/ethereum/prague/trie.py index 082cbb5495..03ae159be7 100644 --- a/src/ethereum/prague/trie.py +++ b/src/ethereum/prague/trie.py @@ -33,8 +33,8 @@ from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint +from ethereum.cancun import trie as previous_trie from ethereum.crypto.hash import keccak256 -from ethereum.shanghai import trie as previous_trie from ethereum.utils.hexadecimal import hex_to_bytes from .blocks import Receipt, Withdrawal diff --git a/tests/prague/test_rlp.py b/tests/prague/test_rlp.py index b6f610ab2f..3a76f83b3d 100644 --- a/tests/prague/test_rlp.py +++ b/tests/prague/test_rlp.py @@ -3,6 +3,7 @@ from ethereum_types.bytes import Bytes, Bytes0, Bytes8, Bytes32 from ethereum_types.numeric import U64, U256, Uint +from ethereum.crypto.hash import keccak256 from ethereum.prague.blocks import Block, Header, Log, Receipt, Withdrawal from ethereum.prague.transactions import ( AccessListTransaction, @@ -13,7 +14,6 @@ encode_transaction, ) from ethereum.prague.utils.hexadecimal import hex_to_address -from ethereum.crypto.hash import keccak256 from ethereum.utils.hexadecimal import hex_to_bytes256 hash1 = keccak256(b"foo") From d35af35ae6aba4d369cb15700d3320938441a554 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 11:38:39 +0100 Subject: [PATCH 03/38] Implement EIP-7702 --- src/ethereum/prague/blocks.py | 37 ++- src/ethereum/prague/fork.py | 47 ++-- src/ethereum/prague/fork_types.py | 17 +- src/ethereum/prague/transactions.py | 87 ++++++- src/ethereum/prague/utils/message.py | 22 +- src/ethereum/prague/vm/__init__.py | 3 +- src/ethereum/prague/vm/eoa_delegation.py | 215 ++++++++++++++++++ .../prague/vm/instructions/environment.py | 35 ++- src/ethereum/prague/vm/instructions/system.py | 57 ++++- src/ethereum/prague/vm/interpreter.py | 20 +- src/ethereum/utils/hexadecimal.py | 19 +- .../evm_tools/loaders/fork_loader.py | 15 ++ .../evm_tools/loaders/transaction_loader.py | 37 ++- .../evm_tools/t8n/t8n_types.py | 3 + whitelist.txt | 2 + 15 files changed, 561 insertions(+), 55 deletions(-) create mode 100644 src/ethereum/prague/vm/eoa_delegation.py diff --git a/src/ethereum/prague/blocks.py b/src/ethereum/prague/blocks.py index 54ef88e15f..684b57aab7 100644 --- a/src/ethereum/prague/blocks.py +++ b/src/ethereum/prague/blocks.py @@ -11,13 +11,21 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) @slotted_freezable @@ -98,3 +106,30 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2, 3, 4) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 5bbb517ed6..f215e86955 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -23,9 +23,9 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt, Withdrawal +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 Address, Authorization, Bloom, Root, VersionedHash from .state import ( State, TransientStorage, @@ -43,6 +43,7 @@ BlobTransaction, FeeMarketTransaction, LegacyTransaction, + SetCodeTransaction, Transaction, calculate_intrinsic_cost, decode_transaction, @@ -54,6 +55,7 @@ from .utils.hexadecimal import hex_to_address from .utils.message import prepare_message from .vm import Message +from .vm.eoa_delegation import is_valid_delegation from .vm.gas import ( calculate_blob_gas_price, calculate_data_fee, @@ -372,7 +374,9 @@ def check_transaction( sender_address = recover_sender(chain_id, tx) sender_account = get_account(state, sender_address) - if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): + 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: @@ -391,8 +395,6 @@ def check_transaction( max_gas_fee = tx.gas * tx.gas_price if isinstance(tx, BlobTransaction): - if not isinstance(tx.to, Address): - raise InvalidBlock if len(tx.blob_versioned_hashes) == 0: raise InvalidBlock for blob_versioned_hash in tx.blob_versioned_hashes: @@ -409,11 +411,22 @@ def check_transaction( blob_versioned_hashes = tx.blob_versioned_hashes else: blob_versioned_hashes = () + + if isinstance(tx, (BlobTransaction, SetCodeTransaction)): + if not isinstance(tx.to, Address): + raise InvalidBlock + + if isinstance(tx, SetCodeTransaction): + if not any(tx.authorizations): + raise InvalidBlock + 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(): + if sender_account.code != bytearray() and not is_valid_delegation( + sender_account.code + ): raise InvalidSenderError("not EOA") return sender_address, effective_gas_price, blob_versioned_hashes @@ -452,14 +465,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - elif isinstance(tx, BlobTransaction): - return b"\x03" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -748,13 +754,23 @@ def process_transaction( preaccessed_storage_keys = set() preaccessed_addresses.add(env.coinbase) if isinstance( - tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), ): for address, keys in tx.access_list: preaccessed_addresses.add(address) for key in keys: preaccessed_storage_keys.add((address, key)) + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + message = prepare_message( sender, tx.to, @@ -764,6 +780,7 @@ def process_transaction( env, preaccessed_addresses=frozenset(preaccessed_addresses), preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + authorizations=authorizations, ) output = process_message_call(message, env) diff --git a/src/ethereum/prague/fork_types.py b/src/ethereum/prague/fork_types.py index dc287c84f1..26b6656190 100644 --- a/src/ethereum/prague/fork_types.py +++ b/src/ethereum/prague/fork_types.py @@ -17,7 +17,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes20, Bytes256 from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U8, U64, U256, Uint from ..crypto.hash import Hash32, keccak256 @@ -62,3 +62,18 @@ def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes: keccak256(raw_account_data.code), ) ) + + +@slotted_freezable +@dataclass +class Authorization: + """ + The authorization for a set code transaction. + """ + + chain_id: U256 + address: Address + nonce: U64 + y_parity: U8 + r: U256 + s: U256 diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index deb594c17c..153f08b655 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -9,14 +9,14 @@ 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 from ethereum.exceptions import InvalidSignatureError from .exceptions import TransactionTypeError -from .fork_types import Address, VersionedHash +from .fork_types import Address, Authorization, VersionedHash TX_BASE_COST = 21000 TX_DATA_COST_PER_NON_ZERO = 16 @@ -108,11 +108,34 @@ class BlobTransaction: s: U256 +@slotted_freezable +@dataclass +class SetCodeTransaction: + """ + The transaction type added in EIP-7702. + """ + + chain_id: U64 + nonce: U64 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Address + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + authorizations: Tuple[Authorization, ...] + y_parity: U256 + r: U256 + s: U256 + + Transaction = Union[ LegacyTransaction, AccessListTransaction, FeeMarketTransaction, BlobTransaction, + SetCodeTransaction, ] @@ -128,6 +151,8 @@ def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]: return b"\x02" + rlp.encode(tx) elif isinstance(tx, BlobTransaction): return b"\x03" + rlp.encode(tx) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(tx) else: raise Exception(f"Unable to encode transaction of type {type(tx)}") @@ -143,6 +168,8 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return rlp.decode_to(FeeMarketTransaction, tx[1:]) elif tx[0] == 3: return rlp.decode_to(BlobTransaction, tx[1:]) + elif tx[0] == 4: + return rlp.decode_to(SetCodeTransaction, tx[1:]) else: raise TransactionTypeError(tx[0]) else: @@ -178,7 +205,7 @@ def validate_transaction(tx: Transaction) -> bool: if calculate_intrinsic_cost(tx) > tx.gas: return False - if tx.nonce >= U256(U64.MAX_VALUE): + if U256(tx.nonce) >= U256(U64.MAX_VALUE): return False if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: return False @@ -209,6 +236,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: verified : `ethereum.base_types.Uint` The intrinsic cost of the transaction. """ + from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST from .vm.gas import init_code_cost data_cost = 0 @@ -220,17 +248,23 @@ 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) + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), ): 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) @@ -291,6 +325,10 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_4844(tx) ) + elif isinstance(tx, SetCodeTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7702(tx) + ) return Address(keccak256(public_key)[12:32]) @@ -451,3 +489,36 @@ def signing_hash_4844(tx: BlobTransaction) -> Hash32: ) ) ) + + +def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP-7702 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x04" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.authorizations, + ) + ) + ) diff --git a/src/ethereum/prague/utils/message.py b/src/ethereum/prague/utils/message.py index 7e91a7872c..3d1420b835 100644 --- a/src/ethereum/prague/utils/message.py +++ b/src/ethereum/prague/utils/message.py @@ -17,9 +17,10 @@ from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U256, Uint -from ..fork_types import Address +from ..fork_types import Address, Authorization from ..state import get_account from ..vm import Environment, Message +from ..vm.eoa_delegation import get_delegated_code_address from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address @@ -38,6 +39,7 @@ def prepare_message( preaccessed_storage_keys: FrozenSet[ Tuple[(Address, Bytes32)] ] = frozenset(), + authorizations: Tuple[Authorization, ...] = (), ) -> Message: """ Execute a transaction against the provided environment. @@ -70,12 +72,19 @@ def prepare_message( 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. Returns ------- message: `ethereum.prague.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) + if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -87,16 +96,18 @@ def prepare_message( current_target = target msg_data = data code = get_account(env.state, target).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 + if code_address is None: code_address = target else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, @@ -113,4 +124,5 @@ def prepare_message( accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), parent_evm=None, + authorizations=authorizations, ) diff --git a/src/ethereum/prague/vm/__init__.py b/src/ethereum/prague/vm/__init__.py index 04bb7a353a..e73dba47bd 100644 --- a/src/ethereum/prague/vm/__init__.py +++ b/src/ethereum/prague/vm/__init__.py @@ -22,7 +22,7 @@ from ethereum.crypto.hash import Hash32 from ..blocks import Log -from ..fork_types import Address, VersionedHash +from ..fork_types import Address, Authorization, VersionedHash from ..state import State, TransientStorage, account_exists_and_is_empty from .precompiled_contracts import RIPEMD160_ADDRESS @@ -73,6 +73,7 @@ class Message: accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] parent_evm: Optional["Evm"] + authorizations: Tuple[Authorization, ...] @dataclass diff --git a/src/ethereum/prague/vm/eoa_delegation.py b/src/ethereum/prague/vm/eoa_delegation.py new file mode 100644 index 0000000000..b06cd144eb --- /dev/null +++ b/src/ethereum/prague/vm/eoa_delegation.py @@ -0,0 +1,215 @@ +""" +Set EOA account code. +""" + + +from typing import Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSignatureError + +from ..fork_types import Address, Authorization +from ..state import account_exists, get_account, increment_nonce, set_code +from ..utils.hexadecimal import hex_to_address +from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS +from . import Environment, Evm, Message + +SET_CODE_TX_MAGIC = b"\x05" +EOA_DELEGATION_MARKER = b"\xEF\x01\x00" +EOA_DELEGATION_SENTINEL = b"\xEF\x01" +EOA_DELEGATION_MARKER_LENGTH = len(EOA_DELEGATION_MARKER) +EOA_DELEGATED_CODE_LENGTH = 23 +PER_EMPTY_ACCOUNT_COST = 25000 +PER_AUTH_BASE_COST = 12500 +NULL_ADDRESS = hex_to_address("0x0000000000000000000000000000000000000000") + + +def is_valid_delegation(code: bytes) -> bool: + """ + Whether the code is a valid delegation designation. + + Parameters + ---------- + code: `bytes` + The code to check. + + Returns + ------- + valid : `bool` + True if the code is a valid delegation designation, + False otherwise. + """ + if ( + len(code) == EOA_DELEGATED_CODE_LENGTH + and code[:EOA_DELEGATION_MARKER_LENGTH] == EOA_DELEGATION_MARKER + ): + return True + return False + + +def get_delegated_code_address(code: bytes) -> Optional[Address]: + """ + Get the address to which the code delegates. + + Parameters + ---------- + code: `bytes` + The code to get the address from. + + Returns + ------- + address : `Optional[Address]` + The address of the delegated code. + """ + if is_valid_delegation(code): + return Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + return None + + +def recover_authority(authorization: Authorization) -> Address: + """ + Recover the authority address from the authorization. + + Parameters + ---------- + authorization + The authorization to recover the authority from. + + Raises + ------ + InvalidSignatureError + If the signature is invalid. + + Returns + ------- + authority : `Address` + The recovered authority address. + """ + y_parity, r, s = authorization.y_parity, authorization.r, authorization.s + if y_parity not in (0, 1): + raise InvalidSignatureError("Invalid y_parity in authorization") + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("Invalid r value in authorization") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("Invalid s value in authorization") + + signing_hash = keccak256( + SET_CODE_TX_MAGIC + + rlp.encode( + ( + authorization.chain_id, + authorization.address, + authorization.nonce, + ) + ) + ) + + public_key = secp256k1_recover(r, s, U256(y_parity), signing_hash) + return Address(keccak256(public_key)[12:32]) + + +def access_delegation( + evm: Evm, address: Address +) -> Tuple[bool, Address, Bytes, Uint]: + """ + Get the delegation address, code, and the cost of access from the address. + + Parameters + ---------- + evm : `Evm` + The execution frame. + address : `Address` + The address to get the delegation from. + + Returns + ------- + delegation : `Tuple[bool, Address, Bytes, Uint]` + The delegation address, code, and access gas cost. + """ + code = get_account(evm.env.state, address).code + if not is_valid_delegation(code): + return False, address, code, Uint(0) + + address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + if address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code = get_account(evm.env.state, address).code + + return True, address, code, access_gas_cost + + +def set_delegation(message: Message, env: Environment) -> U256: + """ + Set the delegation code for the authorities in the message. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + refund_counter: `U256` + Refund from authority which already exists in state. + """ + refund_counter = U256(0) + for auth in message.authorizations: + if auth.chain_id not in (env.chain_id, U256(0)): + continue + + if auth.nonce >= U64.MAX_VALUE: + continue + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + continue + + message.accessed_addresses.add(authority) + + authority_account = get_account(env.state, authority) + authority_code = authority_account.code + + if authority_code != bytearray() 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): + 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) + + increment_nonce(env.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 + + if is_valid_delegation(message.code): + message.code_address = Address( + message.code[EOA_DELEGATION_MARKER_LENGTH:] + ) + message.accessed_addresses.add(message.code_address) + + message.code = get_account(env.state, message.code_address).code + + return refund_counter diff --git a/src/ethereum/prague/vm/instructions/environment.py b/src/ethereum/prague/vm/instructions/environment.py index 4ba384f9ba..08d6e9ae0d 100644 --- a/src/ethereum/prague/vm/instructions/environment.py +++ b/src/ethereum/prague/vm/instructions/environment.py @@ -21,6 +21,7 @@ from ...fork_types import EMPTY_ACCOUNT from ...state import get_account from ...utils.address import to_address +from ...vm.eoa_delegation import EOA_DELEGATION_SENTINEL, is_valid_delegation from ...vm.memory import buffer_read, memory_write from .. import Evm from ..exceptions import OutOfBoundsRead @@ -343,15 +344,19 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + if is_valid_delegation(code): + code = EOA_DELEGATION_SENTINEL + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -382,16 +387,19 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + if is_valid_delegation(code): + code = EOA_DELEGATION_SENTINEL + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -468,10 +476,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -479,7 +489,10 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + if is_valid_delegation(code): + code = EOA_DELEGATION_SENTINEL + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/prague/vm/instructions/system.py b/src/ethereum/prague/vm/instructions/system.py index da47df38cf..a75e036f06 100644 --- a/src/ethereum/prague/vm/instructions/system.py +++ b/src/ethereum/prague/vm/instructions/system.py @@ -32,6 +32,7 @@ compute_create2_contract_address, to_address, ) +from ...vm.eoa_delegation import access_delegation from .. import ( Evm, Message, @@ -128,6 +129,7 @@ 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) @@ -281,6 +283,8 @@ def generic_call( memory_input_size: U256, memory_output_start_position: U256, memory_output_size: U256, + code: bytes, + is_delegated_code: bool, ) -> None: """ Perform the core logic of the `CALL*` family of opcodes. @@ -298,6 +302,12 @@ def generic_call( evm.memory, memory_input_start_position, memory_input_size ) code = get_account(evm.env.state, code_address).code + + if is_delegated_code and len(code) == 0: + evm.gas_left += gas + push(evm.stack, U256(1)) + return + child_message = Message( caller=caller, target=to, @@ -313,6 +323,7 @@ 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) @@ -366,6 +377,15 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + ( + is_delegated_code, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -397,13 +417,15 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, memory_input_size, memory_output_start_position, memory_output_size, + code, + is_delegated_code, ) # PROGRAM COUNTER @@ -445,6 +467,14 @@ def callcode(evm: Evm) -> None: evm.accessed_addresses.add(code_address) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + ( + is_delegated_code, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE message_call_gas = calculate_message_call_gas( value, @@ -478,6 +508,8 @@ def callcode(evm: Evm) -> None: memory_input_size, memory_output_start_position, memory_output_size, + code, + is_delegated_code, ) # PROGRAM COUNTER @@ -573,6 +605,14 @@ def delegatecall(evm: Evm) -> None: evm.accessed_addresses.add(code_address) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + ( + is_delegated_code, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + message_call_gas = calculate_message_call_gas( U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost ) @@ -593,6 +633,8 @@ def delegatecall(evm: Evm) -> None: memory_input_size, memory_output_start_position, memory_output_size, + code, + is_delegated_code, ) # PROGRAM COUNTER @@ -631,6 +673,15 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + ( + is_delegated_code, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -648,13 +699,15 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, memory_input_size, memory_output_start_position, memory_output_size, + code, + is_delegated_code, ) # PROGRAM COUNTER diff --git a/src/ethereum/prague/vm/interpreter.py b/src/ethereum/prague/vm/interpreter.py index 4a7c25a779..df02758171 100644 --- a/src/ethereum/prague/vm/interpreter.py +++ b/src/ethereum/prague/vm/interpreter.py @@ -14,7 +14,7 @@ from dataclasses import dataclass from typing import Iterable, Optional, Set, Tuple, Union -from ethereum_types.bytes import Bytes0 +from ethereum_types.bytes import Bytes, Bytes0 from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import ( @@ -45,6 +45,7 @@ touch_account, ) from ..vm import Message +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 @@ -77,6 +78,7 @@ class MessageCallOutput: 4. `accounts_to_delete`: Contracts which have self-destructed. 5. `touched_accounts`: Accounts that have been touched. 6. `error`: The error from the execution if any. + 7. `return_data`: The output of the execution. """ gas_left: Uint @@ -85,6 +87,7 @@ class MessageCallOutput: accounts_to_delete: Set[Address] touched_accounts: Iterable[Address] error: Optional[Exception] + return_data: Bytes def process_message_call( @@ -107,17 +110,26 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + 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) if is_collision: return MessageCallOutput( - Uint(0), U256(0), tuple(), set(), set(), AddressCollision() + Uint(0), + U256(0), + tuple(), + set(), + set(), + AddressCollision(), + Bytes(b""), ) else: evm = process_create_message(message, env) 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)): evm.touched_accounts.add(Address(message.target)) @@ -126,12 +138,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error @@ -145,6 +156,7 @@ def process_message_call( accounts_to_delete=accounts_to_delete, touched_accounts=touched_accounts, error=evm.error, + return_data=evm.output, ) diff --git a/src/ethereum/utils/hexadecimal.py b/src/ethereum/utils/hexadecimal.py index 56c2452eab..d7a4f20ee5 100644 --- a/src/ethereum/utils/hexadecimal.py +++ b/src/ethereum/utils/hexadecimal.py @@ -12,7 +12,7 @@ Hexadecimal strings specific utility functions used in this specification. """ from ethereum_types.bytes import Bytes, Bytes8, Bytes20, Bytes32, Bytes256 -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U8, U64, U256, Uint from ethereum.crypto.hash import Hash32 @@ -176,6 +176,23 @@ def hex_to_uint(hex_string: str) -> Uint: return Uint(int(remove_hex_prefix(hex_string), 16)) +def hex_to_u8(hex_string: str) -> U8: + """ + Convert hex string to U8. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to U8. + + Returns + ------- + converted : `U8` + The U8 integer obtained from the given hexadecimal string. + """ + return U8(int(remove_hex_prefix(hex_string), 16)) + + def hex_to_u64(hex_string: str) -> U64: """ Convert hex string to U64. 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 5132577567..906775a61d 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -102,6 +102,11 @@ def signing_hash_1559(self) -> Any: """signing_hash_1559 function of the fork""" return self._module("transactions").signing_hash_1559 + @property + def signing_hash_7702(self) -> Any: + """signing_hash_7702 function of the fork""" + return self._module("transactions").signing_hash_7702 + @property def signing_hash_4844(self) -> Any: """signing_hash_4844 function of the fork""" @@ -167,6 +172,11 @@ def BlobTransaction(self) -> Any: """Blob transaction class of the fork""" return self._module("transactions").BlobTransaction + @property + def SetCodeTransaction(self) -> Any: + """Set code transaction class of the fork""" + return self._module("transactions").SetCodeTransaction + @property def Withdrawal(self) -> Any: """Withdrawal class of the fork""" @@ -282,6 +292,11 @@ def Message(self) -> Any: """Message class of the fork""" return self._module("vm").Message + @property + def Authorization(self) -> Any: + """Authorization class of the fork""" + return self._module("vm.eoa_delegation").Authorization + @property def TARGET_BLOB_GAS_PER_BLOCK(self) -> Any: """TARGET_BLOB_GAS_PER_BLOCK of the fork""" diff --git a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py index 76b0c25b8c..70c1e8d82a 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py @@ -14,9 +14,12 @@ hex_to_bytes, hex_to_bytes32, hex_to_hash, + hex_to_u8, + hex_to_u64, hex_to_u256, hex_to_uint, ) +from ethereum_spec_tools.evm_tools.utils import parse_hex_or_int class UnsupportedTx(Exception): @@ -85,6 +88,22 @@ def json_to_access_list(self) -> Any: ) return access_list + def json_to_authorizations(self) -> Any: + """Get the authorization list of the transaction.""" + authorizations = [] + for sublist in self.raw["authorizationList"]: + authorizations.append( + self.fork.Authorization( + chain_id=hex_to_u256(sublist.get("chainId")), + nonce=hex_to_u64(sublist.get("nonce")), + address=self.fork.hex_to_address(sublist.get("address")), + y_parity=hex_to_u8(sublist.get("v")), + r=hex_to_u256(sublist.get("r")), + s=hex_to_u256(sublist.get("s")), + ) + ) + return authorizations + def json_to_max_priority_fee_per_gas(self) -> Uint: """Get the max priority fee per gas of the transaction.""" return hex_to_uint(self.raw.get("maxPriorityFeePerGas")) @@ -145,23 +164,29 @@ def get_legacy_transaction(self) -> Any: def read(self) -> Any: """Convert json transaction data to a transaction object""" if "type" in self.raw: - tx_type = self.raw.get("type") - if tx_type == "0x3": + tx_type = parse_hex_or_int(self.raw.get("type"), Uint) + if tx_type == Uint(4): + tx_cls = self.fork.SetCodeTransaction + tx_byte_prefix = b"\x04" + elif tx_type == Uint(3): tx_cls = self.fork.BlobTransaction tx_byte_prefix = b"\x03" - elif tx_type == "0x2": + elif tx_type == Uint(2): tx_cls = self.fork.FeeMarketTransaction tx_byte_prefix = b"\x02" - elif tx_type == "0x1": + elif tx_type == Uint(1): tx_cls = self.fork.AccessListTransaction tx_byte_prefix = b"\x01" - elif tx_type == "0x0": + elif tx_type == Uint(0): tx_cls = self.get_legacy_transaction() tx_byte_prefix = b"" else: raise ValueError(f"Unknown transaction type: {tx_type}") else: - if "maxFeePerBlobGas" in self.raw: + if "authorizationList" in self.raw: + tx_cls = self.fork.SetCodeTransaction + tx_byte_prefix = b"\x04" + elif "maxFeePerBlobGas" in self.raw: tx_cls = self.fork.BlobTransaction tx_byte_prefix = b"\x03" elif "maxFeePerGas" in self.raw: 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 404f96bfe9..6c76210fac 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -277,6 +277,9 @@ def sign_transaction(self, json_tx: Any) -> None: elif isinstance(tx_decoded, t8n.fork.BlobTransaction): signing_hash = t8n.fork.signing_hash_4844(tx_decoded) v_addend = U256(0) + elif isinstance(tx_decoded, t8n.fork.SetCodeTransaction): + signing_hash = t8n.fork.signing_hash_7702(tx_decoded) + v_addend = U256(0) else: raise FatalException("Unknown transaction type") diff --git a/whitelist.txt b/whitelist.txt index de5d7ee9d7..6dfd7ae214 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -458,3 +458,5 @@ exponentiate monomial impl x2 + +eoa From 877f1a53736a429e2bfee706105eee1a001f2c52 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 11:46:30 +0100 Subject: [PATCH 04/38] Implement EIP-2537 --- setup.cfg | 1 + src/ethereum/prague/vm/gas.py | 7 + .../vm/precompiled_contracts/__init__.py | 14 + .../bls12_381/__init__.py | 583 ++++++++++++++++++ .../bls12_381/bls12_381_g1.py | 148 +++++ .../bls12_381/bls12_381_g2.py | 148 +++++ .../bls12_381/bls12_381_pairing.py | 67 ++ .../vm/precompiled_contracts/mapping.py | 25 + 8 files changed, 993 insertions(+) create mode 100644 src/ethereum/prague/vm/precompiled_contracts/bls12_381/__init__.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g1.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g2.py create mode 100644 src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py diff --git a/setup.cfg b/setup.cfg index d3a418e3ad..4cb3796099 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,6 +109,7 @@ packages = ethereum/prague/vm ethereum/prague/vm/instructions ethereum/prague/vm/precompiled_contracts + ethereum/prague/vm/precompiled_contracts/bls12_381 package_dir = diff --git a/src/ethereum/prague/vm/gas.py b/src/ethereum/prague/vm/gas.py index cc3ccc7c6b..32e35cdd0d 100644 --- a/src/ethereum/prague/vm/gas.py +++ b/src/ethereum/prague/vm/gas.py @@ -73,6 +73,13 @@ MIN_BLOB_GASPRICE = Uint(1) BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) +GAS_BLS_G1_ADD = Uint(375) +GAS_BLS_G1_MUL = Uint(12000) +GAS_BLS_G1_MAP = Uint(5500) +GAS_BLS_G2_ADD = Uint(600) +GAS_BLS_G2_MUL = Uint(22500) +GAS_BLS_G2_MAP = Uint(23800) + @dataclass class ExtendMemory: diff --git a/src/ethereum/prague/vm/precompiled_contracts/__init__.py b/src/ethereum/prague/vm/precompiled_contracts/__init__.py index 7ec48ca69b..8ab92bb0f3 100644 --- a/src/ethereum/prague/vm/precompiled_contracts/__init__.py +++ b/src/ethereum/prague/vm/precompiled_contracts/__init__.py @@ -26,6 +26,13 @@ "ALT_BN128_PAIRING_CHECK_ADDRESS", "BLAKE2F_ADDRESS", "POINT_EVALUATION_ADDRESS", + "BLS12_G1_ADD_ADDRESS", + "BLS12_G1_MSM_ADDRESS", + "BLS12_G2_ADD_ADDRESS", + "BLS12_G2_MSM_ADDRESS", + "BLS12_PAIRING_ADDRESS", + "BLS12_MAP_FP_TO_G1_ADDRESS", + "BLS12_MAP_FP2_TO_G2_ADDRESS", ) ECRECOVER_ADDRESS = hex_to_address("0x01") @@ -38,3 +45,10 @@ ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") BLAKE2F_ADDRESS = hex_to_address("0x09") POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") +BLS12_G1_ADD_ADDRESS = hex_to_address("0x0b") +BLS12_G1_MSM_ADDRESS = hex_to_address("0x0c") +BLS12_G2_ADD_ADDRESS = hex_to_address("0x0d") +BLS12_G2_MSM_ADDRESS = hex_to_address("0x0e") +BLS12_PAIRING_ADDRESS = hex_to_address("0x0f") +BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10") +BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11") diff --git a/src/ethereum/prague/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/__init__.py new file mode 100644 index 0000000000..2126a6ab39 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/__init__.py @@ -0,0 +1,583 @@ +""" +BLS12 381 Precompile +^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Precompile for BLS12-381 curve operations. +""" +from typing import Tuple, Union + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint +from py_ecc.bls12_381.bls12_381_curve import ( + FQ, + FQ2, + b, + b2, + curve_order, + is_on_curve, + multiply, +) +from py_ecc.optimized_bls12_381.optimized_curve import FQ as OPTIMIZED_FQ +from py_ecc.optimized_bls12_381.optimized_curve import FQ2 as OPTIMIZED_FQ2 +from py_ecc.typing import Point2D + +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter + +P = FQ.field_modulus + +G1_K_DISCOUNT = [ + 1000, + 949, + 848, + 797, + 764, + 750, + 738, + 728, + 719, + 712, + 705, + 698, + 692, + 687, + 682, + 677, + 673, + 669, + 665, + 661, + 658, + 654, + 651, + 648, + 645, + 642, + 640, + 637, + 635, + 632, + 630, + 627, + 625, + 623, + 621, + 619, + 617, + 615, + 613, + 611, + 609, + 608, + 606, + 604, + 603, + 601, + 599, + 598, + 596, + 595, + 593, + 592, + 591, + 589, + 588, + 586, + 585, + 584, + 582, + 581, + 580, + 579, + 577, + 576, + 575, + 574, + 573, + 572, + 570, + 569, + 568, + 567, + 566, + 565, + 564, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 551, + 550, + 549, + 548, + 547, + 547, + 546, + 545, + 544, + 543, + 542, + 541, + 540, + 540, + 539, + 538, + 537, + 536, + 536, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 529, + 528, + 528, + 527, + 526, + 525, + 525, + 524, + 523, + 522, + 522, + 521, + 520, + 520, + 519, +] + +G2_K_DISCOUNT = [ + 1000, + 1000, + 923, + 884, + 855, + 832, + 812, + 796, + 782, + 770, + 759, + 749, + 740, + 732, + 724, + 717, + 711, + 704, + 699, + 693, + 688, + 683, + 679, + 674, + 670, + 666, + 663, + 659, + 655, + 652, + 649, + 646, + 643, + 640, + 637, + 634, + 632, + 629, + 627, + 624, + 622, + 620, + 618, + 615, + 613, + 611, + 609, + 607, + 606, + 604, + 602, + 600, + 598, + 597, + 595, + 593, + 592, + 590, + 589, + 587, + 586, + 584, + 583, + 582, + 580, + 579, + 578, + 576, + 575, + 574, + 573, + 571, + 570, + 569, + 568, + 567, + 566, + 565, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 552, + 551, + 550, + 549, + 548, + 547, + 546, + 545, + 545, + 544, + 543, + 542, + 541, + 541, + 540, + 539, + 538, + 537, + 537, + 536, + 535, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 530, + 529, + 528, + 528, + 527, + 526, + 526, + 525, + 524, + 524, +] + +G1_MAX_DISCOUNT = 519 +G2_MAX_DISCOUNT = 524 +MULTIPLIER = Uint(1000) + + +def bytes_to_G1(data: Bytes) -> Point2D: + """ + Decode 128 bytes to a G1 point. Does not perform sub-group check. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point2D + The G1 point. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + """ + if len(data) != 128: + raise InvalidParameter("Input should be 128 bytes long") + + x = int.from_bytes(data[:64], "big") + y = int.from_bytes(data[64:], "big") + + if x >= P: + raise InvalidParameter("Invalid field element") + if y >= P: + raise InvalidParameter("Invalid field element") + + if x == 0 and y == 0: + return None + + point = (FQ(x), FQ(y)) + + # Check if the point is on the curve + if not is_on_curve(point, b): + raise InvalidParameter("Point is not on curve") + + return point + + +def G1_to_bytes(point: Point2D) -> Bytes: + """ + Encode a G1 point to 128 bytes. + + Parameters + ---------- + point : + The G1 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + """ + if point is None: + return b"\x00" * 128 + + x, y = point + + x_bytes = int(x).to_bytes(64, "big") + y_bytes = int(y).to_bytes(64, "big") + + return x_bytes + y_bytes + + +def decode_G1_scalar_pair(data: Bytes) -> Tuple[Point2D, int]: + """ + Decode 160 bytes to a G1 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point2D, int] + The G1 point and the scalar. + + Raises + ------ + InvalidParameter + If the sub-group check failed. + """ + if len(data) != 160: + InvalidParameter("Input should be 160 bytes long") + + p = bytes_to_G1(buffer_read(data, U256(0), U256(128))) + if multiply(p, curve_order) is not None: + raise InvalidParameter("Sub-group check failed.") + + m = int.from_bytes(buffer_read(data, U256(128), U256(32)), "big") + + return p, m + + +def bytes_to_FQ( + data: Bytes, optimized: bool = False +) -> Union[FQ, OPTIMIZED_FQ]: + """ + Decode 64 bytes to a FQ element. + + Parameters + ---------- + data : + The bytes data to decode. + optimized : + Whether to use the optimized FQ implementation. + + Returns + ------- + fq : Union[FQ, OPTIMIZED_FQ] + The FQ element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + """ + if len(data) != 64: + raise InvalidParameter("FQ should be 64 bytes long") + + c = int.from_bytes(data[:64], "big") + + if c >= P: + raise InvalidParameter("Invalid field element") + + if optimized: + return OPTIMIZED_FQ(c) + else: + return FQ(c) + + +def bytes_to_FQ2( + data: Bytes, optimized: bool = False +) -> Union[FQ2, OPTIMIZED_FQ2]: + """ + Decode 128 bytes to a FQ2 element. + + Parameters + ---------- + data : + The bytes data to decode. + optimized : + Whether to use the optimized FQ2 implementation. + + Returns + ------- + fq2 : Union[FQ2, OPTIMIZED_FQ2] + The FQ2 element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + """ + if len(data) != 128: + raise InvalidParameter("FQ2 input should be 128 bytes long") + c_0 = int.from_bytes(data[:64], "big") + c_1 = int.from_bytes(data[64:], "big") + + if c_0 >= P: + raise InvalidParameter("Invalid field element") + if c_1 >= P: + raise InvalidParameter("Invalid field element") + + if optimized: + return OPTIMIZED_FQ2((c_0, c_1)) + else: + return FQ2((c_0, c_1)) + + +def bytes_to_G2(data: Bytes) -> Point2D: + """ + Decode 256 bytes to a G2 point. Does not perform sub-group check. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point2D + The G2 point. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + """ + if len(data) != 256: + raise InvalidParameter("G2 should be 256 bytes long") + + x = bytes_to_FQ2(data[:128]) + y = bytes_to_FQ2(data[128:]) + + assert isinstance(x, FQ2) and isinstance(y, FQ2) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + return None + + point = (x, y) + + # Check if the point is on the curve + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + return point + + +def FQ2_to_bytes(fq2: FQ2) -> Bytes: + """ + Encode a FQ2 point to 128 bytes. + + Parameters + ---------- + fq2 : + The FQ2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + """ + c_0, c_1 = fq2.coeffs + return int(c_0).to_bytes(64, "big") + int(c_1).to_bytes(64, "big") + + +def G2_to_bytes(point: Point2D) -> Bytes: + """ + Encode a G2 point to 256 bytes. + + Parameters + ---------- + point : + The G2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + """ + if point is None: + return b"\x00" * 256 + + x, y = point + + return FQ2_to_bytes(x) + FQ2_to_bytes(y) + + +def decode_G2_scalar_pair(data: Bytes) -> Tuple[Point2D, int]: + """ + Decode 288 bytes to a G2 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point2D, int] + The G2 point and the scalar. + + Raises + ------ + InvalidParameter + If the sub-group check failed. + """ + if len(data) != 288: + InvalidParameter("Input should be 288 bytes long") + + p = bytes_to_G2(buffer_read(data, U256(0), U256(256))) + if multiply(p, curve_order) is not None: + raise InvalidParameter("Sub-group check failed.") + + m = int.from_bytes(buffer_read(data, U256(256), U256(32)), "big") + + return p, m diff --git a/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g1.py new file mode 100644 index 0000000000..541395de40 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g1.py @@ -0,0 +1,148 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 CONTRACTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G1 (curve over base prime field). +""" +from ethereum_types.numeric import U256, Uint +from py_ecc.bls12_381.bls12_381_curve import add, multiply +from py_ecc.bls.hash_to_curve import clear_cofactor_G1, map_to_curve_G1 +from py_ecc.optimized_bls12_381.optimized_curve import FQ as OPTIMIZED_FQ +from py_ecc.optimized_bls12_381.optimized_curve import normalize + +from ....vm import Evm +from ....vm.gas import ( + GAS_BLS_G1_ADD, + GAS_BLS_G1_MAP, + GAS_BLS_G1_MUL, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G1_K_DISCOUNT, + G1_MAX_DISCOUNT, + MULTIPLIER, + G1_to_bytes, + bytes_to_FQ, + bytes_to_G1, + decode_G1_scalar_pair, +) + +LENGTH_PER_PAIR = 160 + + +def bls12_g1_add(evm: Evm) -> None: + """ + The bls12_381 G1 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) != 256: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G1_ADD)) + + # OPERATION + p1 = bytes_to_G1(buffer_read(data, U256(0), U256(128))) + p2 = bytes_to_G1(buffer_read(data, U256(128), U256(128))) + + result = add(p1, p2) + + evm.output = G1_to_bytes(result) + + +def bls12_g1_msm(evm: Evm) -> None: + """ + The bls12_381 G1 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G1_K_DISCOUNT[k - 1]) + else: + discount = Uint(G1_MAX_DISCOUNT) + + gas_cost = Uint(k) * GAS_BLS_G1_MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_G1_scalar_pair(data[start_index:end_index]) + product = multiply(p, m) + + if i == 0: + result = product + else: + result = add(result, product) + + evm.output = G1_to_bytes(result) + + +def bls12_map_fp_to_g1(evm: Evm) -> None: + """ + Precompile to map field element to G1. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) != 64: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G1_MAP)) + + # OPERATION + field_element = bytes_to_FQ(data, True) + assert isinstance(field_element, OPTIMIZED_FQ) + + g1_uncompressed = clear_cofactor_G1(map_to_curve_G1(field_element)) + g1_normalised = normalize(g1_uncompressed) + + evm.output = G1_to_bytes(g1_normalised) diff --git a/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g2.py new file mode 100644 index 0000000000..bda7f3641f --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_g2.py @@ -0,0 +1,148 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 G2 CONTRACTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G2 (curve over base prime field). +""" +from ethereum_types.numeric import U256, Uint +from py_ecc.bls12_381.bls12_381_curve import add, multiply +from py_ecc.bls.hash_to_curve import clear_cofactor_G2, map_to_curve_G2 +from py_ecc.optimized_bls12_381.optimized_curve import FQ2 as OPTIMIZED_FQ2 +from py_ecc.optimized_bls12_381.optimized_curve import normalize + +from ....vm import Evm +from ....vm.gas import ( + GAS_BLS_G2_ADD, + GAS_BLS_G2_MAP, + GAS_BLS_G2_MUL, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G2_K_DISCOUNT, + G2_MAX_DISCOUNT, + MULTIPLIER, + G2_to_bytes, + bytes_to_FQ2, + bytes_to_G2, + decode_G2_scalar_pair, +) + +LENGTH_PER_PAIR = 288 + + +def bls12_g2_add(evm: Evm) -> None: + """ + The bls12_381 G2 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) != 512: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G2_ADD)) + + # OPERATION + p1 = bytes_to_G2(buffer_read(data, U256(0), U256(256))) + p2 = bytes_to_G2(buffer_read(data, U256(256), U256(256))) + + result = add(p1, p2) + + evm.output = G2_to_bytes(result) + + +def bls12_g2_msm(evm: Evm) -> None: + """ + The bls12_381 G2 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G2_K_DISCOUNT[k - 1]) + else: + discount = Uint(G2_MAX_DISCOUNT) + + gas_cost = Uint(k) * GAS_BLS_G2_MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_G2_scalar_pair(data[start_index:end_index]) + product = multiply(p, m) + + if i == 0: + result = product + else: + result = add(result, product) + + evm.output = G2_to_bytes(result) + + +def bls12_map_fp2_to_g2(evm: Evm) -> None: + """ + Precompile to map field element to G2. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + """ + data = evm.message.data + if len(data) != 128: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G2_MAP)) + + # OPERATION + field_element = bytes_to_FQ2(data, True) + assert isinstance(field_element, OPTIMIZED_FQ2) + + g2_uncompressed = clear_cofactor_G2(map_to_curve_G2(field_element)) + g2_normalised = normalize(g2_uncompressed) + + evm.output = G2_to_bytes(g2_normalised) diff --git a/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py new file mode 100644 index 0000000000..2a03a6897a --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py @@ -0,0 +1,67 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 PAIRING PRE-COMPILE +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the BLS12 381 pairing pre-compile. +""" +from ethereum_types.numeric import U256, Uint +from py_ecc.bls12_381.bls12_381_curve import FQ12, curve_order, multiply +from py_ecc.bls12_381.bls12_381_pairing import pairing + +from ....vm import Evm +from ....vm.gas import charge_gas +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import bytes_to_G1, bytes_to_G2 + + +def bls12_pairing(evm: Evm) -> None: + """ + The bls12_381 pairing precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid or if sub-group check fails. + """ + data = evm.message.data + if len(data) == 0 or len(data) % 384 != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // 384 + gas_cost = Uint(32600 * k + 37700) + charge_gas(evm, gas_cost) + + # OPERATION + result = FQ12.one() + for i in range(k): + g1_start = Uint(384 * i) + g2_start = Uint(384 * i + 128) + + g1_point = bytes_to_G1(buffer_read(data, U256(g1_start), U256(128))) + if multiply(g1_point, curve_order) is not None: + raise InvalidParameter("Sub-group check failed.") + + g2_point = bytes_to_G2(buffer_read(data, U256(g2_start), U256(256))) + if multiply(g2_point, curve_order) is not None: + raise InvalidParameter("Sub-group check failed.") + + result *= pairing(g2_point, g1_point) + + if result == FQ12.one(): + evm.output = b"\x00" * 31 + b"\x01" + else: + evm.output = b"\x00" * 32 diff --git a/src/ethereum/prague/vm/precompiled_contracts/mapping.py b/src/ethereum/prague/vm/precompiled_contracts/mapping.py index 7bd3416b6b..3094d8dff2 100644 --- a/src/ethereum/prague/vm/precompiled_contracts/mapping.py +++ b/src/ethereum/prague/vm/precompiled_contracts/mapping.py @@ -19,6 +19,13 @@ ALT_BN128_MUL_ADDRESS, ALT_BN128_PAIRING_CHECK_ADDRESS, BLAKE2F_ADDRESS, + BLS12_G1_ADD_ADDRESS, + BLS12_G1_MSM_ADDRESS, + BLS12_G2_ADD_ADDRESS, + BLS12_G2_MSM_ADDRESS, + BLS12_MAP_FP2_TO_G2_ADDRESS, + BLS12_MAP_FP_TO_G1_ADDRESS, + BLS12_PAIRING_ADDRESS, ECRECOVER_ADDRESS, IDENTITY_ADDRESS, MODEXP_ADDRESS, @@ -28,6 +35,17 @@ ) from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check from .blake2f import blake2f +from .bls12_381.bls12_381_g1 import ( + bls12_g1_add, + bls12_g1_msm, + bls12_map_fp_to_g1, +) +from .bls12_381.bls12_381_g2 import ( + bls12_g2_add, + bls12_g2_msm, + bls12_map_fp2_to_g2, +) +from .bls12_381.bls12_381_pairing import bls12_pairing from .ecrecover import ecrecover from .identity import identity from .modexp import modexp @@ -46,4 +64,11 @@ ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, BLAKE2F_ADDRESS: blake2f, POINT_EVALUATION_ADDRESS: point_evaluation, + BLS12_G1_ADD_ADDRESS: bls12_g1_add, + BLS12_G1_MSM_ADDRESS: bls12_g1_msm, + BLS12_G2_ADD_ADDRESS: bls12_g2_add, + BLS12_G2_MSM_ADDRESS: bls12_g2_msm, + BLS12_PAIRING_ADDRESS: bls12_pairing, + BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1, + BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2, } From f14f9c80ba0677940afafc8c079ae9f990f8f51f Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:14:18 +0100 Subject: [PATCH 05/38] Implement EIP-6110 --- src/ethereum/genesis.py | 6 ++ src/ethereum/prague/blocks.py | 1 + src/ethereum/prague/fork.py | 84 +++++++++++++++++++ src/ethereum/prague/requests.py | 72 ++++++++++++++++ .../evm_tools/loaders/fixture_loader.py | 4 + .../evm_tools/loaders/fork_loader.py | 15 ++++ .../evm_tools/t8n/__init__.py | 55 ++++++++++-- src/ethereum_spec_tools/evm_tools/t8n/env.py | 1 + .../evm_tools/t8n/t8n_types.py | 14 +++- src/ethereum_spec_tools/evm_tools/utils.py | 13 +++ 10 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 src/ethereum/prague/requests.py diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index 330022bd40..152a37c58b 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -255,6 +255,9 @@ def add_genesis_block( if has_field(hardfork.Header, "parent_beacon_block_root"): fields["parent_beacon_block_root"] = Hash32(b"\0" * 32) + if has_field(hardfork.Header, "requests_hash"): + fields["requests_hash"] = Hash32(b"\0" * 32) + genesis_header = hardfork.Header(**fields) block_fields = { @@ -266,6 +269,9 @@ def add_genesis_block( if has_field(hardfork.Block, "withdrawals"): block_fields["withdrawals"] = () + if has_field(hardfork.Block, "requests"): + block_fields["requests"] = () + genesis_block = hardfork.Block(**block_fields) chain.blocks.append(genesis_block) diff --git a/src/ethereum/prague/blocks.py b/src/ethereum/prague/blocks.py index 684b57aab7..15208d4ad0 100644 --- a/src/ethereum/prague/blocks.py +++ b/src/ethereum/prague/blocks.py @@ -68,6 +68,7 @@ class Header: blob_gas_used: U64 excess_blob_gas: U64 parent_beacon_block_root: Root + requests_hash: Hash32 @slotted_freezable diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index f215e86955..8e6da32588 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -26,6 +26,11 @@ 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 .requests import ( + DEPOSIT_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests_from_receipt, +) from .state import ( State, TransientStorage, @@ -212,6 +217,8 @@ def state_transition(chain: BlockChain, block: Block) -> None: raise InvalidBlock if apply_body_output.blob_gas_used != block.header.blob_gas_used: raise InvalidBlock + if apply_body_output.requests_hash != block.header.requests_hash: + raise InvalidBlock chain.blocks.append(block) if len(chain.blocks) > 255: @@ -490,6 +497,8 @@ class ApplyBodyOutput: 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 @@ -499,6 +508,7 @@ class ApplyBodyOutput: state_root: Root withdrawals_root: Root blob_gas_used: Uint + requests_hash: Bytes def apply_body( @@ -576,6 +586,7 @@ def apply_body( secured=False, default=None ) block_logs: Tuple[Log, ...] = () + deposit_requests: Bytes = b"" beacon_block_roots_contract_code = get_account( state, BEACON_ROOTS_ADDRESS @@ -673,6 +684,8 @@ def apply_body( receipt, ) + deposit_requests += parse_deposit_requests_from_receipt(receipt) + block_logs += logs blob_gas_used += calculate_total_blob_gas(tx) if blob_gas_used > MAX_BLOB_GAS_PER_BLOCK: @@ -689,6 +702,22 @@ def apply_body( 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, + ) + + requests_hash = compute_requests_hash(requests_from_execution) + return ApplyBodyOutput( block_gas_used, root(transactions_trie), @@ -697,9 +726,64 @@ def apply_body( state_root(state), root(withdrawals_trie), blob_gas_used, + requests_hash, ) +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]: + """ + 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 + """ + # Requests are to be in ascending order of request type + requests_from_execution: List[Bytes] = [] + if len(deposit_requests) > 0: + requests_from_execution.append(DEPOSIT_REQUEST_TYPE + deposit_requests) + + return requests_from_execution + + def process_transaction( env: vm.Environment, tx: Transaction ) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: diff --git a/src/ethereum/prague/requests.py b/src/ethereum/prague/requests.py new file mode 100644 index 0000000000..80d2c232d9 --- /dev/null +++ b/src/ethereum/prague/requests.py @@ -0,0 +1,72 @@ +""" +Requests were introduced in EIP-7685 as a a general purpose framework for +storing contract-triggered requests. It extends the execution header and +body with a single field each to store the request information. +This inherently exposes the requests to the consensus layer, which can +then process each one. + +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +""" + +from hashlib import sha256 +from typing import List, Union + +from ethereum_types.bytes import Bytes + +from .blocks import Receipt, decode_receipt +from .utils.hexadecimal import hex_to_address + +DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x00000000219ab540356cbb839cbe05303d7705fa" +) +DEPOSIT_REQUEST_TYPE = b"\x00" + + +def extract_deposit_data(data: Bytes) -> Bytes: + """ + Extracts Deposit Request from the DepositContract.DepositEvent data. + """ + return ( + data[192:240] # public_key + + data[288:320] # withdrawal_credentials + + data[352:360] # amount + + data[416:512] # signature + + data[544:552] # index + ) + + +def parse_deposit_requests_from_receipt( + receipt: Union[Bytes, Receipt], +) -> Bytes: + """ + Parse deposit requests from a receipt. + """ + deposit_requests: Bytes = b"" + decoded_receipt = decode_receipt(receipt) + for log in decoded_receipt.logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + request = extract_deposit_data(log.data) + deposit_requests += request + + return deposit_requests + + +def compute_requests_hash(requests: List[Bytes]) -> Bytes: + """ + Get the hash of the requests using the SHA2-256 algorithm. + + Parameters + ---------- + requests : Bytes + The requests to hash. + + Returns + ------- + requests_hash : Bytes + The hash of the requests. + """ + m = sha256() + for request in requests: + m.update(sha256(request).digest()) + + return m.digest() diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py index db6c5ade55..621a69eda3 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -178,4 +178,8 @@ def json_to_header(self, raw: Any) -> Any: ) parameters.append(parent_beacon_block_root) + if "requestsHash" in raw: + requests_hash = hex_to_bytes32(raw.get("requestsHash")) + parameters.append(requests_hash) + return self.fork.Header(*parameters) 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 906775a61d..0a7f1845b9 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -47,6 +47,11 @@ def is_after_fork(self, target_fork_name: str) -> bool: break return return_value + @property + def process_general_purpose_requests(self) -> Any: + """process_general_purpose_requests function of the given fork.""" + return self._module("fork").process_general_purpose_requests + @property def calculate_block_difficulty(self) -> Any: """calculate_block_difficulty function of the given fork.""" @@ -132,6 +137,16 @@ def Block(self) -> Any: """Block class of the fork""" 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 + + @property + def compute_requests_hash(self) -> Any: + """compute_requests_hash function of the fork""" + return self._module("requests").compute_requests_hash + @property def Bloom(self) -> Any: """Bloom class of the fork""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index a5c2be531f..63b7bbf1b5 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -9,6 +9,7 @@ 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 import trace @@ -73,6 +74,8 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: t8n_parser.add_argument("--trace.nostack", action="store_true") t8n_parser.add_argument("--trace.returndata", action="store_true") + t8n_parser.add_argument("--state-test", action="store_true") + class T8N(Load): """The class that carries out the transition""" @@ -124,15 +127,6 @@ def __init__( self.env.block_difficulty, self.env.base_fee_per_gas ) - if self.fork.is_after_fork("ethereum.cancun"): - self.SYSTEM_ADDRESS = self.fork.hex_to_address( - "0xfffffffffffffffffffffffffffffffffffffffe" - ) - self.BEACON_ROOTS_ADDRESS = self.fork.hex_to_address( - "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" - ) - self.SYSTEM_TRANSACTION_GAS = Uint(30000000) - @property def BLOCK_REWARD(self) -> Any: """ @@ -313,6 +307,12 @@ def apply_body(self) -> 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"" + if ( self.fork.is_after_fork("ethereum.cancun") @@ -410,6 +410,13 @@ def apply_body(self) -> None: 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) @@ -447,6 +454,29 @@ def apply_body(self) -> None: 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, + ) + ) + requests_hash = self.fork.compute_requests_hash( + requests_from_execution + ) + 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) @@ -456,6 +486,13 @@ def apply_body(self) -> None: self.result.receipts = self.txs.successful_receipts self.result.gas_used = block_gas_used + 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 + def run(self) -> int: """Run the transition and provide the relevant outputs""" try: diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 802c524304..373d9d0a8b 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -52,6 +52,7 @@ class Env: parent_excess_blob_gas: Optional[U64] parent_blob_gas_used: Optional[U64] excess_blob_gas: Optional[U64] + requests: Any def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): if t8n.options.input_env == "stdin": 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 6c76210fac..d5f0f67284 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -9,11 +9,11 @@ from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint -from ethereum.crypto.hash import keccak256 +from ethereum.crypto.hash import Hash32, keccak256 from ethereum.utils.hexadecimal import hex_to_bytes, hex_to_u256, hex_to_uint from ..loaders.transaction_loader import TransactionLoad, UnsupportedTx -from ..utils import FatalException, secp256k1_sign +from ..utils import FatalException, encode_to_hex, secp256k1_sign if TYPE_CHECKING: from . import T8N @@ -309,6 +309,8 @@ class Result: gas_used: Any = None excess_blob_gas: Optional[U64] = None blob_gas_used: Optional[Uint] = None + requests_hash: Optional[Hash32] = None + requests: Optional[List[Bytes]] = None def to_json(self) -> Any: """Encode the result to JSON""" @@ -351,4 +353,12 @@ def to_json(self) -> Any: for item in self.receipts ] + if self.requests_hash is not None: + assert self.requests is not None + + data["requestsHash"] = encode_to_hex(self.requests_hash) + # T8N doesn't consider the request type byte to be part of the + # request + data["requests"] = [encode_to_hex(req) for req in self.requests] + return data diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index 258f9d1966..fb4ea8870f 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -14,6 +14,7 @@ Sequence, Tuple, TypeVar, + Union, ) import coincurve @@ -183,3 +184,15 @@ def secp256k1_sign(msg_hash: Hash32, secret_key: int) -> Tuple[U256, ...]: U256.from_be_bytes(signature[32:64]), U256(signature[64]), ) + + +def encode_to_hex(data: Union[bytes, int]) -> str: + """ + Encode the data to a hex string. + """ + if isinstance(data, int): + return hex(data) + elif isinstance(data, bytes): + return "0x" + data.hex() + else: + raise Exception("Invalid data type") From 247fd14d0edf10953c6655029a576f8fc0030f2e Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:31:07 +0100 Subject: [PATCH 06/38] Implement EIP-7002 --- src/ethereum/prague/fork.py | 185 +++++++++++++----- src/ethereum/prague/requests.py | 1 + .../evm_tools/loaders/fork_loader.py | 25 +++ .../evm_tools/t8n/__init__.py | 59 ++---- 4 files changed, 180 insertions(+), 90 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 8e6da32588..752a6321c0 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -28,6 +28,7 @@ from .fork_types import Address, Authorization, Bloom, Root, VersionedHash from .requests import ( DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, compute_requests_hash, parse_deposit_requests_from_receipt, ) @@ -67,7 +68,7 @@ calculate_excess_blob_gas, calculate_total_blob_gas, ) -from .vm.interpreter import process_message_call +from .vm.interpreter import MessageCallOutput, process_message_call BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) ELASTICITY_MULTIPLIER = Uint(2) @@ -82,6 +83,10 @@ MAX_BLOB_GAS_PER_BLOCK = Uint(786432) VERSIONED_HASH_VERSION_KZG = b"\x01" +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA" +) + @dataclass class BlockChain: @@ -511,6 +516,108 @@ class ApplyBodyOutput: requests_hash: Bytes +def process_system_transaction( + 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 + ---------- + 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_tx_message = Message( + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=data, + code=system_contract_code, + depth=Uint(0), + current_target=target_address, + code_address=target_address, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + 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) + + # TODO: Empty accounts in post-merge forks are impossible + # see Ethereum Improvement Proposal 7523. + # This line is only included to support invalid tests in the test suite + # 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 + ) + + return system_tx_output + + def apply_body( state: State, block_hashes: List[Hash32], @@ -588,51 +695,21 @@ def apply_body( block_logs: Tuple[Log, ...] = () deposit_requests: Bytes = b"" - beacon_block_roots_contract_code = get_account( - state, BEACON_ROOTS_ADDRESS - ).code - - system_tx_message = Message( - caller=SYSTEM_ADDRESS, - target=BEACON_ROOTS_ADDRESS, - gas=SYSTEM_TRANSACTION_GAS, - value=U256(0), - data=parent_beacon_block_root, - code=beacon_block_roots_contract_code, - depth=Uint(0), - current_target=BEACON_ROOTS_ADDRESS, - code_address=BEACON_ROOTS_ADDRESS, - should_transfer_value=False, - is_static=False, - accessed_addresses=set(), - accessed_storage_keys=set(), - parent_evm=None, - ) - - system_tx_env = vm.Environment( - caller=SYSTEM_ADDRESS, - origin=SYSTEM_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=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(), + 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, ) - system_tx_output = process_message_call(system_tx_message, system_tx_env) - - destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts - ) for i, tx in enumerate(map(decode_transaction, transactions)): trie_set( @@ -781,6 +858,26 @@ def process_general_purpose_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, + ) + + if len(system_withdrawal_tx_output.return_data) > 0: + requests_from_execution.append( + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + ) + return requests_from_execution diff --git a/src/ethereum/prague/requests.py b/src/ethereum/prague/requests.py index 80d2c232d9..2516783ce2 100644 --- a/src/ethereum/prague/requests.py +++ b/src/ethereum/prague/requests.py @@ -20,6 +20,7 @@ "0x00000000219ab540356cbb839cbe05303d7705fa" ) DEPOSIT_REQUEST_TYPE = b"\x00" +WITHDRAWAL_REQUEST_TYPE = b"\x01" def extract_deposit_data(data: Bytes) -> Bytes: 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 0a7f1845b9..d1999ca2b1 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -47,11 +47,36 @@ 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.""" + return self._module("fork").BEACON_ROOTS_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 process_general_purpose_requests(self) -> Any: """process_general_purpose_requests function of the given fork.""" return self._module("fork").process_general_purpose_requests + @property + def process_system_transaction(self) -> Any: + """process_system_transaction function of the given fork.""" + return self._module("fork").process_system_transaction + @property def calculate_block_difficulty(self) -> Any: """calculate_block_difficulty function of the given fork.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 63b7bbf1b5..e08db18aff 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -318,52 +318,19 @@ def apply_body(self) -> None: self.fork.is_after_fork("ethereum.cancun") and self.env.parent_beacon_block_root is not None ): - beacon_block_roots_contract_code = self.fork.get_account( - self.alloc.state, self.BEACON_ROOTS_ADDRESS - ).code - - system_tx_message = self.fork.Message( - caller=self.SYSTEM_ADDRESS, - target=self.BEACON_ROOTS_ADDRESS, - gas=self.SYSTEM_TRANSACTION_GAS, - value=U256(0), - data=self.env.parent_beacon_block_root, - code=beacon_block_roots_contract_code, - depth=Uint(0), - current_target=self.BEACON_ROOTS_ADDRESS, - code_address=self.BEACON_ROOTS_ADDRESS, - should_transfer_value=False, - is_static=False, - accessed_addresses=set(), - accessed_storage_keys=set(), - parent_evm=None, - ) - - system_tx_env = self.fork.Environment( - caller=self.SYSTEM_ADDRESS, - origin=self.SYSTEM_ADDRESS, - block_hashes=self.env.block_hashes, - coinbase=self.env.coinbase, - number=self.env.block_number, - gas_limit=self.env.block_gas_limit, - base_fee_per_gas=self.env.base_fee_per_gas, - gas_price=self.env.base_fee_per_gas, - time=self.env.block_timestamp, - prev_randao=self.env.prev_randao, - state=self.alloc.state, - chain_id=self.chain_id, - traces=[], - excess_blob_gas=self.env.excess_blob_gas, - blob_versioned_hashes=(), - transient_storage=self.fork.TransientStorage(), - ) - - system_tx_output = self.fork.process_message_call( - system_tx_message, system_tx_env - ) - - self.fork.destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts + 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, ) for i, (tx_idx, tx) in enumerate(self.txs.transactions): From 89d6faf2a7307f6197e3e13a29a45f39bee352df Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:38:02 +0100 Subject: [PATCH 07/38] Implement EIP-7251 --- src/ethereum/prague/fork.py | 25 +++++++++++++++++++ src/ethereum/prague/requests.py | 1 + .../evm_tools/loaders/fork_loader.py | 5 ++++ 3 files changed, 31 insertions(+) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 752a6321c0..4a534ad788 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -27,6 +27,7 @@ from .bloom import logs_bloom from .fork_types import Address, Authorization, Bloom, Root, VersionedHash from .requests import ( + CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, WITHDRAWAL_REQUEST_TYPE, compute_requests_hash, @@ -86,6 +87,9 @@ WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA" ) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00431F263cE400f4455c2dCf564e53007Ca4bbBb" +) @dataclass @@ -878,6 +882,27 @@ def process_general_purpose_requests( WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data ) + 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, + ) + + if len(system_consolidation_tx_output.return_data) > 0: + requests_from_execution.append( + CONSOLIDATION_REQUEST_TYPE + + system_consolidation_tx_output.return_data + ) + return requests_from_execution diff --git a/src/ethereum/prague/requests.py b/src/ethereum/prague/requests.py index 2516783ce2..a72f8f35ae 100644 --- a/src/ethereum/prague/requests.py +++ b/src/ethereum/prague/requests.py @@ -21,6 +21,7 @@ ) DEPOSIT_REQUEST_TYPE = b"\x00" WITHDRAWAL_REQUEST_TYPE = b"\x01" +CONSOLIDATION_REQUEST_TYPE = b"\x02" def extract_deposit_data(data: Bytes) -> Bytes: 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 d1999ca2b1..c48444b02e 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -67,6 +67,11 @@ 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 process_general_purpose_requests(self) -> Any: """process_general_purpose_requests function of the given fork.""" From b2177e767aef641e5fb5f7a6060e794e59f39111 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:44:14 +0100 Subject: [PATCH 08/38] Implement EIP-2935 --- src/ethereum/prague/fork.py | 18 ++++++++++++++++++ .../evm_tools/loaders/fork_loader.py | 10 ++++++++++ .../evm_tools/t8n/__init__.py | 14 ++++++++++++++ src/ethereum_spec_tools/evm_tools/t8n/env.py | 9 +++++++-- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 4a534ad788..ba66bccb33 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -90,6 +90,10 @@ CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( "0x00431F263cE400f4455c2dCf564e53007Ca4bbBb" ) +HISTORY_STORAGE_ADDRESS = hex_to_address( + "0x0F792be4B0c0cb4DAE440Ef133E90C0eCD48CCCC" +) +HISTORY_SERVE_WINDOW = 8192 @dataclass @@ -714,6 +718,20 @@ def apply_body( excess_blob_gas, ) + 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, + ) for i, tx in enumerate(map(decode_transaction, transactions)): trie_set( 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 c48444b02e..2409e5a883 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -62,6 +62,11 @@ def BEACON_ROOTS_ADDRESS(self) -> Any: """BEACON_ROOTS_ADDRESS of the given fork.""" return self._module("fork").BEACON_ROOTS_ADDRESS + @property + 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.""" @@ -72,6 +77,11 @@ 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.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index e08db18aff..1868d45717 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -313,6 +313,20 @@ def apply_body(self) -> None: ): deposit_requests: Bytes = b"" + 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, + ) if ( self.fork.is_after_fork("ethereum.cancun") diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 373d9d0a8b..6d272788a7 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -36,6 +36,7 @@ class Env: block_gas_limit: Uint block_number: Uint block_timestamp: U256 + parent_hash: Any withdrawals: Any block_difficulty: Optional[Uint] prev_randao: Optional[Bytes32] @@ -70,7 +71,7 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.read_block_difficulty(data, t8n) self.read_base_fee_per_gas(data, t8n) self.read_randao(data, t8n) - self.read_block_hashes(data) + self.read_block_hashes(data, t8n) self.read_ommers(data, t8n) self.read_withdrawals(data, t8n) @@ -231,10 +232,14 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: args.append(False) self.block_difficulty = t8n.fork.calculate_block_difficulty(*args) - def read_block_hashes(self, data: Any) -> None: + def read_block_hashes(self, data: Any, t8n: "T8N") -> None: """ Read the block hashes. Returns a maximum of 256 block hashes. """ + self.parent_hash = None + if t8n.fork.is_after_fork("ethereum.prague"): + self.parent_hash = Hash32(hex_to_bytes(data["parentHash"])) + # Read the block hashes block_hashes: List[Any] = [] # Store a maximum of 256 block hashes. From 457cb216d703b85956f0e9ef5f1536ce5ab9db7a Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:49:37 +0100 Subject: [PATCH 09/38] Implement EIP-7623 --- src/ethereum/prague/fork.py | 33 +++++++++++++++------ src/ethereum/prague/transactions.py | 46 ++++++++++++++++++++--------- whitelist.txt | 2 ++ 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index ba66bccb33..c673746cda 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -46,6 +46,8 @@ state_root, ) from .transactions import ( + FLOOR_CALLDATA_COST, + TX_BASE_COST, AccessListTransaction, BlobTransaction, FeeMarketTransaction, @@ -966,7 +968,9 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + intrinsic_gas, tokens_in_calldata = calculate_intrinsic_cost(tx) + + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( @@ -1009,17 +1013,28 @@ def process_transaction( output = process_message_call(message, env) - 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 + # For EIP-7623 we first calculate the execution_gas_used, which includes + # the execution gas refund. + execution_gas_used = tx.gas - output.gas_left + gas_refund = min( + execution_gas_used // Uint(5), Uint(output.refund_counter) + ) + execution_gas_used -= gas_refund + + # EIP-7623 floor price (note: no EVM costs) + floor_gas_cost = Uint( + tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST + ) + # Transactions with less execution_gas_used than the floor pay at the + # floor cost. + total_gas_used = max(execution_gas_used, floor_gas_cost) + + output.gas_left = tx.gas - total_gas_used + gas_refund_amount = output.gas_left * env.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 = ( - tx.gas - output.gas_left - gas_refund - ) * priority_fee_per_gas - - total_gas_used = gas_used - gas_refund + transaction_fee = total_gas_used * priority_fee_per_gas # refund gas sender_balance_after_refund = get_account( diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index 153f08b655..00594c790c 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -18,12 +18,12 @@ from .exceptions import TransactionTypeError from .fork_types import Address, Authorization, 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) +FLOOR_CALLDATA_COST = Uint(10) +STANDARD_CALLDATA_TOKEN_COST = Uint(4) +TX_CREATE_COST = Uint(32000) +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) @slotted_freezable @@ -203,7 +203,9 @@ def validate_transaction(tx: Transaction) -> bool: """ from .vm.interpreter import MAX_CODE_SIZE - if calculate_intrinsic_cost(tx) > tx.gas: + intrinsic_gas, tokens_in_calldata = calculate_intrinsic_cost(tx) + gas_floor = Uint(tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST) + if max(intrinsic_gas, gas_floor) > tx.gas: return False if U256(tx.nonce) >= U256(U64.MAX_VALUE): return False @@ -213,7 +215,7 @@ def validate_transaction(tx: Transaction) -> bool: return True -def calculate_intrinsic_cost(tx: Transaction) -> Uint: +def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: """ Calculates the gas that is charged before execution is started. @@ -235,17 +237,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: ------- verified : `ethereum.base_types.Uint` The intrinsic cost of the transaction. + tokens_in_calldata : `ethereum.base_types.Uint` + The eip-7623 calldata tokens used by the transaction. """ from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST from .vm.gas import init_code_cost - data_cost = 0 - + zero_bytes = 0 for byte in tx.data: if byte == 0: - data_cost += TX_DATA_COST_PER_ZERO - else: - data_cost += TX_DATA_COST_PER_NON_ZERO + zero_bytes += 1 + + tokens_in_calldata = zero_bytes + (len(tx.data) - zero_bytes) * 4 + + data_cost = Uint(tokens_in_calldata) * STANDARD_CALLDATA_TOKEN_COST if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) @@ -266,7 +271,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST - return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + auth_cost = Uint(0) + if isinstance(tx, SetCodeTransaction): + auth_cost += Uint(PER_EMPTY_ACCOUNT_COST * len(tx.authorizations)) + + return ( + Uint( + TX_BASE_COST + + data_cost + + create_cost + + access_list_cost + + auth_cost + ), + Uint(tokens_in_calldata), + ) def recover_sender(chain_id: U64, tx: Transaction) -> Address: diff --git a/whitelist.txt b/whitelist.txt index 6dfd7ae214..48b50366b4 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -12,12 +12,14 @@ Bytes64 Bytes8 Bytes256 Bytes0 +calldata copyreg copytree coinbase coincurve crypto E501 +EIP encodings endian eth From 9a5437abe66fed9735a1450484f86bdd53b1b860 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:53:14 +0100 Subject: [PATCH 10/38] Implement EIP-7691 --- src/ethereum/prague/fork.py | 2 +- src/ethereum/prague/vm/gas.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index c673746cda..47ddba0ad1 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -83,7 +83,7 @@ "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" ) SYSTEM_TRANSACTION_GAS = Uint(30000000) -MAX_BLOB_GAS_PER_BLOCK = Uint(786432) +MAX_BLOB_GAS_PER_BLOCK = Uint(1179648) VERSIONED_HASH_VERSION_KZG = b"\x01" WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( diff --git a/src/ethereum/prague/vm/gas.py b/src/ethereum/prague/vm/gas.py index 32e35cdd0d..f01e35b3a5 100644 --- a/src/ethereum/prague/vm/gas.py +++ b/src/ethereum/prague/vm/gas.py @@ -68,10 +68,10 @@ GAS_BLOBHASH_OPCODE = Uint(3) GAS_POINT_EVALUATION = Uint(50000) -TARGET_BLOB_GAS_PER_BLOCK = U64(393216) +TARGET_BLOB_GAS_PER_BLOCK = U64(786432) GAS_PER_BLOB = Uint(2**17) MIN_BLOB_GASPRICE = Uint(1) -BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) +BLOB_GASPRICE_UPDATE_FRACTION = Uint(5007716) GAS_BLS_G1_ADD = Uint(375) GAS_BLS_G1_MUL = Uint(12000) From 8892006958c47ed8efaa01c8241d336b5244812d Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 13:58:05 +0100 Subject: [PATCH 11/38] run tests --- tests/helpers/__init__.py | 5 +++ tests/helpers/load_state_tests.py | 3 ++ tests/prague/test_evm_tools.py | 30 ++++++++++++---- tests/prague/test_rlp.py | 2 ++ tests/prague/test_state_transition.py | 52 +++++++++++++++++---------- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 41cfd188a7..06d833b7bb 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -15,4 +15,9 @@ "commit_hash": "a0e8482", "fixture_path": "tests/fixtures/ethereum_tests", }, + "latest_fork_tests": { + "url": "https://github.com/gurukamath/latest_fork_tests.git", + "commit_hash": "e7ccbb6", + "fixture_path": "tests/fixtures/latest_fork_tests", + }, } diff --git a/tests/helpers/load_state_tests.py b/tests/helpers/load_state_tests.py index eca1953ae3..94ada0bffa 100644 --- a/tests/helpers/load_state_tests.py +++ b/tests/helpers/load_state_tests.py @@ -46,6 +46,9 @@ def run_blockchain_st_test(test_case: Dict, load: Load) -> None: if hasattr(genesis_header, "withdrawals_root"): parameters.append(()) + if hasattr(genesis_header, "requests_root"): + parameters.append(()) + genesis_block = load.fork.Block(*parameters) genesis_header_hash = hex_to_bytes(json_data["genesisBlockHeader"]["hash"]) diff --git a/tests/prague/test_evm_tools.py b/tests/prague/test_evm_tools.py index 781f77bb2b..3879ee1d65 100644 --- a/tests/prague/test_evm_tools.py +++ b/tests/prague/test_evm_tools.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Dict +from typing import Dict, Generator, Tuple import pytest @@ -25,17 +25,35 @@ "CALLBlake2f", "loopExp", "loopMul", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-bls_pairing_non-degeneracy-]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-bls_pairing_bilinearity-]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-bls_pairing_e(G1,-G2)=e(-G1,G2)-]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-bls_pairing_e(aG1,bG2)=e(abG1,G2)-]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-bls_pairing_e(aG1,bG2)=e(G1,abG2)-]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py::test_valid[fork_Prague-state_test-inf_pair-]", + "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", +) + + +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, + ) + @pytest.mark.evm_tools @pytest.mark.parametrize( "test_case", - fetch_evm_tools_tests( - TEST_DIR, - FORK_NAME, - SLOW_TESTS, - ), + fetch_temporary_tests(test_dirs), ids=idfn, ) def test_evm_tools(test_case: Dict) -> None: diff --git a/tests/prague/test_rlp.py b/tests/prague/test_rlp.py index 3a76f83b3d..2dc25b675d 100644 --- a/tests/prague/test_rlp.py +++ b/tests/prague/test_rlp.py @@ -22,6 +22,7 @@ hash4 = keccak256(b"foobar") hash5 = keccak256(b"quux") hash6 = keccak256(b"foobarbaz") +hash7 = keccak256(b"quuxbaz") address1 = hex_to_address("0x00000000219ab540356cbb839cbe05303d7705fa") address2 = hex_to_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") @@ -105,6 +106,7 @@ parent_beacon_block_root=Bytes32(b"1234567890abcdef1234567890abcdef"), blob_gas_used=U64(7), excess_blob_gas=U64(8), + requests_hash=hash7, ) block = Block( diff --git a/tests/prague/test_state_transition.py b/tests/prague/test_state_transition.py index 60e34a24ed..f47d9f6a66 100644 --- a/tests/prague/test_state_transition.py +++ b/tests/prague/test_state_transition.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Dict +from typing import Dict, Generator, Tuple import pytest @@ -11,11 +11,11 @@ run_blockchain_st_test, ) -fetch_cancun_tests = partial(fetch_state_test_files, network="Prague") +fetch_prague_tests = partial(fetch_state_test_files, network="Prague") FIXTURES_LOADER = Load("Prague", "prague") -run_cancun_blockchain_st_tests = partial( +run_prague_blockchain_st_tests = partial( run_blockchain_st_test, load=FIXTURES_LOADER ) @@ -34,8 +34,8 @@ "stTimeConsuming/static_Call50000_sha256.json", "vmPerformance/loopExp.json", "vmPerformance/loopMul.json", - "QuadraticComplexitySolidity_CallDataCopy_d0g1v0_Cancun", - "CALLBlake2f_d9g0v0_Cancun", + "QuadraticComplexitySolidity_CallDataCopy_d0g1v0_Prague", + "CALLBlake2f_d9g0v0_Prague", "CALLCODEBlake2f_d9g0v0", # GeneralStateTests "stRandom/randomStatetest177.json", @@ -45,6 +45,14 @@ # InvalidBlockTest "bcUncleHeaderValidity/nonceWrong.json", "bcUncleHeaderValidity/wrongMixHash.json", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-bls_pairing_non-degeneracy-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-bls_pairing_bilinearity-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-bls_pairing_e\\(G1,-G2\\)=e\\(-G1,G2\\)-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-bls_pairing_e\\(aG1,bG2\\)=e\\(abG1,G2\\)-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-bls_pairing_e\\(aG1,bG2\\)=e\\(G1,abG2\\)-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-inf_pair-\\]", + "tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing\\.py\\:\\:test_valid\\[fork_Prague-blockchain_test-multi_inf_pair-\\]", + "tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes\\.py\\:\\:test_block_hashes_history\\[fork_Prague-blockchain_test-full_history_plus_one_check_blockhash_first\\]", ) # These are tests that are considered to be incorrect, @@ -58,7 +66,7 @@ # InvalidBlockTest "bcForgedTest", "bcMultiChainTest", - "GasLimitHigherThan2p63m1_Cancun", + "GasLimitHigherThan2p63m1_Prague", ) # All tests that recursively create a large number of frames (50000) @@ -75,31 +83,39 @@ ) fetch_state_tests = partial( - fetch_cancun_tests, - test_dir, + fetch_prague_tests, ignore_list=IGNORE_TESTS, slow_list=SLOW_TESTS, big_memory_list=BIG_MEMORY_TESTS, ) -@pytest.mark.parametrize( - "test_case", - fetch_state_tests(), - ids=idfn, +# 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", ) -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 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) @pytest.mark.parametrize( "test_case", - fetch_cancun_tests(test_dir), + fetch_temporary_tests(test_dirs), ids=idfn, ) def test_execution_specs_generated_tests(test_case: Dict) -> None: - run_cancun_blockchain_st_tests(test_case) + run_prague_blockchain_st_tests(test_case) From d55ada18a1f9e6e8f77d2c5d83b0ade30a47e55f Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 14:00:36 +0100 Subject: [PATCH 12/38] catch invalid signatures --- src/ethereum/crypto/elliptic_curve.py | 31 +++++++++++++++++-- .../vm/precompiled_contracts/ecrecover.py | 3 +- whitelist.txt | 2 ++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ethereum/crypto/elliptic_curve.py b/src/ethereum/crypto/elliptic_curve.py index 3e5e5dbdc3..76970900f2 100644 --- a/src/ethereum/crypto/elliptic_curve.py +++ b/src/ethereum/crypto/elliptic_curve.py @@ -9,9 +9,15 @@ from ethereum_types.bytes import Bytes from ethereum_types.numeric import U256 +from ethereum.exceptions import InvalidSignatureError + from .finite_field import Field from .hash import Hash32 +SECP256K1B = U256(7) +SECP256K1P = U256( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +) SECP256K1N = U256( 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 ) @@ -40,6 +46,17 @@ def secp256k1_recover(r: U256, s: U256, v: U256, msg_hash: Hash32) -> Bytes: public_key : `ethereum.base_types.Bytes` Recovered public key. """ + is_square = pow( + pow(r, U256(3), SECP256K1P) + SECP256K1B, + (SECP256K1P - U256(1)) // U256(2), + SECP256K1P, + ) + + if is_square != 1: + raise InvalidSignatureError( + "r is not the x-coordinate of a point on the secp256k1 curve" + ) + r_bytes = r.to_be_bytes32() s_bytes = s.to_be_bytes32() @@ -47,9 +64,17 @@ def secp256k1_recover(r: U256, s: U256, v: U256, msg_hash: Hash32) -> Bytes: signature[32 - len(r_bytes) : 32] = r_bytes signature[64 - len(s_bytes) : 64] = s_bytes signature[64] = v - public_key = coincurve.PublicKey.from_signature_and_message( - bytes(signature), msg_hash, hasher=None - ) + + # If the recovery algorithm returns the point at infinity, + # the signature is considered invalid + # the below function will raise a ValueError. + try: + public_key = coincurve.PublicKey.from_signature_and_message( + bytes(signature), msg_hash, hasher=None + ) + except ValueError as e: + raise InvalidSignatureError from e + public_key = public_key.format(compressed=False)[1:] return public_key diff --git a/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/whitelist.txt b/whitelist.txt index 48b50366b4..8281e2ff3b 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -66,6 +66,8 @@ U8 ulen secp256k1 secp256k1n +secp256k1p +secp256k1b statetest subclasses iadd From c1d262ebcdbe8563f40cd067fd9c67ae89b0a141 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 14:02:49 +0100 Subject: [PATCH 13/38] add state test flag --- .../evm_tools/t8n/__init__.py | 7 +++++-- src/ethereum_spec_tools/evm_tools/t8n/env.py | 19 ++++++++++++------- tests/helpers/load_evm_tools_tests.py | 1 + 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 1868d45717..c835147d4d 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -330,7 +330,7 @@ def apply_body(self) -> None: if ( self.fork.is_after_fork("ethereum.cancun") - and self.env.parent_beacon_block_root is not None + and not self.options.state_test ): self.fork.process_system_transaction( self.fork.BEACON_ROOTS_ADDRESS, @@ -414,7 +414,10 @@ def apply_body(self) -> None: logs_hash = keccak256(rlp.encode(block_logs)) - if self.fork.is_after_fork("ethereum.shanghai"): + 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): diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 6d272788a7..d1cff3975a 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -75,13 +75,15 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.read_ommers(data, t8n) self.read_withdrawals(data, t8n) + self.parent_beacon_block_root = None if t8n.fork.is_after_fork("ethereum.cancun"): - parent_beacon_block_root_hex = data.get("parentBeaconBlockRoot") - self.parent_beacon_block_root = ( - Bytes32(hex_to_bytes(parent_beacon_block_root_hex)) - if parent_beacon_block_root_hex is not None - else None - ) + if not t8n.options.state_test: + parent_beacon_block_root_hex = data["parentBeaconBlockRoot"] + self.parent_beacon_block_root = ( + Bytes32(hex_to_bytes(parent_beacon_block_root_hex)) + if parent_beacon_block_root_hex is not None + else None + ) self.read_excess_blob_gas(data, t8n) def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: @@ -237,7 +239,10 @@ def read_block_hashes(self, data: Any, t8n: "T8N") -> None: Read the block hashes. Returns a maximum of 256 block hashes. """ self.parent_hash = None - if t8n.fork.is_after_fork("ethereum.prague"): + if ( + t8n.fork.is_after_fork("ethereum.prague") + and not t8n.options.state_test + ): self.parent_hash = Hash32(hex_to_bytes(data["parentHash"])) # Read the block hashes diff --git a/tests/helpers/load_evm_tools_tests.py b/tests/helpers/load_evm_tools_tests.py index f02f77c91d..82ff9abe37 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -122,6 +122,7 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: "stdin", "--state.fork", f"{fork_name}", + "--state-test", ] t8n_options = parser.parse_args(t8n_args) From 5a32df1d8e184aae903ef3ed6774a57f8659af42 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 23 Jan 2025 14:48:45 +0100 Subject: [PATCH 14/38] run only prague tests --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 647714587b..7df4d42969 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ commands = pytest \ -m "not slow" \ -n auto --maxprocesses 5 \ + tests/prague \ --cov=ethereum --cov-report=term --cov-report "xml:{toxworkdir}/coverage.xml" \ --ignore-glob='tests/fixtures/*' \ --basetemp="{temp_dir}/pytest" @@ -38,6 +39,7 @@ commands = --disable-warnings \ -m "not slow" \ -n auto --maxprocesses 2 \ + tests/prague \ --ignore-glob='tests/fixtures/*' \ --basetemp="{temp_dir}/pytest" @@ -51,6 +53,7 @@ commands = pytest \ -m "not slow and not evm_tools" \ -n auto --maxprocesses 5 \ + tests/prague \ --ignore-glob='tests/fixtures/*' \ --ignore-glob='tests/test_t8n.py' \ --basetemp="{temp_dir}/pytest" \ From 0c56b700a9ce5a1d336c81468eda9d2ad7e55626 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 24 Jan 2025 09:27:42 +0100 Subject: [PATCH 15/38] re-factor validate_transaction --- src/ethereum/prague/fork.py | 14 ++------- src/ethereum/prague/transactions.py | 46 ++++++++++++++++++----------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 47ddba0ad1..f18c28d11d 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -46,15 +46,12 @@ state_root, ) from .transactions import ( - FLOOR_CALLDATA_COST, - TX_BASE_COST, AccessListTransaction, BlobTransaction, FeeMarketTransaction, LegacyTransaction, SetCodeTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -955,8 +952,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -968,8 +964,6 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - intrinsic_gas, tokens_in_calldata = calculate_intrinsic_cost(tx) - gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) @@ -1021,13 +1015,9 @@ def process_transaction( ) execution_gas_used -= gas_refund - # EIP-7623 floor price (note: no EVM costs) - floor_gas_cost = Uint( - tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST - ) # Transactions with less execution_gas_used than the floor pay at the # floor cost. - total_gas_used = max(execution_gas_used, floor_gas_cost) + total_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 diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index 00594c790c..f10f954fab 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address, Authorization, VersionedHash @@ -176,7 +176,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: """ Verifies a transaction. @@ -198,21 +198,28 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + calldata_floor_gas_cost : `ethereum.base_types.Uint` + The eip-7623 minimum gas cost charged to the transaction + based on the calldata size. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ from .vm.interpreter import MAX_CODE_SIZE - intrinsic_gas, tokens_in_calldata = calculate_intrinsic_cost(tx) - gas_floor = Uint(tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST) - if max(intrinsic_gas, gas_floor) > tx.gas: - return False + intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) + if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: + raise InvalidBlock if U256(tx.nonce) >= U256(U64.MAX_VALUE): - return False + raise InvalidBlock if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - return False + raise InvalidBlock - return True + return intrinsic_gas, calldata_floor_gas_cost def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: @@ -235,10 +242,11 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: Returns ------- - verified : `ethereum.base_types.Uint` + intrinsic_gas : `ethereum.base_types.Uint` The intrinsic cost of the transaction. - tokens_in_calldata : `ethereum.base_types.Uint` - The eip-7623 calldata tokens used by the transaction. + calldata_floor_gas_cost : `ethereum.base_types.Uint` + The eip-7623 minimum gas cost used by the transaction + based on the calldata size. """ from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST from .vm.gas import init_code_cost @@ -248,9 +256,13 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: if byte == 0: zero_bytes += 1 - tokens_in_calldata = zero_bytes + (len(tx.data) - zero_bytes) * 4 + tokens_in_calldata = Uint(zero_bytes + (len(tx.data) - zero_bytes) * 4) + # EIP-7623 floor price (note: no EVM costs) + calldata_floor_gas_cost = ( + tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST + ) - data_cost = Uint(tokens_in_calldata) * STANDARD_CALLDATA_TOKEN_COST + data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) @@ -283,7 +295,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: + access_list_cost + auth_cost ), - Uint(tokens_in_calldata), + calldata_floor_gas_cost, ) From b151dfe238b22bb7e9ec7ed4b1cda220fae13e0b Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:11:40 -0500 Subject: [PATCH 16/38] change type of branch node subnodes (#1066) (#1095) --- setup.cfg | 2 +- src/ethereum/prague/trie.py | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4cb3796099..1666e6b37f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -119,7 +119,7 @@ python_requires = >=3.10 install_requires = pycryptodome>=3,<4 coincurve>=20,<21 - typing_extensions>=4 + typing_extensions>=4.2 py_ecc @ git+https://github.com/petertdavies/py_ecc.git@127184f4c57b1812da959586d0fe8f43bb1a2389 ethereum-types>=0.2.1,<0.3 ethereum-rlp>=0.1.1,<0.2 diff --git a/src/ethereum/prague/trie.py b/src/ethereum/prague/trie.py index 03ae159be7..c81ef86b61 100644 --- a/src/ethereum/prague/trie.py +++ b/src/ethereum/prague/trie.py @@ -24,14 +24,17 @@ MutableMapping, Optional, Sequence, + Tuple, TypeVar, Union, + cast, ) from ethereum_rlp import rlp from ethereum_types.bytes import Bytes from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint +from typing_extensions import assert_type from ethereum.cancun import trie as previous_trie from ethereum.crypto.hash import keccak256 @@ -95,12 +98,32 @@ class ExtensionNode: subnode: rlp.Extended +BranchSubnodes = Tuple[ + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, + rlp.Extended, +] + + @slotted_freezable @dataclass class BranchNode: """Branch node in the Merkle Trie""" - subnodes: List[rlp.Extended] + subnodes: BranchSubnodes value: rlp.Extended @@ -140,7 +163,7 @@ def encode_internal_node(node: Optional[InternalNode]) -> rlp.Extended: node.subnode, ) elif isinstance(node, BranchNode): - unencoded = node.subnodes + [node.value] + unencoded = list(node.subnodes) + [node.value] else: raise AssertionError(f"Invalid internal node type {type(node)}!") @@ -461,10 +484,11 @@ def patricialize( else: branches[key[level]][key] = obj[key] + subnodes = tuple( + encode_internal_node(patricialize(branches[k], level + Uint(1))) + for k in range(16) + ) return BranchNode( - [ - encode_internal_node(patricialize(branches[k], level + Uint(1))) - for k in range(16) - ], + cast(BranchSubnodes, assert_type(subnodes, Tuple[rlp.Extended, ...])), value, ) From aa0daf98e82c71a6eb3c8c65a154cee1afd6ffe2 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 25 Jan 2025 13:47:05 -0500 Subject: [PATCH 17/38] update storage trie type (#1070 #1071) --- src/ethereum/prague/state.py | 21 +++++++++++---------- src/ethereum_optimized/state_db.py | 10 +++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ethereum/prague/state.py b/src/ethereum/prague/state.py index 1cb9cdcdc7..eae92ed528 100644 --- a/src/ethereum/prague/state.py +++ b/src/ethereum/prague/state.py @@ -19,7 +19,7 @@ from dataclasses import dataclass, field from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple -from ethereum_types.bytes import Bytes +from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.frozen import modify from ethereum_types.numeric import U256, Uint @@ -37,12 +37,13 @@ class State: _main_trie: Trie[Address, Optional[Account]] = field( default_factory=lambda: Trie(secured=True, default=None) ) - _storage_tries: Dict[Address, Trie[Bytes, U256]] = field( + _storage_tries: Dict[Address, Trie[Bytes32, U256]] = field( default_factory=dict ) _snapshots: List[ Tuple[ - Trie[Address, Optional[Account]], Dict[Address, Trie[Bytes, U256]] + Trie[Address, Optional[Account]], + Dict[Address, Trie[Bytes32, U256]], ] ] = field(default_factory=list) created_accounts: Set[Address] = field(default_factory=set) @@ -55,8 +56,8 @@ class TransientStorage: within a transaction. """ - _tries: Dict[Address, Trie[Bytes, U256]] = field(default_factory=dict) - _snapshots: List[Dict[Address, Trie[Bytes, U256]]] = field( + _tries: Dict[Address, Trie[Bytes32, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes32, U256]]] = field( default_factory=list ) @@ -261,7 +262,7 @@ def mark_account_created(state: State, address: Address) -> None: state.created_accounts.add(address) -def get_storage(state: State, address: Address, key: Bytes) -> U256: +def get_storage(state: State, address: Address, key: Bytes32) -> U256: """ Get a value at a storage key on an account. Returns `U256(0)` if the storage key has not been set previously. @@ -291,7 +292,7 @@ def get_storage(state: State, address: Address, key: Bytes) -> U256: def set_storage( - state: State, address: Address, key: Bytes, value: U256 + state: State, address: Address, key: Bytes32, value: U256 ) -> None: """ Set a value at a storage key on an account. Setting to `U256(0)` deletes @@ -626,7 +627,7 @@ def write_code(sender: Account) -> None: modify_state(state, address, write_code) -def get_storage_original(state: State, address: Address, key: Bytes) -> U256: +def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: """ Get the original value in a storage slot i.e. the value before the current transaction began. This function reads the value from the snapshots taken @@ -660,7 +661,7 @@ def get_storage_original(state: State, address: Address, key: Bytes) -> U256: def get_transient_storage( - transient_storage: TransientStorage, address: Address, key: Bytes + transient_storage: TransientStorage, address: Address, key: Bytes32 ) -> U256: """ Get a value at a storage key on an account from transient storage. @@ -691,7 +692,7 @@ def get_transient_storage( def set_transient_storage( transient_storage: TransientStorage, address: Address, - key: Bytes, + key: Bytes32, value: U256, ) -> None: """ diff --git a/src/ethereum_optimized/state_db.py b/src/ethereum_optimized/state_db.py index edcb165b38..d9c9ced6be 100644 --- a/src/ethereum_optimized/state_db.py +++ b/src/ethereum_optimized/state_db.py @@ -26,7 +26,7 @@ "package" ) -from ethereum_types.bytes import Bytes, Bytes20 +from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.numeric import U256, Uint from ethereum.crypto.hash import Hash32 @@ -76,7 +76,7 @@ class State: db: Any dirty_accounts: Dict[Address, Optional[Account_]] - dirty_storage: Dict[Address, Dict[Bytes, U256]] + dirty_storage: Dict[Address, Dict[Bytes32, U256]] destroyed_accounts: Set[Address] tx_restore_points: List[int] journal: List[Any] @@ -328,7 +328,7 @@ def rollback_transaction(state: State) -> None: _rollback_transaction(state) @add_item(patches) - def get_storage(state: State, address: Address, key: Bytes) -> U256: + def get_storage(state: State, address: Address, key: Bytes32) -> U256: """ See `state`. """ @@ -345,7 +345,7 @@ def get_storage(state: State, address: Address, key: Bytes) -> U256: @add_item(patches) def get_storage_original( - state: State, address: Address, key: Bytes + state: State, address: Address, key: Bytes32 ) -> U256: """ See `state`. @@ -357,7 +357,7 @@ def get_storage_original( @add_item(patches) def set_storage( - state: State, address: Address, key: Bytes, value: U256 + state: State, address: Address, key: Bytes32, value: U256 ) -> None: """ See `state`. From 33d50c9f1f4016cd57b884c7963626513e43a493 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:41:53 +0100 Subject: [PATCH 18/38] update EXTCODE* behavior in 7702 (#1094) See https://github.com/ethereum/EIPs/pull/9248/files --- src/ethereum/prague/vm/eoa_delegation.py | 1 - src/ethereum/prague/vm/instructions/environment.py | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/ethereum/prague/vm/eoa_delegation.py b/src/ethereum/prague/vm/eoa_delegation.py index b06cd144eb..8728375a25 100644 --- a/src/ethereum/prague/vm/eoa_delegation.py +++ b/src/ethereum/prague/vm/eoa_delegation.py @@ -21,7 +21,6 @@ SET_CODE_TX_MAGIC = b"\x05" EOA_DELEGATION_MARKER = b"\xEF\x01\x00" -EOA_DELEGATION_SENTINEL = b"\xEF\x01" EOA_DELEGATION_MARKER_LENGTH = len(EOA_DELEGATION_MARKER) EOA_DELEGATED_CODE_LENGTH = 23 PER_EMPTY_ACCOUNT_COST = 25000 diff --git a/src/ethereum/prague/vm/instructions/environment.py b/src/ethereum/prague/vm/instructions/environment.py index 08d6e9ae0d..6a524f1496 100644 --- a/src/ethereum/prague/vm/instructions/environment.py +++ b/src/ethereum/prague/vm/instructions/environment.py @@ -21,7 +21,6 @@ from ...fork_types import EMPTY_ACCOUNT from ...state import get_account from ...utils.address import to_address -from ...vm.eoa_delegation import EOA_DELEGATION_SENTINEL, is_valid_delegation from ...vm.memory import buffer_read, memory_write from .. import Evm from ..exceptions import OutOfBoundsRead @@ -353,8 +352,6 @@ def extcodesize(evm: Evm) -> None: # OPERATION code = get_account(evm.env.state, address).code - if is_valid_delegation(code): - code = EOA_DELEGATION_SENTINEL codesize = U256(len(code)) push(evm.stack, codesize) @@ -397,8 +394,6 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code - if is_valid_delegation(code): - code = EOA_DELEGATION_SENTINEL value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -490,8 +485,6 @@ def extcodehash(evm: Evm) -> None: codehash = U256(0) else: code = account.code - if is_valid_delegation(code): - code = EOA_DELEGATION_SENTINEL codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) From fa33d824decac0a6ac55adc943e7f139b0719f0e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 30 Jan 2025 10:50:43 -0600 Subject: [PATCH 19/38] Update EIP-2935, EIP-7002, EIP-7251 system contract addresses (#1100) --- src/ethereum/prague/fork.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index f18c28d11d..d71648eb23 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -84,13 +84,13 @@ VERSIONED_HASH_VERSION_KZG = b"\x01" WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( - "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA" + "0x00000961Ef480Eb55e80D19ad83579A64c007002" ) CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( - "0x00431F263cE400f4455c2dCf564e53007Ca4bbBb" + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" ) HISTORY_STORAGE_ADDRESS = hex_to_address( - "0x0F792be4B0c0cb4DAE440Ef133E90C0eCD48CCCC" + "0x0000F90827F1C53a10cb7A02335B175320002935" ) HISTORY_SERVE_WINDOW = 8192 From ef644edc3fa4350ee3be551675da1c6054294e92 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Mon, 3 Feb 2025 08:44:19 +0100 Subject: [PATCH 20/38] port #1098 and #1011 to `forks/prague` (#1102) * port #1098 to prague * port #1011 to prague --- .github/workflows/gh-pages.yaml | 5 +++-- src/ethereum/prague/fork.py | 10 +++++++--- src/ethereum/prague/state.py | 6 +++--- src/ethereum/prague/vm/__init__.py | 3 ++- src/ethereum/prague/vm/interpreter.py | 9 +++++---- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 320f82f7f1..52d972d8cb 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -33,7 +33,7 @@ jobs: - name: Upload Pages Artifact id: artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: .tox/docs @@ -46,6 +46,7 @@ jobs: permissions: pages: write id-token: write + actions: read environment: name: github-pages @@ -54,4 +55,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index f18c28d11d..32fbb8efac 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -20,7 +20,11 @@ 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 @@ -453,7 +457,7 @@ def check_transaction( def make_receipt( tx: Transaction, - error: Optional[Exception], + error: Optional[EthereumException], cumulative_gas_used: Uint, logs: Tuple[Log, ...], ) -> Union[Bytes, Receipt]: @@ -925,7 +929,7 @@ def process_general_purpose_requests( def process_transaction( env: vm.Environment, tx: Transaction -) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: +) -> Tuple[Uint, Tuple[Log, ...], Optional[EthereumException]]: """ Execute a transaction against the provided environment. diff --git a/src/ethereum/prague/state.py b/src/ethereum/prague/state.py index eae92ed528..920ee6dd49 100644 --- a/src/ethereum/prague/state.py +++ b/src/ethereum/prague/state.py @@ -17,7 +17,7 @@ `EMPTY_ACCOUNT`. """ from dataclasses import dataclass, field -from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple +from typing import Callable, Dict, List, Optional, Set, Tuple from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.frozen import modify @@ -719,7 +719,7 @@ def set_transient_storage( def destroy_touched_empty_accounts( - state: State, touched_accounts: Iterable[Address] + state: State, touched_accounts: Set[Address] ) -> None: """ Destroy all touched accounts that are empty. @@ -727,7 +727,7 @@ def destroy_touched_empty_accounts( ---------- state: `State` The current state. - touched_accounts: `Iterable[Address]` + touched_accounts: `Set[Address]` All the accounts that have been touched in the current transaction. """ for address in touched_accounts: diff --git a/src/ethereum/prague/vm/__init__.py b/src/ethereum/prague/vm/__init__.py index e73dba47bd..e2abe20d98 100644 --- a/src/ethereum/prague/vm/__init__.py +++ b/src/ethereum/prague/vm/__init__.py @@ -20,6 +20,7 @@ from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException from ..blocks import Log from ..fork_types import Address, Authorization, VersionedHash @@ -95,7 +96,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]] diff --git a/src/ethereum/prague/vm/interpreter.py b/src/ethereum/prague/vm/interpreter.py index df02758171..d105142452 100644 --- a/src/ethereum/prague/vm/interpreter.py +++ b/src/ethereum/prague/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 Bytes, Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.exceptions import EthereumException from ethereum.trace import ( EvmStop, OpEnd, @@ -83,10 +84,10 @@ 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] return_data: Bytes From ad4d15b45a7ed158c0275eac334108e99842516b Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Wed, 5 Feb 2025 10:24:03 +0100 Subject: [PATCH 21/38] run pectra-devnet-6@v1.0.0 tests --- tests/helpers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 06d833b7bb..b551825535 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -17,7 +17,7 @@ }, "latest_fork_tests": { "url": "https://github.com/gurukamath/latest_fork_tests.git", - "commit_hash": "e7ccbb6", + "commit_hash": "e15efcb", "fixture_path": "tests/fixtures/latest_fork_tests", }, } From 2c86028067adb79448fa6e08d29b5148b20cca2b Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:59:32 +0100 Subject: [PATCH 22/38] Backport changes from prague (#1113) * enable testing for all forks * catch invalid signatures * refactor validate_transaction * define accessed_addresses at the start * update variable name to BLOB_BASE_FEE_UPDATE_FRACTION * refund_counter definition * create encode_receipt function * create process_system_transaction * update gas calc * explicitly define code_address in call* * fix decode_receipt --- src/ethereum/arrow_glacier/blocks.py | 31 ++- src/ethereum/arrow_glacier/fork.py | 15 +- src/ethereum/arrow_glacier/transactions.py | 24 ++- src/ethereum/arrow_glacier/utils/message.py | 8 +- .../vm/instructions/environment.py | 28 +-- .../arrow_glacier/vm/instructions/system.py | 8 +- src/ethereum/arrow_glacier/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/berlin/blocks.py | 24 ++- src/ethereum/berlin/fork.py | 13 +- src/ethereum/berlin/transactions.py | 24 ++- src/ethereum/berlin/utils/message.py | 8 +- .../berlin/vm/instructions/environment.py | 28 +-- src/ethereum/berlin/vm/instructions/system.py | 8 +- src/ethereum/berlin/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/byzantium/fork.py | 12 +- src/ethereum/byzantium/transactions.py | 24 ++- .../byzantium/vm/instructions/environment.py | 4 +- .../byzantium/vm/instructions/system.py | 10 +- src/ethereum/byzantium/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/cancun/blocks.py | 34 +++- src/ethereum/cancun/fork.py | 177 ++++++++++++------ src/ethereum/cancun/transactions.py | 26 ++- src/ethereum/cancun/utils/message.py | 8 +- src/ethereum/cancun/vm/gas.py | 4 +- .../cancun/vm/instructions/environment.py | 28 +-- src/ethereum/cancun/vm/instructions/system.py | 8 +- src/ethereum/cancun/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/constantinople/fork.py | 12 +- src/ethereum/constantinople/transactions.py | 24 ++- .../vm/instructions/environment.py | 8 +- .../constantinople/vm/instructions/system.py | 10 +- src/ethereum/constantinople/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/dao_fork/fork.py | 12 +- src/ethereum/dao_fork/transactions.py | 24 ++- .../dao_fork/vm/instructions/environment.py | 4 +- .../dao_fork/vm/instructions/system.py | 5 +- src/ethereum/dao_fork/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/frontier/fork.py | 12 +- src/ethereum/frontier/transactions.py | 24 ++- .../frontier/vm/instructions/environment.py | 4 +- .../frontier/vm/instructions/system.py | 5 +- src/ethereum/frontier/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/gray_glacier/blocks.py | 31 ++- src/ethereum/gray_glacier/fork.py | 15 +- src/ethereum/gray_glacier/transactions.py | 24 ++- src/ethereum/gray_glacier/utils/message.py | 8 +- .../vm/instructions/environment.py | 28 +-- .../gray_glacier/vm/instructions/system.py | 8 +- src/ethereum/gray_glacier/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/homestead/fork.py | 12 +- src/ethereum/homestead/transactions.py | 24 ++- .../homestead/vm/instructions/environment.py | 4 +- .../homestead/vm/instructions/system.py | 5 +- src/ethereum/homestead/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/istanbul/fork.py | 12 +- src/ethereum/istanbul/transactions.py | 24 ++- .../istanbul/vm/instructions/environment.py | 8 +- .../istanbul/vm/instructions/system.py | 10 +- src/ethereum/istanbul/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/london/blocks.py | 31 ++- src/ethereum/london/fork.py | 15 +- src/ethereum/london/transactions.py | 24 ++- src/ethereum/london/utils/message.py | 8 +- .../london/vm/instructions/environment.py | 28 +-- src/ethereum/london/vm/instructions/system.py | 8 +- src/ethereum/london/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/muir_glacier/fork.py | 12 +- src/ethereum/muir_glacier/transactions.py | 24 ++- .../vm/instructions/environment.py | 8 +- .../muir_glacier/vm/instructions/system.py | 10 +- src/ethereum/muir_glacier/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/paris/blocks.py | 31 ++- src/ethereum/paris/fork.py | 15 +- src/ethereum/paris/transactions.py | 24 ++- src/ethereum/paris/utils/message.py | 8 +- .../paris/vm/instructions/environment.py | 28 +-- src/ethereum/paris/vm/instructions/system.py | 8 +- src/ethereum/paris/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/prague/vm/gas.py | 4 +- src/ethereum/shanghai/blocks.py | 31 ++- src/ethereum/shanghai/fork.py | 15 +- src/ethereum/shanghai/transactions.py | 26 ++- src/ethereum/shanghai/utils/message.py | 8 +- .../shanghai/vm/instructions/environment.py | 28 +-- .../shanghai/vm/instructions/system.py | 8 +- src/ethereum/shanghai/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/spurious_dragon/fork.py | 12 +- src/ethereum/spurious_dragon/transactions.py | 24 ++- .../vm/instructions/environment.py | 4 +- .../spurious_dragon/vm/instructions/system.py | 5 +- .../spurious_dragon/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- src/ethereum/tangerine_whistle/fork.py | 12 +- .../tangerine_whistle/transactions.py | 24 ++- .../vm/instructions/environment.py | 4 +- .../vm/instructions/system.py | 5 +- .../tangerine_whistle/vm/interpreter.py | 4 +- .../vm/precompiled_contracts/ecrecover.py | 3 +- tests/berlin/test_transaction.py | 14 +- tests/byzantium/test_transaction.py | 14 +- tests/constantinople/test_transaction.py | 13 +- tests/frontier/test_transaction.py | 14 +- tests/homestead/test_transaction.py | 14 +- tests/istanbul/test_transaction.py | 14 +- tests/london/test_transaction.py | 14 +- tests/spurious_dragon/test_transaction.py | 13 +- tests/tangerine_whistle/test_transaction.py | 13 +- tox.ini | 3 - 122 files changed, 1023 insertions(+), 618 deletions(-) diff --git a/src/ethereum/arrow_glacier/blocks.py b/src/ethereum/arrow_glacier/blocks.py index 87c9acac7f..38002554ca 100644 --- a/src/ethereum/arrow_glacier/blocks.py +++ b/src/ethereum/arrow_glacier/blocks.py @@ -11,13 +11,19 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -80,3 +86,26 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/arrow_glacier/fork.py b/src/ethereum/arrow_glacier/fork.py index 2108a5fba7..c6fd9d4291 100644 --- a/src/ethereum/arrow_glacier/fork.py +++ b/src/ethereum/arrow_glacier/fork.py @@ -24,7 +24,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -42,7 +42,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -472,12 +471,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -775,8 +769,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -795,7 +788,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/arrow_glacier/transactions.py b/src/ethereum/arrow_glacier/transactions.py index 9d9bb6bd17..27cc083538 100644 --- a/src/ethereum/arrow_glacier/transactions.py +++ b/src/ethereum/arrow_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -119,7 +119,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -141,14 +141,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/arrow_glacier/utils/message.py b/src/ethereum/arrow_glacier/utils/message.py index 6cdf44aae8..c1268d4f6a 100644 --- a/src/ethereum/arrow_glacier/utils/message.py +++ b/src/ethereum/arrow_glacier/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.arrow_glacier.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/arrow_glacier/vm/instructions/environment.py b/src/ethereum/arrow_glacier/vm/instructions/environment.py index 33d8396a48..dc59e168bd 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/environment.py +++ b/src/ethereum/arrow_glacier/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/arrow_glacier/vm/instructions/system.py b/src/ethereum/arrow_glacier/vm/instructions/system.py index 4ace48ad27..961b3e0c00 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/system.py +++ b/src/ethereum/arrow_glacier/vm/instructions/system.py @@ -342,6 +342,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -373,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -605,6 +607,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -622,7 +626,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/arrow_glacier/vm/interpreter.py b/src/ethereum/arrow_glacier/vm/interpreter.py index 6bd29aeee3..1ca1087fad 100644 --- a/src/ethereum/arrow_glacier/vm/interpreter.py +++ b/src/ethereum/arrow_glacier/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/arrow_glacier/vm/precompiled_contracts/ecrecover.py b/src/ethereum/arrow_glacier/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/arrow_glacier/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/arrow_glacier/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/berlin/blocks.py b/src/ethereum/berlin/blocks.py index df486c7a9d..ce975fa4db 100644 --- a/src/ethereum/berlin/blocks.py +++ b/src/ethereum/berlin/blocks.py @@ -11,13 +11,14 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import AccessListTransaction, LegacyTransaction, Transaction @slotted_freezable @@ -79,3 +80,24 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] == 1 + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/berlin/fork.py b/src/ethereum/berlin/fork.py index 8ec009f424..653995ca92 100644 --- a/src/ethereum/berlin/fork.py +++ b/src/ethereum/berlin/fork.py @@ -24,7 +24,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -41,7 +41,6 @@ AccessListTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -374,10 +373,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -669,8 +665,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -682,7 +677,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/berlin/transactions.py b/src/ethereum/berlin/transactions.py index fcc54d0485..73b00dae30 100644 --- a/src/ethereum/berlin/transactions.py +++ b/src/ethereum/berlin/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -91,7 +91,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -113,14 +113,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/berlin/utils/message.py b/src/ethereum/berlin/utils/message.py index 80490e0735..122d127a1c 100644 --- a/src/ethereum/berlin/utils/message.py +++ b/src/ethereum/berlin/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.berlin.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/berlin/vm/instructions/environment.py b/src/ethereum/berlin/vm/instructions/environment.py index e8f2505a51..0c2e50ab2c 100644 --- a/src/ethereum/berlin/vm/instructions/environment.py +++ b/src/ethereum/berlin/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/berlin/vm/instructions/system.py b/src/ethereum/berlin/vm/instructions/system.py index d5a19b237d..5f1be5a398 100644 --- a/src/ethereum/berlin/vm/instructions/system.py +++ b/src/ethereum/berlin/vm/instructions/system.py @@ -343,6 +343,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -374,7 +376,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -616,6 +618,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -633,7 +637,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/berlin/vm/interpreter.py b/src/ethereum/berlin/vm/interpreter.py index 28cec7c2cb..1050727b5c 100644 --- a/src/ethereum/berlin/vm/interpreter.py +++ b/src/ethereum/berlin/vm/interpreter.py @@ -106,6 +106,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -125,12 +126,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/berlin/vm/precompiled_contracts/ecrecover.py b/src/ethereum/berlin/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/berlin/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/berlin/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/byzantium/fork.py b/src/ethereum/byzantium/fork.py index 68001e3ee1..dff26a8483 100644 --- a/src/ethereum/byzantium/fork.py +++ b/src/ethereum/byzantium/fork.py @@ -37,12 +37,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -659,8 +654,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -672,7 +666,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/byzantium/transactions.py b/src/ethereum/byzantium/transactions.py index 142929587d..1c67eeaf02 100644 --- a/src/ethereum/byzantium/transactions.py +++ b/src/ethereum/byzantium/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/byzantium/vm/instructions/environment.py b/src/ethereum/byzantium/vm/instructions/environment.py index 1a2a295c52..5a18a1c86d 100644 --- a/src/ethereum/byzantium/vm/instructions/environment.py +++ b/src/ethereum/byzantium/vm/instructions/environment.py @@ -336,8 +336,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -371,6 +372,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/byzantium/vm/instructions/system.py index ce20f3ecdd..52388abb2b 100644 --- a/src/ethereum/byzantium/vm/instructions/system.py +++ b/src/ethereum/byzantium/vm/instructions/system.py @@ -261,6 +261,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + create_gas_cost = ( Uint(0) if value == 0 or is_account_alive(evm.env.state, to) @@ -292,7 +295,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -509,6 +512,9 @@ def staticcall(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -526,7 +532,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/byzantium/vm/interpreter.py b/src/ethereum/byzantium/vm/interpreter.py index ec9fc879e8..70576c537d 100644 --- a/src/ethereum/byzantium/vm/interpreter.py +++ b/src/ethereum/byzantium/vm/interpreter.py @@ -105,6 +105,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -124,12 +125,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/byzantium/vm/precompiled_contracts/ecrecover.py b/src/ethereum/byzantium/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/byzantium/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/byzantium/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/cancun/blocks.py b/src/ethereum/cancun/blocks.py index 54ef88e15f..f1ac3fd758 100644 --- a/src/ethereum/cancun/blocks.py +++ b/src/ethereum/cancun/blocks.py @@ -11,13 +11,20 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -98,3 +105,28 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2, 3) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 5bbb517ed6..22d7d5a532 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -23,7 +23,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt, Withdrawal +from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root, VersionedHash from .state import ( @@ -44,7 +44,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -60,7 +59,7 @@ calculate_excess_blob_gas, calculate_total_blob_gas, ) -from .vm.interpreter import process_message_call +from .vm.interpreter import MessageCallOutput, process_message_call BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) ELASTICITY_MULTIPLIER = Uint(2) @@ -452,14 +451,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - elif isinstance(tx, BlobTransaction): - return b"\x03" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -495,6 +487,107 @@ class ApplyBodyOutput: blob_gas_used: Uint +def process_system_transaction( + 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 + ---------- + 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_tx_message = Message( + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=data, + code=system_contract_code, + depth=Uint(0), + current_target=target_address, + code_address=target_address, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + 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) + + # TODO: Empty accounts in post-merge forks are impossible + # see Ethereum Improvement Proposal 7523. + # This line is only included to support invalid tests in the test suite + # 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 + ) + + return system_tx_output + + def apply_body( state: State, block_hashes: List[Hash32], @@ -571,50 +664,19 @@ def apply_body( ) block_logs: Tuple[Log, ...] = () - beacon_block_roots_contract_code = get_account( - state, BEACON_ROOTS_ADDRESS - ).code - - system_tx_message = Message( - caller=SYSTEM_ADDRESS, - target=BEACON_ROOTS_ADDRESS, - gas=SYSTEM_TRANSACTION_GAS, - value=U256(0), - data=parent_beacon_block_root, - code=beacon_block_roots_contract_code, - depth=Uint(0), - current_target=BEACON_ROOTS_ADDRESS, - code_address=BEACON_ROOTS_ADDRESS, - should_transfer_value=False, - is_static=False, - accessed_addresses=set(), - accessed_storage_keys=set(), - parent_evm=None, - ) - - system_tx_env = vm.Environment( - caller=SYSTEM_ADDRESS, - origin=SYSTEM_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=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) - - destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts + 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, ) for i, tx in enumerate(map(decode_transaction, transactions)): @@ -723,8 +785,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -736,7 +797,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/cancun/transactions.py b/src/ethereum/cancun/transactions.py index deb594c17c..bd9167e9df 100644 --- a/src/ethereum/cancun/transactions.py +++ b/src/ethereum/cancun/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address, VersionedHash @@ -149,7 +149,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -171,19 +171,25 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ from .vm.interpreter import MAX_CODE_SIZE - if calculate_intrinsic_cost(tx) > tx.gas: - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - return False + raise InvalidBlock - return True + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/cancun/utils/message.py b/src/ethereum/cancun/utils/message.py index 95057a0d2d..68deb1a62f 100644 --- a/src/ethereum/cancun/utils/message.py +++ b/src/ethereum/cancun/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.cancun.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/cancun/vm/gas.py b/src/ethereum/cancun/vm/gas.py index cc3ccc7c6b..794396cbf2 100644 --- a/src/ethereum/cancun/vm/gas.py +++ b/src/ethereum/cancun/vm/gas.py @@ -71,7 +71,7 @@ TARGET_BLOB_GAS_PER_BLOCK = U64(393216) GAS_PER_BLOB = Uint(2**17) MIN_BLOB_GASPRICE = Uint(1) -BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) +BLOB_BASE_FEE_UPDATE_FRACTION = Uint(3338477) @dataclass @@ -337,7 +337,7 @@ def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: return taylor_exponential( MIN_BLOB_GASPRICE, Uint(excess_blob_gas), - BLOB_GASPRICE_UPDATE_FRACTION, + BLOB_BASE_FEE_UPDATE_FRACTION, ) diff --git a/src/ethereum/cancun/vm/instructions/environment.py b/src/ethereum/cancun/vm/instructions/environment.py index 4ba384f9ba..6a524f1496 100644 --- a/src/ethereum/cancun/vm/instructions/environment.py +++ b/src/ethereum/cancun/vm/instructions/environment.py @@ -343,15 +343,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -382,16 +384,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -468,10 +471,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -479,7 +484,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/cancun/vm/instructions/system.py b/src/ethereum/cancun/vm/instructions/system.py index da47df38cf..58cec71f07 100644 --- a/src/ethereum/cancun/vm/instructions/system.py +++ b/src/ethereum/cancun/vm/instructions/system.py @@ -366,6 +366,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -397,7 +399,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -631,6 +633,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -648,7 +652,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/cancun/vm/interpreter.py b/src/ethereum/cancun/vm/interpreter.py index c6576ae8eb..c743ade3c5 100644 --- a/src/ethereum/cancun/vm/interpreter.py +++ b/src/ethereum/cancun/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py b/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/constantinople/fork.py b/src/ethereum/constantinople/fork.py index e195589a13..fb57e0f5ba 100644 --- a/src/ethereum/constantinople/fork.py +++ b/src/ethereum/constantinople/fork.py @@ -37,12 +37,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -659,8 +654,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -672,7 +666,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/constantinople/transactions.py b/src/ethereum/constantinople/transactions.py index 142929587d..1c67eeaf02 100644 --- a/src/ethereum/constantinople/transactions.py +++ b/src/ethereum/constantinople/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/constantinople/vm/instructions/environment.py b/src/ethereum/constantinople/vm/instructions/environment.py index 3062ffee77..af92af6891 100644 --- a/src/ethereum/constantinople/vm/instructions/environment.py +++ b/src/ethereum/constantinople/vm/instructions/environment.py @@ -338,9 +338,9 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -374,6 +374,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -457,7 +458,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/constantinople/vm/instructions/system.py b/src/ethereum/constantinople/vm/instructions/system.py index b74c3d3116..0c7608d40d 100644 --- a/src/ethereum/constantinople/vm/instructions/system.py +++ b/src/ethereum/constantinople/vm/instructions/system.py @@ -329,6 +329,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + create_gas_cost = ( Uint(0) if value == 0 or is_account_alive(evm.env.state, to) @@ -360,7 +363,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -577,6 +580,9 @@ def staticcall(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -594,7 +600,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/constantinople/vm/interpreter.py b/src/ethereum/constantinople/vm/interpreter.py index e2ebbcdbbc..72679f5de1 100644 --- a/src/ethereum/constantinople/vm/interpreter.py +++ b/src/ethereum/constantinople/vm/interpreter.py @@ -105,6 +105,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -124,12 +125,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/constantinople/vm/precompiled_contracts/ecrecover.py b/src/ethereum/constantinople/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/constantinople/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/constantinople/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/dao_fork/fork.py b/src/ethereum/dao_fork/fork.py index 1835a2525f..7b653c5bc2 100644 --- a/src/ethereum/dao_fork/fork.py +++ b/src/ethereum/dao_fork/fork.py @@ -39,12 +39,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -665,8 +660,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -678,7 +672,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/dao_fork/transactions.py b/src/ethereum/dao_fork/transactions.py index 57d697d615..96d825e2ba 100644 --- a/src/ethereum/dao_fork/transactions.py +++ b/src/ethereum/dao_fork/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/dao_fork/vm/instructions/environment.py b/src/ethereum/dao_fork/vm/instructions/environment.py index 9d936e7f5f..1bce21fda7 100644 --- a/src/ethereum/dao_fork/vm/instructions/environment.py +++ b/src/ethereum/dao_fork/vm/instructions/environment.py @@ -334,8 +334,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -369,6 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/dao_fork/vm/instructions/system.py index fd65ab1a42..1e87e93319 100644 --- a/src/ethereum/dao_fork/vm/instructions/system.py +++ b/src/ethereum/dao_fork/vm/instructions/system.py @@ -242,6 +242,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( evm.env.state, gas, to, value ) @@ -262,7 +265,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, diff --git a/src/ethereum/dao_fork/vm/interpreter.py b/src/ethereum/dao_fork/vm/interpreter.py index daa28bb6db..a1a29f231d 100644 --- a/src/ethereum/dao_fork/vm/interpreter.py +++ b/src/ethereum/dao_fork/vm/interpreter.py @@ -98,6 +98,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -114,11 +115,10 @@ def process_message_call( if evm.error: logs: Tuple[Log, ...] = () accounts_to_delete = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/dao_fork/vm/precompiled_contracts/ecrecover.py b/src/ethereum/dao_fork/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/dao_fork/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/dao_fork/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/frontier/fork.py b/src/ethereum/frontier/fork.py index 46749732f4..3e96a9aa73 100644 --- a/src/ethereum/frontier/fork.py +++ b/src/ethereum/frontier/fork.py @@ -36,12 +36,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -647,8 +642,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -660,7 +654,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/frontier/transactions.py b/src/ethereum/frontier/transactions.py index a09bba38ba..396765e22c 100644 --- a/src/ethereum/frontier/transactions.py +++ b/src/ethereum/frontier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -40,7 +40,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -62,14 +62,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/frontier/vm/instructions/environment.py b/src/ethereum/frontier/vm/instructions/environment.py index 9d936e7f5f..1bce21fda7 100644 --- a/src/ethereum/frontier/vm/instructions/environment.py +++ b/src/ethereum/frontier/vm/instructions/environment.py @@ -334,8 +334,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -369,6 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/frontier/vm/instructions/system.py index f5165e1100..a86174de7e 100644 --- a/src/ethereum/frontier/vm/instructions/system.py +++ b/src/ethereum/frontier/vm/instructions/system.py @@ -238,6 +238,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( evm.env.state, gas, to, value ) @@ -258,7 +261,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, memory_input_start_position, memory_input_size, memory_output_start_position, diff --git a/src/ethereum/frontier/vm/interpreter.py b/src/ethereum/frontier/vm/interpreter.py index 6d6a36c5df..a4b8399a78 100644 --- a/src/ethereum/frontier/vm/interpreter.py +++ b/src/ethereum/frontier/vm/interpreter.py @@ -98,6 +98,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -114,11 +115,10 @@ def process_message_call( if evm.error: logs: Tuple[Log, ...] = () accounts_to_delete = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/frontier/vm/precompiled_contracts/ecrecover.py b/src/ethereum/frontier/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/frontier/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/frontier/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/gray_glacier/blocks.py b/src/ethereum/gray_glacier/blocks.py index 87c9acac7f..38002554ca 100644 --- a/src/ethereum/gray_glacier/blocks.py +++ b/src/ethereum/gray_glacier/blocks.py @@ -11,13 +11,19 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -80,3 +86,26 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/gray_glacier/fork.py b/src/ethereum/gray_glacier/fork.py index ce070e7c08..7889c1aa70 100644 --- a/src/ethereum/gray_glacier/fork.py +++ b/src/ethereum/gray_glacier/fork.py @@ -24,7 +24,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -42,7 +42,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -472,12 +471,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -775,8 +769,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -795,7 +788,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/gray_glacier/transactions.py b/src/ethereum/gray_glacier/transactions.py index 9d9bb6bd17..27cc083538 100644 --- a/src/ethereum/gray_glacier/transactions.py +++ b/src/ethereum/gray_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -119,7 +119,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -141,14 +141,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/gray_glacier/utils/message.py b/src/ethereum/gray_glacier/utils/message.py index 28091f510f..73e4bc320f 100644 --- a/src/ethereum/gray_glacier/utils/message.py +++ b/src/ethereum/gray_glacier/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.gray_glacier.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/gray_glacier/vm/instructions/environment.py b/src/ethereum/gray_glacier/vm/instructions/environment.py index 33d8396a48..dc59e168bd 100644 --- a/src/ethereum/gray_glacier/vm/instructions/environment.py +++ b/src/ethereum/gray_glacier/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/gray_glacier/vm/instructions/system.py b/src/ethereum/gray_glacier/vm/instructions/system.py index 4ace48ad27..961b3e0c00 100644 --- a/src/ethereum/gray_glacier/vm/instructions/system.py +++ b/src/ethereum/gray_glacier/vm/instructions/system.py @@ -342,6 +342,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -373,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -605,6 +607,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -622,7 +626,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/gray_glacier/vm/interpreter.py b/src/ethereum/gray_glacier/vm/interpreter.py index 6bd29aeee3..1ca1087fad 100644 --- a/src/ethereum/gray_glacier/vm/interpreter.py +++ b/src/ethereum/gray_glacier/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/gray_glacier/vm/precompiled_contracts/ecrecover.py b/src/ethereum/gray_glacier/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/gray_glacier/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/gray_glacier/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/homestead/fork.py b/src/ethereum/homestead/fork.py index 1419c87b6d..cf39821f14 100644 --- a/src/ethereum/homestead/fork.py +++ b/src/ethereum/homestead/fork.py @@ -36,12 +36,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -647,8 +642,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -660,7 +654,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/homestead/transactions.py b/src/ethereum/homestead/transactions.py index 57d697d615..96d825e2ba 100644 --- a/src/ethereum/homestead/transactions.py +++ b/src/ethereum/homestead/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/homestead/vm/instructions/environment.py b/src/ethereum/homestead/vm/instructions/environment.py index 9d936e7f5f..1bce21fda7 100644 --- a/src/ethereum/homestead/vm/instructions/environment.py +++ b/src/ethereum/homestead/vm/instructions/environment.py @@ -334,8 +334,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -369,6 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/homestead/vm/instructions/system.py index fd65ab1a42..1e87e93319 100644 --- a/src/ethereum/homestead/vm/instructions/system.py +++ b/src/ethereum/homestead/vm/instructions/system.py @@ -242,6 +242,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( evm.env.state, gas, to, value ) @@ -262,7 +265,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, diff --git a/src/ethereum/homestead/vm/interpreter.py b/src/ethereum/homestead/vm/interpreter.py index b653a78117..6f9839d729 100644 --- a/src/ethereum/homestead/vm/interpreter.py +++ b/src/ethereum/homestead/vm/interpreter.py @@ -98,6 +98,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -114,11 +115,10 @@ def process_message_call( if evm.error: logs: Tuple[Log, ...] = () accounts_to_delete = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/homestead/vm/precompiled_contracts/ecrecover.py b/src/ethereum/homestead/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/homestead/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/homestead/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/istanbul/fork.py b/src/ethereum/istanbul/fork.py index 35eb37c9c5..ac55974300 100644 --- a/src/ethereum/istanbul/fork.py +++ b/src/ethereum/istanbul/fork.py @@ -37,12 +37,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -660,8 +655,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -673,7 +667,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/istanbul/transactions.py b/src/ethereum/istanbul/transactions.py index 2bdb3603b9..e455e2549a 100644 --- a/src/ethereum/istanbul/transactions.py +++ b/src/ethereum/istanbul/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/istanbul/vm/instructions/environment.py b/src/ethereum/istanbul/vm/instructions/environment.py index a641fe167b..fc300ca74a 100644 --- a/src/ethereum/istanbul/vm/instructions/environment.py +++ b/src/ethereum/istanbul/vm/instructions/environment.py @@ -339,9 +339,9 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -375,6 +375,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -458,7 +459,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/istanbul/vm/instructions/system.py b/src/ethereum/istanbul/vm/instructions/system.py index b74c3d3116..0c7608d40d 100644 --- a/src/ethereum/istanbul/vm/instructions/system.py +++ b/src/ethereum/istanbul/vm/instructions/system.py @@ -329,6 +329,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + create_gas_cost = ( Uint(0) if value == 0 or is_account_alive(evm.env.state, to) @@ -360,7 +363,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -577,6 +580,9 @@ def staticcall(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -594,7 +600,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/istanbul/vm/interpreter.py b/src/ethereum/istanbul/vm/interpreter.py index 7cbc0f00aa..0805a095da 100644 --- a/src/ethereum/istanbul/vm/interpreter.py +++ b/src/ethereum/istanbul/vm/interpreter.py @@ -106,6 +106,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -125,12 +126,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/istanbul/vm/precompiled_contracts/ecrecover.py b/src/ethereum/istanbul/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/istanbul/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/istanbul/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/london/blocks.py b/src/ethereum/london/blocks.py index 87c9acac7f..38002554ca 100644 --- a/src/ethereum/london/blocks.py +++ b/src/ethereum/london/blocks.py @@ -11,13 +11,19 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -80,3 +86,26 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/london/fork.py b/src/ethereum/london/fork.py index a9599345c9..4bd5dda7bf 100644 --- a/src/ethereum/london/fork.py +++ b/src/ethereum/london/fork.py @@ -24,7 +24,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import FORK_CRITERIA, vm -from .blocks import Block, Header, Log, Receipt +from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -42,7 +42,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -478,12 +477,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -781,8 +775,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -801,7 +794,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/london/transactions.py b/src/ethereum/london/transactions.py index 9d9bb6bd17..27cc083538 100644 --- a/src/ethereum/london/transactions.py +++ b/src/ethereum/london/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -119,7 +119,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -141,14 +141,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/london/utils/message.py b/src/ethereum/london/utils/message.py index f867fcab98..fcd8b1dc59 100644 --- a/src/ethereum/london/utils/message.py +++ b/src/ethereum/london/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.london.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/london/vm/instructions/environment.py b/src/ethereum/london/vm/instructions/environment.py index 33d8396a48..dc59e168bd 100644 --- a/src/ethereum/london/vm/instructions/environment.py +++ b/src/ethereum/london/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/london/vm/instructions/system.py b/src/ethereum/london/vm/instructions/system.py index 4ace48ad27..961b3e0c00 100644 --- a/src/ethereum/london/vm/instructions/system.py +++ b/src/ethereum/london/vm/instructions/system.py @@ -342,6 +342,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -373,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -605,6 +607,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -622,7 +626,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/london/vm/interpreter.py b/src/ethereum/london/vm/interpreter.py index 6bd29aeee3..1ca1087fad 100644 --- a/src/ethereum/london/vm/interpreter.py +++ b/src/ethereum/london/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/london/vm/precompiled_contracts/ecrecover.py b/src/ethereum/london/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/london/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/london/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/muir_glacier/fork.py b/src/ethereum/muir_glacier/fork.py index 4060049c84..cfd3b01dea 100644 --- a/src/ethereum/muir_glacier/fork.py +++ b/src/ethereum/muir_glacier/fork.py @@ -37,12 +37,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -660,8 +655,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -673,7 +667,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/muir_glacier/transactions.py b/src/ethereum/muir_glacier/transactions.py index 2bdb3603b9..e455e2549a 100644 --- a/src/ethereum/muir_glacier/transactions.py +++ b/src/ethereum/muir_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/muir_glacier/vm/instructions/environment.py b/src/ethereum/muir_glacier/vm/instructions/environment.py index a641fe167b..fc300ca74a 100644 --- a/src/ethereum/muir_glacier/vm/instructions/environment.py +++ b/src/ethereum/muir_glacier/vm/instructions/environment.py @@ -339,9 +339,9 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, GAS_EXTERNAL) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -375,6 +375,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -458,7 +459,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/muir_glacier/vm/instructions/system.py b/src/ethereum/muir_glacier/vm/instructions/system.py index b74c3d3116..0c7608d40d 100644 --- a/src/ethereum/muir_glacier/vm/instructions/system.py +++ b/src/ethereum/muir_glacier/vm/instructions/system.py @@ -329,6 +329,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + create_gas_cost = ( Uint(0) if value == 0 or is_account_alive(evm.env.state, to) @@ -360,7 +363,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -577,6 +580,9 @@ def staticcall(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -594,7 +600,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/muir_glacier/vm/interpreter.py b/src/ethereum/muir_glacier/vm/interpreter.py index 83d00366de..6b1d1d4fff 100644 --- a/src/ethereum/muir_glacier/vm/interpreter.py +++ b/src/ethereum/muir_glacier/vm/interpreter.py @@ -106,6 +106,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -125,12 +126,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/muir_glacier/vm/precompiled_contracts/ecrecover.py b/src/ethereum/muir_glacier/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/muir_glacier/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/muir_glacier/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/paris/blocks.py b/src/ethereum/paris/blocks.py index 5bfb604816..7d2f79dd72 100644 --- a/src/ethereum/paris/blocks.py +++ b/src/ethereum/paris/blocks.py @@ -11,13 +11,19 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -80,3 +86,26 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index 1e34b33159..67feb05538 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -23,7 +23,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -40,7 +40,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -385,12 +384,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -565,8 +559,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -584,7 +577,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/paris/transactions.py b/src/ethereum/paris/transactions.py index 5944d96b6e..27cc083538 100644 --- a/src/ethereum/paris/transactions.py +++ b/src/ethereum/paris/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -119,7 +119,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -141,14 +141,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > tx.gas: - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/paris/utils/message.py b/src/ethereum/paris/utils/message.py index 13e5b0d306..d0a74169b0 100644 --- a/src/ethereum/paris/utils/message.py +++ b/src/ethereum/paris/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.paris.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/paris/vm/instructions/environment.py b/src/ethereum/paris/vm/instructions/environment.py index 33d8396a48..dc59e168bd 100644 --- a/src/ethereum/paris/vm/instructions/environment.py +++ b/src/ethereum/paris/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/paris/vm/instructions/system.py b/src/ethereum/paris/vm/instructions/system.py index 4ace48ad27..961b3e0c00 100644 --- a/src/ethereum/paris/vm/instructions/system.py +++ b/src/ethereum/paris/vm/instructions/system.py @@ -342,6 +342,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -373,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -605,6 +607,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -622,7 +626,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/paris/vm/interpreter.py b/src/ethereum/paris/vm/interpreter.py index 24a03fb1ed..8fc8d1ad73 100644 --- a/src/ethereum/paris/vm/interpreter.py +++ b/src/ethereum/paris/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/paris/vm/precompiled_contracts/ecrecover.py b/src/ethereum/paris/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/paris/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/paris/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/prague/vm/gas.py b/src/ethereum/prague/vm/gas.py index f01e35b3a5..624a8f86e2 100644 --- a/src/ethereum/prague/vm/gas.py +++ b/src/ethereum/prague/vm/gas.py @@ -71,7 +71,7 @@ TARGET_BLOB_GAS_PER_BLOCK = U64(786432) GAS_PER_BLOB = Uint(2**17) MIN_BLOB_GASPRICE = Uint(1) -BLOB_GASPRICE_UPDATE_FRACTION = Uint(5007716) +BLOB_BASE_FEE_UPDATE_FRACTION = Uint(5007716) GAS_BLS_G1_ADD = Uint(375) GAS_BLS_G1_MUL = Uint(12000) @@ -344,7 +344,7 @@ def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: return taylor_exponential( MIN_BLOB_GASPRICE, Uint(excess_blob_gas), - BLOB_GASPRICE_UPDATE_FRACTION, + BLOB_BASE_FEE_UPDATE_FRACTION, ) diff --git a/src/ethereum/shanghai/blocks.py b/src/ethereum/shanghai/blocks.py index c76584859c..9a3f6a5927 100644 --- a/src/ethereum/shanghai/blocks.py +++ b/src/ethereum/shanghai/blocks.py @@ -11,13 +11,19 @@ from dataclasses import dataclass from typing import Tuple, Union +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, +) @slotted_freezable @@ -95,3 +101,26 @@ class Receipt: cumulative_gas_used: Uint bloom: Bloom logs: Tuple[Log, ...] + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: + """ + Encodes a receipt. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: + """ + Decodes a receipt. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index d0347b7d1e..f0fb77eb66 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -23,7 +23,7 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt, Withdrawal +from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom from .fork_types import Address, Bloom, Root from .state import ( @@ -41,7 +41,6 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, decode_transaction, encode_transaction, recover_sender, @@ -389,12 +388,7 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + rlp.encode(receipt) - elif isinstance(tx, FeeMarketTransaction): - return b"\x02" + rlp.encode(receipt) - else: - return receipt + return encode_receipt(tx, receipt) @dataclass @@ -587,8 +581,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -606,7 +599,7 @@ def process_transaction( effective_gas_fee = tx.gas * env.gas_price - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = ( diff --git a/src/ethereum/shanghai/transactions.py b/src/ethereum/shanghai/transactions.py index efd3b2fcda..5323b3bd09 100644 --- a/src/ethereum/shanghai/transactions.py +++ b/src/ethereum/shanghai/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .exceptions import TransactionTypeError from .fork_types import Address @@ -119,7 +119,7 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return tx -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -141,19 +141,25 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ from .vm.interpreter import MAX_CODE_SIZE - if calculate_intrinsic_cost(tx) > tx.gas: - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - return False + raise InvalidBlock - return True + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/shanghai/utils/message.py b/src/ethereum/shanghai/utils/message.py index 26e950573d..2d5c3b50ef 100644 --- a/src/ethereum/shanghai/utils/message.py +++ b/src/ethereum/shanghai/utils/message.py @@ -76,6 +76,10 @@ def prepare_message( message: `ethereum.shanghai.vm.Message` Items containing contract creation or message call specific data. """ + accessed_addresses = set() + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) if isinstance(target, Bytes0): current_target = compute_contract_address( caller, @@ -92,11 +96,7 @@ def prepare_message( else: raise AssertionError("Target must be address or empty bytes") - accessed_addresses = set() accessed_addresses.add(current_target) - accessed_addresses.add(caller) - accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) - accessed_addresses.update(preaccessed_addresses) return Message( caller=caller, diff --git a/src/ethereum/shanghai/vm/instructions/environment.py b/src/ethereum/shanghai/vm/instructions/environment.py index 33d8396a48..dc59e168bd 100644 --- a/src/ethereum/shanghai/vm/instructions/environment.py +++ b/src/ethereum/shanghai/vm/instructions/environment.py @@ -340,15 +340,17 @@ def extcodesize(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION - # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -379,16 +381,17 @@ def extcodecopy(evm: Evm) -> None: ) if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas( - evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost - ) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -465,10 +468,12 @@ def extcodehash(evm: Evm) -> None: # GAS if address in evm.accessed_addresses: - charge_gas(evm, GAS_WARM_ACCESS) + access_gas_cost = GAS_WARM_ACCESS else: evm.accessed_addresses.add(address) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) # OPERATION account = get_account(evm.env.state, address) @@ -476,7 +481,8 @@ def extcodehash(evm: Evm) -> None: if account == EMPTY_ACCOUNT: codehash = U256(0) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) diff --git a/src/ethereum/shanghai/vm/instructions/system.py b/src/ethereum/shanghai/vm/instructions/system.py index 12eb5dd34f..2498d11792 100644 --- a/src/ethereum/shanghai/vm/instructions/system.py +++ b/src/ethereum/shanghai/vm/instructions/system.py @@ -365,6 +365,8 @@ def call(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + create_gas_cost = ( Uint(0) if is_account_alive(evm.env.state, to) or value == 0 @@ -396,7 +398,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -628,6 +630,8 @@ def staticcall(evm: Evm) -> None: evm.accessed_addresses.add(to) access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code_address = to + message_call_gas = calculate_message_call_gas( U256(0), gas, @@ -645,7 +649,7 @@ def staticcall(evm: Evm) -> None: U256(0), evm.message.current_target, to, - to, + code_address, True, True, memory_input_start_position, diff --git a/src/ethereum/shanghai/vm/interpreter.py b/src/ethereum/shanghai/vm/interpreter.py index 55ab2b9528..c82dc5e2c6 100644 --- a/src/ethereum/shanghai/vm/interpreter.py +++ b/src/ethereum/shanghai/vm/interpreter.py @@ -107,6 +107,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -126,12 +127,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py b/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/spurious_dragon/fork.py b/src/ethereum/spurious_dragon/fork.py index cb4d0b0702..420fce593e 100644 --- a/src/ethereum/spurious_dragon/fork.py +++ b/src/ethereum/spurious_dragon/fork.py @@ -37,12 +37,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -655,8 +650,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -668,7 +662,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/spurious_dragon/transactions.py b/src/ethereum/spurious_dragon/transactions.py index 142929587d..1c67eeaf02 100644 --- a/src/ethereum/spurious_dragon/transactions.py +++ b/src/ethereum/spurious_dragon/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/spurious_dragon/vm/instructions/environment.py b/src/ethereum/spurious_dragon/vm/instructions/environment.py index 9d936e7f5f..1bce21fda7 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/environment.py +++ b/src/ethereum/spurious_dragon/vm/instructions/environment.py @@ -334,8 +334,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -369,6 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/spurious_dragon/vm/instructions/system.py index 98e5cc0c66..3ce0780272 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/system.py +++ b/src/ethereum/spurious_dragon/vm/instructions/system.py @@ -249,6 +249,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + create_gas_cost = ( Uint(0) if value == 0 or is_account_alive(evm.env.state, to) @@ -279,7 +282,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, diff --git a/src/ethereum/spurious_dragon/vm/interpreter.py b/src/ethereum/spurious_dragon/vm/interpreter.py index 1b9a021f7b..a219901806 100644 --- a/src/ethereum/spurious_dragon/vm/interpreter.py +++ b/src/ethereum/spurious_dragon/vm/interpreter.py @@ -104,6 +104,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -123,12 +124,11 @@ def process_message_call( logs: Tuple[Log, ...] = () accounts_to_delete = set() touched_accounts = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/src/ethereum/tangerine_whistle/fork.py b/src/ethereum/tangerine_whistle/fork.py index 1419c87b6d..cf39821f14 100644 --- a/src/ethereum/tangerine_whistle/fork.py +++ b/src/ethereum/tangerine_whistle/fork.py @@ -36,12 +36,7 @@ set_account_balance, state_root, ) -from .transactions import ( - Transaction, - calculate_intrinsic_cost, - recover_sender, - validate_transaction, -) +from .transactions import Transaction, recover_sender, validate_transaction from .trie import Trie, root, trie_set from .utils.message import prepare_message from .vm.interpreter import process_message_call @@ -647,8 +642,7 @@ def process_transaction( logs : `Tuple[ethereum.blocks.Log, ...]` Logs generated during execution. """ - if not validate_transaction(tx): - raise InvalidBlock + intrinsic_gas = validate_transaction(tx) sender = env.origin sender_account = get_account(env.state, sender) @@ -660,7 +654,7 @@ def process_transaction( if sender_account.code != bytearray(): raise InvalidSenderError("not EOA") - gas = tx.gas - calculate_intrinsic_cost(tx) + gas = tx.gas - intrinsic_gas increment_nonce(env.state, sender) sender_balance_after_gas_fee = Uint(sender_account.balance) - gas_fee set_account_balance(env.state, sender, U256(sender_balance_after_gas_fee)) diff --git a/src/ethereum/tangerine_whistle/transactions.py b/src/ethereum/tangerine_whistle/transactions.py index 57d697d615..96d825e2ba 100644 --- a/src/ethereum/tangerine_whistle/transactions.py +++ b/src/ethereum/tangerine_whistle/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidSignatureError +from ethereum.exceptions import InvalidBlock, InvalidSignatureError from .fork_types import Address @@ -41,7 +41,7 @@ class Transaction: s: U256 -def validate_transaction(tx: Transaction) -> bool: +def validate_transaction(tx: Transaction) -> Uint: """ Verifies a transaction. @@ -63,14 +63,20 @@ def validate_transaction(tx: Transaction) -> bool: Returns ------- - verified : `bool` - True if the transaction can be executed, or False otherwise. + intrinsic_gas : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not valid. """ - if calculate_intrinsic_cost(tx) > Uint(tx.gas): - return False - if tx.nonce >= U256(U64.MAX_VALUE): - return False - return True + intrinsic_gas = calculate_intrinsic_cost(tx) + if intrinsic_gas > tx.gas: + raise InvalidBlock + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise InvalidBlock + return intrinsic_gas def calculate_intrinsic_cost(tx: Transaction) -> Uint: diff --git a/src/ethereum/tangerine_whistle/vm/instructions/environment.py b/src/ethereum/tangerine_whistle/vm/instructions/environment.py index 9d936e7f5f..1bce21fda7 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/environment.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/environment.py @@ -334,8 +334,9 @@ def extcodesize(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. - codesize = U256(len(get_account(evm.env.state, address).code)) + code = get_account(evm.env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -369,6 +370,7 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.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/system.py b/src/ethereum/tangerine_whistle/vm/instructions/system.py index 35d9cecdeb..b931d8e8eb 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/system.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/system.py @@ -248,6 +248,9 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) + + code_address = to + _account_exists = account_exists(evm.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 @@ -275,7 +278,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, diff --git a/src/ethereum/tangerine_whistle/vm/interpreter.py b/src/ethereum/tangerine_whistle/vm/interpreter.py index 13d753ac0b..a02a0673ba 100644 --- a/src/ethereum/tangerine_whistle/vm/interpreter.py +++ b/src/ethereum/tangerine_whistle/vm/interpreter.py @@ -98,6 +98,7 @@ def process_message_call( output : `MessageCallOutput` Output of the message call """ + refund_counter = U256(0) if message.target == Bytes0(b""): is_collision = account_has_code_or_nonce( env.state, message.current_target @@ -114,11 +115,10 @@ def process_message_call( if evm.error: logs: Tuple[Log, ...] = () accounts_to_delete = set() - refund_counter = U256(0) else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = U256(evm.refund_counter) + refund_counter += U256(evm.refund_counter) tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error diff --git a/src/ethereum/tangerine_whistle/vm/precompiled_contracts/ecrecover.py b/src/ethereum/tangerine_whistle/vm/precompiled_contracts/ecrecover.py index 293e977575..1f047d3a44 100644 --- a/src/ethereum/tangerine_whistle/vm/precompiled_contracts/ecrecover.py +++ b/src/ethereum/tangerine_whistle/vm/precompiled_contracts/ecrecover.py @@ -15,6 +15,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError from ethereum.utils.byte import left_pad_zero_bytes from ...vm import Evm @@ -53,7 +54,7 @@ def ecrecover(evm: Evm) -> None: try: public_key = secp256k1_recover(r, s, v - U256(27), message_hash) - except ValueError: + except InvalidSignatureError: # unable to extract public key return diff --git a/tests/berlin/test_transaction.py b/tests/berlin/test_transaction.py index 9c86d1643b..280530d07a 100644 --- a/tests/berlin/test_transaction.py +++ b/tests/berlin/test_transaction.py @@ -3,8 +3,11 @@ import pytest from ethereum_rlp import rlp -from ethereum.berlin.fork import calculate_intrinsic_cost, validate_transaction -from ethereum.berlin.transactions import LegacyTransaction +from ethereum.berlin.transactions import ( + LegacyTransaction, + validate_transaction, +) +from ethereum.exceptions import InvalidBlock from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -30,7 +33,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(LegacyTransaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -49,5 +53,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/byzantium/test_transaction.py b/tests/byzantium/test_transaction.py index a133df527d..a9003bac9e 100644 --- a/tests/byzantium/test_transaction.py +++ b/tests/byzantium/test_transaction.py @@ -3,11 +3,8 @@ import pytest from ethereum_rlp import rlp -from ethereum.byzantium.fork import ( - calculate_intrinsic_cost, - validate_transaction, -) -from ethereum.byzantium.transactions import Transaction +from ethereum.byzantium.transactions import Transaction, validate_transaction +from ethereum.exceptions import InvalidBlock from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +32,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -54,5 +52,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/constantinople/test_transaction.py b/tests/constantinople/test_transaction.py index 0916fedcbd..367bf2e4b3 100644 --- a/tests/constantinople/test_transaction.py +++ b/tests/constantinople/test_transaction.py @@ -3,11 +3,11 @@ import pytest from ethereum_rlp import rlp -from ethereum.constantinople.fork import ( - calculate_intrinsic_cost, +from ethereum.constantinople.transactions import ( + Transaction, validate_transaction, ) -from ethereum.constantinople.transactions import Transaction +from ethereum.exceptions import InvalidBlock from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +35,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -54,5 +55,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/frontier/test_transaction.py b/tests/frontier/test_transaction.py index 6b0c12f2e8..dff4ab44bb 100644 --- a/tests/frontier/test_transaction.py +++ b/tests/frontier/test_transaction.py @@ -3,11 +3,8 @@ import pytest from ethereum_rlp import rlp -from ethereum.frontier.fork import ( - calculate_intrinsic_cost, - validate_transaction, -) -from ethereum.frontier.transactions import Transaction +from ethereum.exceptions import InvalidBlock +from ethereum.frontier.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -33,7 +30,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -52,5 +50,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/homestead/test_transaction.py b/tests/homestead/test_transaction.py index 5fff1c1bbc..3f30927fed 100644 --- a/tests/homestead/test_transaction.py +++ b/tests/homestead/test_transaction.py @@ -3,11 +3,8 @@ import pytest from ethereum_rlp import rlp -from ethereum.homestead.fork import ( - calculate_intrinsic_cost, - validate_transaction, -) -from ethereum.homestead.transactions import Transaction +from ethereum.exceptions import InvalidBlock +from ethereum.homestead.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +32,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -54,5 +52,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/istanbul/test_transaction.py b/tests/istanbul/test_transaction.py index 6d0719fc3c..ec46c371b8 100644 --- a/tests/istanbul/test_transaction.py +++ b/tests/istanbul/test_transaction.py @@ -3,11 +3,8 @@ import pytest from ethereum_rlp import rlp -from ethereum.istanbul.fork import ( - calculate_intrinsic_cost, - validate_transaction, -) -from ethereum.istanbul.transactions import Transaction +from ethereum.exceptions import InvalidBlock +from ethereum.istanbul.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -33,7 +30,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -52,5 +50,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/london/test_transaction.py b/tests/london/test_transaction.py index 21b418d62c..760ad3351b 100644 --- a/tests/london/test_transaction.py +++ b/tests/london/test_transaction.py @@ -3,8 +3,11 @@ import pytest from ethereum_rlp import rlp -from ethereum.london.fork import calculate_intrinsic_cost, validate_transaction -from ethereum.london.transactions import LegacyTransaction +from ethereum.exceptions import InvalidBlock +from ethereum.london.transactions import ( + LegacyTransaction, + validate_transaction, +) from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -30,7 +33,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(LegacyTransaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -49,5 +53,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/spurious_dragon/test_transaction.py b/tests/spurious_dragon/test_transaction.py index fc70c08d14..ad68dafd68 100644 --- a/tests/spurious_dragon/test_transaction.py +++ b/tests/spurious_dragon/test_transaction.py @@ -3,11 +3,11 @@ import pytest from ethereum_rlp import rlp -from ethereum.spurious_dragon.fork import ( - calculate_intrinsic_cost, +from ethereum.exceptions import InvalidBlock +from ethereum.spurious_dragon.transactions import ( + Transaction, validate_transaction, ) -from ethereum.spurious_dragon.transactions import Transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +35,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -54,5 +55,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tests/tangerine_whistle/test_transaction.py b/tests/tangerine_whistle/test_transaction.py index 7e3a253fdb..3d70cff571 100644 --- a/tests/tangerine_whistle/test_transaction.py +++ b/tests/tangerine_whistle/test_transaction.py @@ -3,11 +3,11 @@ import pytest from ethereum_rlp import rlp -from ethereum.tangerine_whistle.fork import ( - calculate_intrinsic_cost, +from ethereum.exceptions import InvalidBlock +from ethereum.tangerine_whistle.transactions import ( + Transaction, validate_transaction, ) -from ethereum.tangerine_whistle.transactions import Transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +35,8 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - assert not validate_transaction(tx) + with pytest.raises(InvalidBlock): + validate_transaction(tx) @pytest.mark.parametrize( @@ -54,5 +55,5 @@ def test_nonce(test_file_nonce: str) -> None: test["test_result"]["intrinsicGas"] ) - assert validate_transaction(tx) - assert calculate_intrinsic_cost(tx) == result_intrinsic_gas_cost + intrinsic_gas = validate_transaction(tx) + assert intrinsic_gas == result_intrinsic_gas_cost diff --git a/tox.ini b/tox.ini index 7df4d42969..647714587b 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,6 @@ commands = pytest \ -m "not slow" \ -n auto --maxprocesses 5 \ - tests/prague \ --cov=ethereum --cov-report=term --cov-report "xml:{toxworkdir}/coverage.xml" \ --ignore-glob='tests/fixtures/*' \ --basetemp="{temp_dir}/pytest" @@ -39,7 +38,6 @@ commands = --disable-warnings \ -m "not slow" \ -n auto --maxprocesses 2 \ - tests/prague \ --ignore-glob='tests/fixtures/*' \ --basetemp="{temp_dir}/pytest" @@ -53,7 +51,6 @@ commands = pytest \ -m "not slow and not evm_tools" \ -n auto --maxprocesses 5 \ - tests/prague \ --ignore-glob='tests/fixtures/*' \ --ignore-glob='tests/test_t8n.py' \ --basetemp="{temp_dir}/pytest" \ From 8d6093a3983dbbf98ebdb84daadef74445050dad Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Tue, 11 Feb 2025 18:39:09 -0500 Subject: [PATCH 23/38] Prepare to release --- setup.cfg | 12 +++++++++--- src/ethereum/__init__.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1666e6b37f..0ec3656199 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] -name = ethereum -description = Ethereum specification, provided as a Python package for tooling and testing +name = ethereum-execution +description = Ethereum execution layer specification, provided as a Python package for tooling and testing long_description = file: README.md long_description_content_type = text/markdown version = attr: ethereum.__version__ @@ -9,6 +9,12 @@ license_files = LICENSE.md classifiers = License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: Implementation :: PyPy + Programming Language :: Python :: Implementation :: CPython + Intended Audience :: Developers + Natural Language :: English [options] packages = @@ -120,7 +126,7 @@ install_requires = pycryptodome>=3,<4 coincurve>=20,<21 typing_extensions>=4.2 - py_ecc @ git+https://github.com/petertdavies/py_ecc.git@127184f4c57b1812da959586d0fe8f43bb1a2389 + py-ecc>=8.0.0b2,<9 ethereum-types>=0.2.1,<0.3 ethereum-rlp>=0.1.1,<0.2 diff --git a/src/ethereum/__init__.py b/src/ethereum/__init__.py index f7eeebb47c..8b5958b584 100644 --- a/src/ethereum/__init__.py +++ b/src/ethereum/__init__.py @@ -18,7 +18,7 @@ """ import sys -__version__ = "0.1.0" +__version__ = "1.17.0rc6.dev1" # # Ensure we can reach 1024 frames of recursion From f7392934916bdb953b1496cf47813b1ac3bfa7a4 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Wed, 12 Feb 2025 09:51:10 -0500 Subject: [PATCH 24/38] fix: check y_parity value (#1107) --- src/ethereum/prague/transactions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index f10f954fab..b79b9ab572 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -344,14 +344,20 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: signing_hash_155(tx, chain_id), ) elif isinstance(tx, AccessListTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_2930(tx) ) elif isinstance(tx, FeeMarketTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_1559(tx) ) elif isinstance(tx, BlobTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_4844(tx) ) From a818bd2add412e7654712c7c6679cbfe5d85af10 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Wed, 12 Feb 2025 19:50:22 -0500 Subject: [PATCH 25/38] Remove unused file --- src/ethereum/utils/safe_arithmetic.py | 95 --------------------------- 1 file changed, 95 deletions(-) delete mode 100644 src/ethereum/utils/safe_arithmetic.py diff --git a/src/ethereum/utils/safe_arithmetic.py b/src/ethereum/utils/safe_arithmetic.py deleted file mode 100644 index 0c1bfbe32e..0000000000 --- a/src/ethereum/utils/safe_arithmetic.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Safe Arithmetic for U256 Integer Type -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. contents:: Table of Contents - :backlinks: none - :local: - -Introduction ------------- - -Safe arithmetic utility functions for U256 integer type. -""" -from typing import Optional, Type, Union - -from ethereum_types.numeric import U256, Uint - - -def u256_safe_add( - *numbers: Union[U256, Uint], - exception_type: Optional[Type[BaseException]] = None -) -> U256: - """ - Adds together the given sequence of numbers. If the total sum of the - numbers exceeds `U256.MAX_VALUE` then an exception is raised. - If `exception_type` = None then the exception raised defaults to the one - raised by `U256` when `U256.value > U256.MAX_VALUE` - else `exception_type` is raised. - - Parameters - ---------- - numbers : - The sequence of numbers that need to be added together. - - exception_type: - The exception that needs to be raised if the sum of the `numbers` - exceeds `U256.MAX_VALUE`. - - Returns - ------- - result : `ethereum.base_types.U256` - The sum of the given sequence of numbers if the total is less than - `U256.MAX_VALUE` else an exception is raised. - If `exception_type` = None then the exception raised defaults to the - one raised by `U256` when `U256.value > U256.MAX_VALUE` - else `exception_type` is raised. - """ - try: - return U256(sum(int(n) for n in numbers)) - except ValueError as e: - if exception_type: - raise exception_type from e - else: - raise e - - -def u256_safe_multiply( - *numbers: Union[U256, Uint], - exception_type: Optional[Type[BaseException]] = None -) -> U256: - """ - Multiplies together the given sequence of numbers. If the net product of - the numbers exceeds `U256.MAX_VALUE` then an exception is raised. - If `exception_type` = None then the exception raised defaults to the one - raised by `U256` when `U256.value > U256.MAX_VALUE` else - `exception_type` is raised. - - Parameters - ---------- - numbers : - The sequence of numbers that need to be multiplies together. - - exception_type: - The exception that needs to be raised if the sum of the `numbers` - exceeds `U256.MAX_VALUE`. - - Returns - ------- - result : `ethereum.base_types.U256` - The multiplication product of the given sequence of numbers if the - net product is less than `U256.MAX_VALUE` else an exception is raised. - If `exception_type` = None then the exception raised defaults to the - one raised by `U256` when `U256.value > U256.MAX_VALUE` - else `exception_type` is raised. - """ - result = Uint(numbers[0]) - try: - for number in numbers[1:]: - result *= Uint(number) - return U256(result) - except ValueError as e: - if exception_type: - raise exception_type from e - else: - raise e From 6023917e18ac8e6fe8f987762c305c4cac8110e5 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Thu, 13 Feb 2025 11:49:30 -0500 Subject: [PATCH 26/38] Bump ethash to 1.1.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0ec3656199..499c9c7720 100644 --- a/setup.cfg +++ b/setup.cfg @@ -191,7 +191,7 @@ doc = optimized = rust-pyspec-glue>=0.0.9,<0.1.0 - ethash @ git+https://github.com/chfast/ethash.git@e08bd0fadb8785f7ccf1e2fb07b75f54fe47f92e + ethash>=1.1.0,<2 [flake8] dictionaries=en_US,python,technical From 51f2d6a0bad598b391de9ca4e3d5a928708254fe Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Wed, 5 Mar 2025 07:38:13 +0100 Subject: [PATCH 27/38] Refactor Environment (#1131) * refactor blob gas logic There are parallels between how the regular block gas is handled and how the blob gas is handled. This commit refactors blob gas canculation to bring them in line with block gas * create BlockEnvironment and TransactionEnvironment Create the BlockEnvironment and TransactionEnvironment dataclasses BlockEnvironment holds data that is chain or block scoped TransactionEnvironment holds data that is transaction scoped Message holds data that is specific to a message call * refactor validate_header * update t8n * update evm tracing * backport changes to cancun * port to shanghai * port changes to paris * port to london * port berlin * port istanbul * port constantinople * port byzantium * port spurious_dragon * port older forks * fix t8n receipts * update testing * process_withdrawals update * fixes for doc * remove execution_spec_tests entry The tests from execution_spec_tests should now be a part of EEST * use default_factory in BlockOutput * minor fixes 1 * define current_block_number in block_hash opcode * rename tx_index to index_in_block * fix vm test runs --- src/ethereum/arrow_glacier/fork.py | 394 +++++----- src/ethereum/arrow_glacier/state.py | 19 +- src/ethereum/arrow_glacier/transactions.py | 45 +- src/ethereum/arrow_glacier/utils/message.py | 97 +-- src/ethereum/arrow_glacier/vm/__init__.py | 70 +- .../arrow_glacier/vm/instructions/block.py | 22 +- .../vm/instructions/environment.py | 18 +- .../arrow_glacier/vm/instructions/storage.py | 11 +- .../arrow_glacier/vm/instructions/system.py | 63 +- src/ethereum/arrow_glacier/vm/interpreter.py | 72 +- src/ethereum/berlin/fork.py | 369 +++++---- src/ethereum/berlin/state.py | 19 +- src/ethereum/berlin/transactions.py | 45 +- src/ethereum/berlin/utils/message.py | 97 +-- src/ethereum/berlin/vm/__init__.py | 70 +- src/ethereum/berlin/vm/instructions/block.py | 22 +- .../berlin/vm/instructions/environment.py | 16 +- .../berlin/vm/instructions/storage.py | 11 +- src/ethereum/berlin/vm/instructions/system.py | 64 +- src/ethereum/berlin/vm/interpreter.py | 68 +- src/ethereum/byzantium/fork.py | 357 ++++----- src/ethereum/byzantium/state.py | 19 +- src/ethereum/byzantium/transactions.py | 32 +- src/ethereum/byzantium/utils/message.py | 79 +- src/ethereum/byzantium/vm/__init__.py | 69 +- .../byzantium/vm/instructions/block.py | 20 +- .../byzantium/vm/instructions/environment.py | 11 +- .../byzantium/vm/instructions/storage.py | 9 +- .../byzantium/vm/instructions/system.py | 57 +- src/ethereum/byzantium/vm/interpreter.py | 66 +- src/ethereum/cancun/fork.py | 578 ++++++--------- src/ethereum/cancun/state.py | 15 - src/ethereum/cancun/transactions.py | 47 +- src/ethereum/cancun/utils/message.py | 97 +-- src/ethereum/cancun/vm/__init__.py | 83 ++- src/ethereum/cancun/vm/instructions/block.py | 22 +- .../cancun/vm/instructions/environment.py | 26 +- .../cancun/vm/instructions/storage.py | 18 +- src/ethereum/cancun/vm/instructions/system.py | 51 +- src/ethereum/cancun/vm/interpreter.py | 72 +- src/ethereum/constantinople/fork.py | 357 ++++----- src/ethereum/constantinople/state.py | 19 +- src/ethereum/constantinople/transactions.py | 32 +- src/ethereum/constantinople/utils/message.py | 79 +- src/ethereum/constantinople/vm/__init__.py | 69 +- .../constantinople/vm/instructions/block.py | 20 +- .../vm/instructions/environment.py | 12 +- .../constantinople/vm/instructions/storage.py | 9 +- .../constantinople/vm/instructions/system.py | 60 +- src/ethereum/constantinople/vm/interpreter.py | 66 +- src/ethereum/dao_fork/fork.py | 334 ++++----- src/ethereum/dao_fork/transactions.py | 31 +- src/ethereum/dao_fork/utils/message.py | 73 +- src/ethereum/dao_fork/vm/__init__.py | 65 +- .../dao_fork/vm/instructions/block.py | 20 +- .../dao_fork/vm/instructions/environment.py | 11 +- .../dao_fork/vm/instructions/storage.py | 9 +- .../dao_fork/vm/instructions/system.py | 50 +- src/ethereum/dao_fork/vm/interpreter.py | 56 +- src/ethereum/frontier/fork.py | 333 ++++----- src/ethereum/frontier/transactions.py | 27 +- src/ethereum/frontier/utils/message.py | 68 +- src/ethereum/frontier/vm/__init__.py | 65 +- .../frontier/vm/instructions/block.py | 20 +- .../frontier/vm/instructions/environment.py | 11 +- .../frontier/vm/instructions/storage.py | 9 +- .../frontier/vm/instructions/system.py | 50 +- src/ethereum/frontier/vm/interpreter.py | 54 +- src/ethereum/gray_glacier/fork.py | 394 +++++----- src/ethereum/gray_glacier/state.py | 19 +- src/ethereum/gray_glacier/transactions.py | 45 +- src/ethereum/gray_glacier/utils/message.py | 97 +-- src/ethereum/gray_glacier/vm/__init__.py | 70 +- .../gray_glacier/vm/instructions/block.py | 22 +- .../vm/instructions/environment.py | 18 +- .../gray_glacier/vm/instructions/storage.py | 11 +- .../gray_glacier/vm/instructions/system.py | 63 +- src/ethereum/gray_glacier/vm/interpreter.py | 72 +- src/ethereum/homestead/fork.py | 333 ++++----- src/ethereum/homestead/transactions.py | 31 +- src/ethereum/homestead/utils/message.py | 73 +- src/ethereum/homestead/vm/__init__.py | 65 +- .../homestead/vm/instructions/block.py | 20 +- .../homestead/vm/instructions/environment.py | 11 +- .../homestead/vm/instructions/storage.py | 9 +- .../homestead/vm/instructions/system.py | 50 +- src/ethereum/homestead/vm/interpreter.py | 56 +- src/ethereum/istanbul/fork.py | 358 ++++----- src/ethereum/istanbul/state.py | 19 +- src/ethereum/istanbul/transactions.py | 32 +- src/ethereum/istanbul/utils/message.py | 79 +- src/ethereum/istanbul/vm/__init__.py | 68 +- .../istanbul/vm/instructions/block.py | 22 +- .../istanbul/vm/instructions/environment.py | 16 +- .../istanbul/vm/instructions/storage.py | 11 +- .../istanbul/vm/instructions/system.py | 60 +- src/ethereum/istanbul/vm/interpreter.py | 68 +- src/ethereum/london/fork.py | 394 +++++----- src/ethereum/london/state.py | 19 +- src/ethereum/london/transactions.py | 45 +- src/ethereum/london/utils/message.py | 97 +-- src/ethereum/london/vm/__init__.py | 70 +- src/ethereum/london/vm/instructions/block.py | 22 +- .../london/vm/instructions/environment.py | 18 +- .../london/vm/instructions/storage.py | 11 +- src/ethereum/london/vm/instructions/system.py | 63 +- src/ethereum/london/vm/interpreter.py | 68 +- src/ethereum/muir_glacier/fork.py | 356 ++++----- src/ethereum/muir_glacier/state.py | 19 +- src/ethereum/muir_glacier/transactions.py | 32 +- src/ethereum/muir_glacier/utils/message.py | 79 +- src/ethereum/muir_glacier/vm/__init__.py | 68 +- .../muir_glacier/vm/instructions/block.py | 22 +- .../vm/instructions/environment.py | 16 +- .../muir_glacier/vm/instructions/storage.py | 11 +- .../muir_glacier/vm/instructions/system.py | 60 +- src/ethereum/muir_glacier/vm/interpreter.py | 68 +- src/ethereum/paris/fork.py | 389 +++++----- src/ethereum/paris/state.py | 19 +- src/ethereum/paris/transactions.py | 45 +- src/ethereum/paris/utils/message.py | 97 +-- src/ethereum/paris/vm/__init__.py | 70 +- src/ethereum/paris/vm/instructions/block.py | 22 +- .../paris/vm/instructions/environment.py | 18 +- src/ethereum/paris/vm/instructions/storage.py | 11 +- src/ethereum/paris/vm/instructions/system.py | 63 +- src/ethereum/paris/vm/interpreter.py | 70 +- src/ethereum/prague/fork.py | 699 +++++++----------- src/ethereum/prague/state.py | 15 - src/ethereum/prague/transactions.py | 19 + src/ethereum/prague/utils/message.py | 103 +-- src/ethereum/prague/vm/__init__.py | 86 ++- src/ethereum/prague/vm/eoa_delegation.py | 30 +- src/ethereum/prague/vm/instructions/block.py | 22 +- .../prague/vm/instructions/environment.py | 26 +- .../prague/vm/instructions/storage.py | 18 +- src/ethereum/prague/vm/instructions/system.py | 53 +- src/ethereum/prague/vm/interpreter.py | 67 +- src/ethereum/shanghai/fork.py | 438 +++++------ src/ethereum/shanghai/state.py | 34 +- src/ethereum/shanghai/transactions.py | 47 +- src/ethereum/shanghai/utils/message.py | 97 +-- src/ethereum/shanghai/vm/__init__.py | 75 +- .../shanghai/vm/instructions/block.py | 22 +- .../shanghai/vm/instructions/environment.py | 18 +- .../shanghai/vm/instructions/storage.py | 11 +- .../shanghai/vm/instructions/system.py | 55 +- src/ethereum/shanghai/vm/interpreter.py | 70 +- src/ethereum/spurious_dragon/fork.py | 354 ++++----- src/ethereum/spurious_dragon/state.py | 19 +- src/ethereum/spurious_dragon/transactions.py | 32 +- src/ethereum/spurious_dragon/utils/message.py | 73 +- src/ethereum/spurious_dragon/vm/__init__.py | 69 +- .../spurious_dragon/vm/instructions/block.py | 20 +- .../vm/instructions/environment.py | 11 +- .../vm/instructions/storage.py | 9 +- .../spurious_dragon/vm/instructions/system.py | 58 +- .../spurious_dragon/vm/interpreter.py | 66 +- src/ethereum/tangerine_whistle/fork.py | 334 ++++----- .../tangerine_whistle/transactions.py | 31 +- .../tangerine_whistle/utils/message.py | 73 +- src/ethereum/tangerine_whistle/vm/__init__.py | 65 +- .../vm/instructions/block.py | 20 +- .../vm/instructions/environment.py | 11 +- .../vm/instructions/storage.py | 9 +- .../vm/instructions/system.py | 50 +- .../tangerine_whistle/vm/interpreter.py | 56 +- .../evm_tools/loaders/fork_loader.py | 117 +-- .../evm_tools/statetest/__init__.py | 2 +- .../evm_tools/t8n/__init__.py | 379 ++-------- .../evm_tools/t8n/evm_trace.py | 59 +- .../evm_tools/t8n/t8n_types.py | 132 ++-- tests/berlin/test_evm_tools.py | 48 +- tests/berlin/test_state_transition.py | 135 +--- tests/byzantium/test_evm_tools.py | 38 +- tests/byzantium/test_state_transition.py | 144 +--- tests/cancun/test_evm_tools.py | 46 +- tests/cancun/test_state_transition.py | 51 +- tests/conftest.py | 22 + tests/constantinople/test_evm_tools.py | 37 +- tests/constantinople/test_state_transition.py | 146 +--- tests/frontier/test_evm_tools.py | 38 +- tests/frontier/test_state_transition.py | 147 +--- tests/helpers/__init__.py | 10 +- tests/helpers/load_evm_tools_tests.py | 2 +- tests/helpers/load_vm_tests.py | 75 +- tests/homestead/test_evm_tools.py | 39 +- tests/homestead/test_state_transition.py | 145 +--- tests/istanbul/test_evm_tools.py | 48 +- tests/istanbul/test_state_transition.py | 135 +--- tests/london/test_evm_tools.py | 48 +- tests/london/test_state_transition.py | 137 +--- tests/paris/test_evm_tools.py | 48 +- tests/paris/test_state_transition.py | 137 +--- tests/prague/test_evm_tools.py | 50 +- tests/prague/test_state_transition.py | 65 +- tests/shanghai/test_evm_tools.py | 48 +- tests/shanghai/test_state_transition.py | 50 +- tests/spurious_dragon/test_evm_tools.py | 38 +- .../spurious_dragon/test_state_transition.py | 145 +--- tests/tangerine_whistle/test_evm_tools.py | 39 +- .../test_state_transition.py | 147 +--- whitelist.txt | 3 + 203 files changed, 7818 insertions(+), 8360 deletions(-) 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 b79b9ab572..4bfc077b02 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -558,3 +558,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 From 24f8cfb96146444cc5daae2a9b23e6270331756f Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Mon, 10 Mar 2025 11:23:57 +0100 Subject: [PATCH 28/38] add state test flag --- src/ethereum_spec_tools/evm_tools/statetest/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py index 1e1ac04bcb..6ecca69ad5 100644 --- a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -135,6 +135,7 @@ def run_test_case( "stdin", "--state.fork", f"{test_case.fork_name}", + "--state-test", ] if t8n_extra is not None: From 4509c1d3ee380a366bb5ecfdc8d1c3c71f08c378 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Mon, 10 Mar 2025 11:52:01 +0100 Subject: [PATCH 29/38] output traces to correct location --- src/ethereum_spec_tools/evm_tools/t8n/__init__.py | 1 + src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py | 10 ++++++---- tests/conftest.py | 4 ---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 4d9de304fc..23415756e1 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -109,6 +109,7 @@ def __init__( trace_memory=trace_memory, trace_stack=trace_stack, trace_return_data=trace_return_data, + output_basedir=self.options.output_basedir, ) self.logger = get_stream_logger("T8N") 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 3802d36209..99acf28e7e 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py @@ -32,7 +32,6 @@ ) EXCLUDE_FROM_OUTPUT = ["gasCostTraced", "errorTraced", "precompile"] -OUTPUT_DIR = "." @dataclass @@ -141,6 +140,7 @@ def evm_trace( trace_memory: bool = False, trace_stack: bool = True, trace_return_data: bool = False, + output_basedir: str | TextIO = ".", ) -> None: """ Create a trace of the event. @@ -189,6 +189,7 @@ def evm_trace( traces, evm.message.tx_env.index_in_block, evm.message.tx_env.tx_hash, + output_basedir, ) elif isinstance(event, PrecompileStart): new_trace = Trace( @@ -349,6 +350,7 @@ def output_traces( traces: List[Union[Trace, FinalTrace]], index_in_block: int, tx_hash: bytes, + output_basedir: str | TextIO, ) -> None: """ Output the traces to a json file. @@ -356,15 +358,15 @@ def output_traces( with ExitStack() as stack: json_file: TextIO - if isinstance(OUTPUT_DIR, str): + if isinstance(output_basedir, str): tx_hash_str = "0x" + tx_hash.hex() output_path = os.path.join( - OUTPUT_DIR, f"trace-{index_in_block}-{tx_hash_str}.jsonl" + output_basedir, f"trace-{index_in_block}-{tx_hash_str}.jsonl" ) json_file = open(output_path, "w") stack.push(json_file) else: - json_file = OUTPUT_DIR + json_file = output_basedir for trace in traces: if getattr(trace, "precompile", False): diff --git a/tests/conftest.py b/tests/conftest.py index d61805f4b4..c1f7d03b2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,17 +47,13 @@ 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: From 0ae1b11c98969d7189890eebe4795621480bd14a Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Tue, 11 Mar 2025 08:30:32 +0100 Subject: [PATCH 30/38] update ethereum tests to latest --- tests/helpers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index b25e2049ff..42fae17f80 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -8,7 +8,7 @@ }, "ethereum_tests": { "url": "https://github.com/ethereum/tests.git", - "commit_hash": "a0e8482", + "commit_hash": "afed83b", "fixture_path": "tests/fixtures/ethereum_tests", }, "latest_fork_tests": { From 91903030f31fcd5ec165dde68423a41c7d9a08ca Mon Sep 17 00:00:00 2001 From: Shashwat_Nautiyal <148687529+Shashwat-Nautiyal@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:53:46 +0530 Subject: [PATCH 31/38] validate_transaction now raises InvalidTransaction exception (#1138) * validate_transaction now raises InvalidTransaction * Update src/ethereum/cancun/transactions.py Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> * Update src/ethereum/cancun/transactions.py Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> * Update src/ethereum/cancun/transactions.py Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> * exception specified within InvalidTransaction * isort formatting issue resolved * fix issues from static checks --------- Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Co-authored-by: Guruprasad Kamath --- src/ethereum/arrow_glacier/transactions.py | 8 ++++---- src/ethereum/berlin/transactions.py | 8 ++++---- src/ethereum/byzantium/transactions.py | 8 ++++---- src/ethereum/cancun/transactions.py | 10 +++++----- src/ethereum/constantinople/transactions.py | 8 ++++---- src/ethereum/dao_fork/transactions.py | 8 ++++---- src/ethereum/frontier/transactions.py | 8 ++++---- src/ethereum/gray_glacier/transactions.py | 8 ++++---- src/ethereum/homestead/transactions.py | 8 ++++---- src/ethereum/istanbul/transactions.py | 8 ++++---- src/ethereum/london/transactions.py | 8 ++++---- src/ethereum/muir_glacier/transactions.py | 8 ++++---- src/ethereum/paris/transactions.py | 8 ++++---- src/ethereum/prague/transactions.py | 8 ++++---- src/ethereum/shanghai/transactions.py | 10 +++++----- src/ethereum/spurious_dragon/transactions.py | 8 ++++---- src/ethereum/tangerine_whistle/transactions.py | 8 ++++---- tests/berlin/test_transaction.py | 4 ++-- tests/byzantium/test_transaction.py | 4 ++-- tests/constantinople/test_transaction.py | 4 ++-- tests/frontier/test_transaction.py | 4 ++-- tests/homestead/test_transaction.py | 4 ++-- tests/istanbul/test_transaction.py | 4 ++-- tests/london/test_transaction.py | 4 ++-- tests/spurious_dragon/test_transaction.py | 4 ++-- tests/tangerine_whistle/test_transaction.py | 4 ++-- 26 files changed, 88 insertions(+), 88 deletions(-) diff --git a/src/ethereum/arrow_glacier/transactions.py b/src/ethereum/arrow_glacier/transactions.py index b853357506..86c92caa48 100644 --- a/src/ethereum/arrow_glacier/transactions.py +++ b/src/ethereum/arrow_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -146,14 +146,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/berlin/transactions.py b/src/ethereum/berlin/transactions.py index 3a3b4bd749..b120948b7e 100644 --- a/src/ethereum/berlin/transactions.py +++ b/src/ethereum/berlin/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -118,14 +118,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/byzantium/transactions.py b/src/ethereum/byzantium/transactions.py index 21ffbc1b6f..65e622b5e1 100644 --- a/src/ethereum/byzantium/transactions.py +++ b/src/ethereum/byzantium/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/cancun/transactions.py b/src/ethereum/cancun/transactions.py index b2e5c47ffd..aae608986c 100644 --- a/src/ethereum/cancun/transactions.py +++ b/src/ethereum/cancun/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address, VersionedHash @@ -176,18 +176,18 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ from .vm.interpreter import MAX_CODE_SIZE intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - raise InvalidBlock + raise InvalidTransaction("Code size too large") return intrinsic_gas diff --git a/src/ethereum/constantinople/transactions.py b/src/ethereum/constantinople/transactions.py index 21ffbc1b6f..65e622b5e1 100644 --- a/src/ethereum/constantinople/transactions.py +++ b/src/ethereum/constantinople/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/dao_fork/transactions.py b/src/ethereum/dao_fork/transactions.py index 6db00e736d..6761b34ca9 100644 --- a/src/ethereum/dao_fork/transactions.py +++ b/src/ethereum/dao_fork/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/frontier/transactions.py b/src/ethereum/frontier/transactions.py index e9801dfafe..99e6a6bb05 100644 --- a/src/ethereum/frontier/transactions.py +++ b/src/ethereum/frontier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -67,14 +67,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("nonce too high") return intrinsic_gas diff --git a/src/ethereum/gray_glacier/transactions.py b/src/ethereum/gray_glacier/transactions.py index b853357506..86c92caa48 100644 --- a/src/ethereum/gray_glacier/transactions.py +++ b/src/ethereum/gray_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -146,14 +146,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/homestead/transactions.py b/src/ethereum/homestead/transactions.py index 6db00e736d..6761b34ca9 100644 --- a/src/ethereum/homestead/transactions.py +++ b/src/ethereum/homestead/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/istanbul/transactions.py b/src/ethereum/istanbul/transactions.py index 28115976e8..4f2f9efe6d 100644 --- a/src/ethereum/istanbul/transactions.py +++ b/src/ethereum/istanbul/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/london/transactions.py b/src/ethereum/london/transactions.py index b853357506..86c92caa48 100644 --- a/src/ethereum/london/transactions.py +++ b/src/ethereum/london/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -146,14 +146,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/muir_glacier/transactions.py b/src/ethereum/muir_glacier/transactions.py index 28115976e8..4f2f9efe6d 100644 --- a/src/ethereum/muir_glacier/transactions.py +++ b/src/ethereum/muir_glacier/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/paris/transactions.py b/src/ethereum/paris/transactions.py index b853357506..86c92caa48 100644 --- a/src/ethereum/paris/transactions.py +++ b/src/ethereum/paris/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -146,14 +146,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/prague/transactions.py b/src/ethereum/prague/transactions.py index 4bfc077b02..3e5adcfb46 100644 --- a/src/ethereum/prague/transactions.py +++ b/src/ethereum/prague/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address, Authorization, VersionedHash @@ -213,11 +213,11 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - raise InvalidBlock + raise InvalidTransaction("Code size too large") return intrinsic_gas, calldata_floor_gas_cost diff --git a/src/ethereum/shanghai/transactions.py b/src/ethereum/shanghai/transactions.py index f78f3af49e..07a99c48f7 100644 --- a/src/ethereum/shanghai/transactions.py +++ b/src/ethereum/shanghai/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .exceptions import TransactionTypeError from .fork_types import Address @@ -146,18 +146,18 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ from .vm.interpreter import MAX_CODE_SIZE intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: - raise InvalidBlock + raise InvalidTransaction("Code size too large") return intrinsic_gas diff --git a/src/ethereum/spurious_dragon/transactions.py b/src/ethereum/spurious_dragon/transactions.py index 21ffbc1b6f..65e622b5e1 100644 --- a/src/ethereum/spurious_dragon/transactions.py +++ b/src/ethereum/spurious_dragon/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/src/ethereum/tangerine_whistle/transactions.py b/src/ethereum/tangerine_whistle/transactions.py index 6db00e736d..6761b34ca9 100644 --- a/src/ethereum/tangerine_whistle/transactions.py +++ b/src/ethereum/tangerine_whistle/transactions.py @@ -13,7 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.exceptions import InvalidSignatureError, InvalidTransaction from .fork_types import Address @@ -68,14 +68,14 @@ def validate_transaction(tx: Transaction) -> Uint: Raises ------ - InvalidBlock : + InvalidTransaction : If the transaction is not valid. """ intrinsic_gas = calculate_intrinsic_cost(tx) if intrinsic_gas > tx.gas: - raise InvalidBlock + raise InvalidTransaction("Insufficient gas") if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise InvalidBlock + raise InvalidTransaction("Nonce too high") return intrinsic_gas diff --git a/tests/berlin/test_transaction.py b/tests/berlin/test_transaction.py index 280530d07a..ce5fc68e21 100644 --- a/tests/berlin/test_transaction.py +++ b/tests/berlin/test_transaction.py @@ -7,7 +7,7 @@ LegacyTransaction, validate_transaction, ) -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -33,7 +33,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(LegacyTransaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/byzantium/test_transaction.py b/tests/byzantium/test_transaction.py index a9003bac9e..acb3e9070c 100644 --- a/tests/byzantium/test_transaction.py +++ b/tests/byzantium/test_transaction.py @@ -4,7 +4,7 @@ from ethereum_rlp import rlp from ethereum.byzantium.transactions import Transaction, validate_transaction -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -32,7 +32,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/constantinople/test_transaction.py b/tests/constantinople/test_transaction.py index 367bf2e4b3..b11626d45d 100644 --- a/tests/constantinople/test_transaction.py +++ b/tests/constantinople/test_transaction.py @@ -7,7 +7,7 @@ Transaction, validate_transaction, ) -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -35,7 +35,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/frontier/test_transaction.py b/tests/frontier/test_transaction.py index dff4ab44bb..a2e72140d4 100644 --- a/tests/frontier/test_transaction.py +++ b/tests/frontier/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.frontier.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -30,7 +30,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/homestead/test_transaction.py b/tests/homestead/test_transaction.py index 3f30927fed..7562afa3d5 100644 --- a/tests/homestead/test_transaction.py +++ b/tests/homestead/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.homestead.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -32,7 +32,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/istanbul/test_transaction.py b/tests/istanbul/test_transaction.py index ec46c371b8..37a096445d 100644 --- a/tests/istanbul/test_transaction.py +++ b/tests/istanbul/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.istanbul.transactions import Transaction, validate_transaction from ethereum.utils.hexadecimal import hex_to_uint from tests.helpers import TEST_FIXTURES @@ -30,7 +30,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/london/test_transaction.py b/tests/london/test_transaction.py index 760ad3351b..5925474afe 100644 --- a/tests/london/test_transaction.py +++ b/tests/london/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.london.transactions import ( LegacyTransaction, validate_transaction, @@ -33,7 +33,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(LegacyTransaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/spurious_dragon/test_transaction.py b/tests/spurious_dragon/test_transaction.py index ad68dafd68..5e25d340ac 100644 --- a/tests/spurious_dragon/test_transaction.py +++ b/tests/spurious_dragon/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.spurious_dragon.transactions import ( Transaction, validate_transaction, @@ -35,7 +35,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) diff --git a/tests/tangerine_whistle/test_transaction.py b/tests/tangerine_whistle/test_transaction.py index 3d70cff571..455630e8a3 100644 --- a/tests/tangerine_whistle/test_transaction.py +++ b/tests/tangerine_whistle/test_transaction.py @@ -3,7 +3,7 @@ import pytest from ethereum_rlp import rlp -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import InvalidTransaction from ethereum.tangerine_whistle.transactions import ( Transaction, validate_transaction, @@ -35,7 +35,7 @@ def test_high_nonce(test_file_high_nonce: str) -> None: tx = rlp.decode_to(Transaction, test["tx_rlp"]) - with pytest.raises(InvalidBlock): + with pytest.raises(InvalidTransaction): validate_transaction(tx) From 8b9282349708b0ec9e530695c0d6d77d59551bda Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com> Date: Thu, 13 Mar 2025 05:51:36 +0100 Subject: [PATCH 32/38] default to empty accesslist when unavailable (#1151) --- .../evm_tools/loaders/transaction_loader.py | 4 ++-- src/ethereum_spec_tools/evm_tools/statetest/__init__.py | 2 +- tests/helpers/load_evm_tools_tests.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py index 70c1e8d82a..b788601400 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py @@ -76,7 +76,7 @@ def json_to_data(self) -> Bytes: def json_to_access_list(self) -> Any: """Get the access list of the transaction.""" access_list = [] - for sublist in self.raw["accessList"]: + for sublist in self.raw.get("accessLists", []): access_list.append( ( self.fork.hex_to_address(sublist.get("address")), @@ -192,7 +192,7 @@ def read(self) -> Any: elif "maxFeePerGas" in self.raw: tx_cls = self.fork.FeeMarketTransaction tx_byte_prefix = b"\x02" - elif "accessList" in self.raw: + elif "accessLists" in self.raw: tx_cls = self.fork.AccessListTransaction tx_byte_prefix = b"\x01" else: diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py index 6ecca69ad5..7b42033892 100644 --- a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -107,7 +107,7 @@ def run_test_case( tx[k] = value[v] elif k == "accessLists": if value[d] is not None: - tx["accessList"] = value[d] + tx["accessLists"] = value[d] else: tx[k] = value diff --git a/tests/helpers/load_evm_tools_tests.py b/tests/helpers/load_evm_tools_tests.py index 38af0eafff..66d183523b 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -95,7 +95,7 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: tx[k] = value[v] elif k == "accessLists": if value[d] is not None: - tx["accessList"] = value[d] + tx["accessLists"] = value[d] else: tx[k] = value From 7e3a623a4c32aa12c23cace164e3f00177cf627a Mon Sep 17 00:00:00 2001 From: Limer_Beast <153820464+Antrikshgwal@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:38:53 +0530 Subject: [PATCH 33/38] Simplified validate_header func in post-merge forks (#1163) * Simplified validate_header func in post-merge forks * Simplified parent hash calculation in post-merge forks * validate_header modification in paris and prague forks * fix formatting --------- Co-authored-by: Guruprasad Kamath --- src/ethereum/cancun/fork.py | 13 +------------ src/ethereum/paris/fork.py | 13 +------------ src/ethereum/prague/fork.py | 13 +------------ src/ethereum/shanghai/fork.py | 13 +------------ 4 files changed, 4 insertions(+), 48 deletions(-) diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 7932db33d3..89426989c5 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -310,19 +310,8 @@ def validate_header(chain: BlockChain, header: Header) -> None: """ 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 + parent_header = chain.blocks[-1].header excess_blob_gas = calculate_excess_blob_gas(parent_header) if header.excess_blob_gas != excess_blob_gas: diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index ac9a916675..714a68864e 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -284,19 +284,8 @@ def validate_header(chain: BlockChain, header: Header) -> None: """ 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 + parent_header = chain.blocks[-1].header if header.gas_used > header.gas_limit: raise InvalidBlock diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 50b5033e53..9b9beaaa4b 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -333,19 +333,8 @@ def validate_header(chain: BlockChain, header: Header) -> None: """ 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 + parent_header = chain.blocks[-1].header excess_blob_gas = calculate_excess_blob_gas(parent_header) if header.excess_blob_gas != excess_blob_gas: diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index 46e39874c1..0dbf404b78 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -289,19 +289,8 @@ def validate_header(chain: BlockChain, header: Header) -> None: """ 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 + parent_header = chain.blocks[-1].header if header.gas_used > header.gas_limit: raise InvalidBlock From ef9f068e831ce97bb1bf137df5230727e4f2e039 Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:38:07 -0400 Subject: [PATCH 34/38] update to use CodeCov v5 (#1162) (#1174) Co-authored-by: Carson Co-authored-by: Carson --- .github/workflows/test.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 27f95dc17d..5b98ad54ce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,9 +49,10 @@ jobs: env: PYPY_GC_MAX: "10G" - - name: Upload coverage to Codecov + - name: Upload coverage reports to Codecov if: "${{ !startsWith(matrix.py, 'pypy') }}" - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: files: .tox/coverage.xml flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} From 4fd0bdd019bee38f7d9dc6c0a47318abc0a40f96 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Fri, 28 Mar 2025 20:44:54 -0400 Subject: [PATCH 35/38] pyright fixes (ethash.py) --- src/ethereum/ethash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ethereum/ethash.py b/src/ethereum/ethash.py index 6cb9aca590..b831a4c0f1 100644 --- a/src/ethereum/ethash.py +++ b/src/ethereum/ethash.py @@ -26,7 +26,7 @@ [mem-hard]: https://en.wikipedia.org/wiki/Memory-hard_function """ -from typing import Callable, Tuple, Union +from typing import Callable, List, Tuple, Union from ethereum_types.bytes import Bytes8 from ethereum_types.numeric import U32, Uint, ulen @@ -380,7 +380,7 @@ def hashimoto( mix = fnv_hash(mix, new_data) - compressed_mix = [] + compressed_mix: List[U32] = [] for i in range(0, len(mix), 4): compressed_mix.append( fnv(fnv(fnv(mix[i], mix[i + 1]), mix[i + 2]), mix[i + 3]) From 33433c8c4f2966b1328a8d010ca09e4a3d38a2dc Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Mon, 31 Mar 2025 16:16:29 -0400 Subject: [PATCH 36/38] pyright fixes (fork_criteria.py) --- src/ethereum/fork_criteria.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ethereum/fork_criteria.py b/src/ethereum/fork_criteria.py index 2a70b8e74f..aa7fcf2844 100644 --- a/src/ethereum/fork_criteria.py +++ b/src/ethereum/fork_criteria.py @@ -29,6 +29,7 @@ from typing import Final, Literal, SupportsInt, Tuple from ethereum_types.numeric import U256, Uint +from typing_extensions import override @functools.total_ordering @@ -68,6 +69,7 @@ class ForkCriteria(ABC): _internal: Tuple[int, int] + @override def __eq__(self, other: object) -> bool: """ Equality for fork criteria. @@ -92,6 +94,7 @@ def __lt__(self, other: object) -> bool: return self._internal < other._internal return NotImplemented + @override def __hash__(self) -> int: """ Compute a hash for this instance, so it can be stored in dictionaries. @@ -108,6 +111,7 @@ def check(self, block_number: Uint, timestamp: U256) -> bool: """ raise NotImplementedError() + @override @abstractmethod def __repr__(self) -> str: """ @@ -127,9 +131,11 @@ class ByBlockNumber(ForkCriteria): """ def __init__(self, block_number: SupportsInt): + super().__init__() self._internal = (ForkCriteria.BLOCK_NUMBER, int(block_number)) self.block_number = Uint(int(block_number)) + @override def check(self, block_number: Uint, timestamp: U256) -> bool: """ Check whether the block number has been reached. @@ -141,6 +147,7 @@ def check(self, block_number: Uint, timestamp: U256) -> bool: """ return block_number >= self.block_number + @override def __repr__(self) -> str: """ String representation of this object. @@ -159,9 +166,11 @@ class ByTimestamp(ForkCriteria): """ def __init__(self, timestamp: SupportsInt): + super().__init__() self._internal = (ForkCriteria.TIMESTAMP, int(timestamp)) self.timestamp = U256(timestamp) + @override def check(self, block_number: Uint, timestamp: U256) -> bool: """ Check whether the timestamp has been reached. @@ -173,6 +182,7 @@ def check(self, block_number: Uint, timestamp: U256) -> bool: """ return timestamp >= self.timestamp + @override def __repr__(self) -> str: """ String representation of this object. @@ -186,14 +196,17 @@ class Unscheduled(ForkCriteria): """ def __init__(self) -> None: + super().__init__() self._internal = (ForkCriteria.UNSCHEDULED, 0) + @override def check(self, block_number: Uint, timestamp: U256) -> Literal[False]: """ Unscheduled forks never occur; always returns `False`. """ return False + @override def __repr__(self) -> str: """ String representation of this object. From 4223590c5ef8c13aab8008c33de51a3bb73c8e7b Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Mon, 31 Mar 2025 16:18:36 -0400 Subject: [PATCH 37/38] pyright fixes (genesis.py) --- src/ethereum/genesis.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index 152a37c58b..245c536712 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -15,7 +15,7 @@ import json import pkgutil from dataclasses import dataclass -from typing import Any, Callable, Dict, Generic, Type, TypeVar +from typing import Any, Callable, Dict, Generic, Type, TypeVar, Union from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes8, Bytes32, FixedBytes @@ -83,7 +83,7 @@ class GenesisConfiguration: [`timestamp`]: ref:ethereum.frontier.blocks.Header.timestamp """ - initial_accounts: Dict[str, Dict] + initial_accounts: Dict[str, Dict[str, Union[str, Dict[str, str]]]] """ State of the blockchain at genesis. """ @@ -204,16 +204,31 @@ def add_genesis_block( for hex_address, account in genesis.initial_accounts.items(): address = hardfork.hex_to_address(hex_address) + + nonce = account.get("nonce", "0") + assert isinstance(nonce, (str, int)) + + balance = account.get("balance", "0") + assert isinstance(balance, str) + + code = account.get("code", "0x") + assert isinstance(code, str) + hardfork.set_account( chain.state, address, hardfork.Account( - Uint(int(account.get("nonce", "0"))), - hex_or_base_10_str_to_u256(account.get("balance", 0)), - hex_to_bytes(account.get("code", "0x")), + Uint(int(nonce)), + hex_or_base_10_str_to_u256(balance), + hex_to_bytes(code), ), ) - for key, value in account.get("storage", {}).items(): + + maybe_storage = account.get("storage", {}) + assert isinstance(maybe_storage, dict) + storage: Dict[str, str] = maybe_storage + + for key, value in storage.items(): hardfork.set_storage( chain.state, address, hex_to_bytes32(key), hex_to_u256(value) ) From 75c0570049a11ac84de4f0847d905e9ac2d2e800 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Tue, 1 Apr 2025 10:22:46 -0400 Subject: [PATCH 38/38] pyright fixes (*/fork.py) --- src/ethereum/arrow_glacier/fork.py | 3 +-- src/ethereum/berlin/fork.py | 3 +-- src/ethereum/byzantium/fork.py | 3 +-- src/ethereum/cancun/fork.py | 3 +-- src/ethereum/constantinople/fork.py | 3 +-- src/ethereum/dao_fork/fork.py | 3 +-- src/ethereum/frontier/fork.py | 3 +-- src/ethereum/gray_glacier/fork.py | 3 +-- src/ethereum/homestead/fork.py | 3 +-- src/ethereum/istanbul/fork.py | 3 +-- src/ethereum/london/fork.py | 3 +-- src/ethereum/muir_glacier/fork.py | 3 +-- src/ethereum/paris/fork.py | 3 +-- src/ethereum/prague/fork.py | 3 +-- src/ethereum/shanghai/fork.py | 3 +-- src/ethereum/spurious_dragon/fork.py | 3 +-- src/ethereum/tangerine_whistle/fork.py | 3 +-- 17 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/ethereum/arrow_glacier/fork.py b/src/ethereum/arrow_glacier/fork.py index a6faa5429a..f682ca290f 100644 --- a/src/ethereum/arrow_glacier/fork.py +++ b/src/ethereum/arrow_glacier/fork.py @@ -122,11 +122,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/berlin/fork.py b/src/ethereum/berlin/fork.py index 662974467f..5dc38259bb 100644 --- a/src/ethereum/berlin/fork.py +++ b/src/ethereum/berlin/fork.py @@ -118,11 +118,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/byzantium/fork.py b/src/ethereum/byzantium/fork.py index bd924561d1..b22b8c6455 100644 --- a/src/ethereum/byzantium/fork.py +++ b/src/ethereum/byzantium/fork.py @@ -113,11 +113,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 89426989c5..841a581777 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -134,11 +134,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/constantinople/fork.py b/src/ethereum/constantinople/fork.py index 02d1e3f75b..3a23f67314 100644 --- a/src/ethereum/constantinople/fork.py +++ b/src/ethereum/constantinople/fork.py @@ -113,11 +113,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/dao_fork/fork.py b/src/ethereum/dao_fork/fork.py index 20bc645ef9..8568255004 100644 --- a/src/ethereum/dao_fork/fork.py +++ b/src/ethereum/dao_fork/fork.py @@ -117,11 +117,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/frontier/fork.py b/src/ethereum/frontier/fork.py index 3127970d36..7affa719c6 100644 --- a/src/ethereum/frontier/fork.py +++ b/src/ethereum/frontier/fork.py @@ -107,11 +107,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/gray_glacier/fork.py b/src/ethereum/gray_glacier/fork.py index 0dee77a920..e58ee75cf0 100644 --- a/src/ethereum/gray_glacier/fork.py +++ b/src/ethereum/gray_glacier/fork.py @@ -122,11 +122,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/homestead/fork.py b/src/ethereum/homestead/fork.py index 813f5766f0..c2023e8291 100644 --- a/src/ethereum/homestead/fork.py +++ b/src/ethereum/homestead/fork.py @@ -107,11 +107,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/istanbul/fork.py b/src/ethereum/istanbul/fork.py index 02d1e3f75b..3a23f67314 100644 --- a/src/ethereum/istanbul/fork.py +++ b/src/ethereum/istanbul/fork.py @@ -113,11 +113,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/london/fork.py b/src/ethereum/london/fork.py index fc8ea7adbc..b39ad0c362 100644 --- a/src/ethereum/london/fork.py +++ b/src/ethereum/london/fork.py @@ -123,11 +123,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/muir_glacier/fork.py b/src/ethereum/muir_glacier/fork.py index d8e65346dc..86ec80da30 100644 --- a/src/ethereum/muir_glacier/fork.py +++ b/src/ethereum/muir_glacier/fork.py @@ -113,11 +113,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index 714a68864e..68e37d9ddb 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -116,11 +116,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/prague/fork.py b/src/ethereum/prague/fork.py index 9b9beaaa4b..e2ccbd1d1b 100644 --- a/src/ethereum/prague/fork.py +++ b/src/ethereum/prague/fork.py @@ -154,11 +154,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index 0dbf404b78..98dd01ef98 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -117,11 +117,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/spurious_dragon/fork.py b/src/ethereum/spurious_dragon/fork.py index 77c4f74360..8743cf3c48 100644 --- a/src/ethereum/spurious_dragon/fork.py +++ b/src/ethereum/spurious_dragon/fork.py @@ -108,11 +108,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash diff --git a/src/ethereum/tangerine_whistle/fork.py b/src/ethereum/tangerine_whistle/fork.py index 9a13f28c3b..58c6fdff28 100644 --- a/src/ethereum/tangerine_whistle/fork.py +++ b/src/ethereum/tangerine_whistle/fork.py @@ -106,11 +106,10 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: Hashes of the recent 256 blocks in order of increasing block number. """ recent_blocks = chain.blocks[-255:] - # TODO: This function has not been tested rigorously if len(recent_blocks) == 0: return [] - recent_block_hashes = [] + recent_block_hashes: List[Hash32] = [] for block in recent_blocks: prev_block_hash = block.header.parent_hash