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/.github/workflows/test.yaml b/.github/workflows/test.yaml index 27f95dc17d..47aaf6ad6d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,11 +41,11 @@ jobs: - name: Run Tox (CPython) if: "${{ !startsWith(matrix.py, 'pypy') }}" - run: tox -e static,optimized,py3 + run: tox -e optimized,py3 - name: Run Tox (PyPy) if: "${{ startsWith(matrix.py, 'pypy') }}" - run: tox -e pypy3 + run: tox -e static,pypy3 env: PYPY_GC_MAX: "10G" diff --git a/setup.cfg b/setup.cfg index 3bd4401f5e..21438949c2 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 = @@ -104,6 +110,12 @@ 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 + ethereum/prague/vm/precompiled_contracts/bls12_381 package_dir = @@ -113,10 +125,10 @@ python_requires = >=3.10 install_requires = pycryptodome>=3,<4 coincurve>=20,<21 - typing_extensions>=4 - py_ecc @ git+https://github.com/petertdavies/py_ecc.git@127184f4c57b1812da959586d0fe8f43bb1a2389 + typing_extensions>=4.2 + py-ecc>=8.0.0b2,<9 ethereum-types>=0.2.1,<0.3 - ethereum-rlp>=0.1.1,<0.2 + ethereum-rlp>=0.1.2,<0.2 [options.package_data] ethereum = @@ -163,7 +175,7 @@ test = lint = types-setuptools>=68.1.0.1,<69 isort==5.13.2 - mypy==1.14.1 + mypy==1.15.0 black==23.12.0 flake8==6.1.0 flake8-bugbear==23.12.2 @@ -179,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 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 diff --git a/src/ethereum/arrow_glacier/blocks.py b/src/ethereum/arrow_glacier/blocks.py index 87c9acac7f..03ece58862 100644 --- a/src/ethereum/arrow_glacier/blocks.py +++ b/src/ethereum/arrow_glacier/blocks.py @@ -9,15 +9,25 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.london import blocks as previous_blocks 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 @@ -45,6 +55,49 @@ class Header: base_fee_per_gas: Uint +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -54,7 +107,14 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable @@ -80,3 +140,36 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) diff --git a/src/ethereum/arrow_glacier/fork.py b/src/ethereum/arrow_glacier/fork.py index 2108a5fba7..44a4bdaf46 100644 --- a/src/ethereum/arrow_glacier/fork.py +++ b/src/ethereum/arrow_glacier/fork.py @@ -21,17 +21,32 @@ 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 ethereum.london import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + encode_receipt, + header_base_fee_per_gas, +) 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, @@ -42,13 +57,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -58,6 +73,7 @@ GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) GAS_LIMIT_MINIMUM = Uint(5000) MINIMUM_DIFFICULTY = Uint(131072) +INITIAL_BASE_FEE = Uint(1000000000) MAX_OMMER_DEPTH = Uint(6) BOMB_DELAY_BLOCKS = 10700000 EMPTY_OMMER_HASH = keccak256(rlp.encode([])) @@ -69,7 +85,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -158,33 +174,43 @@ 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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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 +282,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -274,15 +317,25 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + 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, - ) + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: raise InvalidBlock @@ -385,24 +438,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 ------- @@ -416,32 +466,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]: @@ -472,54 +533,14 @@ 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 - - -@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 + return encode_receipt(tx, receipt) 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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -532,102 +553,31 @@ 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -662,10 +612,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -708,7 +656,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -747,8 +695,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,104 +714,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(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, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(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, + ) - 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 9d9bb6bd17..b853357506 100644 --- a/src/ethereum/arrow_glacier/transactions.py +++ b/src/ethereum/arrow_glacier/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -171,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: @@ -185,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: @@ -374,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 6cdf44aae8..34461d7656 100644 --- a/src/ethereum/arrow_glacier/utils/message.py +++ b/src/ethereum/arrow_glacier/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.arrow_glacier.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 33d8396a48..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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) @@ -527,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 4ace48ad27..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) @@ -342,9 +350,11 @@ 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 + 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 @@ -360,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)) @@ -373,7 +383,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -434,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)) @@ -479,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 @@ -489,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 @@ -605,6 +624,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 +643,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..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,39 +98,39 @@ 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: 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 @@ -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/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..9665705669 100644 --- a/src/ethereum/berlin/blocks.py +++ b/src/ethereum/berlin/blocks.py @@ -9,15 +9,20 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.muir_glacier import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .transactions import LegacyTransaction +from .transactions import AccessListTransaction, LegacyTransaction, Transaction @slotted_freezable @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable @@ -79,3 +134,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..8eaf7f734a 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,31 @@ 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 ethereum.muir_glacier import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import ( + AnyBlock, + AnyHeader, + 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, @@ -41,13 +54,13 @@ AccessListTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -66,7 +79,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -155,32 +168,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -190,7 +213,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -208,6 +248,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -308,21 +355,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 ------- @@ -334,16 +381,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]: @@ -374,51 +432,14 @@ def make_receipt( logs=logs, ) - if isinstance(tx, AccessListTransaction): - return b"\x01" + 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. - """ - - block_gas_used: Uint - transactions_root: Root - receipt_root: Root - block_logs_bloom: Bloom - state_root: Root + return encode_receipt(tx, receipt) 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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -431,97 +452,31 @@ 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) - ) + process_transaction(block_env, block_output, tx, Uint(i)) - 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, - ) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_logs += logs - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -556,10 +511,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -602,7 +555,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -641,8 +594,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. @@ -657,88 +613,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_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") + intrinsic_gas = validate_transaction(tx) + + sender = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 - 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 - 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 + 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 output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(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, + ) - 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 fcc54d0485..3a3b4bd749 100644 --- a/src/ethereum/berlin/transactions.py +++ b/src/ethereum/berlin/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -143,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: @@ -157,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: @@ -310,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 80490e0735..caeecac1fc 100644 --- a/src/ethereum/berlin/utils/message.py +++ b/src/ethereum/berlin/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.berlin.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 e8f2505a51..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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 d5a19b237d..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) @@ -343,9 +351,11 @@ 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 + 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 @@ -361,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)) @@ -374,7 +384,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -435,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)) @@ -480,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 @@ -500,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 @@ -616,6 +636,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 +655,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..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,39 +97,39 @@ 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: 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 @@ -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/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/blocks.py b/src/ethereum/byzantium/blocks.py index f74ead6616..76690bd846 100644 --- a/src/ethereum/byzantium/blocks.py +++ b/src/ethereum/byzantium/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.spurious_dragon import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/byzantium/fork.py b/src/ethereum/byzantium/fork.py index 68001e3ee1..46968c84d1 100644 --- a/src/ethereum/byzantium/fork.py +++ b/src/ethereum/byzantium/fork.py @@ -11,27 +11,31 @@ 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 ethereum.spurious_dragon import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -39,11 +43,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -62,7 +66,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -151,32 +155,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -186,7 +200,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -204,6 +235,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -304,21 +342,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 ------- @@ -330,16 +368,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: @@ -348,8 +396,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -373,45 +419,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -424,94 +436,31 @@ 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, - ) - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -546,10 +495,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -592,7 +539,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -631,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. @@ -647,78 +597,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 142929587d..21ffbc1b6f 100644 --- a/src/ethereum/byzantium/transactions.py +++ b/src/ethereum/byzantium/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -151,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]) @@ -213,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 1a2a295c52..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,9 +335,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -370,7 +370,8 @@ 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 ce20f3ecdd..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) @@ -261,9 +271,12 @@ 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) + 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 @@ -279,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)) @@ -292,7 +305,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -346,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)) @@ -387,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 @@ -407,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 @@ -509,6 +531,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 +551,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..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,39 +96,39 @@ 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: 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 @@ -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/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..ce4ed73d76 100644 --- a/src/ethereum/cancun/blocks.py +++ b/src/ethereum/cancun/blocks.py @@ -9,15 +9,26 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.shanghai import blocks as previous_blocks 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 @@ -62,6 +73,49 @@ class Header: parent_beacon_block_root: Root +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -71,10 +125,17 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] withdrawals: Tuple[Withdrawal, ...] +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" + + @slotted_freezable @dataclass class Log: @@ -98,3 +159,38 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 5bbb517ed6..77dce99fe1 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -16,16 +16,31 @@ 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 ethereum.shanghai import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt, Withdrawal +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + Withdrawal, + encode_receipt, + header_base_fee_per_gas, +) 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 +49,7 @@ destroy_touched_empty_accounts, get_account, increment_nonce, - process_withdrawal, + modify_state, set_account_balance, state_root, ) @@ -44,13 +59,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -60,12 +75,13 @@ 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) GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) GAS_LIMIT_MINIMUM = Uint(5000) +INITIAL_BASE_FEE = Uint(1000000000) EMPTY_OMMER_HASH = keccak256(rlp.encode([])) SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") BEACON_ROOTS_ADDRESS = hex_to_address( @@ -82,7 +98,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -171,44 +187,51 @@ 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) + parent = parent_header(chain, block.header) + validate_header(parent, 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) @@ -280,7 +303,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -298,15 +338,29 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_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 - 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, - ) + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: raise InvalidBlock if header.timestamp <= parent_header.timestamp: @@ -328,30 +382,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 ------- @@ -361,31 +406,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 @@ -399,7 +454,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 @@ -413,15 +468,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]: @@ -452,139 +512,58 @@ 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 -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. +def process_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: """ - - 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. + Process a system transaction. 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. + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. Returns ------- - apply_body_output : `ApplyBodyOutput` - Output of applying the block body to the state. + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. """ - 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, ...] = () + system_contract_code = get_account(block_env.state, target_address).code - beacon_block_roots_contract_code = get_account( - state, BEACON_ROOTS_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=BEACON_ROOTS_ADDRESS, + target=target_address, gas=SYSTEM_TRANSACTION_GAS, value=U256(0), - data=parent_beacon_block_root, - code=beacon_block_roots_contract_code, + data=data, + code=system_contract_code, depth=Uint(0), - current_target=BEACON_ROOTS_ADDRESS, - code_address=BEACON_ROOTS_ADDRESS, + current_target=target_address, + code_address=target_address, should_transfer_value=False, is_static=False, accessed_addresses=set(), @@ -592,111 +571,71 @@ def apply_body( 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) + system_tx_output = process_message_call(system_tx_message) + # 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 + block_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) - ) + return system_tx_output - ( - 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 +def apply_body( + block_env: vm.BlockEnvironment, + transactions: Tuple[Union[LegacyTransaction, Bytes], ...], + withdrawals: Tuple[Withdrawal, ...], +) -> vm.BlockOutput: + """ + Executes a block. - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) + 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. - trie_set( - receipts_trie, - rlp.encode(Uint(i)), - receipt, - ) + Parameters + ---------- + block_env : + The block scoped environment. + transactions : + Transactions included in the block. + withdrawals : + Withdrawals to be processed in the current block. - 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 + Returns + ------- + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() - block_logs_bloom = logs_bloom(block_logs) + process_system_transaction( + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) - for i, wd in enumerate(withdrawals): - trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd)) + for i, tx in enumerate(map(decode_transaction, transactions)): + process_transaction(block_env, block_output, tx, Uint(i)) - process_withdrawal(state, wd) + process_withdrawals(block_env, block_output, withdrawals) - 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. @@ -711,98 +650,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(tx) + + ( + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 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) + + 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 deb594c17c..b2e5c47ffd 100644 --- a/src/ethereum/cancun/transactions.py +++ b/src/ethereum/cancun/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, 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 +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,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: @@ -206,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: @@ -220,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: @@ -451,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 95057a0d2d..37aff9a59a 100644 --- a/src/ethereum/cancun/utils/message.py +++ b/src/ethereum/cancun/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.cancun.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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/gas.py b/src/ethereum/cancun/vm/gas.py index cc3ccc7c6b..a03f5c3c06 100644 --- a/src/ethereum/cancun/vm/gas.py +++ b/src/ethereum/cancun/vm/gas.py @@ -19,7 +19,7 @@ from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32, taylor_exponential -from ..blocks import Header +from ..blocks import AnyHeader, Header from ..transactions import BlobTransaction, Transaction from . import Evm from .exceptions import OutOfGasError @@ -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 @@ -269,7 +269,7 @@ def init_code_cost(init_code_length: Uint) -> Uint: return GAS_INIT_CODE_WORD_COST * ceil32(init_code_length) // Uint(32) -def calculate_excess_blob_gas(parent_header: Header) -> U64: +def calculate_excess_blob_gas(parent_header: AnyHeader) -> U64: """ Calculated the excess blob gas for the current block based on the gas used in the parent block. @@ -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/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 4ba384f9ba..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) @@ -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.message.block_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 + 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) @@ -468,18 +471,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -505,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) @@ -530,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) @@ -553,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)) @@ -580,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 da47df38cf..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) @@ -366,9 +374,11 @@ 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 + 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 @@ -384,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)) @@ -397,7 +407,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -458,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)) @@ -503,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 @@ -513,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, @@ -524,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 @@ -631,6 +646,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 +665,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..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,39 +98,39 @@ 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: 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 @@ -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/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/blocks.py b/src/ethereum/constantinople/blocks.py index f74ead6616..8d0d38762b 100644 --- a/src/ethereum/constantinople/blocks.py +++ b/src/ethereum/constantinople/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.byzantium import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/constantinople/fork.py b/src/ethereum/constantinople/fork.py index e195589a13..035e4fd010 100644 --- a/src/ethereum/constantinople/fork.py +++ b/src/ethereum/constantinople/fork.py @@ -11,27 +11,31 @@ 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.byzantium import fork as previous_fork 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 .blocks import AnyBlock, AnyHeader, 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, @@ -39,11 +43,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -62,7 +66,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -151,32 +155,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -186,7 +200,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -204,6 +235,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -304,21 +342,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 ------- @@ -330,16 +368,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: @@ -348,8 +396,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -373,45 +419,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -424,94 +436,31 @@ 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, - ) - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -546,10 +495,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -592,7 +539,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -631,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. @@ -647,78 +597,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 142929587d..21ffbc1b6f 100644 --- a/src/ethereum/constantinople/transactions.py +++ b/src/ethereum/constantinople/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -151,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]) @@ -213,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 3062ffee77..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,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -373,7 +373,8 @@ 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) @@ -452,12 +453,13 @@ 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) 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/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 b74c3d3116..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) @@ -329,9 +341,12 @@ 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) + 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 @@ -347,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)) @@ -360,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -414,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)) @@ -455,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 @@ -475,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 @@ -577,6 +602,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 +622,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..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,39 +96,39 @@ 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: 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 @@ -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/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/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/dao_fork/blocks.py b/src/ethereum/dao_fork/blocks.py index 713b5aecf5..91cd4abb79 100644 --- a/src/ethereum/dao_fork/blocks.py +++ b/src/ethereum/dao_fork/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.homestead import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/dao_fork/fork.py b/src/ethereum/dao_fork/fork.py index 1835a2525f..864037b700 100644 --- a/src/ethereum/dao_fork/fork.py +++ b/src/ethereum/dao_fork/fork.py @@ -13,23 +13,23 @@ 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 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.homestead import fork as previous_fork from . import FORK_CRITERIA, vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -41,11 +41,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -62,7 +62,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -159,31 +159,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -193,7 +204,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -211,6 +239,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -316,18 +351,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 ------- @@ -339,15 +377,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, ...], @@ -357,8 +405,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -382,44 +428,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -432,21 +445,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 : @@ -455,69 +455,21 @@ 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, - ) + 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -552,10 +504,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -598,7 +548,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -637,8 +587,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. @@ -653,71 +606,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(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 57d697d615..6db00e736d 100644 --- a/src/ethereum/dao_fork/transactions.py +++ b/src/ethereum/dao_fork/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -174,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 9d936e7f5f..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,9 +333,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -368,7 +368,8 @@ 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 fd65ab1a42..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) @@ -242,15 +252,18 @@ 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 + 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)) @@ -262,7 +275,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, @@ -303,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)) @@ -363,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 daa28bb6db..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,35 +89,33 @@ 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, ...] = () 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 @@ -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/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/blocks.py b/src/ethereum/frontier/blocks.py index 713b5aecf5..e8be8d6af4 100644 --- a/src/ethereum/frontier/blocks.py +++ b/src/ethereum/frontier/blocks.py @@ -11,9 +11,11 @@ from dataclasses import dataclass from typing import Tuple +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 typing_extensions import TypeAlias from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +46,22 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Header +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + """ + # Because frontier is the first fork, we know what type the header is. + return rlp.deserialize_to(Header, raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +71,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[AnyHeader, ...] + + +AnyBlock: TypeAlias = Block +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/frontier/fork.py b/src/ethereum/frontier/fork.py index 46749732f4..52c6cfbdea 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 @@ -24,9 +24,9 @@ from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -38,11 +38,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -59,7 +59,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -126,7 +126,7 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: return recent_block_hashes -def state_transition(chain: BlockChain, block: Block) -> None: +def state_transition(chain: BlockChain, block: AnyBlock) -> None: """ Attempts to apply a block to an existing block chain. @@ -148,31 +148,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -182,7 +193,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -200,6 +228,9 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + 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): @@ -225,7 +256,7 @@ def validate_header(header: Header, parent_header: Header) -> None: validate_proof_of_work(header) -def generate_header_hash_for_pow(header: Header) -> Hash32: +def generate_header_hash_for_pow(header: AnyHeader) -> Hash32: """ Generate rlp hash of the header which is to be used for Proof-of-Work verification. @@ -267,7 +298,7 @@ def generate_header_hash_for_pow(header: Header) -> Hash32: return keccak256(rlp.encode(header_data_without_pow_artefacts)) -def validate_proof_of_work(header: Header) -> None: +def validate_proof_of_work(header: AnyHeader) -> None: """ Validates the Proof of Work constraints. @@ -298,18 +329,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 ------- @@ -321,15 +355,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, ...], @@ -339,8 +383,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -364,44 +406,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -414,21 +423,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 : @@ -437,69 +433,21 @@ 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: AnyHeader, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -534,10 +482,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -580,7 +526,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -619,8 +565,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. @@ -635,74 +584,91 @@ 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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(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: +def compute_header_hash(header: AnyHeader) -> Hash32: """ Computes the hash of a block header. diff --git a/src/ethereum/frontier/transactions.py b/src/ethereum/frontier/transactions.py index a09bba38ba..e9801dfafe 100644 --- a/src/ethereum/frontier/transactions.py +++ b/src/ethereum/frontier/transactions.py @@ -13,13 +13,13 @@ 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 -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 @@ -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: @@ -92,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: @@ -103,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: @@ -168,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 9d936e7f5f..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,9 +333,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -368,7 +368,8 @@ 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 f5165e1100..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) @@ -238,15 +248,18 @@ 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 + 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)) @@ -258,7 +271,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, @@ -298,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)) @@ -357,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 6d6a36c5df..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,35 +89,33 @@ 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, ...] = () 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 @@ -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/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/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/gray_glacier/blocks.py b/src/ethereum/gray_glacier/blocks.py index 87c9acac7f..3a42980c3c 100644 --- a/src/ethereum/gray_glacier/blocks.py +++ b/src/ethereum/gray_glacier/blocks.py @@ -9,15 +9,25 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.arrow_glacier import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock 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 @@ -45,6 +55,49 @@ class Header: base_fee_per_gas: Uint +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -54,7 +107,14 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable @@ -80,3 +140,36 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) diff --git a/src/ethereum/gray_glacier/fork.py b/src/ethereum/gray_glacier/fork.py index ce070e7c08..a56c91f0d4 100644 --- a/src/ethereum/gray_glacier/fork.py +++ b/src/ethereum/gray_glacier/fork.py @@ -19,19 +19,34 @@ from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint +from ethereum.arrow_glacier import fork as previous_fork 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 .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + encode_receipt, + header_base_fee_per_gas, +) 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, @@ -42,13 +57,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -58,6 +73,7 @@ GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) GAS_LIMIT_MINIMUM = Uint(5000) MINIMUM_DIFFICULTY = Uint(131072) +INITIAL_BASE_FEE = Uint(1000000000) MAX_OMMER_DEPTH = Uint(6) BOMB_DELAY_BLOCKS = 11400000 EMPTY_OMMER_HASH = keccak256(rlp.encode([])) @@ -69,7 +85,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -158,33 +174,43 @@ 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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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 +282,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -274,15 +317,25 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + 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, - ) + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: raise InvalidBlock @@ -385,24 +438,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 ------- @@ -416,32 +466,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]: @@ -472,54 +533,14 @@ 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 - - -@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 + return encode_receipt(tx, receipt) 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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -532,102 +553,31 @@ 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -662,10 +612,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -708,7 +656,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -747,8 +695,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,104 +714,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(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, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(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, + ) - 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 9d9bb6bd17..b853357506 100644 --- a/src/ethereum/gray_glacier/transactions.py +++ b/src/ethereum/gray_glacier/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -171,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: @@ -185,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: @@ -374,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 28091f510f..ffd49e9790 100644 --- a/src/ethereum/gray_glacier/utils/message.py +++ b/src/ethereum/gray_glacier/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.gray_glacier.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 33d8396a48..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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) @@ -527,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 4ace48ad27..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) @@ -342,9 +350,11 @@ 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 + 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 @@ -360,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)) @@ -373,7 +383,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -434,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)) @@ -479,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 @@ -489,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 @@ -605,6 +624,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 +643,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..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,39 +98,39 @@ 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: 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 @@ -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/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/blocks.py b/src/ethereum/homestead/blocks.py index 713b5aecf5..327207bb07 100644 --- a/src/ethereum/homestead/blocks.py +++ b/src/ethereum/homestead/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.frontier import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/homestead/fork.py b/src/ethereum/homestead/fork.py index 1419c87b6d..9653a08d69 100644 --- a/src/ethereum/homestead/fork.py +++ b/src/ethereum/homestead/fork.py @@ -13,20 +13,21 @@ """ 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 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.frontier import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -38,11 +39,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -59,7 +60,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -148,31 +149,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -182,7 +194,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -200,6 +229,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -298,18 +334,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 ------- @@ -321,15 +360,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, ...], @@ -339,8 +388,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -364,44 +411,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -414,21 +428,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 : @@ -437,69 +438,21 @@ 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, - ) + 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -534,10 +487,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -580,7 +531,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -619,8 +570,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. @@ -635,71 +589,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(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 57d697d615..6db00e736d 100644 --- a/src/ethereum/homestead/transactions.py +++ b/src/ethereum/homestead/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -174,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 9d936e7f5f..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,9 +333,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -368,7 +368,8 @@ 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 fd65ab1a42..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) @@ -242,15 +252,18 @@ 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 + 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)) @@ -262,7 +275,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, @@ -303,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)) @@ -363,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 b653a78117..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,35 +89,33 @@ 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, ...] = () 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 @@ -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/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/blocks.py b/src/ethereum/istanbul/blocks.py index f74ead6616..046c8b4162 100644 --- a/src/ethereum/istanbul/blocks.py +++ b/src/ethereum/istanbul/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.constantinople import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/istanbul/fork.py b/src/ethereum/istanbul/fork.py index 35eb37c9c5..77739902fe 100644 --- a/src/ethereum/istanbul/fork.py +++ b/src/ethereum/istanbul/fork.py @@ -11,27 +11,31 @@ 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.constantinople import fork as previous_fork 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 .blocks import AnyBlock, AnyHeader, 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, @@ -39,11 +43,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -62,7 +66,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -151,32 +155,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -186,7 +200,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -204,6 +235,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -304,21 +342,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 ------- @@ -330,16 +368,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: @@ -348,8 +396,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. error : Error in the top level frame of the transaction, if any. cumulative_gas_used : @@ -373,45 +419,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -424,95 +436,31 @@ 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, - ) - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -547,10 +495,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -593,7 +539,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -632,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. @@ -648,78 +597,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 2bdb3603b9..28115976e8 100644 --- a/src/ethereum/istanbul/transactions.py +++ b/src/ethereum/istanbul/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -151,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]) @@ -213,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 a641fe167b..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,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -374,7 +374,8 @@ 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,12 +454,13 @@ 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) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) @@ -484,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 b74c3d3116..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) @@ -329,9 +341,12 @@ 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) + 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 @@ -347,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)) @@ -360,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -414,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)) @@ -455,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 @@ -475,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 @@ -577,6 +602,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 +622,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..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,39 +97,39 @@ 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: 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 @@ -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/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..0ee7667c78 100644 --- a/src/ethereum/london/blocks.py +++ b/src/ethereum/london/blocks.py @@ -9,15 +9,25 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.berlin import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock 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 @@ -45,6 +55,49 @@ class Header: base_fee_per_gas: Uint +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -54,7 +107,14 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable @@ -80,3 +140,36 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return None diff --git a/src/ethereum/london/fork.py b/src/ethereum/london/fork.py index a9599345c9..44517b9d4e 100644 --- a/src/ethereum/london/fork.py +++ b/src/ethereum/london/fork.py @@ -19,19 +19,34 @@ from ethereum_types.bytes import Bytes from ethereum_types.numeric import U64, U256, Uint +from ethereum.berlin import fork as previous_fork 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 +from . import vm +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + encode_receipt, + header_base_fee_per_gas, +) 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, @@ -42,13 +57,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -70,7 +85,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -159,33 +174,43 @@ 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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -257,7 +282,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -275,18 +317,23 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + if header.gas_used > header.gas_limit: raise InvalidBlock expected_base_fee_per_gas = INITIAL_BASE_FEE - if header.number != FORK_CRITERIA.block_number: + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: # For every block except the first, calculate the base fee per gas # based on the parent block. 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, + parent_base_fee_per_gas, ) if expected_base_fee_per_gas != header.base_fee_per_gas: @@ -391,24 +438,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 ------- @@ -422,32 +466,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]: @@ -478,54 +533,14 @@ 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 - - -@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 + return encode_receipt(tx, receipt) 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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -538,102 +553,31 @@ 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) - ) + process_transaction(block_env, block_output, tx, Uint(i)) - 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, - ) + pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers) - block_logs += logs - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -668,10 +612,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -714,7 +656,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -753,8 +695,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. @@ -769,104 +714,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(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, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 output.accounts_to_delete: - destroy_account(env.state, address) + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(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, + ) - 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 9d9bb6bd17..b853357506 100644 --- a/src/ethereum/london/transactions.py +++ b/src/ethereum/london/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -171,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: @@ -185,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: @@ -374,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 f867fcab98..54caa37e91 100644 --- a/src/ethereum/london/utils/message.py +++ b/src/ethereum/london/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.london.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 33d8396a48..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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) @@ -527,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 4ace48ad27..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) @@ -342,9 +350,11 @@ 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 + 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 @@ -360,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)) @@ -373,7 +383,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -434,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)) @@ -479,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 @@ -489,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 @@ -605,6 +624,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 +643,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..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,39 +98,39 @@ 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: 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 @@ -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/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/blocks.py b/src/ethereum/muir_glacier/blocks.py index f74ead6616..55669f5745 100644 --- a/src/ethereum/muir_glacier/blocks.py +++ b/src/ethereum/muir_glacier/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.istanbul import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/muir_glacier/fork.py b/src/ethereum/muir_glacier/fork.py index 4060049c84..3fbe89fcaa 100644 --- a/src/ethereum/muir_glacier/fork.py +++ b/src/ethereum/muir_glacier/fork.py @@ -11,27 +11,31 @@ 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 ethereum.istanbul import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -39,11 +43,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -62,7 +66,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -151,32 +155,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -186,7 +200,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -204,6 +235,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + parent_has_ommers = parent_header.ommers_hash != EMPTY_OMMER_HASH if header.timestamp <= parent_header.timestamp: raise InvalidBlock @@ -304,21 +342,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 ------- @@ -330,16 +368,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: @@ -373,45 +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, ...], - chain_id: U64, -) -> ApplyBodyOutput: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -424,95 +438,31 @@ 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, - ) - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -547,10 +497,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -593,7 +541,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -632,8 +580,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. @@ -648,78 +599,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 2bdb3603b9..28115976e8 100644 --- a/src/ethereum/muir_glacier/transactions.py +++ b/src/ethereum/muir_glacier/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -151,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]) @@ -213,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 a641fe167b..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,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -374,7 +374,8 @@ 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,12 +454,13 @@ 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) else: - codehash = U256.from_be_bytes(keccak256(account.code)) + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) push(evm.stack, codehash) @@ -484,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 b74c3d3116..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) @@ -329,9 +341,12 @@ 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) + 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 @@ -347,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)) @@ -360,7 +375,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -414,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)) @@ -455,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 @@ -475,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 @@ -577,6 +602,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 +622,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..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,39 +97,39 @@ 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: 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 @@ -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/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..3509c965f7 100644 --- a/src/ethereum/paris/blocks.py +++ b/src/ethereum/paris/blocks.py @@ -9,15 +9,25 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.gray_glacier import blocks as previous_blocks 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 @@ -45,6 +55,49 @@ class Header: base_fee_per_gas: Uint +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -54,7 +107,14 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable @@ -80,3 +140,36 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index 1e34b33159..b1e09151f8 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -16,20 +16,35 @@ 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 ethereum.gray_glacier import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + encode_receipt, + header_base_fee_per_gas, +) 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, @@ -40,13 +55,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -54,6 +69,7 @@ ELASTICITY_MULTIPLIER = Uint(2) GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) GAS_LIMIT_MINIMUM = Uint(5000) +INITIAL_BASE_FEE = Uint(1000000000) EMPTY_OMMER_HASH = keccak256(rlp.encode([])) @@ -63,7 +79,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -152,33 +168,43 @@ 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) + parent = parent_header(chain, block.header) + validate_header(parent, 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, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, ) - 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) @@ -250,7 +276,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -268,15 +311,25 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + 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, - ) + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: raise InvalidBlock if header.timestamp <= parent_header.timestamp: @@ -298,24 +351,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 ------- @@ -329,32 +379,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]: @@ -366,7 +427,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. @@ -385,53 +446,13 @@ 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 - - -@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 + return encode_receipt(tx, receipt) 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. @@ -444,101 +465,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) - ) + process_transaction(block_env, block_output, tx, Uint(i)) - 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=[], - ) - - 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. @@ -553,103 +503,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(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, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) - effective_gas_fee = tx.gas * env.gas_price + sender_account = get_account(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + effective_gas_fee = tx.gas * effective_gas_price + + gas = tx.gas - intrinsic_gas + 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 5944d96b6e..b853357506 100644 --- a/src/ethereum/paris/transactions.py +++ b/src/ethereum/paris/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -171,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: @@ -185,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: @@ -374,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 13e5b0d306..38554c73a8 100644 --- a/src/ethereum/paris/utils/message.py +++ b/src/ethereum/paris/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.paris.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 33d8396a48..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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) @@ -527,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 4ace48ad27..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) @@ -342,9 +350,11 @@ 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 + 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 @@ -360,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)) @@ -373,7 +383,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -434,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)) @@ -479,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 @@ -489,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 @@ -605,6 +624,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 +643,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..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,39 +98,39 @@ 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: 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 @@ -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/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/__init__.py b/src/ethereum/prague/__init__.py new file mode 100644 index 0000000000..09a10407a3 --- /dev/null +++ b/src/ethereum/prague/__init__.py @@ -0,0 +1,7 @@ +""" +The Prague fork. +""" + +from ethereum.fork_criteria import Unscheduled + +FORK_CRITERIA = Unscheduled() diff --git a/src/ethereum/prague/blocks.py b/src/ethereum/prague/blocks.py new file mode 100644 index 0000000000..110b1b291e --- /dev/null +++ b/src/ethereum/prague/blocks.py @@ -0,0 +1,200 @@ +""" +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 Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.cancun import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock + +from ..crypto.hash import Hash32 +from .fork_types import Address, Bloom, Root +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) + + +@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 + requests_hash: Hash32 + + +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + +@slotted_freezable +@dataclass +class Block: + """ + A complete block. + """ + + header: Header + transactions: Tuple[Union[Bytes, LegacyTransaction], ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + withdrawals: Tuple[Withdrawal, ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" + + +@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, ...] + + +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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) 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..2e2ba13043 --- /dev/null +++ b/src/ethereum/prague/fork.py @@ -0,0 +1,986 @@ +""" +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 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.cancun import fork as previous_fork +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + EthereumException, + InvalidBlock, + InvalidSenderError, +) + +from . import vm +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + Withdrawal, + encode_receipt, + header_base_fee_per_gas, +) +from .bloom import logs_bloom +from .fork_types import Account, Address, Authorization, VersionedHash +from .requests import ( + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests_from_receipt, +) +from .state import ( + State, + TransientStorage, + account_exists_and_is_empty, + destroy_account, + destroy_touched_empty_accounts, + get_account, + increment_nonce, + modify_state, + set_account_balance, + state_root, +) +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, + decode_transaction, + encode_transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set +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, + calculate_excess_blob_gas, + calculate_total_blob_gas, +) +from .vm.interpreter import MessageCallOutput, 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) +INITIAL_BASE_FEE = Uint(1000000000) +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(1179648) +VERSIONED_HASH_VERSION_KZG = b"\x01" + +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00000961Ef480Eb55e80D19ad83579A64c007002" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +) +HISTORY_STORAGE_ADDRESS = hex_to_address( + "0x0000F90827F1C53a10cb7A02335B175320002935" +) +HISTORY_SERVE_WINDOW = 8192 + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[AnyBlock] + 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 = parent_header(chain, block.header) + validate_header(parent, block.header) + if block.ommers != (): + raise InvalidBlock + + 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, + ) + 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"{block_output.block_gas_used} != {block.header.gas_used}" + ) + if transactions_root != block.header.transactions_root: + raise InvalidBlock + if block_state_root != block.header.state_root: + raise InvalidBlock + if receipt_root != block.header.receipt_root: + raise InvalidBlock + if block_logs_bloom != block.header.bloom: + raise InvalidBlock + if withdrawals_root != block.header.withdrawals_root: + raise InvalidBlock + if block_output.blob_gas_used != block.header.blob_gas_used: + raise InvalidBlock + if requests_hash != block.header.requests_hash: + 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 parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> 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 not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_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 + + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_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( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], 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. + + 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. + 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 + + 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 < block_env.base_fee_per_gas: + raise InvalidBlock + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - block_env.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 < block_env.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 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(block_env.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 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 and not is_valid_delegation(sender_account.code): + raise InvalidSenderError("not EOA") + + return ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) + + +def make_receipt( + tx: Transaction, + error: Optional[EthereumException], + 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, + ) + + return encode_receipt(tx, receipt) + + +def process_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> 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. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + """ + 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, + 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_output = process_message_call(system_tx_message) + + # 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( + block_env.state, system_tx_output.touched_accounts + ) + + return system_tx_output + + +def apply_body( + block_env: vm.BlockEnvironment, + transactions: Tuple[Union[LegacyTransaction, Bytes], ...], + withdrawals: Tuple[Withdrawal, ...], +) -> vm.BlockOutput: + """ + 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 + ---------- + block_env : + The block scoped environment. + transactions : + Transactions included in the block. + withdrawals : + Withdrawals to be processed in the current block. + + Returns + ------- + block_output : + The block output for the current block. + """ + block_output = vm.BlockOutput() + + process_system_transaction( + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) + + process_system_transaction( + 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)): + process_transaction(block_env, block_output, tx, Uint(i)) + + process_withdrawals(block_env, block_output, withdrawals) + + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, + ) + + return block_output + + +def process_general_purpose_requests( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, +) -> None: + """ + Process all the requests in the block. + + Parameters + ---------- + 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 + 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( + block_env=block_env, + target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_withdrawal_tx_output.return_data) > 0: + requests_from_execution.append( + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + ) + + system_consolidation_tx_output = process_system_transaction( + block_env=block_env, + target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_consolidation_tx_output.return_data) > 0: + requests_from_execution.append( + CONSOLIDATION_REQUEST_TYPE + + system_consolidation_tx_output.return_data + ) + + +def process_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: + """ + 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 + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. + tx : + Transaction to execute. + 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, + 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(block_env.excess_blob_gas, tx) + else: + blob_gas_fee = Uint(0) + + effective_gas_fee = tx.gas * effective_gas_price + + gas = tx.gas - intrinsic_gas + increment_nonce(block_env.state, sender) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee + ) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) + + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) + if isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + for address, keys in tx.access_list: + access_list_addresses.add(address) + for key in keys: + access_list_storage_keys.add((address, key)) + + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + + 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=[], + ) + + 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 - tx_output.gas_left + gas_refund = min( + 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. + tx_gas_used = max(execution_gas_used, calldata_floor_gas_cost) + + tx_output.gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_output.gas_left * effective_gas_price + + # 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( + block_env.state, sender + ).balance + U256(gas_refund_amount) + set_account_balance(block_env.state, sender, sender_balance_after_refund) + + # transfer miner fees + coinbase_balance_after_mining_fee = get_account( + block_env.state, block_env.coinbase + ).balance + U256(transaction_fee) + if coinbase_balance_after_mining_fee != 0: + set_account_balance( + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, + ) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) + + 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 + + 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), + ) + + modify_state(block_env.state, wd.address, increase_recipient_balance) + + 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: + """ + 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..26b6656190 --- /dev/null +++ b/src/ethereum/prague/fork_types.py @@ -0,0 +1,79 @@ +""" +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 U8, U64, 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), + ) + ) + + +@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/requests.py b/src/ethereum/prague/requests.py new file mode 100644 index 0000000000..a72f8f35ae --- /dev/null +++ b/src/ethereum/prague/requests.py @@ -0,0 +1,74 @@ +""" +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" +WITHDRAWAL_REQUEST_TYPE = b"\x01" +CONSOLIDATION_REQUEST_TYPE = b"\x02" + + +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/prague/state.py b/src/ethereum/prague/state.py new file mode 100644 index 0000000000..8a0e14728e --- /dev/null +++ b/src/ethereum/prague/state.py @@ -0,0 +1,720 @@ +""" +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, List, Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import modify +from ethereum_types.numeric import U256, Uint + +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[Bytes32, U256]] = field( + default_factory=dict + ) + _snapshots: List[ + Tuple[ + Trie[Address, Optional[Account]], + Dict[Address, Trie[Bytes32, 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[Bytes32, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes32, 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: Bytes32) -> 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: Bytes32, 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 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: 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 + 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: Bytes32 +) -> 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: Bytes32, + 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: Set[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Set[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..4bfc077b02 --- /dev/null +++ b/src/ethereum/prague/transactions.py @@ -0,0 +1,579 @@ +""" +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, ulen + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSignatureError + +from .exceptions import TransactionTypeError +from .fork_types import Address, Authorization, VersionedHash + +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 +@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 + + +@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, +] + + +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) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + 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:]) + elif tx[0] == 4: + return rlp.decode_to(SetCodeTransaction, tx[1:]) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: + """ + 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 + ------- + 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, 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): + raise InvalidBlock + if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: + raise InvalidBlock + + return intrinsic_gas, calldata_floor_gas_cost + + +def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, 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 + ------- + 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 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 + + zero_bytes = 0 + for byte in tx.data: + if byte == 0: + zero_bytes += 1 + + 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 = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST + + if tx.to == Bytes0(b""): + create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) + else: + create_cost = Uint(0) + + access_list_cost = Uint(0) + if isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + for _address, keys in tx.access_list: + access_list_cost += TX_ACCESS_LIST_ADDRESS_COST + access_list_cost += ulen(keys) * TX_ACCESS_LIST_STORAGE_KEY_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 + ), + calldata_floor_gas_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): + 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) + ) + elif isinstance(tx, SetCodeTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7702(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, + ) + ) + ) + + +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, + ) + ) + ) + + +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/trie.py b/src/ethereum/prague/trie.py new file mode 100644 index 0000000000..c81ef86b61 --- /dev/null +++ b/src/ethereum/prague/trie.py @@ -0,0 +1,494 @@ +""" +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, + 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 +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 + + +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: BranchSubnodes + 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 = list(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] + + subnodes = tuple( + encode_internal_node(patricialize(branches[k], level + Uint(1))) + for k in range(16) + ) + return BranchNode( + cast(BranchSubnodes, assert_type(subnodes, Tuple[rlp.Extended, ...])), + 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..9d3f615739 --- /dev/null +++ b/src/ethereum/prague/utils/message.py @@ -0,0 +1,95 @@ +""" +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 ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint + +from ..fork_types import Address +from ..state import get_account +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment +from ..vm.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( + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, +) -> Message: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. + + Returns + ------- + message: `ethereum.prague.vm.Message` + Items containing contract creation or message call specific data. + """ + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): + current_target = compute_contract_address( + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), + ) + msg_data = Bytes(b"") + 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(block_env.state, delegated_address).code + + code_address = tx.to + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses.add(current_target) + + return Message( + 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=True, + is_static=False, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(tx_env.access_list_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..55f1b92499 --- /dev/null +++ b/src/ethereum/prague/vm/__init__.py @@ -0,0 +1,209 @@ +""" +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, 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, 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 BlockEnvironment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + chain_id: U64 + state: State + block_gas_limit: Uint + block_hashes: List[Hash32] + coinbase: Address + number: Uint + base_fee_per_gas: Uint + time: U256 + prev_randao: Bytes32 + excess_blob_gas: U64 + 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 +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 + 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 + 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[EthereumException] + 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.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) + 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.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 new file mode 100644 index 0000000000..bb6a7e33d9 --- /dev/null +++ b/src/ethereum/prague/vm/eoa_delegation.py @@ -0,0 +1,214 @@ +""" +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 Evm, Message + +SET_CODE_TX_MAGIC = b"\x05" +EOA_DELEGATION_MARKER = b"\xEF\x01\x00" +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. + """ + state = evm.message.block_env.state + code = get_account(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(state, address).code + + return True, address, code, access_gas_cost + + +def set_delegation(message: Message) -> 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. + """ + state = message.block_env.state + refund_counter = 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: + continue + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + continue + + message.accessed_addresses.add(authority) + + authority_account = get_account(state, authority) + authority_code = authority_account.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(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(state, authority, code_to_set) + + increment_nonce(state, authority) + + if message.code_address is None: + raise InvalidBlock("Invalid type 4 transaction: no target") + message.code = get_account(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(state, message.code_address).code + + return refund_counter 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..8597b05ac0 --- /dev/null +++ b/src/ethereum/prague/vm/gas.py @@ -0,0 +1,369 @@ +""" +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 AnyHeader, 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(786432) +GAS_PER_BLOB = Uint(2**17) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_BASE_FEE_UPDATE_FRACTION = Uint(5007716) + +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: + """ + 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: AnyHeader) -> 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_BASE_FEE_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..e47a99de85 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/block.py @@ -0,0 +1,255 @@ +""" +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) + 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.message.block_env.block_hashes[ + -(current_block_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.message.block_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.message.block_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.message.block_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.message.block_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.message.block_env.block_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.message.block_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..5ddd12dac8 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/environment.py @@ -0,0 +1,597 @@ +""" +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.message.block_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.message.tx_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.message.tx_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: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + code = get_account(evm.message.block_env.state, address).code + + codesize = U256(len(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: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + 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.message.block_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: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + account = get_account(evm.message.block_env.state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + code = account.code + codehash = U256.from_be_bytes(keccak256(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.message.block_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.message.block_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.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)) + + # 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.message.block_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..65a0d5a9b6 --- /dev/null +++ b/src/ethereum/prague/vm/instructions/storage.py @@ -0,0 +1,184 @@ +""" +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.message.block_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 + + state = evm.message.block_env.state + original_value = get_storage_original( + state, evm.message.current_target, key + ) + current_value = get_storage(state, evm.message.current_target, key) + + gas_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(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.message.tx_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.message.tx_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..88ba466e8e --- /dev/null +++ b/src/ethereum/prague/vm/instructions/system.py @@ -0,0 +1,755 @@ +""" +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 ...vm.eoa_delegation import access_delegation +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.message.block_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.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.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, + 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) + + 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.message.block_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, + code: bytes, + is_delegated_code: bool, +) -> 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.message.block_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( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + 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) + + 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 + + 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.message.block_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.message.block_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, + code, + is_delegated_code, + ) + + # 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 + + ( + 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, + 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.message.block_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, + code, + is_delegated_code, + ) + + # 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.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 + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + + originator = evm.message.current_target + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance + + move_ether( + evm.message.block_env.state, + originator, + beneficiary, + originator_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + 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.message.block_env.state, originator, U256(0)) + evm.accounts_to_delete.add(originator) + + # mark beneficiary as touched + if account_exists_and_is_empty(evm.message.block_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 + + ( + 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 + ) + 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, + code, + is_delegated_code, + ) + + # 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 + + 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, + 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, + code_address, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + is_delegated_code, + ) + + # 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..5856c5af9c --- /dev/null +++ b/src/ethereum/prague/vm/interpreter.py @@ -0,0 +1,328 @@ +""" +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 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, + 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.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 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. + 7. `return_data`: The output of the execution. + """ + + gas_left: Uint + refund_counter: U256 + logs: Tuple[Log, ...] + accounts_to_delete: Set[Address] + touched_accounts: Set[Address] + error: Optional[EthereumException] + return_data: Bytes + + +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`. + + Parameters + ---------- + message : + Transaction specific items. + + 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( + 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(), + Bytes(b""), + ) + else: + evm = process_create_message(message) + else: + 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: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + touched_accounts = set() + 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, + return_data=evm.output, + ) + + +def process_create_message(message: Message) -> 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. + """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage + # take snapshot of state before processing the message + 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 + # 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(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(state, message.current_target) + + 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 + 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(state, transient_storage) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + set_code(state, message.current_target, contract_code) + commit_transaction(state, transient_storage) + else: + rollback_transaction(state, transient_storage) + return evm + + +def process_message(message: Message) -> 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 + """ + 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(state, transient_storage) + + touch_account(state, message.current_target) + + if message.should_transfer_value and message.value != 0: + move_ether( + state, message.caller, message.current_target, message.value + ) + + evm = execute_code(message) + if evm.error: + # revert state to the last saved checkpoint + # since the message call resulted in an error + rollback_transaction(state, transient_storage) + else: + commit_transaction(state, transient_storage) + return evm + + +def execute_code(message: Message) -> 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, + 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..8ab92bb0f3 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/__init__.py @@ -0,0 +1,54 @@ +""" +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", + "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") +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") +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/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/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/ecrecover.py b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..1f047d3a44 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,63 @@ +""" +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.exceptions import InvalidSignatureError +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 InvalidSignatureError: + # 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..3094d8dff2 --- /dev/null +++ b/src/ethereum/prague/vm/precompiled_contracts/mapping.py @@ -0,0 +1,74 @@ +""" +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, + 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, + 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 .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 +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, + 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, +} 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/src/ethereum/shanghai/blocks.py b/src/ethereum/shanghai/blocks.py index c76584859c..0b90c87f20 100644 --- a/src/ethereum/shanghai/blocks.py +++ b/src/ethereum/shanghai/blocks.py @@ -9,15 +9,25 @@ chain. """ from dataclasses import dataclass -from typing import Tuple, Union +from typing import Annotated, Optional, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.paris import blocks as previous_blocks 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 @@ -59,6 +69,49 @@ class Header: withdrawals_root: Root +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -68,10 +121,17 @@ class Block: header: Header transactions: Tuple[Union[Bytes, LegacyTransaction], ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] withdrawals: Tuple[Withdrawal, ...] +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" + + @slotted_freezable @dataclass class Log: @@ -95,3 +155,36 @@ 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 + + +def header_base_fee_per_gas(header: AnyHeader) -> Optional[Uint]: + """ + Returns the `base_fee_per_gas` of the given header, or `None` for headers + without that field. + """ + if isinstance(header, Header): + return header.base_fee_per_gas + return previous_blocks.header_base_fee_per_gas(header) diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index d0347b7d1e..0e0f897ebb 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -16,23 +16,39 @@ 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 ethereum.paris import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt, Withdrawal +from .blocks import ( + AnyBlock, + AnyHeader, + Block, + Header, + Log, + Receipt, + Withdrawal, + encode_receipt, + header_base_fee_per_gas, +) 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, ) @@ -41,13 +57,13 @@ FeeMarketTransaction, LegacyTransaction, Transaction, - calculate_intrinsic_cost, 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 @@ -55,6 +71,7 @@ ELASTICITY_MULTIPLIER = Uint(2) GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) GAS_LIMIT_MINIMUM = Uint(5000) +INITIAL_BASE_FEE = Uint(1000000000) EMPTY_OMMER_HASH = keccak256(rlp.encode([])) @@ -64,7 +81,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -153,36 +170,47 @@ 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) + parent = parent_header(chain, block.header) + validate_header(parent, 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, ) - 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) + + 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) @@ -254,7 +282,24 @@ def calculate_base_fee_per_gas( return Uint(expected_base_fee_per_gas) -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -272,15 +317,25 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + 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, - ) + expected_base_fee_per_gas = INITIAL_BASE_FEE + parent_base_fee_per_gas = header_base_fee_per_gas(parent_header) + if parent_base_fee_per_gas is not None: + # For every block except the first, calculate the base fee per gas + # based on the parent block. + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: raise InvalidBlock if header.timestamp <= parent_header.timestamp: @@ -302,24 +357,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 ------- @@ -333,32 +385,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]: @@ -389,57 +452,14 @@ 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 - - -@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 + return encode_receipt(tx, receipt) 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. @@ -452,115 +472,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 - ) - - 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_transaction(block_env, block_output, tx, Uint(i)) - receipt = make_receipt( - tx, error, (block_gas_limit - gas_available), logs - ) + process_withdrawals(block_env, block_output, withdrawals) - 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. @@ -575,104 +516,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. """ - if not validate_transaction(tx): - raise InvalidBlock + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) - sender = env.origin - sender_account = get_account(env.state, sender) + intrinsic_gas = validate_transaction(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, + effective_gas_price, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 + + +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 address in output.accounts_to_delete: - destroy_account(env.state, address) + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) - for address in output.touched_accounts: - if account_exists_and_is_empty(env.state, address): - destroy_account(env.state, address) + 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 efd3b2fcda..f78f3af49e 100644 --- a/src/ethereum/shanghai/transactions.py +++ b/src/ethereum/shanghai/transactions.py @@ -9,21 +9,21 @@ 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 ethereum.exceptions import InvalidBlock, InvalidSignatureError 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 @@ -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: @@ -176,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: @@ -190,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: @@ -381,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 26e950573d..07832dd672 100644 --- a/src/ethereum/shanghai/utils/message.py +++ b/src/ethereum/shanghai/utils/message.py @@ -12,105 +12,78 @@ 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 ------- message: `ethereum.shanghai.vm.Message` Items containing contract creation or message call specific data. """ - if isinstance(target, Bytes0): + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + 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 = 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, + 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 33d8396a48..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) @@ -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.message.block_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 + 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) @@ -465,18 +468,21 @@ 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) + account = get_account(evm.message.block_env.state, address) 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) @@ -502,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) @@ -527,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 12eb5dd34f..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) @@ -365,9 +373,11 @@ 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 + 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 @@ -383,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)) @@ -396,7 +406,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, False, memory_input_start_position, @@ -457,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)) @@ -502,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 @@ -512,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 @@ -628,6 +647,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 +666,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..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,39 +98,39 @@ 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: 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 @@ -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/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/blocks.py b/src/ethereum/spurious_dragon/blocks.py index 713b5aecf5..a1a59b3dc4 100644 --- a/src/ethereum/spurious_dragon/blocks.py +++ b/src/ethereum/spurious_dragon/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.exceptions import InvalidBlock +from ethereum.tangerine_whistle import blocks as previous_blocks from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/spurious_dragon/fork.py b/src/ethereum/spurious_dragon/fork.py index cb4d0b0702..bd174118e2 100644 --- a/src/ethereum/spurious_dragon/fork.py +++ b/src/ethereum/spurious_dragon/fork.py @@ -11,27 +11,28 @@ 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 from ethereum.ethash import dataset_size, generate_cache, hashimoto_light from ethereum.exceptions import InvalidBlock, InvalidSenderError +from ethereum.tangerine_whistle import fork as previous_fork from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -39,11 +40,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -60,7 +61,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -149,32 +150,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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, ) - 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) @@ -184,7 +195,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -202,6 +230,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -300,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 ------- @@ -326,15 +361,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, ...], @@ -344,8 +389,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -369,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -420,94 +429,31 @@ 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, - ) - - 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -542,10 +488,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -588,7 +532,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -627,8 +571,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. @@ -643,78 +590,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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 - calculate_intrinsic_cost(tx) - increment_nonce(env.state, sender) + gas = tx.gas - intrinsic_gas + 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 142929587d..21ffbc1b6f 100644 --- a/src/ethereum/spurious_dragon/transactions.py +++ b/src/ethereum/spurious_dragon/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -151,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]) @@ -213,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 9d936e7f5f..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,9 +333,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -368,7 +368,8 @@ 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 98e5cc0c66..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) @@ -249,9 +259,12 @@ 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) + 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 @@ -267,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)) @@ -279,7 +292,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, @@ -332,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)) @@ -371,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 @@ -389,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 1b9a021f7b..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,39 +95,39 @@ 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: 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,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/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/blocks.py b/src/ethereum/tangerine_whistle/blocks.py index 713b5aecf5..943de99e63 100644 --- a/src/ethereum/tangerine_whistle/blocks.py +++ b/src/ethereum/tangerine_whistle/blocks.py @@ -9,11 +9,16 @@ chain. """ from dataclasses import dataclass -from typing import Tuple +from typing import Annotated, 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 typing_extensions import TypeAlias + +from ethereum.dao_fork import blocks as previous_blocks +from ethereum.exceptions import InvalidBlock from ..crypto.hash import Hash32 from .fork_types import Address, Bloom, Root @@ -44,6 +49,49 @@ class Header: nonce: Bytes8 +AnyHeader: TypeAlias = Union[previous_blocks.AnyHeader, Header] +""" +Represents all headers that may have appeared in the blockchain before or in +the current fork. +""" + + +def decode_header(raw_header: rlp.Simple) -> AnyHeader: + """ + Convert `raw_header` from raw sequences and bytes to a structured block + header. + + Checks `raw_header` against this fork's `FORK_CRITERIA`, and if it belongs + to this fork, decodes it accordingly. If not, this function forwards to the + preceding fork where the process is repeated. + """ + from . import FORK_CRITERIA + + # First, ensure that `raw_header` is not `bytes` (and is therefore a + # sequence.) + if isinstance(raw_header, bytes): + raise InvalidBlock("header is bytes, expected sequence") + + # Next, extract the block number and timestamp (which are always at index 8 + # and 11 respectively.) + raw_number = raw_header[8] + if not isinstance(raw_number, bytes): + raise InvalidBlock("header number is sequence, expected bytes") + number = Uint.from_be_bytes(raw_number) + + raw_timestamp = raw_header[11] + if not isinstance(raw_timestamp, bytes): + raise InvalidBlock("header timestamp is sequence, expected bytes") + timestamp = U256.from_be_bytes(raw_timestamp) + + # Finally, check if this header belongs to this fork. + if FORK_CRITERIA.check(number, timestamp): + return rlp.deserialize_to(Header, raw_header) + + # If it doesn't, forward to the preceding fork. + return previous_blocks.decode_header(raw_header) + + @slotted_freezable @dataclass class Block: @@ -53,7 +101,14 @@ class Block: header: Header transactions: Tuple[Transaction, ...] - ommers: Tuple[Header, ...] + ommers: Tuple[Annotated[AnyHeader, rlp.With(decode_header)], ...] + + +AnyBlock: TypeAlias = Union[previous_blocks.AnyBlock, Block] +""" +Represents all blocks that may have appeared in the blockchain before or in the +current fork. +""" @slotted_freezable diff --git a/src/ethereum/tangerine_whistle/fork.py b/src/ethereum/tangerine_whistle/fork.py index 1419c87b6d..071369cb78 100644 --- a/src/ethereum/tangerine_whistle/fork.py +++ b/src/ethereum/tangerine_whistle/fork.py @@ -11,22 +11,22 @@ 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 +from ethereum.dao_fork import fork as previous_fork from ethereum.ethash import dataset_size, generate_cache, hashimoto_light from ethereum.exceptions import InvalidBlock, InvalidSenderError from . import vm -from .blocks import Block, Header, Log, Receipt +from .blocks import AnyBlock, AnyHeader, 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, @@ -38,11 +38,11 @@ ) from .transactions import ( Transaction, - calculate_intrinsic_cost, + 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 @@ -59,7 +59,7 @@ class BlockChain: History and current state of the block chain. """ - blocks: List[Block] + blocks: List[AnyBlock] state: State chain_id: U64 @@ -148,31 +148,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) + parent = parent_header(chain, block.header) + validate_header(block.header, parent) 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) @@ -182,7 +193,24 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.blocks = chain.blocks[-255:] -def validate_header(header: Header, parent_header: Header) -> None: +def parent_header(chain: BlockChain, header: AnyHeader) -> AnyHeader: + """ + Gets the parent of a block, given that block's `header` and `chain`. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_number = header.number - Uint(1) + first_number = chain.blocks[0].header.number + last_number = chain.blocks[-1].header.number + + if parent_number < first_number or parent_number > last_number: + raise InvalidBlock + + return chain.blocks[parent_number - first_number].header + + +def validate_header(header: AnyHeader, parent_header: AnyHeader) -> None: """ Verifies a block header. @@ -200,6 +228,13 @@ def validate_header(header: Header, parent_header: Header) -> None: parent_header : Parent Header of the header to check for correctness """ + if header.gas_used > header.gas_limit: + raise InvalidBlock + + if not isinstance(header, Header): + assert not isinstance(parent_header, Header) + return previous_fork.validate_header(header, parent_header) + if header.timestamp <= parent_header.timestamp: raise InvalidBlock if header.number != parent_header.number + Uint(1): @@ -298,18 +333,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 ------- @@ -321,15 +359,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, ...], @@ -339,8 +387,6 @@ def make_receipt( Parameters ---------- - tx : - The executed transaction. post_state : The state root immediately after this transaction. cumulative_gas_used : @@ -364,44 +410,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: + ommers: Tuple[AnyHeader, ...], +) -> vm.BlockOutput: """ Executes a block. @@ -414,21 +427,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 : @@ -437,69 +437,21 @@ 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, - ) + 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( - ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain + ommers: Tuple[AnyHeader, ...], block_header: Header, chain: BlockChain ) -> None: """ Validates the ommers mentioned in the block. @@ -534,10 +486,8 @@ 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) + parent = parent_header(chain, ommer) + validate_header(ommer, parent) if len(ommers) > 2: raise InvalidBlock @@ -580,7 +530,7 @@ def pay_rewards( state: State, block_number: Uint, coinbase: Address, - ommers: Tuple[Header, ...], + ommers: Tuple[AnyHeader, ...], ) -> None: """ Pay rewards to the block miner as well as the ommers miners. @@ -619,8 +569,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. @@ -635,71 +588,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. """ - if not validate_transaction(tx): - raise InvalidBlock + 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(block_env.state, sender) - gas = tx.gas - calculate_intrinsic_cost(tx) - increment_nonce(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 57d697d615..6db00e736d 100644 --- a/src/ethereum/tangerine_whistle/transactions.py +++ b/src/ethereum/tangerine_whistle/transactions.py @@ -13,14 +13,14 @@ 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 -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 @@ -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: @@ -93,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: @@ -107,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: @@ -174,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 9d936e7f5f..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,9 +333,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.message.block_env.state, address).code + codesize = U256(len(code)) push(evm.stack, codesize) # PROGRAM COUNTER @@ -368,7 +368,8 @@ 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 35d9cecdeb..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) @@ -248,7 +258,10 @@ def call(evm: Evm) -> None: (memory_output_start_position, memory_output_size), ], ) - _account_exists = account_exists(evm.env.state, to) + + code_address = 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( @@ -263,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)) @@ -275,7 +288,7 @@ def call(evm: Evm) -> None: value, evm.message.current_target, to, - to, + code_address, True, memory_input_start_position, memory_input_size, @@ -328,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)) @@ -366,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 @@ -383,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 13d753ac0b..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,35 +89,33 @@ 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, ...] = () 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 @@ -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/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/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/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 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`. 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 5132577567..f9dbd59801 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -5,6 +5,7 @@ import importlib from typing import Any +from ethereum.fork_criteria import ForkCriteria from ethereum_spec_tools.forks import Hardfork @@ -20,6 +21,17 @@ def __init__(self, fork_module: str): self._fork_module = fork_module self._forks = Hardfork.discover() + @property + def fork_criteria(self) -> ForkCriteria: + """Activation criteria for the loaded fork.""" + mod = importlib.import_module(f"ethereum.{self._fork_module}") + return mod.FORK_CRITERIA + + @fork_criteria.setter + def fork_criteria(self, value: ForkCriteria) -> None: + mod = importlib.import_module(f"ethereum.{self._fork_module}") + mod.FORK_CRITERIA = value # type: ignore[attr-defined] + @property def fork_module(self) -> str: """Module that contains the fork code""" @@ -47,6 +59,31 @@ def is_after_fork(self, target_fork_name: str) -> bool: break return return_value + @property + 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 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 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.""" @@ -73,9 +110,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: @@ -102,15 +139,20 @@ 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""" 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: @@ -127,6 +169,16 @@ def Block(self) -> Any: """Block class of the fork""" return self._module("blocks").Block + @property + def decode_receipt(self) -> Any: + """decode_receipt function of the fork""" + return self._module("blocks").decode_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""" @@ -167,6 +219,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""" @@ -187,51 +244,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""" @@ -242,11 +264,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""" @@ -257,11 +274,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""" @@ -273,30 +285,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("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/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/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 a5c2be531f..4d9de304fc 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -9,11 +9,10 @@ from typing import Any, TextIO from ethereum_rlp import rlp -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 @@ -25,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 @@ -73,6 +72,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,59 +125,7 @@ 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: - """ - 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. @@ -185,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.""" @@ -298,168 +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.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, - ) + 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)}") - 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(), - ) + self.result.update(self, block_env, block_output) + self.result.rejected = self.txs.rejected_txs - system_tx_output = self.fork.process_message_call( - system_tx_message, system_tx_env + 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( + block_env=block_env, + target_address=self.fork.HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash ) - self.fork.destroy_touched_empty_accounts( - system_tx_env.state, system_tx_output.touched_accounts + if self.fork.is_after_fork("ethereum.cancun"): + self.fork.process_system_transaction( + 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, - ) - - 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)) + 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, + ) if self.fork.is_after_fork("ethereum.shanghai"): - 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.fork.process_withdrawals( + block_env, block_output, self.env.withdrawals + ) - self.result.withdrawals_root = self.fork.root(withdrawals_trie) + 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.cancun"): - self.result.blob_gas_used = blob_gas_used - self.result.excess_blob_gas = self.env.excess_blob_gas - - 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.update(self, block_env, block_output) self.result.rejected = self.txs.rejected_txs - self.result.receipts = self.txs.successful_receipts - self.result.gas_used = block_gas_used 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/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 802c524304..d1cff3975a 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] @@ -52,6 +53,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": @@ -69,17 +71,19 @@ 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) + 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: @@ -230,10 +234,17 @@ 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") + and not t8n.options.state_test + ): + self.parent_hash = Hash32(hex_to_bytes(data["parentHash"])) + # Read the block hashes block_hashes: List[Any] = [] # Store a maximum of 256 block hashes. 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 404f96bfe9..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,17 +3,17 @@ """ 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 -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 @@ -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` @@ -277,6 +233,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") @@ -306,6 +265,70 @@ 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 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""" @@ -340,12 +363,14 @@ 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 + + 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") diff --git a/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py b/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py index 81bf0403ee..c737a30ef3 100644 --- a/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py +++ b/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py @@ -24,11 +24,9 @@ # graffiti near the fork block. ("dao_fork", ".fork", "apply_fork"), ("dao_fork", ".fork", "validate_header"), - # There are some differences between london and arrow_glacier - # in terms of how the fork block is handled. - ("arrow_glacier", ".fork", "calculate_base_fee_per_gas"), - ("arrow_glacier", ".fork", "validate_header"), - ("arrow_glacier", ".fork", "INITIAL_BASE_FEE"), + # Arrow Glacier must check the preceding fork when getting the + # `base_fee_per_gas`. + ("arrow_glacier", ".blocks", "header_base_fee_per_gas"), ] 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_rlp.py b/tests/berlin/test_rlp.py index e853f4e577..0ca8842432 100644 --- a/tests/berlin/test_rlp.py +++ b/tests/berlin/test_rlp.py @@ -74,7 +74,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(12244000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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_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_rlp.py b/tests/byzantium/test_rlp.py index fa0985ac9c..ef9945f3e5 100644 --- a/tests/byzantium/test_rlp.py +++ b/tests/byzantium/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(4370000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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/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_rlp.py b/tests/cancun/test_rlp.py index 6478fe25d9..80ba30408e 100644 --- a/tests/cancun/test_rlp.py +++ b/tests/cancun/test_rlp.py @@ -93,7 +93,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(19426587), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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_rlp.py b/tests/constantinople/test_rlp.py index bdb13cc502..f1b942417a 100644 --- a/tests/constantinople/test_rlp.py +++ b/tests/constantinople/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(7280000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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_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/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/helpers/__init__.py b/tests/helpers/__init__.py index 41cfd188a7..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", @@ -15,4 +11,13 @@ "commit_hash": "a0e8482", "fixture_path": "tests/fixtures/ethereum_tests", }, + "latest_fork_tests": { + "url": "https://github.com/gurukamath/latest_fork_tests.git", + "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 f02f77c91d..38af0eafff 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -122,10 +122,11 @@ 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) 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_state_tests.py b/tests/helpers/load_state_tests.py index eca1953ae3..3bb1497d82 100644 --- a/tests/helpers/load_state_tests.py +++ b/tests/helpers/load_state_tests.py @@ -14,6 +14,7 @@ from ethereum.crypto.hash import keccak256 from ethereum.exceptions import EthereumException +from ethereum.fork_criteria import ByBlockNumber from ethereum.utils.hexadecimal import hex_to_bytes from ethereum_spec_tools.evm_tools.loaders.fixture_loader import Load @@ -26,6 +27,7 @@ class NoTestsFound(Exception): def run_blockchain_st_test(test_case: Dict, load: Load) -> None: + load.fork.fork_criteria = ByBlockNumber(0) test_file = test_case["test_file"] test_key = test_case["test_key"] @@ -46,6 +48,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/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_rlp.py b/tests/homestead/test_rlp.py index 647ddcafc5..2fdbdfeb9c 100644 --- a/tests/homestead/test_rlp.py +++ b/tests/homestead/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(1150000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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_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_rlp.py b/tests/istanbul/test_rlp.py index 3eb39041ac..50df6027f9 100644 --- a/tests/istanbul/test_rlp.py +++ b/tests/istanbul/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(9069000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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_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_rlp.py b/tests/london/test_rlp.py index a18856e6e7..25fd7cc6ed 100644 --- a/tests/london/test_rlp.py +++ b/tests/london/test_rlp.py @@ -91,7 +91,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(12965000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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/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_rlp.py b/tests/paris/test_rlp.py index 33497fde81..6de37432a1 100644 --- a/tests/paris/test_rlp.py +++ b/tests/paris/test_rlp.py @@ -91,7 +91,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(15537394), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/__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..93770bf686 --- /dev/null +++ b/tests/prague/test_evm_tools.py @@ -0,0 +1,66 @@ +from functools import partial +from typing import Dict + +import pytest + +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_STATE_TESTS_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +EEST_STATE_TESTS_DIR = f"{EEST_TESTS_PATH}/state_tests/" +FORK_NAME = "Prague" + + +SLOW_TESTS = ( + "CALLBlake2f_MaxRounds", + "CALLCODEBlake2f", + "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-]", +) + + +# 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_tests(EEST_STATE_TESTS_DIR), + ids=idfn, +) +def test_eest_evm_tools(test_case: Dict) -> None: + run_tests(test_case) diff --git a/tests/prague/test_rlp.py b/tests/prague/test_rlp.py new file mode 100644 index 0000000000..d8a7c4e047 --- /dev/null +++ b/tests/prague/test_rlp.py @@ -0,0 +1,167 @@ +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.crypto.hash import keccak256 +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.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") +hash7 = keccak256(b"quuxbaz") + +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(19426587), + 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), + requests_hash=hash7, +) + +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..dd2bd52b43 --- /dev/null +++ b/tests/prague/test_state_transition.py @@ -0,0 +1,104 @@ +from functools import partial +from typing import Dict + +import pytest + +from tests.helpers import EEST_TESTS_PATH, ETHEREUM_TESTS_PATH +from tests.helpers.load_state_tests import ( + Load, + fetch_state_test_files, + idfn, + run_blockchain_st_test, +) + +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 + "stTimeConsuming/CALLBlake2f_MaxRounds.json", + "stTimeConsuming/static_Call50000_sha256.json", + "vmPerformance/loopExp.json", + "vmPerformance/loopMul.json", + "QuadraticComplexitySolidity_CallDataCopy_d0g1v0_Prague", + "CALLBlake2f_d9g0v0_Prague", + "CALLCODEBlake2f_d9g0v0", + # GeneralStateTests + "stRandom/randomStatetest177.json", + "stCreateTest/CreateOOGafterMaxCodesize.json", + # ValidBlockTest + "bcExploitTest/DelegateCallSpam.json", + # 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, +# Please provide an explanation when adding entries +IGNORE_TESTS = ( + # ValidBlockTest + "bcForkStressTest/ForkStressTest.json", + "bcGasPricerTest/RPC_API_Test.json", + "bcMultiChainTest", + "bcTotalDifficultyTest", + # InvalidBlockTest + "bcForgedTest", + "bcMultiChainTest", + "GasLimitHigherThan2p63m1_Prague", +) + +# All tests that recursively create a large number of frames (50000) +BIG_MEMORY_TESTS = ( + # GeneralStateTests + "50000_", + "/stQuadraticComplexityTest/", + "/stRandom2/", + "/stRandom/", + "/stSpecialTest/", + "stTimeConsuming/", + "stBadOpcode/", + "stStaticCall/", +) + +# 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_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_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_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 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_rlp.py b/tests/shanghai/test_rlp.py index 7a7d689ef6..25e00b4e2e 100644 --- a/tests/shanghai/test_rlp.py +++ b/tests/shanghai/test_rlp.py @@ -93,7 +93,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(17034870), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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_rlp.py b/tests/spurious_dragon/test_rlp.py index 05506ef6f1..87f00fc9a2 100644 --- a/tests/spurious_dragon/test_rlp.py +++ b/tests/spurious_dragon/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(2675000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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_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_rlp.py b/tests/tangerine_whistle/test_rlp.py index 3a4a30074a..368dac3127 100644 --- a/tests/tangerine_whistle/test_rlp.py +++ b/tests/tangerine_whistle/test_rlp.py @@ -66,7 +66,7 @@ receipt_root=hash5, bloom=bloom, difficulty=Uint(1), - number=Uint(2), + number=Uint(2463000), gas_limit=Uint(3), gas_used=Uint(4), timestamp=U256(5), 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/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 647714587b..34da82d764 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ min_version = 2.0 envlist = py3,pypy3,static [testenv:static] +basepython = pypy3 extras = lint optimized diff --git a/whitelist.txt b/whitelist.txt index de5d7ee9d7..74cbb64d59 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 @@ -64,6 +66,8 @@ U8 ulen secp256k1 secp256k1n +secp256k1p +secp256k1b statetest subclasses iadd @@ -458,3 +462,8 @@ exponentiate monomial impl x2 + +eoa + +blockchain +listdir \ No newline at end of file