diff --git a/setup.cfg b/setup.cfg index 609820a8c0..8bf1f9c709 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ packages = ethereum_spec_tools/evm_tools ethereum_spec_tools/evm_tools/t8n ethereum_spec_tools/evm_tools/b11r + ethereum_spec_tools/evm_tools/statetest + ethereum_spec_tools/evm_tools/loaders ethereum_spec_tools/lint ethereum_spec_tools/lint/lints ethereum @@ -97,6 +99,11 @@ packages = ethereum/shanghai/vm ethereum/shanghai/vm/instructions ethereum/shanghai/vm/precompiled_contracts + ethereum/cancun + ethereum/cancun/utils + ethereum/cancun/vm + ethereum/cancun/vm/instructions + ethereum/cancun/vm/precompiled_contracts package_dir = @@ -107,6 +114,7 @@ install_requires = pycryptodome>=3,<4 coincurve>=18,<19 typing_extensions>=4 + eth2spec @ git+https://github.com/ethereum/consensus-specs.git@d302b35d40c72842d444ef2ea64344e3cb889804 [options.package_data] ethereum = @@ -119,6 +127,9 @@ ethereum = assets/blocks/block_1234567.json assets/blocks/block_12964999.json +ethereum_spec_tools = + py.typed + [options.entry_points] console_scripts = ethereum-spec-lint = ethereum_spec_tools.lint:main @@ -145,6 +156,7 @@ test = pytest-xdist>=3.3.1,<4 GitPython>=3.1.0,<3.2 filelock>=3.12.3,<3.13 + platformdirs>=4.2,<5 requests lint = diff --git a/src/ethereum/arrow_glacier/vm/exceptions.py b/src/ethereum/arrow_glacier/vm/exceptions.py index 2d54fbb5b3..6d30e2846d 100644 --- a/src/ethereum/arrow_glacier/vm/exceptions.py +++ b/src/ethereum/arrow_glacier/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/arrow_glacier/vm/precompiled_contracts/modexp.py b/src/ethereum/arrow_glacier/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/arrow_glacier/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/arrow_glacier/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/base_types.py b/src/ethereum/base_types.py index 50f4f05707..3e44fe8478 100644 --- a/src/ethereum/base_types.py +++ b/src/ethereum/base_types.py @@ -92,7 +92,7 @@ def __init__(self, value: int) -> None: raise TypeError() if value < 0: - raise ValueError() + raise OverflowError() def __radd__(self, left: int) -> "Uint": return self.__add__(left) @@ -102,7 +102,7 @@ def __add__(self, right: int) -> "Uint": return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__add__(self, right)) @@ -114,7 +114,7 @@ def __sub__(self, right: int) -> "Uint": return NotImplemented if right < 0 or self < right: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__sub__(self, right)) @@ -123,7 +123,7 @@ def __rsub__(self, left: int) -> "Uint": return NotImplemented if left < 0 or self > left: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rsub__(self, left)) @@ -135,7 +135,7 @@ def __mul__(self, right: int) -> "Uint": return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__mul__(self, right)) @@ -153,7 +153,7 @@ def __floordiv__(self, right: int) -> "Uint": return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__floordiv__(self, right)) @@ -162,7 +162,7 @@ def __rfloordiv__(self, left: int) -> "Uint": return NotImplemented if left < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rfloordiv__(self, left)) @@ -174,7 +174,7 @@ def __mod__(self, right: int) -> "Uint": return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__mod__(self, right)) @@ -183,7 +183,7 @@ def __rmod__(self, left: int) -> "Uint": return NotImplemented if left < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rmod__(self, left)) @@ -195,7 +195,7 @@ def __divmod__(self, right: int) -> Tuple["Uint", "Uint"]: return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() result = int.__divmod__(self, right) return ( @@ -208,7 +208,7 @@ def __rdivmod__(self, left: int) -> Tuple["Uint", "Uint"]: return NotImplemented if left < 0: - raise ValueError() + raise OverflowError() result = int.__rdivmod__(self, left) return ( @@ -224,13 +224,13 @@ def __pow__( # type: ignore[override] return NotImplemented if modulo < 0: - raise ValueError() + raise OverflowError() if not isinstance(right, int): return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__pow__(self, right, modulo)) @@ -242,13 +242,13 @@ def __rpow__( # type: ignore[misc] return NotImplemented if modulo < 0: - raise ValueError() + raise OverflowError() if not isinstance(left, int): return NotImplemented if left < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rpow__(self, left, modulo)) @@ -262,7 +262,7 @@ def __xor__(self, right: int) -> "Uint": return NotImplemented if right < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__xor__(self, right)) @@ -271,7 +271,7 @@ def __rxor__(self, left: int) -> "Uint": return NotImplemented if left < 0: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rxor__(self, left)) @@ -328,7 +328,7 @@ def __init__(self: T, value: int) -> None: raise TypeError() if value < 0 or value > self.MAX_VALUE: - raise ValueError() + raise OverflowError() def __radd__(self: T, left: int) -> T: return self.__add__(left) @@ -340,7 +340,7 @@ def __add__(self: T, right: int) -> T: result = int.__add__(self, right) if right < 0 or result > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, result) @@ -358,7 +358,7 @@ def wrapping_add(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() # This is a fast way of ensuring that the result is < (2 ** 256) return int.__new__( @@ -373,7 +373,7 @@ def __sub__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE or self < right: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__sub__(self, right)) @@ -391,7 +391,7 @@ def wrapping_sub(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() # This is a fast way of ensuring that the result is < (2 ** 256) return int.__new__( @@ -403,7 +403,7 @@ def __rsub__(self: T, left: int) -> T: return NotImplemented if left < 0 or left > self.MAX_VALUE or self > left: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rsub__(self, left)) @@ -417,7 +417,7 @@ def __mul__(self: T, right: int) -> T: result = int.__mul__(self, right) if right < 0 or result > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, result) @@ -435,7 +435,7 @@ def wrapping_mul(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() # This is a fast way of ensuring that the result is < (2 ** 256) return int.__new__( @@ -456,7 +456,7 @@ def __floordiv__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__floordiv__(self, right)) @@ -465,7 +465,7 @@ def __rfloordiv__(self: T, left: int) -> T: return NotImplemented if left < 0 or left > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rfloordiv__(self, left)) @@ -477,7 +477,7 @@ def __mod__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__mod__(self, right)) @@ -486,7 +486,7 @@ def __rmod__(self: T, left: int) -> T: return NotImplemented if left < 0 or left > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rmod__(self, left)) @@ -498,7 +498,7 @@ def __divmod__(self: T, right: int) -> Tuple[T, T]: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() result = super(FixedUint, self).__divmod__(right) return ( @@ -511,7 +511,7 @@ def __rdivmod__(self: T, left: int) -> Tuple[T, T]: return NotImplemented if left < 0 or left > self.MAX_VALUE: - raise ValueError() + raise OverflowError() result = super(FixedUint, self).__rdivmod__(left) return ( @@ -527,7 +527,7 @@ def __pow__( # type: ignore[override] return NotImplemented if modulo < 0 or modulo > self.MAX_VALUE: - raise ValueError() + raise OverflowError() if not isinstance(right, int): return NotImplemented @@ -535,7 +535,7 @@ def __pow__( # type: ignore[override] result = int.__pow__(self, right, modulo) if right < 0 or right > self.MAX_VALUE or result > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, result) @@ -556,13 +556,13 @@ def wrapping_pow(self: T, right: int, modulo: Optional[int] = None) -> T: return NotImplemented if modulo < 0 or modulo > self.MAX_VALUE: - raise ValueError() + raise OverflowError() if not isinstance(right, int): return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() # This is a fast way of ensuring that the result is < (2 ** 256) return int.__new__( @@ -577,13 +577,13 @@ def __rpow__( # type: ignore[misc] return NotImplemented if modulo < 0 or modulo > self.MAX_VALUE: - raise ValueError() + raise OverflowError() if not isinstance(left, int): return NotImplemented if left < 0 or left > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rpow__(self, left, modulo)) @@ -597,7 +597,7 @@ def __and__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__and__(self, right)) @@ -606,7 +606,7 @@ def __or__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__or__(self, right)) @@ -615,7 +615,7 @@ def __xor__(self: T, right: int) -> T: return NotImplemented if right < 0 or right > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__xor__(self, right)) @@ -624,7 +624,7 @@ def __rxor__(self: T, left: int) -> T: return NotImplemented if left < 0 or left > self.MAX_VALUE: - raise ValueError() + raise OverflowError() return int.__new__(self.__class__, int.__rxor__(self, left)) @@ -891,6 +891,14 @@ class Bytes32(FixedBytes): """ +class Bytes48(FixedBytes): + """ + Byte array of exactly 48 elements. + """ + + LENGTH = 48 + + class Bytes64(FixedBytes): """ Byte array of exactly 64 elements. diff --git a/src/ethereum/berlin/vm/exceptions.py b/src/ethereum/berlin/vm/exceptions.py index cb98c39c17..789d302089 100644 --- a/src/ethereum/berlin/vm/exceptions.py +++ b/src/ethereum/berlin/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/berlin/vm/precompiled_contracts/modexp.py b/src/ethereum/berlin/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/berlin/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/berlin/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/byzantium/vm/exceptions.py b/src/ethereum/byzantium/vm/exceptions.py index 2862dae480..116d4b93cf 100644 --- a/src/ethereum/byzantium/vm/exceptions.py +++ b/src/ethereum/byzantium/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/byzantium/vm/precompiled_contracts/modexp.py b/src/ethereum/byzantium/vm/precompiled_contracts/modexp.py index 4fa2afa369..813de1d79c 100644 --- a/src/ethereum/byzantium/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/byzantium/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = U256.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -61,6 +60,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/cancun/__init__.py b/src/ethereum/cancun/__init__.py new file mode 100644 index 0000000000..6812aa47d7 --- /dev/null +++ b/src/ethereum/cancun/__init__.py @@ -0,0 +1,10 @@ +""" +The Cancun fork introduces transient storage, exposes beacon chain roots, +introduces a new blob-carrying transaction type, adds a memory copying +instruction, limits self-destruct to only work for contracts created in the +same transaction, and adds an instruction to read the blob base fee. +""" + +from ethereum.fork_criteria import ByTimestamp + +FORK_CRITERIA = ByTimestamp(1710338135) diff --git a/src/ethereum/cancun/blocks.py b/src/ethereum/cancun/blocks.py new file mode 100644 index 0000000000..e08214ee34 --- /dev/null +++ b/src/ethereum/cancun/blocks.py @@ -0,0 +1,105 @@ +""" +A `Block` is a single link in the chain that is Ethereum. Each `Block` contains +a `Header` and zero or more transactions. Each `Header` contains associated +metadata like the block number, parent block hash, and how much gas was +consumed by its transactions. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" +from dataclasses import dataclass +from typing import Tuple, Union + +from ..base_types import ( + U64, + U256, + Bytes, + Bytes8, + Bytes32, + Uint, + slotted_freezable, +) +from ..crypto.hash import Hash32 +from .fork_types import Address, Bloom, Root +from .transactions import LegacyTransaction + + +@slotted_freezable +@dataclass +class Withdrawal: + """ + Withdrawals that have been validated on the consensus layer. + """ + + index: U64 + validator_index: U64 + address: Address + amount: U256 + + +@slotted_freezable +@dataclass +class Header: + """ + Header portion of a block on the chain. + """ + + parent_hash: Hash32 + ommers_hash: Hash32 + coinbase: Address + state_root: Root + transactions_root: Root + receipt_root: Root + bloom: Bloom + difficulty: Uint + number: Uint + gas_limit: Uint + gas_used: Uint + timestamp: U256 + extra_data: Bytes + prev_randao: Bytes32 + nonce: Bytes8 + base_fee_per_gas: Uint + withdrawals_root: Root + blob_gas_used: U64 + excess_blob_gas: U64 + parent_beacon_block_root: Root + + +@slotted_freezable +@dataclass +class Block: + """ + A complete block. + """ + + header: Header + transactions: Tuple[Union[Bytes, LegacyTransaction], ...] + ommers: Tuple[Header, ...] + withdrawals: Tuple[Withdrawal, ...] + + +@slotted_freezable +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. + """ + + address: Address + topics: Tuple[Hash32, ...] + data: bytes + + +@slotted_freezable +@dataclass +class Receipt: + """ + Result of a transaction. + """ + + succeeded: bool + cumulative_gas_used: Uint + bloom: Bloom + logs: Tuple[Log, ...] diff --git a/src/ethereum/cancun/bloom.py b/src/ethereum/cancun/bloom.py new file mode 100644 index 0000000000..d10c21ec63 --- /dev/null +++ b/src/ethereum/cancun/bloom.py @@ -0,0 +1,84 @@ +""" +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.base_types 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]) & 0x07FF + # Below is the index of the bit in the bytearray (where 0-indexed + # byte is the most significant byte) + bit_index = 0x07FF - 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/cancun/fork.py b/src/ethereum/cancun/fork.py new file mode 100644 index 0000000000..9e87099457 --- /dev/null +++ b/src/ethereum/cancun/fork.py @@ -0,0 +1,1131 @@ +""" +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.base_types import Bytes0, Bytes32 +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidBlock +from ethereum.utils.ensure import ensure + +from .. import rlp +from ..base_types import U64, U256, Bytes, Uint +from . import vm +from .blocks import Block, Header, Log, Receipt, Withdrawal +from .bloom import logs_bloom +from .fork_types import Address, Bloom, Root, VersionedHash +from .state import ( + State, + TransientStorage, + account_exists_and_is_empty, + destroy_account, + destroy_touched_empty_accounts, + get_account, + increment_nonce, + process_withdrawal, + set_account_balance, + state_root, +) +from .transactions import ( + TX_ACCESS_LIST_ADDRESS_COST, + TX_ACCESS_LIST_STORAGE_KEY_COST, + TX_BASE_COST, + TX_CREATE_COST, + TX_DATA_COST_PER_NON_ZERO, + TX_DATA_COST_PER_ZERO, + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, + decode_transaction, + encode_transaction, +) +from .trie import Trie, root, trie_set +from .utils.hexadecimal import hex_to_address +from .utils.message import prepare_message +from .vm import Message +from .vm.gas import ( + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, + init_code_cost, +) +from .vm.interpreter import MAX_CODE_SIZE, process_message_call + +BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 +ELASTICITY_MULTIPLIER = 2 +GAS_LIMIT_ADJUSTMENT_FACTOR = 1024 +GAS_LIMIT_MINIMUM = 5000 +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK = 786432 +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + chain_id: U64 + + +def apply_fork(old: BlockChain) -> BlockChain: + """ + Transforms the state from the previous hard fork (`old`) into the block + chain object for this hard fork and returns it. + + When forks need to implement an irregular state transition, this function + is used to handle the irregularity. See the :ref:`DAO Fork ` for + an example. + + Parameters + ---------- + old : + Previous block chain object. + + Returns + ------- + new : `BlockChain` + Upgraded block chain object for this hard fork. + """ + return old + + +def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: + """ + Obtain the list of hashes of the previous 256 blocks in order of + increasing block number. + + This function will return less hashes for the first 256 blocks. + + The ``BLOCKHASH`` opcode needs to access the latest hashes on the chain, + therefore this function retrieves them. + + Parameters + ---------- + chain : + History and current state. + + Returns + ------- + recent_block_hashes : `List[Hash32]` + Hashes of the recent 256 blocks in order of increasing block number. + """ + recent_blocks = chain.blocks[-255:] + # TODO: This function has not been tested rigorously + if len(recent_blocks) == 0: + return [] + + recent_block_hashes = [] + + for block in recent_blocks: + prev_block_hash = block.header.parent_hash + recent_block_hashes.append(prev_block_hash) + + # We are computing the hash only for the most recent block and not for + # the rest of the blocks as they have successors which have the hash of + # the current block as parent hash. + most_recent_block_hash = keccak256(rlp.encode(recent_blocks[-1].header)) + recent_block_hashes.append(most_recent_block_hash) + + return recent_block_hashes + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + All parts of the block's contents need to be verified before being added + to the chain. Blocks are verified by ensuring that the contents of the + block make logical sense with the contents of the parent block. The + information in the block's header must also match the corresponding + information in the block. + + To implement Ethereum, in theory clients are only required to store the + most recent 255 blocks of the chain since as far as execution is + concerned, only those blocks are accessed. Practically, however, clients + should store more blocks to handle reorgs. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + """ + parent_header = chain.blocks[-1].header + excess_blob_gas = calculate_excess_blob_gas(parent_header) + ensure(block.header.excess_blob_gas == excess_blob_gas, InvalidBlock) + + validate_header(block.header, parent_header) + ensure(block.ommers == (), InvalidBlock) + ( + gas_used, + transactions_root, + receipt_root, + block_logs_bloom, + state, + withdrawals_root, + blob_gas_used, + ) = 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, + ) + ensure(gas_used == block.header.gas_used, InvalidBlock) + ensure(transactions_root == block.header.transactions_root, InvalidBlock) + ensure(state_root(state) == block.header.state_root, InvalidBlock) + ensure(receipt_root == block.header.receipt_root, InvalidBlock) + ensure(block_logs_bloom == block.header.bloom, InvalidBlock) + ensure(withdrawals_root == block.header.withdrawals_root, InvalidBlock) + ensure(blob_gas_used == block.header.blob_gas_used, 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 + + ensure( + check_gas_limit(block_gas_limit, parent_gas_limit), + 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, + 1, + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas + base_fee_per_gas_delta + ) + else: + gas_used_delta = parent_gas_target - parent_gas_used + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = ( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas - base_fee_per_gas_delta + ) + + return Uint(expected_base_fee_per_gas) + + +def validate_header(header: Header, parent_header: Header) -> None: + """ + Verifies a block header. + + In order to consider a block's header valid, the logic for the + quantities in the header should match the logic for the block itself. + For example the header timestamp should be greater than the block's parent + timestamp because the block was created *after* the parent block. + Additionally, the block's number should be directly following the parent + block's number since it is the next block in the sequence. + + Parameters + ---------- + header : + Header to check for correctness. + parent_header : + Parent Header of the header to check for correctness + """ + ensure(header.gas_used <= header.gas_limit, 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, + ) + + ensure(expected_base_fee_per_gas == header.base_fee_per_gas, InvalidBlock) + + ensure(header.timestamp > parent_header.timestamp, InvalidBlock) + ensure(header.number == parent_header.number + 1, InvalidBlock) + ensure(len(header.extra_data) <= 32, InvalidBlock) + + ensure(header.difficulty == 0, InvalidBlock) + ensure(header.nonce == b"\x00\x00\x00\x00\x00\x00\x00\x00", InvalidBlock) + ensure(header.ommers_hash == EMPTY_OMMER_HASH, InvalidBlock) + + block_parent_hash = keccak256(rlp.encode(parent_header)) + ensure(header.parent_hash == block_parent_hash, InvalidBlock) + + +def check_transaction( + tx: Transaction, + base_fee_per_gas: Uint, + gas_available: Uint, + chain_id: U64, +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...]]: + """ + Check if the transaction is includable in the block. + + Parameters + ---------- + 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 + ------- + sender_address : + The sender of the transaction. + effective_gas_price : + The price to charge for gas when the transaction is executed. + blob_versioned_hashes : + The blob versioned hashes of the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not includable. + """ + ensure(tx.gas <= gas_available, InvalidBlock) + sender_address = recover_sender(chain_id, tx) + + if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): + ensure(tx.max_fee_per_gas >= tx.max_priority_fee_per_gas, InvalidBlock) + ensure(tx.max_fee_per_gas >= base_fee_per_gas, InvalidBlock) + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + base_fee_per_gas + else: + ensure(tx.gas_price >= base_fee_per_gas, InvalidBlock) + effective_gas_price = tx.gas_price + + if isinstance(tx, BlobTransaction): + ensure(isinstance(tx.to, Address), InvalidBlock) + blob_versioned_hashes = tx.blob_versioned_hashes + else: + blob_versioned_hashes = () + + return sender_address, effective_gas_price, blob_versioned_hashes + + +def make_receipt( + tx: Transaction, + error: Optional[Exception], + cumulative_gas_used: Uint, + logs: Tuple[Log, ...], +) -> Union[Bytes, Receipt]: + """ + Make the receipt for a transaction that was executed. + + Parameters + ---------- + tx : + The executed transaction. + error : + Error in the top level frame of the transaction, if any. + cumulative_gas_used : + The total gas used so far in the block after the transaction was + executed. + logs : + The logs produced by the transaction. + + Returns + ------- + receipt : + The receipt for the transaction. + """ + receipt = Receipt( + succeeded=error is None, + cumulative_gas_used=cumulative_gas_used, + bloom=logs_bloom(logs), + logs=logs, + ) + + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + else: + return receipt + + +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, +) -> Tuple[Uint, Root, Root, Bloom, State, Root, Uint]: + """ + Executes a block. + + Many of the contents of a block are stored in data structures called + tries. There is a transactions trie which is similar to a ledger of the + transactions stored in the current block. There is also a receipts trie + which stores the results of executing a transaction, like the post state + and gas used. This function creates and executes the block that is to be + added to the chain. + + Parameters + ---------- + state : + Current account state. + block_hashes : + List of hashes of the previous 256 blocks in the order of + increasing block number. + coinbase : + Address of account which receives block reward and transaction fees. + block_number : + Position of the block within the chain. + base_fee_per_gas : + Base fee per gas of within the block. + block_gas_limit : + Initial amount of gas available for execution in this block. + block_time : + Time the block was produced, measured in seconds since the epoch. + prev_randao : + The previous randao from the beacon chain. + transactions : + Transactions included in the block. + ommers : + Headers of ancestor blocks which are not direct parents (formerly + uncles.) + chain_id : + ID of the executing chain. + withdrawals : + Withdrawals to be processed in the current block. + parent_beacon_block_root : + The root of the beacon block from the parent block. + excess_blob_gas : + Excess blob gas calculated from the previous block. + + Returns + ------- + 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 : `ethereum.fork_types.State` + State after all transactions have been executed. + """ + blob_gas_used = Uint(0) + gas_available = block_gas_limit + transactions_trie: Trie[ + Bytes, Optional[Union[Bytes, LegacyTransaction]] + ] = Trie(secured=False, default=None) + receipts_trie: Trie[Bytes, Optional[Union[Bytes, Receipt]]] = Trie( + secured=False, default=None + ) + withdrawals_trie: Trie[Bytes, Optional[Union[Bytes, Withdrawal]]] = Trie( + secured=False, default=None + ) + block_logs: Tuple[Log, ...] = () + + beacon_block_roots_contract_code = get_account( + state, BEACON_ROOTS_ADDRESS + ).code + + system_tx_message = Message( + caller=SYSTEM_ADDRESS, + target=BEACON_ROOTS_ADDRESS, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=parent_beacon_block_root, + code=beacon_block_roots_contract_code, + depth=Uint(0), + current_target=BEACON_ROOTS_ADDRESS, + code_address=BEACON_ROOTS_ADDRESS, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + parent_evm=None, + ) + + system_tx_env = vm.Environment( + caller=SYSTEM_ADDRESS, + origin=SYSTEM_ADDRESS, + block_hashes=block_hashes, + coinbase=coinbase, + number=block_number, + gas_limit=block_gas_limit, + base_fee_per_gas=base_fee_per_gas, + gas_price=base_fee_per_gas, + time=block_time, + prev_randao=prev_randao, + state=state, + chain_id=chain_id, + traces=[], + excess_blob_gas=excess_blob_gas, + blob_versioned_hashes=(), + transient_storage=TransientStorage(), + ) + + system_tx_output = process_message_call(system_tx_message, system_tx_env) + + destroy_touched_empty_accounts( + system_tx_env.state, system_tx_output.touched_accounts + ) + + for i, tx in enumerate(map(decode_transaction, transactions)): + trie_set( + transactions_trie, rlp.encode(Uint(i)), encode_transaction(tx) + ) + + ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + ) = check_transaction(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=[], + excess_blob_gas=excess_blob_gas, + blob_versioned_hashes=blob_versioned_hashes, + transient_storage=TransientStorage(), + ) + + gas_used, logs, error = process_transaction(env, tx) + gas_available -= gas_used + + receipt = make_receipt( + tx, error, (block_gas_limit - gas_available), logs + ) + + trie_set( + receipts_trie, + rlp.encode(Uint(i)), + receipt, + ) + + block_logs += logs + blob_gas_used += calculate_total_blob_gas(tx) + + ensure(blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK, InvalidBlock) + block_gas_used = block_gas_limit - gas_available + + block_logs_bloom = logs_bloom(block_logs) + + for i, wd in enumerate(withdrawals): + trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd)) + + process_withdrawal(state, wd) + + if account_exists_and_is_empty(state, wd.address): + destroy_account(state, wd.address) + + return ( + block_gas_used, + root(transactions_trie), + root(receipts_trie), + block_logs_bloom, + state, + root(withdrawals_trie), + blob_gas_used, + ) + + +def process_transaction( + env: vm.Environment, tx: Transaction +) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]: + """ + Execute a transaction against the provided environment. + + This function processes the actions needed to execute a transaction. + It decrements the sender's account after calculating the gas fee and + refunds them the proper amount after execution. Calling contracts, + deploying code, and incrementing nonces are all examples of actions that + happen within this function or from a call made within this function. + + Accounts that are marked for deletion are processed and destroyed after + execution. + + Parameters + ---------- + env : + Environment for the Ethereum Virtual Machine. + tx : + Transaction to execute. + + Returns + ------- + gas_left : `ethereum.base_types.U256` + Remaining gas after execution. + logs : `Tuple[ethereum.blocks.Log, ...]` + Logs generated during execution. + """ + ensure(validate_transaction(tx), InvalidBlock) + + sender = env.origin + sender_account = get_account(env.state, sender) + + if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): + max_gas_fee = tx.gas * tx.max_fee_per_gas + else: + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + ensure(len(tx.blob_versioned_hashes) > 0, InvalidBlock) + for blob_versioned_hash in tx.blob_versioned_hashes: + ensure( + blob_versioned_hash[0:1] == VERSIONED_HASH_VERSION_KZG, + InvalidBlock, + ) + + ensure( + tx.max_fee_per_blob_gas >= calculate_blob_gas_price(env), + InvalidBlock, + ) + + max_gas_fee += calculate_total_blob_gas(tx) * tx.max_fee_per_blob_gas + blob_gas_fee = calculate_data_fee(env, tx) + else: + blob_gas_fee = Uint(0) + + ensure(sender_account.nonce == tx.nonce, InvalidBlock) + ensure(sender_account.balance >= max_gas_fee + tx.value, InvalidBlock) + ensure(sender_account.code == bytearray(), InvalidBlock) + + effective_gas_fee = tx.gas * env.gas_price + + gas = tx.gas - calculate_intrinsic_cost(tx) + increment_nonce(env.state, sender) + + sender_balance_after_gas_fee = ( + sender_account.balance - effective_gas_fee - blob_gas_fee + ) + set_account_balance(env.state, sender, sender_balance_after_gas_fee) + + preaccessed_addresses = set() + preaccessed_storage_keys = set() + preaccessed_addresses.add(env.coinbase) + if isinstance( + tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + ): + for address, keys in tx.access_list: + preaccessed_addresses.add(address) + for key in keys: + preaccessed_storage_keys.add((address, key)) + + message = prepare_message( + sender, + tx.to, + tx.value, + tx.data, + gas, + env, + preaccessed_addresses=frozenset(preaccessed_addresses), + preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + ) + + output = process_message_call(message, env) + + gas_used = tx.gas - output.gas_left + gas_refund = min(gas_used // 5, output.refund_counter) + gas_refund_amount = (output.gas_left + gas_refund) * env.gas_price + + # For non-1559 transactions env.gas_price == tx.gas_price + priority_fee_per_gas = env.gas_price - env.base_fee_per_gas + transaction_fee = ( + tx.gas - output.gas_left - gas_refund + ) * priority_fee_per_gas + + total_gas_used = gas_used - gas_refund + + # refund gas + sender_balance_after_refund = ( + get_account(env.state, sender).balance + gas_refund_amount + ) + set_account_balance(env.state, sender, sender_balance_after_refund) + + # transfer miner fees + coinbase_balance_after_mining_fee = ( + get_account(env.state, env.coinbase).balance + transaction_fee + ) + if coinbase_balance_after_mining_fee != 0: + set_account_balance( + env.state, env.coinbase, coinbase_balance_after_mining_fee + ) + elif account_exists_and_is_empty(env.state, env.coinbase): + destroy_account(env.state, env.coinbase) + + for address in output.accounts_to_delete: + destroy_account(env.state, address) + + destroy_touched_empty_accounts(env.state, output.touched_accounts) + + return total_gas_used, output.logs, output.error + + +def validate_transaction(tx: Transaction) -> bool: + """ + Verifies a transaction. + + The gas in a transaction gets used to pay for the intrinsic cost of + operations, therefore if there is insufficient gas then it would not + be possible to execute a transaction and it will be declared invalid. + + Additionally, the nonce of a transaction must not equal or exceed the + limit defined in `EIP-2681 `_. + In practice, defining the limit as ``2**64-1`` has no impact because + sending ``2**64-1`` transactions is improbable. It's not strictly + impossible though, ``2**64-1`` transactions is the entire capacity of the + Ethereum blockchain at 2022 gas limits for a little over 22 years. + + Parameters + ---------- + tx : + Transaction to validate. + + Returns + ------- + verified : `bool` + True if the transaction can be executed, or False otherwise. + """ + if calculate_intrinsic_cost(tx) > tx.gas: + return False + if tx.nonce >= 2**64 - 1: + return False + if tx.to == Bytes0(b"") and len(tx.data) > 2 * MAX_CODE_SIZE: + return False + + return True + + +def calculate_intrinsic_cost(tx: Transaction) -> Uint: + """ + Calculates the gas that is charged before execution is started. + + The intrinsic cost of the transaction is charged before execution has + begun. Functions/operations in the EVM cost money to execute so this + intrinsic cost is for the operations that need to be paid for as part of + the transaction. Data transfer, for example, is part of this intrinsic + cost. It costs ether to send data over the wire and that ether is + accounted for in the intrinsic cost calculated in this function. This + intrinsic cost must be calculated and paid for before execution in order + for all operations to be implemented. + + Parameters + ---------- + tx : + Transaction to compute the intrinsic cost of. + + Returns + ------- + verified : `ethereum.base_types.Uint` + The intrinsic cost of the transaction. + """ + data_cost = 0 + + for byte in tx.data: + if byte == 0: + data_cost += TX_DATA_COST_PER_ZERO + else: + data_cost += TX_DATA_COST_PER_NON_ZERO + + if tx.to == Bytes0(b""): + create_cost = TX_CREATE_COST + int(init_code_cost(Uint(len(tx.data)))) + else: + create_cost = 0 + + access_list_cost = 0 + if isinstance( + tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + ): + for _address, keys in tx.access_list: + access_list_cost += TX_ACCESS_LIST_ADDRESS_COST + access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST + + return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost) + + +def recover_sender(chain_id: U64, tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + The v, r, and s values are the three parts that make up the signature + of a transaction. In order to recover the sender of a transaction the two + components needed are the signature (``v``, ``r``, and ``s``) and the + signing hash of the transaction. The sender's public key can be obtained + with these two values and therefore the sender address can be retrieved. + + Parameters + ---------- + tx : + Transaction of interest. + chain_id : + ID of the executing chain. + + Returns + ------- + sender : `ethereum.fork_types.Address` + The address of the account that signed the transaction. + """ + r, s = tx.r, tx.s + + ensure(0 < r and r < SECP256K1N, InvalidBlock) + ensure(0 < s and s <= SECP256K1N // 2, InvalidBlock) + + if isinstance(tx, LegacyTransaction): + v = tx.v + if v == 27 or v == 28: + public_key = secp256k1_recover( + r, s, v - 27, signing_hash_pre155(tx) + ) + else: + ensure( + v == 35 + chain_id * 2 or v == 36 + chain_id * 2, InvalidBlock + ) + public_key = secp256k1_recover( + r, s, v - 35 - chain_id * 2, signing_hash_155(tx, chain_id) + ) + elif isinstance(tx, AccessListTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_2930(tx) + ) + elif isinstance(tx, FeeMarketTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_1559(tx) + ) + elif isinstance(tx, BlobTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) + + return Address(keccak256(public_key)[12:32]) + + +def signing_hash_pre155(tx: LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre EIP 155) signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: LegacyTransaction, chain_id: U64) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 155 signature. + + Parameters + ---------- + tx : + Transaction of interest. + chain_id : + The id of the current chain. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + chain_id, + Uint(0), + Uint(0), + ) + ) + ) + + +def signing_hash_2930(tx: AccessListTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 2930 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x01" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 1559 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x02" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP-4844 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x03" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.max_fee_per_blob_gas, + tx.blob_versioned_hashes, + ) + ) + ) + + +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/cancun/fork_types.py b/src/ethereum/cancun/fork_types.py new file mode 100644 index 0000000000..8d97937789 --- /dev/null +++ b/src/ethereum/cancun/fork_types.py @@ -0,0 +1,68 @@ +""" +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 .. import rlp +from ..base_types import ( + U256, + Bytes, + Bytes20, + Bytes256, + Uint, + slotted_freezable, +) +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 with providing a storage root. + """ + return rlp.encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + storage_root, + keccak256(raw_account_data.code), + ) + ) diff --git a/src/ethereum/cancun/state.py b/src/ethereum/cancun/state.py new file mode 100644 index 0000000000..1d0fd8f476 --- /dev/null +++ b/src/ethereum/cancun/state.py @@ -0,0 +1,713 @@ +""" +State +^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state contains all information that is preserved between transactions. + +It consists of a main account trie and storage tries for each contract. + +There is a distinction between an account that does not exist and +`EMPTY_ACCOUNT`. +""" +from dataclasses import dataclass, field +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple + +from ethereum.base_types import U256, Bytes, Uint, modify +from ethereum.utils.ensure import ensure + +from .blocks import Withdrawal +from .fork_types import EMPTY_ACCOUNT, Account, Address, Root +from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set + + +@dataclass +class State: + """ + Contains all information that is preserved between transactions. + """ + + _main_trie: Trie[Address, Optional[Account]] = field( + default_factory=lambda: Trie(secured=True, default=None) + ) + _storage_tries: Dict[Address, Trie[Bytes, U256]] = field( + default_factory=dict + ) + _snapshots: List[ + Tuple[ + Trie[Address, Optional[Account]], Dict[Address, Trie[Bytes, U256]] + ] + ] = field(default_factory=list) + created_accounts: Set[Address] = field(default_factory=set) + + +@dataclass +class TransientStorage: + """ + Contains all information that is preserved between message calls + within a transaction. + """ + + _tries: Dict[Address, Trie[Bytes, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes, U256]]] = field( + default_factory=list + ) + + +def close_state(state: State) -> None: + """ + Free resources held by the state. Used by optimized implementations to + release file descriptors. + """ + del state._main_trie + del state._storage_tries + del state._snapshots + del state.created_accounts + + +def begin_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Start a state transaction. + + Transactions are entirely implicit and can be nested. It is not possible to + calculate the state root during a transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.append( + ( + copy_trie(state._main_trie), + {k: copy_trie(t) for (k, t) in state._storage_tries.items()}, + ) + ) + transient_storage._snapshots.append( + {k: copy_trie(t) for (k, t) in transient_storage._tries.items()} + ) + + +def commit_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Commit a state transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._snapshots.pop() + + +def rollback_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Rollback a state transaction, resetting the state to the point when the + corresponding `start_transaction()` call was made. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._main_trie, state._storage_tries = state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._tries = transient_storage._snapshots.pop() + + +def get_account(state: State, address: Address) -> Account: + """ + Get the `Account` object at an address. Returns `EMPTY_ACCOUNT` if there + is no account at the address. + + Use `get_account_optional()` if you care about the difference between a + non-existent account and `EMPTY_ACCOUNT`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = get_account_optional(state, address) + if isinstance(account, Account): + return account + else: + return EMPTY_ACCOUNT + + +def get_account_optional(state: State, address: Address) -> Optional[Account]: + """ + Get the `Account` object at an address. Returns `None` (rather than + `EMPTY_ACCOUNT`) if there is no account at the address. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = trie_get(state._main_trie, address) + return account + + +def set_account( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. Setting to `None` deletes + the account (but not its storage, see `destroy_account()`). + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + trie_set(state._main_trie, address, account) + + +def destroy_account(state: State, address: Address) -> None: + """ + Completely remove the account at `address` and all of its storage. + + This function is made available exclusively for the `SELFDESTRUCT` + opcode. It is expected that `SELFDESTRUCT` will be disabled in a future + hardfork and this function will be removed. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account to destroy. + """ + destroy_storage(state, address) + set_account(state, address, None) + + +def destroy_storage(state: State, address: Address) -> None: + """ + Completely remove the storage at `address`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account whose storage is to be deleted. + """ + if address in state._storage_tries: + del state._storage_tries[address] + + +def mark_account_created(state: State, address: Address) -> None: + """ + Mark an account as having been created in the current transaction. + This information is used by `get_storage_original()` to handle an obscure + edgecase. + + The marker is not removed even if the account creation reverts. Since the + account cannot have had code prior to its creation and can't call + `get_storage_original()`, this is harmless. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account that has been created. + """ + state.created_accounts.add(address) + + +def get_storage(state: State, address: Address, key: Bytes) -> U256: + """ + Get a value at a storage key on an account. Returns `U256(0)` if the + storage key has not been set previously. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + + Returns + ------- + value : `U256` + Value at the key. + """ + trie = state._storage_tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_storage( + state: State, address: Address, key: Bytes, value: U256 +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + assert trie_get(state._main_trie, address) is not None + + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + +def storage_root(state: State, address: Address) -> Root: + """ + Calculate the storage root of an account. + + Parameters + ---------- + state: + The state + address : + Address of the account. + + Returns + ------- + root : `Root` + Storage root of the account. + """ + assert not state._snapshots + if address in state._storage_tries: + return root(state._storage_tries[address]) + else: + return EMPTY_TRIE_ROOT + + +def state_root(state: State) -> Root: + """ + Calculate the state root. + + Parameters + ---------- + state: + The current state. + + Returns + ------- + root : `Root` + The state root. + """ + assert not state._snapshots + + def get_storage_root(address: Address) -> Root: + return storage_root(state, address) + + return root(state._main_trie, get_storage_root=get_storage_root) + + +def account_exists(state: State, address: Address) -> bool: + """ + Checks if an account exists in the state trie + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + account_exists : `bool` + True if account exists in the state trie, False otherwise + """ + return get_account_optional(state, address) is not None + + +def account_has_code_or_nonce(state: State, address: Address) -> bool: + """ + Checks if an account has non zero nonce or non empty code + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_code_or_nonce : `bool` + True if if an 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 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: + ensure(sender.balance >= amount, AssertionError) + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(state, sender_address, reduce_sender_balance) + modify_state(state, recipient_address, increase_recipient_balance) + + +def process_withdrawal( + state: State, + wd: Withdrawal, +) -> None: + """ + Increase the balance of the withdrawing account. + """ + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * 10**9 + + modify_state(state, wd.address, increase_recipient_balance) + + +def set_account_balance(state: State, address: Address, amount: U256) -> None: + """ + Sets the balance of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + + amount: + The amount that needs to set in balance. + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(state, address, set_balance) + + +def touch_account(state: State, address: Address) -> None: + """ + Initializes an account to state. + + Parameters + ---------- + state: + The current state. + + address: + The address of the account that need to initialised. + """ + if not account_exists(state, address): + set_account(state, address, EMPTY_ACCOUNT) + + +def increment_nonce(state: State, address: Address) -> None: + """ + Increments the nonce of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += 1 + + modify_state(state, address, increase_nonce) + + +def set_code(state: State, address: Address, code: Bytes) -> None: + """ + Sets Account code. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose code needs to be update. + + code: + The bytecode that needs to be set. + """ + + def write_code(sender: Account) -> None: + sender.code = code + + modify_state(state, address, write_code) + + +def get_storage_original(state: State, address: Address, key: Bytes) -> U256: + """ + Get the original value in a storage slot i.e. the value before the current + transaction began. This function reads the value from the snapshots taken + before executing the transaction. + + Parameters + ---------- + state: + The current state. + address: + Address of the account to read the value from. + key: + Key of the storage slot. + """ + # In the transaction where an account is created, its preexisting storage + # is ignored. + if address in state.created_accounts: + return U256(0) + + _, original_trie = state._snapshots[0] + original_account_trie = original_trie.get(address) + + if original_account_trie is None: + original_value = U256(0) + else: + original_value = trie_get(original_account_trie, key) + + assert isinstance(original_value, U256) + + return original_value + + +def get_transient_storage( + transient_storage: TransientStorage, address: Address, key: Bytes +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Returns `U256(0)` if the storage key has not been set previously. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + Returns + ------- + value : `U256` + Value at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_transient_storage( + transient_storage: TransientStorage, + address: Address, + key: Bytes, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + transient_storage._tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del transient_storage._tries[address] + + +def destroy_touched_empty_accounts( + state: State, touched_accounts: Iterable[Address] +) -> None: + """ + Destroy all touched accounts that are empty. + Parameters + ---------- + state: `State` + The current state. + touched_accounts: `Iterable[Address]` + All the accounts that have been touched in the current transaction. + """ + for address in touched_accounts: + if account_exists_and_is_empty(state, address): + destroy_account(state, address) diff --git a/src/ethereum/cancun/transactions.py b/src/ethereum/cancun/transactions.py new file mode 100644 index 0000000000..d81d11ed13 --- /dev/null +++ b/src/ethereum/cancun/transactions.py @@ -0,0 +1,150 @@ +""" +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 .. import rlp +from ..base_types import ( + U64, + U256, + Bytes, + Bytes0, + Bytes32, + Uint, + slotted_freezable, +) +from ..exceptions import InvalidBlock +from .fork_types import Address, VersionedHash + +TX_BASE_COST = 21000 +TX_DATA_COST_PER_NON_ZERO = 16 +TX_DATA_COST_PER_ZERO = 4 +TX_CREATE_COST = 32000 +TX_ACCESS_LIST_ADDRESS_COST = 2400 +TX_ACCESS_LIST_STORAGE_KEY_COST = 1900 + + +@slotted_freezable +@dataclass +class LegacyTransaction: + """ + Atomic operation performed on the block chain. + """ + + nonce: U256 + gas_price: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + v: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class AccessListTransaction: + """ + The transaction type added in EIP-2930 to support access lists. + """ + + chain_id: U64 + nonce: U256 + gas_price: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + y_parity: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class FeeMarketTransaction: + """ + The transaction type added in EIP-1559. + """ + + chain_id: U64 + nonce: U256 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + y_parity: U256 + r: U256 + s: U256 + + +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in EIP-4844. + """ + + chain_id: U64 + nonce: U256 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Address + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + max_fee_per_blob_gas: U256 + blob_versioned_hashes: Tuple[VersionedHash, ...] + y_parity: U256 + r: U256 + s: U256 + + +Transaction = Union[ + LegacyTransaction, + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, +] + + +def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]: + """ + Encode a transaction. Needed because non-legacy transactions aren't RLP. + """ + if isinstance(tx, LegacyTransaction): + return tx + elif isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(tx) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(tx) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(tx) + else: + raise Exception(f"Unable to encode transaction of type {type(tx)}") + + +def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: + """ + Decode a transaction. Needed because non-legacy transactions aren't RLP. + """ + if isinstance(tx, Bytes): + if tx[0] == 1: + return rlp.decode_to(AccessListTransaction, tx[1:]) + elif tx[0] == 2: + return rlp.decode_to(FeeMarketTransaction, tx[1:]) + elif tx[0] == 3: + return rlp.decode_to(BlobTransaction, tx[1:]) + else: + raise InvalidBlock + else: + return tx diff --git a/src/ethereum/cancun/trie.py b/src/ethereum/cancun/trie.py new file mode 100644 index 0000000000..f7a7c29e17 --- /dev/null +++ b/src/ethereum/cancun/trie.py @@ -0,0 +1,467 @@ +""" +State Trie +^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`.fork_types.Account` objects. +""" + +import copy +from dataclasses import dataclass, field +from typing import ( + Callable, + Dict, + Generic, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + TypeVar, + Union, + cast, +) + +from ethereum.crypto.hash import keccak256 +from ethereum.shanghai import trie as previous_trie +from ethereum.utils.ensure import ensure +from ethereum.utils.hexadecimal import hex_to_bytes + +from .. import rlp +from ..base_types import U256, Bytes, Uint, slotted_freezable +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.RLP + + +@slotted_freezable +@dataclass +class ExtensionNode: + """Extension node in the Merkle Trie""" + + key_segment: Bytes + subnode: rlp.RLP + + +@slotted_freezable +@dataclass +class BranchNode: + """Branch node in the Merkle Trie""" + + subnodes: List[rlp.RLP] + value: rlp.RLP + + +InternalNode = Union[LeafNode, ExtensionNode, BranchNode] + + +def encode_internal_node(node: Optional[InternalNode]) -> rlp.RLP: + """ + 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.RLP` + The node encoded as RLP. + """ + unencoded: rlp.RLP + if node is None: + unencoded = b"" + elif isinstance(node, LeafNode): + unencoded = ( + nibble_list_to_compact(node.rest_of_key, True), + node.value, + ) + elif isinstance(node, ExtensionNode): + unencoded = ( + nibble_list_to_compact(node.key_segment, False), + node.subnode, + ) + elif isinstance(node, BranchNode): + unencoded = node.subnodes + [node.value] + else: + raise AssertionError(f"Invalid internal node type {type(node)}!") + + encoded = rlp.encode(unencoded) + if len(encoded) < 32: + return unencoded + else: + return keccak256(encoded) + + +def encode_node(node: Node, storage_root: Optional[Bytes] = None) -> Bytes: + """ + Encode a Node for storage in the Merkle Trie. + + Currently mostly an unimplemented stub. + """ + if isinstance(node, Account): + assert storage_root is not None + return encode_account(node, storage_root) + elif isinstance(node, (LegacyTransaction, Receipt, Withdrawal, U256)): + return rlp.encode(cast(rlp.RLP, 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) + # Empty values are represented by their absence + ensure(encoded_value != b"", 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[level : level + prefix_length] + return ExtensionNode( + prefix, + encode_internal_node(patricialize(obj, level + prefix_length)), + ) + + branches: List[MutableMapping[Bytes, Bytes]] = [] + for _ in range(16): + branches.append({}) + value = b"" + for key in obj: + if len(key) == level: + # shouldn't ever have an account or receipt in an internal node + if isinstance(obj[key], (Account, Receipt, Uint)): + raise AssertionError + value = obj[key] + else: + branches[key[level]][key] = obj[key] + + return BranchNode( + [ + encode_internal_node(patricialize(branches[k], level + 1)) + for k in range(16) + ], + value, + ) diff --git a/src/ethereum/cancun/utils/__init__.py b/src/ethereum/cancun/utils/__init__.py new file mode 100644 index 0000000000..224a4d269b --- /dev/null +++ b/src/ethereum/cancun/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/cancun/utils/address.py b/src/ethereum/cancun/utils/address.py new file mode 100644 index 0000000000..409108333c --- /dev/null +++ b/src/ethereum/cancun/utils/address.py @@ -0,0 +1,91 @@ +""" +Hardfork Utility Functions For Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this cancun version of +specification. +""" +from typing import Union + +from ethereum.base_types import U256, Bytes32, Uint +from ethereum.crypto.hash import keccak256 +from ethereum.utils.byte import left_pad_zero_bytes + +from ... import rlp +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.cancun.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/cancun/utils/hexadecimal.py b/src/ethereum/cancun/utils/hexadecimal.py new file mode 100644 index 0000000000..cbf892c73e --- /dev/null +++ b/src/ethereum/cancun/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 +Cancun 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/cancun/utils/message.py b/src/ethereum/cancun/utils/message.py new file mode 100644 index 0000000000..7253eb3db3 --- /dev/null +++ b/src/ethereum/cancun/utils/message.py @@ -0,0 +1,115 @@ +""" +Hardfork Utility Functions For The Message Data-structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this cancun version of +specification. +""" +from typing import FrozenSet, Optional, Tuple, Union + +from ethereum.base_types import U256, Bytes, Bytes0, Bytes32, Uint + +from ..fork_types import Address +from ..state import get_account +from ..vm import Environment, Message +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from .address import compute_contract_address + + +def prepare_message( + caller: Address, + target: Union[Bytes0, Address], + value: U256, + data: Bytes, + gas: Uint, + env: Environment, + code_address: Optional[Address] = None, + should_transfer_value: bool = True, + is_static: bool = False, + preaccessed_addresses: FrozenSet[Address] = frozenset(), + preaccessed_storage_keys: FrozenSet[ + Tuple[(Address, Bytes32)] + ] = frozenset(), +) -> Message: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + caller : + Address which initiated the transaction + target : + Address whose code will be executed + value : + Value to be transferred. + data : + Array of bytes provided to the code in `target`. + gas : + Gas provided for the code in `target`. + env : + Environment for the Ethereum Virtual Machine. + code_address : + This is usually same as the `target` address except when an alternative + accounts code needs to be executed. + eg. `CALLCODE` calling a precompile. + should_transfer_value : + if True ETH should be transferred while executing a message call. + is_static: + if True then it prevents all state-changing operations from being + executed. + preaccessed_addresses: + Addresses that should be marked as accessed prior to the message call + preaccessed_storage_keys: + Storage keys that should be marked as accessed prior to the message + call + + Returns + ------- + message: `ethereum.cancun.vm.Message` + Items containing contract creation or message call specific data. + """ + if isinstance(target, Bytes0): + current_target = compute_contract_address( + caller, + get_account(env.state, caller).nonce - U256(1), + ) + msg_data = Bytes(b"") + code = data + elif isinstance(target, Address): + current_target = target + msg_data = data + code = get_account(env.state, target).code + if code_address is None: + code_address = target + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses = set() + accessed_addresses.add(current_target) + accessed_addresses.add(caller) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(preaccessed_addresses) + + return Message( + caller=caller, + target=target, + gas=gas, + value=value, + data=msg_data, + code=code, + depth=Uint(0), + current_target=current_target, + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=is_static, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, + ) diff --git a/src/ethereum/cancun/vm/__init__.py b/src/ethereum/cancun/vm/__init__.py new file mode 100644 index 0000000000..8b36a1f12b --- /dev/null +++ b/src/ethereum/cancun/vm/__init__.py @@ -0,0 +1,149 @@ +""" +Ethereum Virtual Machine (EVM) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`.fork_types.Account`. +""" + +from dataclasses import dataclass +from typing import List, Optional, Set, Tuple, Union + +from ethereum.base_types import U64, U256, Bytes, Bytes0, Bytes32, Uint +from ethereum.crypto.hash import Hash32 + +from ..blocks import Log +from ..fork_types import Address, VersionedHash +from ..state import State, TransientStorage, account_exists_and_is_empty +from .precompiled_contracts import RIPEMD160_ADDRESS + +__all__ = ("Environment", "Evm", "Message") + + +@dataclass +class Environment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + caller: Address + block_hashes: List[Hash32] + origin: Address + coinbase: Address + number: Uint + base_fee_per_gas: Uint + gas_limit: Uint + gas_price: Uint + time: U256 + prev_randao: Bytes32 + state: State + chain_id: U64 + traces: List[dict] + excess_blob_gas: U64 + blob_versioned_hashes: Tuple[VersionedHash, ...] + transient_storage: TransientStorage + + +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + caller: Address + target: Union[Bytes0, Address] + current_target: Address + gas: Uint + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + is_static: bool + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: Uint + env: Environment + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: int + running: bool + message: Message + output: Bytes + accounts_to_delete: Set[Address] + touched_accounts: Set[Address] + return_data: Bytes + error: Optional[Exception] + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + evm.gas_left += child_evm.gas_left + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + evm.accounts_to_delete.update(child_evm.accounts_to_delete) + evm.touched_accounts.update(child_evm.touched_accounts) + if account_exists_and_is_empty( + evm.env.state, child_evm.message.current_target + ): + evm.touched_accounts.add(child_evm.message.current_target) + evm.accessed_addresses.update(child_evm.accessed_addresses) + evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + # In block 2675119, the empty account at 0x3 (the RIPEMD160 precompile) was + # cleared despite running out of gas. This is an obscure edge case that can + # only happen to a precompile. + # According to the general rules governing clearing of empty accounts, the + # touch should have been reverted. Due to client bugs, this event went + # unnoticed and 0x3 has been exempted from the rule that touches are + # reverted in order to preserve this historical behaviour. + if RIPEMD160_ADDRESS in child_evm.touched_accounts: + evm.touched_accounts.add(RIPEMD160_ADDRESS) + if child_evm.message.current_target == RIPEMD160_ADDRESS: + if account_exists_and_is_empty( + evm.env.state, child_evm.message.current_target + ): + evm.touched_accounts.add(RIPEMD160_ADDRESS) + evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/cancun/vm/exceptions.py b/src/ethereum/cancun/vm/exceptions.py new file mode 100644 index 0000000000..2a4f2d2f65 --- /dev/null +++ b/src/ethereum/cancun/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/cancun/vm/gas.py b/src/ethereum/cancun/vm/gas.py new file mode 100644 index 0000000000..d19d7a2f16 --- /dev/null +++ b/src/ethereum/cancun/vm/gas.py @@ -0,0 +1,352 @@ +""" +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.base_types import U64, U256, Uint +from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..transactions import BlobTransaction, Transaction +from . import Environment, 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 = 2 +GAS_BLOBHASH_OPCODE = Uint(3) +GAS_POINT_EVALUATION = Uint(50000) + +TARGET_BLOB_GAS_PER_BLOCK = U64(393216) +GAS_PER_BLOB = Uint(2**17) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) + + +@dataclass +class ExtendMemory: + """ + Define the parameters for memory extension in opcodes + + `cost`: `ethereum.base_types.Uint` + The gas required to perform the extension + `expand_by`: `ethereum.base_types.Uint` + The size by which the memory will be extended + """ + + cost: Uint + expand_by: Uint + + +@dataclass +class MessageCallGas: + """ + Define the gas cost and stipend for executing the call opcodes. + + `cost`: `ethereum.base_types.Uint` + The non-refundable portion of gas reserved for executing the + call opcode. + `stipend`: `ethereum.base_types.Uint` + The portion of gas available to sub-calls that is refundable + if not consumed + """ + + cost: Uint + stipend: Uint + + +def charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left`. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(amount)) + + if evm.gas_left < amount: + raise OutOfGasError + else: + evm.gas_left -= U256(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) // 32 + linear_cost = size_in_words * GAS_MEMORY + quadratic_cost = size_in_words**2 // 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 // 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) // 32 + + +def calculate_excess_blob_gas(parent_header: Header) -> U64: + """ + Calculated the excess blob gas for the current block based + on the gas used in the parent block. + + Parameters + ---------- + parent_header : + The parent block of the current block. + + Returns + ------- + excess_blob_gas: `ethereum.base_types.U64` + The excess blob gas for the current block. + """ + parent_blob_gas = ( + parent_header.excess_blob_gas + parent_header.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 * len(tx.blob_versioned_hashes) + else: + return Uint(0) + + +def calculate_blob_gas_price(env: Environment) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + env : + The execution environment. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + """ + return taylor_exponential( + MIN_BLOB_GASPRICE, + Uint(env.excess_blob_gas), + BLOB_GASPRICE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(env: Environment, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + env : + The execution environment. + 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(env) diff --git a/src/ethereum/cancun/vm/instructions/__init__.py b/src/ethereum/cancun/vm/instructions/__init__.py new file mode 100644 index 0000000000..e85592574d --- /dev/null +++ b/src/ethereum/cancun/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 + RETURN = 0xF3 + CALL = 0xF1 + CALLCODE = 0xF2 + DELEGATECALL = 0xF4 + STATICCALL = 0xFA + REVERT = 0xFD + SELFDESTRUCT = 0xFF + CREATE2 = 0xF5 + + +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/cancun/vm/instructions/arithmetic.py b/src/ethereum/cancun/vm/instructions/arithmetic.py new file mode 100644 index 0000000000..e4120063a2 --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/arithmetic.py @@ -0,0 +1,369 @@ +""" +Ethereum Virtual Machine (EVM) Arithmetic Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from ethereum.base_types import U255_CEIL_VALUE, U256, U256_CEIL_VALUE, 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 += 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 += 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 += 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 += 1 + + +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 += 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 += 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 += 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 += 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 += 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 + 7) // 8 + charge_gas( + evm, GAS_EXPONENTIATION + GAS_EXPONENTIATION_PER_BYTE * exponent_bytes + ) + + # OPERATION + result = U256(pow(base, exponent, U256_CEIL_VALUE)) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += 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 > 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 = 32 - (byte_num + 1) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += 1 diff --git a/src/ethereum/cancun/vm/instructions/bitwise.py b/src/ethereum/cancun/vm/instructions/bitwise.py new file mode 100644 index 0000000000..68d5b0749c --- /dev/null +++ b/src/ethereum/cancun/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.base_types import U256, U256_CEIL_VALUE + +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 += 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 += 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 += 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 += 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 >= 32: + result = U256(0) + else: + extra_bytes_to_right = 31 - byte_index + # Remove the extra bytes in the right + word = word >> (extra_bytes_to_right * 8) + # Remove the extra bytes in the left + word = word & 0xFF + result = U256(word) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += 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 = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < 256: + result = U256((value << shift) % U256_CEIL_VALUE) + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += 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 < 256: + result = value >> shift + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += 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 = 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 += 1 diff --git a/src/ethereum/cancun/vm/instructions/block.py b/src/ethereum/cancun/vm/instructions/block.py new file mode 100644 index 0000000000..ab05c786a1 --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/block.py @@ -0,0 +1,248 @@ +""" +Ethereum Virtual Machine (EVM) Block Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM block instructions. +""" + +from ethereum.base_types import U256 + +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.cancun.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + """ + # STACK + block_number = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BLOCK_HASH) + + # OPERATION + if evm.env.number <= block_number or evm.env.number > block_number + 256: + # Default hash to 0, if the block of interest is not yet on the chain + # (including the block which has the current executing transaction), + # or if the block's age is more than 256. + hash = b"\x00" + else: + hash = evm.env.block_hashes[-(evm.env.number - block_number)] + + push(evm.stack, U256.from_be_bytes(hash)) + + # PROGRAM COUNTER + evm.pc += 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.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + + # PROGRAM COUNTER + evm.pc += 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.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, evm.env.time) + + # PROGRAM COUNTER + evm.pc += 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.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.number)) + + # PROGRAM COUNTER + evm.pc += 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.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.prev_randao)) + + # PROGRAM COUNTER + evm.pc += 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.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.gas_limit)) + + # PROGRAM COUNTER + evm.pc += 1 + + +def chain_id(evm: Evm) -> None: + """ + Push the chain id onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.cancun.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.cancun.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.chain_id)) + + # PROGRAM COUNTER + evm.pc += 1 diff --git a/src/ethereum/cancun/vm/instructions/comparison.py b/src/ethereum/cancun/vm/instructions/comparison.py new file mode 100644 index 0000000000..3d7458f5e3 --- /dev/null +++ b/src/ethereum/cancun/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.base_types import U256 + +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 += 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 += 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 += 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 += 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 += 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 += 1 diff --git a/src/ethereum/cancun/vm/instructions/control_flow.py b/src/ethereum/cancun/vm/instructions/control_flow.py new file mode 100644 index 0000000000..a967ade61d --- /dev/null +++ b/src/ethereum/cancun/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.base_types 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 += 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 + 1 + elif jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + else: + destination = jump_dest + + # PROGRAM COUNTER + evm.pc = Uint(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 += 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 += 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 += 1 diff --git a/src/ethereum/cancun/vm/instructions/environment.py b/src/ethereum/cancun/vm/instructions/environment.py new file mode 100644 index 0000000000..fd5d2b1b4e --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/environment.py @@ -0,0 +1,590 @@ +""" +Ethereum Virtual Machine (EVM) Environmental Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM environment related instructions. +""" + +from ethereum.base_types import U256, Bytes32, Uint +from ethereum.crypto.hash import keccak256 +from ethereum.utils.ensure import ensure +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 += 1 + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.env.state, address).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += 1 + + +def origin(evm: Evm) -> None: + """ + Pushes the address of the original transaction sender to the stack. + The origin address can only be an EOA. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.env.origin)) + + # PROGRAM COUNTER + evm.pc += 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 += 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 += 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 += 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 += 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)) // 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 += 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 += 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)) // 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 += 1 + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.gas_price)) + + # PROGRAM COUNTER + evm.pc += 1 + + +def extcodesize(evm: Evm) -> None: + """ + Push the code size of a given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. + codesize = U256(len(get_account(evm.env.state, address).code)) + + push(evm.stack, codesize) + + # PROGRAM COUNTER + evm.pc += 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)) // 32 + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS + copy_gas_cost + extend_memory.cost) + else: + evm.accessed_addresses.add(address) + charge_gas( + evm, GAS_COLD_ACCOUNT_ACCESS + copy_gas_cost + extend_memory.cost + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + code = get_account(evm.env.state, address).code + value = buffer_read(code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += 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 += 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)) // 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) + + # OPERATION + ensure( + Uint(return_data_start_position) + Uint(size) <= len(evm.return_data), + 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 += 1 + + +def extcodehash(evm: Evm) -> None: + """ + Returns the keccak256 hash of a contract’s bytecode + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + address = to_address(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + account = get_account(evm.env.state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + codehash = U256.from_be_bytes(keccak256(account.code)) + + push(evm.stack, codehash) + + # PROGRAM COUNTER + evm.pc += 1 + + +def self_balance(evm: Evm) -> None: + """ + Pushes the balance of the current address to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.env.state, evm.message.current_target).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += 1 + + +def base_fee(evm: Evm) -> None: + """ + Pushes the base fee of the current block on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.env.base_fee_per_gas)) + + # PROGRAM COUNTER + evm.pc += 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 index < len(evm.env.blob_versioned_hashes): + blob_hash = evm.env.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += 1 + + +def blob_base_fee(evm: Evm) -> None: + """ + Pushes the blob base fee on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + blob_base_fee = calculate_blob_gas_price(evm.env) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += 1 diff --git a/src/ethereum/cancun/vm/instructions/keccak.py b/src/ethereum/cancun/vm/instructions/keccak.py new file mode 100644 index 0000000000..0751dea845 --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/keccak.py @@ -0,0 +1,63 @@ +""" +Ethereum Virtual Machine (EVM) Keccak Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM keccak instructions. +""" + +from ethereum.base_types 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)) // 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 += 1 diff --git a/src/ethereum/cancun/vm/instructions/log.py b/src/ethereum/cancun/vm/instructions/log.py new file mode 100644 index 0000000000..43c0fbfbd7 --- /dev/null +++ b/src/ethereum/cancun/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.base_types import U256 +from ethereum.utils.ensure import ensure + +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: U256) -> 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 * size + + GAS_LOG_TOPIC * num_topics + + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + ensure(not evm.message.is_static, 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 += 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/cancun/vm/instructions/memory.py b/src/ethereum/cancun/vm/instructions/memory.py new file mode 100644 index 0000000000..e97046149b --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/memory.py @@ -0,0 +1,175 @@ +""" +Ethereum Virtual Machine (EVM) Memory Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Memory instructions. +""" +from ethereum.base_types import U256, Bytes, 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 += 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 & 0xFF]) + memory_write(evm.memory, start_position, normalized_bytes_value) + + # PROGRAM COUNTER + evm.pc += 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 += 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 += 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)) // 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 += 1 diff --git a/src/ethereum/cancun/vm/instructions/stack.py b/src/ethereum/cancun/vm/instructions/stack.py new file mode 100644 index 0000000000..ca1084d1f4 --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/stack.py @@ -0,0 +1,212 @@ +""" +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.base_types import U256 +from ethereum.utils.ensure import ensure + +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 += 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 + 1), U256(num_bytes)) + ) + stack.push(evm.stack, data_to_push) + + # PROGRAM COUNTER + evm.pc += 1 + 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) + + # OPERATION + ensure(item_number < len(evm.stack), StackUnderflowError) + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += 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) + + # OPERATION + ensure(item_number < len(evm.stack), StackUnderflowError) + evm.stack[-1], evm.stack[-1 - item_number] = ( + evm.stack[-1 - item_number], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += 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/cancun/vm/instructions/storage.py b/src/ethereum/cancun/vm/instructions/storage.py new file mode 100644 index 0000000000..4c04acc9c7 --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/storage.py @@ -0,0 +1,182 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" +from ethereum.base_types import Uint +from ethereum.utils.ensure import ensure + +from ...state import ( + get_storage, + get_storage_original, + get_transient_storage, + set_storage, + set_transient_storage, +) +from .. import Evm +from ..exceptions import OutOfGasError, WriteInStaticContext +from ..gas import ( + GAS_CALL_STIPEND, + GAS_COLD_SLOAD, + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + GAS_WARM_ACCESS, + charge_gas, +) +from ..stack import pop, push + + +def sload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + if (evm.message.current_target, key) in evm.accessed_storage_keys: + charge_gas(evm, GAS_WARM_ACCESS) + else: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + charge_gas(evm, GAS_COLD_SLOAD) + + # OPERATION + value = get_storage(evm.env.state, evm.message.current_target, key) + + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += 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) + + # GAS + ensure(evm.gas_left > GAS_CALL_STIPEND, OutOfGasError) + + original_value = get_storage_original( + evm.env.state, evm.message.current_target, key + ) + current_value = get_storage(evm.env.state, evm.message.current_target, key) + + gas_cost = Uint(0) + + if (evm.message.current_target, key) not in evm.accessed_storage_keys: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + gas_cost += GAS_COLD_SLOAD + + if original_value == current_value and current_value != new_value: + if original_value == 0: + gas_cost += GAS_STORAGE_SET + else: + gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD + else: + gas_cost += GAS_WARM_ACCESS + + # Refund Counter Calculation + if current_value != new_value: + if original_value != 0 and current_value != 0 and new_value == 0: + # Storage is cleared for the first time in the transaction + evm.refund_counter += int(GAS_STORAGE_CLEAR_REFUND) + + if original_value != 0 and current_value == 0: + # Gas refund issued earlier to be reversed + evm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND) + + if original_value == new_value: + # Storage slot being restored to its original value + if original_value == 0: + # Slot was originally empty and was SET earlier + evm.refund_counter += int(GAS_STORAGE_SET - GAS_WARM_ACCESS) + else: + # Slot was originally non-empty and was UPDATED earlier + evm.refund_counter += int( + GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS + ) + + charge_gas(evm, gas_cost) + + # OPERATION + ensure(not evm.message.is_static, WriteInStaticContext) + set_storage(evm.env.state, evm.message.current_target, key, new_value) + + # PROGRAM COUNTER + evm.pc += 1 + + +def tload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + transient storage of the current account. + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + + # OPERATION + value = get_transient_storage( + evm.env.transient_storage, evm.message.current_target, key + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += 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) + + # OPERATION + ensure(not evm.message.is_static, WriteInStaticContext) + set_transient_storage( + evm.env.transient_storage, evm.message.current_target, key, new_value + ) + + # PROGRAM COUNTER + evm.pc += 1 diff --git a/src/ethereum/cancun/vm/instructions/system.py b/src/ethereum/cancun/vm/instructions/system.py new file mode 100644 index 0000000000..07f3ab043f --- /dev/null +++ b/src/ethereum/cancun/vm/instructions/system.py @@ -0,0 +1,689 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" +from ethereum.base_types import U256, Bytes0, Uint +from ethereum.utils.ensure import ensure +from ethereum.utils.numeric import ceil32 + +from ...fork_types import Address +from ...state import ( + account_exists_and_is_empty, + account_has_code_or_nonce, + get_account, + increment_nonce, + is_account_alive, + move_ether, + set_account_balance, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address, +) +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import OutOfGasError, Revert, WriteInStaticContext +from ..gas import ( + GAS_CALL_VALUE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_CREATE, + GAS_KECCAK256_WORD, + GAS_NEW_ACCOUNT, + GAS_SELF_DESTRUCT, + GAS_SELF_DESTRUCT_NEW_ACCOUNT, + GAS_WARM_ACCESS, + GAS_ZERO, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_gas, + init_code_cost, + max_message_call_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def generic_create( + evm: Evm, + endowment: U256, + contract_address: Address, + memory_start_position: U256, + memory_size: U256, + init_code_gas: Uint, +) -> None: + """ + Core logic used by the `CREATE*` family of opcodes. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import ( + MAX_CODE_SIZE, + STACK_DEPTH_LIMIT, + process_create_message, + ) + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + ensure(len(call_data) <= 2 * MAX_CODE_SIZE, OutOfGasError) + + evm.accessed_addresses.add(contract_address) + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + + ensure(not evm.message.is_static, WriteInStaticContext) + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(evm.env.state, sender_address) + + if ( + sender.balance < endowment + or sender.nonce == Uint(2**64 - 1) + or evm.message.depth + 1 > STACK_DEPTH_LIMIT + ): + evm.gas_left += create_message_gas + push(evm.stack, U256(0)) + return + + if account_has_code_or_nonce(evm.env.state, contract_address): + increment_nonce(evm.env.state, evm.message.current_target) + push(evm.stack, U256(0)) + return + + increment_nonce(evm.env.state, evm.message.current_target) + + child_message = Message( + caller=evm.message.current_target, + target=Bytes0(), + gas=create_message_gas, + value=endowment, + data=b"", + code=call_data, + current_target=contract_address, + depth=evm.message.depth + 1, + code_address=None, + should_transfer_value=True, + is_static=False, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, + ) + child_evm = process_create_message(child_message, evm.env) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = b"" + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + init_code_gas = init_code_cost(Uint(memory_size)) + + charge_gas(evm, GAS_CREATE + extend_memory.cost + init_code_gas) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_contract_address( + evm.message.current_target, + get_account(evm.env.state, evm.message.current_target).nonce, + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + init_code_gas, + ) + + # PROGRAM COUNTER + evm.pc += 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)) // 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 += 1 + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + + charge_gas(evm, GAS_ZERO + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + +def generic_call( + evm: Evm, + gas: Uint, + value: U256, + caller: Address, + to: Address, + code_address: Address, + should_transfer_value: bool, + is_staticcall: bool, + memory_input_start_position: U256, + memory_input_size: U256, + memory_output_start_position: U256, + memory_output_size: U256, +) -> None: + """ + Perform the core logic of the `CALL*` family of opcodes. + """ + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_message + + evm.return_data = b"" + + if evm.message.depth + 1 > STACK_DEPTH_LIMIT: + evm.gas_left += gas + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + code = get_account(evm.env.state, code_address).code + child_message = Message( + caller=caller, + target=to, + gas=gas, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + 1, + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=True if is_staticcall else evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, + ) + child_evm = process_message(child_message, evm.env) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(1)) + + actual_output_size = min(memory_output_size, U256(len(child_evm.output))) + memory_write( + evm.memory, + memory_output_start_position, + child_evm.output[:actual_output_size], + ) + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + create_gas_cost = ( + Uint(0) + if is_account_alive(evm.env.state, to) or value == 0 + else GAS_NEW_ACCOUNT + ) + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + create_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + ensure(not evm.message.is_static or value == U256(0), WriteInStaticContext) + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.stipend + else: + generic_call( + evm, + message_call_gas.stipend, + value, + evm.message.current_target, + to, + to, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += 1 + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account’s code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + to = evm.message.current_target + + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.stipend + else: + generic_call( + evm, + message_call_gas.stipend, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += 1 + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + beneficiary = to_address(pop(evm.stack)) + + # GAS + gas_cost = GAS_SELF_DESTRUCT + if beneficiary not in evm.accessed_addresses: + evm.accessed_addresses.add(beneficiary) + gas_cost += GAS_COLD_ACCOUNT_ACCESS + + if ( + not is_account_alive(evm.env.state, beneficiary) + and get_account(evm.env.state, evm.message.current_target).balance != 0 + ): + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) + + # OPERATION + ensure(not evm.message.is_static, WriteInStaticContext) + + originator = evm.message.current_target + originator_balance = get_account(evm.env.state, originator).balance + + move_ether( + evm.env.state, + originator, + beneficiary, + originator_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + if originator in evm.env.state.created_accounts: + # If beneficiary is the same as originator, then + # the ether is burnt. + set_account_balance(evm.env.state, originator, U256(0)) + evm.accounts_to_delete.add(originator) + + # mark beneficiary as touched + if account_exists_and_is_empty(evm.env.state, beneficiary): + evm.touched_accounts.add(beneficiary) + + # HALT the execution + evm.running = False + + # PROGRAM COUNTER + pass + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + message_call_gas = calculate_message_call_gas( + U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.stipend, + evm.message.value, + evm.message.caller, + evm.message.current_target, + code_address, + False, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += 1 + + +def staticcall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.stipend, + U256(0), + evm.message.current_target, + to, + to, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) + + # PROGRAM COUNTER + evm.pc += 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/cancun/vm/interpreter.py b/src/ethereum/cancun/vm/interpreter.py new file mode 100644 index 0000000000..410c88db8b --- /dev/null +++ b/src/ethereum/cancun/vm/interpreter.py @@ -0,0 +1,308 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" +from dataclasses import dataclass +from typing import Iterable, Optional, Set, Tuple, Union + +from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) +from ethereum.utils.ensure import ensure + +from ..blocks import Log +from ..fork_types import Address +from ..state import ( + account_exists_and_is_empty, + account_has_code_or_nonce, + begin_transaction, + commit_transaction, + destroy_storage, + increment_nonce, + mark_account_created, + move_ether, + rollback_transaction, + set_code, + touch_account, +) +from ..vm import Message +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import Environment, Evm +from .exceptions import ( + AddressCollision, + ExceptionalHalt, + InvalidContractPrefix, + InvalidOpcode, + OutOfGasError, + Revert, + StackDepthLimitError, +) +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = U256(1024) +MAX_CODE_SIZE = 0x6000 + + +@dataclass +class MessageCallOutput: + """ + Output of a particular message call + + Contains the following: + + 1. `gas_left`: remaining gas after execution. + 2. `refund_counter`: gas to refund after execution. + 3. `logs`: list of `Log` generated during execution. + 4. `accounts_to_delete`: Contracts which have self-destructed. + 5. `touched_accounts`: Accounts that have been touched. + 6. `error`: The error from the execution if any. + """ + + gas_left: Uint + refund_counter: U256 + logs: Union[Tuple[()], Tuple[Log, ...]] + accounts_to_delete: Set[Address] + touched_accounts: Iterable[Address] + error: Optional[Exception] + + +def process_message_call( + message: Message, env: Environment +) -> MessageCallOutput: + """ + If `message.current` is empty then it creates a smart contract + else it executes a call from the `message.caller` to the `message.target`. + + Parameters + ---------- + message : + Transaction specific items. + + env : + External items required for EVM execution. + + Returns + ------- + output : `MessageCallOutput` + Output of the message call + """ + if message.target == Bytes0(b""): + is_collision = account_has_code_or_nonce( + env.state, message.current_target + ) + if is_collision: + return MessageCallOutput( + Uint(0), U256(0), tuple(), set(), set(), AddressCollision() + ) + else: + evm = process_create_message(message, env) + else: + evm = process_message(message, env) + if account_exists_and_is_empty(env.state, Address(message.target)): + evm.touched_accounts.add(Address(message.target)) + + if evm.error: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + touched_accounts = set() + refund_counter = U256(0) + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + touched_accounts = evm.touched_accounts + refund_counter = U256(evm.refund_counter) + + tx_end = TransactionEnd(message.gas - evm.gas_left, evm.output, evm.error) + evm_trace(evm, tx_end) + + return MessageCallOutput( + gas_left=evm.gas_left, + refund_counter=refund_counter, + logs=logs, + accounts_to_delete=accounts_to_delete, + touched_accounts=touched_accounts, + error=evm.error, + ) + + +def process_create_message(message: Message, env: Environment) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: :py:class:`~ethereum.cancun.vm.Evm` + Items containing execution specific objects. + """ + # take snapshot of state before processing the message + begin_transaction(env.state, env.transient_storage) + + # If the address where the account is being created has storage, it is + # destroyed. This can only happen in the following highly unlikely + # circumstances: + # * The address created by a `CREATE` call collides with a subsequent + # `CREATE` or `CREATE2` call. + # * The first `CREATE` happened before Spurious Dragon and left empty + # code. + destroy_storage(env.state, message.current_target) + + # In the previously mentioned edge case the preexisting storage is ignored + # for gas refund purposes. In order to do this we must track created + # accounts. + mark_account_created(env.state, message.current_target) + + increment_nonce(env.state, message.current_target) + evm = process_message(message, env) + if not evm.error: + contract_code = evm.output + contract_code_gas = len(contract_code) * GAS_CODE_DEPOSIT + try: + if len(contract_code) > 0: + ensure(contract_code[0] != 0xEF, InvalidContractPrefix) + charge_gas(evm, contract_code_gas) + ensure(len(contract_code) <= MAX_CODE_SIZE, OutOfGasError) + except ExceptionalHalt as error: + rollback_transaction(env.state, env.transient_storage) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + set_code(env.state, message.current_target, contract_code) + commit_transaction(env.state, env.transient_storage) + else: + rollback_transaction(env.state, env.transient_storage) + return evm + + +def process_message(message: Message, env: Environment) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: :py:class:`~ethereum.cancun.vm.Evm` + Items containing execution specific objects + """ + if message.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + + # take snapshot of state before processing the message + begin_transaction(env.state, env.transient_storage) + + touch_account(env.state, message.current_target) + + if message.should_transfer_value and message.value != 0: + move_ether( + env.state, message.caller, message.current_target, message.value + ) + + evm = execute_code(message, env) + if evm.error: + # revert state to the last saved checkpoint + # since the message call resulted in an error + rollback_transaction(env.state, env.transient_storage) + else: + commit_transaction(env.state, env.transient_storage) + return evm + + +def execute_code(message: Message, env: Environment) -> Evm: + """ + Executes bytecode present in the `message`. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + evm: `ethereum.vm.EVM` + Items containing execution specific objects + """ + code = message.code + valid_jump_destinations = get_valid_jump_destinations(code) + + evm = Evm( + pc=Uint(0), + stack=[], + memory=bytearray(), + code=code, + gas_left=message.gas, + env=env, + valid_jump_destinations=valid_jump_destinations, + logs=(), + refund_counter=0, + running=True, + message=message, + output=b"", + accounts_to_delete=set(), + touched_accounts=set(), + return_data=b"", + error=None, + accessed_addresses=message.accessed_addresses, + accessed_storage_keys=message.accessed_storage_keys, + ) + try: + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + return evm + + while evm.running and evm.pc < len(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/cancun/vm/memory.py b/src/ethereum/cancun/vm/memory.py new file mode 100644 index 0000000000..d8a904c352 --- /dev/null +++ b/src/ethereum/cancun/vm/memory.py @@ -0,0 +1,80 @@ +""" +Ethereum Virtual Machine (EVM) Memory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" +from ethereum.utils.byte import right_pad_zero_bytes + +from ...base_types import U256, Bytes, Uint + + +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 : Uint(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/cancun/vm/precompiled_contracts/__init__.py b/src/ethereum/cancun/vm/precompiled_contracts/__init__.py new file mode 100644 index 0000000000..7ec48ca69b --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/__init__.py @@ -0,0 +1,40 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" + +from ...utils.hexadecimal import hex_to_address + +__all__ = ( + "ECRECOVER_ADDRESS", + "SHA256_ADDRESS", + "RIPEMD160_ADDRESS", + "IDENTITY_ADDRESS", + "MODEXP_ADDRESS", + "ALT_BN128_ADD_ADDRESS", + "ALT_BN128_MUL_ADDRESS", + "ALT_BN128_PAIRING_CHECK_ADDRESS", + "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", +) + +ECRECOVER_ADDRESS = hex_to_address("0x01") +SHA256_ADDRESS = hex_to_address("0x02") +RIPEMD160_ADDRESS = hex_to_address("0x03") +IDENTITY_ADDRESS = hex_to_address("0x04") +MODEXP_ADDRESS = hex_to_address("0x05") +ALT_BN128_ADD_ADDRESS = hex_to_address("0x06") +ALT_BN128_MUL_ADDRESS = hex_to_address("0x07") +ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") +BLAKE2F_ADDRESS = hex_to_address("0x09") +POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") diff --git a/src/ethereum/cancun/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/cancun/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 0000000000..70a9e51b63 --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,156 @@ +""" +Ethereum Virtual Machine (EVM) ALT_BN128 CONTRACTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ALT_BN128 precompiled contracts. +""" +from ethereum.base_types import U256, Uint +from ethereum.crypto.alt_bn128 import ( + ALT_BN128_CURVE_ORDER, + ALT_BN128_PRIME, + BNF, + BNF2, + BNF12, + BNP, + BNP2, + pairing, +) +from ethereum.utils.ensure import ensure + +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 = U256.from_be_bytes(x0_bytes) + y0_bytes = buffer_read(data, U256(32), U256(32)) + y0_value = U256.from_be_bytes(y0_bytes) + x1_bytes = buffer_read(data, U256(64), U256(32)) + x1_value = U256.from_be_bytes(x1_bytes) + y1_bytes = buffer_read(data, U256(96), U256(32)) + y1_value = 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 = U256.from_be_bytes(x0_bytes) + y0_bytes = buffer_read(data, U256(32), U256(32)) + y0_value = U256.from_be_bytes(y0_bytes) + n = 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 = U256.from_be_bytes( + data[i * 192 + 32 * j : i * 192 + 32 * (j + 1)] + ) + if value >= ALT_BN128_PRIME: + raise OutOfGasError + values.append(int(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() + ensure( + p.mul_by(ALT_BN128_CURVE_ORDER) == BNP.point_at_infinity(), + OutOfGasError, + ) + ensure( + q.mul_by(ALT_BN128_CURVE_ORDER) == BNP2.point_at_infinity(), + 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/cancun/vm/precompiled_contracts/blake2f.py b/src/ethereum/cancun/vm/precompiled_contracts/blake2f.py new file mode 100644 index 0000000000..6af10909f0 --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,44 @@ +""" +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 ethereum.utils.ensure import ensure + +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 + + # GAS + ensure(len(data) == 213, InvalidParameter) + + blake2b = Blake2b() + rounds, h, m, t_0, t_1, f = blake2b.get_blake2_parameters(data) + + charge_gas(evm, GAS_BLAKE2_PER_ROUND * rounds) + + # OPERATION + ensure(f in [0, 1], InvalidParameter) + + evm.output = blake2b.compress(rounds, h, m, t_0, t_1, f) diff --git a/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py b/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..664d1c145f --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,61 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ECRECOVER precompiled contract. +""" +from ethereum.base_types import U256 +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GAS_ECRECOVER, charge_gas +from ...vm.memory import buffer_read + + +def ecrecover(evm: Evm) -> None: + """ + Decrypts the address using elliptic curve DSA recovery mechanism and writes + the address to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + + # GAS + charge_gas(evm, GAS_ECRECOVER) + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + v = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + r = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(96), U256(32))) + + if v != 27 and v != 28: + return + if 0 >= r or r >= SECP256K1N: + return + if 0 >= s or s >= SECP256K1N: + return + + try: + public_key = secp256k1_recover(r, s, v - 27, message_hash) + except ValueError: + # unable to extract public key + return + + address = keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/cancun/vm/precompiled_contracts/identity.py b/src/ethereum/cancun/vm/precompiled_contracts/identity.py new file mode 100644 index 0000000000..c6ed826776 --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/identity.py @@ -0,0 +1,37 @@ +""" +Ethereum Virtual Machine (EVM) IDENTITY PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `IDENTITY` precompiled contract. +""" +from ethereum.base_types 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))) // 32 + charge_gas(evm, GAS_IDENTITY + GAS_IDENTITY_WORD * word_count) + + # OPERATION + evm.output = data diff --git a/src/ethereum/cancun/vm/precompiled_contracts/mapping.py b/src/ethereum/cancun/vm/precompiled_contracts/mapping.py new file mode 100644 index 0000000000..7bd3416b6b --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/mapping.py @@ -0,0 +1,49 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts their implementations. +""" +from typing import Callable, Dict + +from ...fork_types import Address +from . import ( + ALT_BN128_ADD_ADDRESS, + ALT_BN128_MUL_ADDRESS, + ALT_BN128_PAIRING_CHECK_ADDRESS, + BLAKE2F_ADDRESS, + ECRECOVER_ADDRESS, + IDENTITY_ADDRESS, + MODEXP_ADDRESS, + POINT_EVALUATION_ADDRESS, + RIPEMD160_ADDRESS, + SHA256_ADDRESS, +) +from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check +from .blake2f import blake2f +from .ecrecover import ecrecover +from .identity import identity +from .modexp import modexp +from .point_evaluation import point_evaluation +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + ECRECOVER_ADDRESS: ecrecover, + SHA256_ADDRESS: sha256, + RIPEMD160_ADDRESS: ripemd160, + IDENTITY_ADDRESS: identity, + MODEXP_ADDRESS: modexp, + ALT_BN128_ADD_ADDRESS: alt_bn128_add, + ALT_BN128_MUL_ADDRESS: alt_bn128_mul, + ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, + BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, +} diff --git a/src/ethereum/cancun/vm/precompiled_contracts/modexp.py b/src/ethereum/cancun/vm/precompiled_contracts/modexp.py new file mode 100644 index 0000000000..82a3c94d04 --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/modexp.py @@ -0,0 +1,168 @@ +""" +Ethereum Virtual Machine (EVM) MODEXP PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `MODEXP` precompiled contract. +""" +from ethereum.base_types import U256, Bytes, Uint + +from ...vm import Evm +from ...vm.gas import charge_gas +from ..memory import buffer_read + +GQUADDIVISOR = 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 = Uint(pow(base, exp, modulus)).to_bytes( + 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 + 7) // 8 + return words**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 <= 32 and exponent_head == 0: + count = Uint(0) + elif exponent_length <= 32: + bit_length = Uint(exponent_head.bit_length()) + + if bit_length > 0: + bit_length -= 1 + + count = bit_length + else: + length_part = 8 * (Uint(exponent_length) - 32) + bits_part = Uint(exponent_head.bit_length()) + + if bits_part > 0: + bits_part -= 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/cancun/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/cancun/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 0000000000..841056287f --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,76 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the POINT EVALUATION precompiled contract. +""" +from eth2spec.deneb.mainnet import ( + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ethereum.base_types import U256, Bytes +from ethereum.utils.ensure import ensure + +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 + + ensure(len(data) == 192, KZGProofError) + + versioned_hash = data[:32] + z = data[32:64] + y = data[64:96] + commitment = KZGCommitment(data[96:144]) + proof = data[144:192] + + # GAS + charge_gas(evm, GAS_POINT_EVALUATION) + + # OPERATION + # Verify commitment matches versioned_hash + ensure( + kzg_commitment_to_versioned_hash(commitment) == versioned_hash, + 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 + + ensure(kzg_proof_verification, 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/cancun/vm/precompiled_contracts/ripemd160.py b/src/ethereum/cancun/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 0000000000..2e042c0ab2 --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,42 @@ +""" +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.base_types 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))) // 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/cancun/vm/precompiled_contracts/sha256.py b/src/ethereum/cancun/vm/precompiled_contracts/sha256.py new file mode 100644 index 0000000000..f2b45de15d --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/sha256.py @@ -0,0 +1,39 @@ +""" +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.base_types 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))) // 32 + charge_gas(evm, GAS_SHA256 + GAS_SHA256_WORD * word_count) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/cancun/vm/runtime.py b/src/ethereum/cancun/vm/runtime.py new file mode 100644 index 0000000000..67a7cda453 --- /dev/null +++ b/src/ethereum/cancun/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.base_types import Uint + +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 < len(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 += 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 += push_data_size + + pc += 1 + + return valid_jump_destinations diff --git a/src/ethereum/cancun/vm/stack.py b/src/ethereum/cancun/vm/stack.py new file mode 100644 index 0000000000..8c6934fab8 --- /dev/null +++ b/src/ethereum/cancun/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.base_types 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/constantinople/vm/exceptions.py b/src/ethereum/constantinople/vm/exceptions.py index 2862dae480..116d4b93cf 100644 --- a/src/ethereum/constantinople/vm/exceptions.py +++ b/src/ethereum/constantinople/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/constantinople/vm/precompiled_contracts/modexp.py b/src/ethereum/constantinople/vm/precompiled_contracts/modexp.py index 4fa2afa369..813de1d79c 100644 --- a/src/ethereum/constantinople/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/constantinople/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = U256.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -61,6 +60,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/dao_fork/vm/exceptions.py b/src/ethereum/dao_fork/vm/exceptions.py index 9493d81c07..14fc6d4301 100644 --- a/src/ethereum/dao_fork/vm/exceptions.py +++ b/src/ethereum/dao_fork/vm/exceptions.py @@ -52,7 +52,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/frontier/vm/exceptions.py b/src/ethereum/frontier/vm/exceptions.py index 9493d81c07..14fc6d4301 100644 --- a/src/ethereum/frontier/vm/exceptions.py +++ b/src/ethereum/frontier/vm/exceptions.py @@ -52,7 +52,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/gray_glacier/vm/exceptions.py b/src/ethereum/gray_glacier/vm/exceptions.py index 2d54fbb5b3..6d30e2846d 100644 --- a/src/ethereum/gray_glacier/vm/exceptions.py +++ b/src/ethereum/gray_glacier/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/gray_glacier/vm/precompiled_contracts/modexp.py b/src/ethereum/gray_glacier/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/gray_glacier/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/gray_glacier/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/homestead/vm/exceptions.py b/src/ethereum/homestead/vm/exceptions.py index 9493d81c07..14fc6d4301 100644 --- a/src/ethereum/homestead/vm/exceptions.py +++ b/src/ethereum/homestead/vm/exceptions.py @@ -52,7 +52,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/istanbul/vm/exceptions.py b/src/ethereum/istanbul/vm/exceptions.py index cb98c39c17..789d302089 100644 --- a/src/ethereum/istanbul/vm/exceptions.py +++ b/src/ethereum/istanbul/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/istanbul/vm/precompiled_contracts/modexp.py b/src/ethereum/istanbul/vm/precompiled_contracts/modexp.py index 4fa2afa369..813de1d79c 100644 --- a/src/ethereum/istanbul/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/istanbul/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = U256.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -61,6 +60,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/london/vm/exceptions.py b/src/ethereum/london/vm/exceptions.py index 2d54fbb5b3..6d30e2846d 100644 --- a/src/ethereum/london/vm/exceptions.py +++ b/src/ethereum/london/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/london/vm/precompiled_contracts/modexp.py b/src/ethereum/london/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/london/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/london/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/muir_glacier/vm/exceptions.py b/src/ethereum/muir_glacier/vm/exceptions.py index cb98c39c17..789d302089 100644 --- a/src/ethereum/muir_glacier/vm/exceptions.py +++ b/src/ethereum/muir_glacier/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/muir_glacier/vm/precompiled_contracts/modexp.py b/src/ethereum/muir_glacier/vm/precompiled_contracts/modexp.py index 4fa2afa369..813de1d79c 100644 --- a/src/ethereum/muir_glacier/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/muir_glacier/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = U256.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -61,6 +60,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/paris/vm/exceptions.py b/src/ethereum/paris/vm/exceptions.py index 2d54fbb5b3..6d30e2846d 100644 --- a/src/ethereum/paris/vm/exceptions.py +++ b/src/ethereum/paris/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/paris/vm/precompiled_contracts/modexp.py b/src/ethereum/paris/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/paris/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/paris/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/rlp.py b/src/ethereum/rlp.py index ba4a560818..265bbaaf00 100644 --- a/src/ethereum/rlp.py +++ b/src/ethereum/rlp.py @@ -274,7 +274,7 @@ def _decode_to(cls: Type[T], raw_rlp: RLP) -> T: elif issubclass(cls, FixedBytes): ensure(isinstance(raw_rlp, Bytes), RLPDecodingError) ensure(len(raw_rlp) == cls.LENGTH, RLPDecodingError) - return raw_rlp + return cls(raw_rlp) # type: ignore elif issubclass(cls, Bytes): ensure(isinstance(raw_rlp, Bytes), RLPDecodingError) return raw_rlp diff --git a/src/ethereum/shanghai/vm/exceptions.py b/src/ethereum/shanghai/vm/exceptions.py index 2d54fbb5b3..6d30e2846d 100644 --- a/src/ethereum/shanghai/vm/exceptions.py +++ b/src/ethereum/shanghai/vm/exceptions.py @@ -63,7 +63,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/shanghai/vm/instructions/system.py b/src/ethereum/shanghai/vm/instructions/system.py index 9123cb577f..53a3659cb4 100644 --- a/src/ethereum/shanghai/vm/instructions/system.py +++ b/src/ethereum/shanghai/vm/instructions/system.py @@ -75,6 +75,12 @@ def generic_create( process_create_message, ) + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + ensure(len(call_data) <= 2 * MAX_CODE_SIZE, OutOfGasError) + evm.accessed_addresses.add(contract_address) create_message_gas = max_message_call_gas(Uint(evm.gas_left)) @@ -100,12 +106,6 @@ def generic_create( push(evm.stack, U256(0)) return - call_data = memory_read_bytes( - evm.memory, memory_start_position, memory_size - ) - - ensure(len(call_data) <= 2 * MAX_CODE_SIZE, OutOfGasError) - increment_nonce(evm.env.state, evm.message.current_target) child_message = Message( diff --git a/src/ethereum/shanghai/vm/precompiled_contracts/modexp.py b/src/ethereum/shanghai/vm/precompiled_contracts/modexp.py index f88bdc8f61..82a3c94d04 100644 --- a/src/ethereum/shanghai/vm/precompiled_contracts/modexp.py +++ b/src/ethereum/shanghai/vm/precompiled_contracts/modexp.py @@ -33,7 +33,6 @@ def modexp(evm: Evm) -> None: modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) exp_start = U256(96) + base_length - modulus_start = exp_start + exp_length exp_head = Uint.from_be_bytes( buffer_read(data, exp_start, min(U256(32), exp_length)) @@ -51,6 +50,8 @@ def modexp(evm: Evm) -> None: 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) ) diff --git a/src/ethereum/spurious_dragon/vm/exceptions.py b/src/ethereum/spurious_dragon/vm/exceptions.py index 9493d81c07..14fc6d4301 100644 --- a/src/ethereum/spurious_dragon/vm/exceptions.py +++ b/src/ethereum/spurious_dragon/vm/exceptions.py @@ -52,7 +52,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/tangerine_whistle/vm/exceptions.py b/src/ethereum/tangerine_whistle/vm/exceptions.py index 9493d81c07..14fc6d4301 100644 --- a/src/ethereum/tangerine_whistle/vm/exceptions.py +++ b/src/ethereum/tangerine_whistle/vm/exceptions.py @@ -52,7 +52,11 @@ class InvalidOpcode(ExceptionalHalt): Raised when an invalid opcode is encountered. """ - pass + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code class InvalidJumpDestError(ExceptionalHalt): diff --git a/src/ethereum/utils/numeric.py b/src/ethereum/utils/numeric.py index 94d66bec3e..06f785bbbc 100644 --- a/src/ethereum/utils/numeric.py +++ b/src/ethereum/utils/numeric.py @@ -165,3 +165,37 @@ def le_uint32_sequence_to_uint(sequence: Sequence[U32]) -> Uint: """ sequence_as_bytes = le_uint32_sequence_to_bytes(sequence) return Uint.from_le_bytes(sequence_as_bytes) + + +def taylor_exponential( + factor: Uint, numerator: Uint, denominator: Uint +) -> Uint: + """ + Approximates factor * e ** (numerator / denominator) using + Taylor expansion. + + Parameters + ---------- + factor : + The factor. + numerator : + The numerator of the exponential. + denominator : + The denominator of the exponential. + + Returns + ------- + output : `ethereum.base_types.Uint` + The approximation of factor * e ** (numerator / denominator). + + """ + i = 1 + output = 0 + numerator_accumulated = factor * denominator + while numerator_accumulated > 0: + output += numerator_accumulated + numerator_accumulated = (numerator_accumulated * numerator) // ( + denominator * i + ) + i += 1 + return output // denominator diff --git a/src/ethereum_optimized/state_db.py b/src/ethereum_optimized/state_db.py index 9fe03cb000..c3b9071194 100644 --- a/src/ethereum_optimized/state_db.py +++ b/src/ethereum_optimized/state_db.py @@ -54,8 +54,11 @@ def get_optimized_state_patches(fork: str) -> Dict[str, Any]: """ patches: Dict[str, Any] = {} - mod = cast(Any, import_module("ethereum." + fork + ".fork_types")) - Account = mod.Account + types_mod = cast(Any, import_module("ethereum." + fork + ".fork_types")) + state_mod = cast(Any, import_module("ethereum." + fork + ".state")) + Account = types_mod.Account + + has_transient_storage = hasattr(state_mod, "TransientStorage") @add_item(patches) @dataclass @@ -202,8 +205,7 @@ def rollback_db_transaction(state: State) -> None: state.dirty_storage.clear() state.destroyed_accounts = set() - @add_item(patches) - def begin_transaction(state: State) -> None: + def _begin_transaction(state: State) -> None: """ See `state`. """ @@ -211,8 +213,35 @@ def begin_transaction(state: State) -> None: flush(state) state.tx_restore_points.append(len(state.journal)) - @add_item(patches) - def commit_transaction(state: State) -> None: + if has_transient_storage: + + @add_item(patches) + def begin_transaction( + state: State, + transient_storage: Optional[Any] = None, + ) -> None: + """ + See `state` + """ + _begin_transaction(state) + if transient_storage is not None: + transient_storage._snapshots.append( + { + k: state_mod.copy_trie(t) + for (k, t) in transient_storage._tries.items() + } + ) + + else: + + @add_item(patches) + def begin_transaction(state: State) -> None: + """ + See `state` + """ + _begin_transaction(state) + + def _commit_transaction(state: State) -> None: """ See `state`. """ @@ -222,8 +251,30 @@ def commit_transaction(state: State) -> None: state.created_accounts.clear() flush(state) - @add_item(patches) - def rollback_transaction(state: State) -> None: + if has_transient_storage: + + @add_item(patches) + def commit_transaction( + state: State, transient_storage: Optional[Any] = None + ) -> None: + """ + See `state`. + """ + _commit_transaction(state) + + if transient_storage and transient_storage._snapshots: + transient_storage._snapshots.pop() + + else: + + @add_item(patches) + def commit_transaction(state: State) -> None: + """ + See `state`. + """ + _commit_transaction(state) + + def _rollback_transaction(state: State) -> None: """ See `state`. """ @@ -250,6 +301,30 @@ def rollback_transaction(state: State) -> None: if not state.tx_restore_points: state.created_accounts.clear() + if has_transient_storage: + + @add_item(patches) + def rollback_transaction( + state: State, + transient_storage: Optional[Any] = None, + ) -> None: + """ + See `state` + """ + _rollback_transaction(state) + + if transient_storage and transient_storage._snapshots: + transient_storage._tries = transient_storage._snapshots.pop() + + else: + + @add_item(patches) + def rollback_transaction(state: State) -> None: + """ + See `state`. + """ + _rollback_transaction(state) + @add_item(patches) def get_storage(state: State, address: Address, key: Bytes) -> U256: """ diff --git a/src/ethereum_spec_tools/evm_tools/__init__.py b/src/ethereum_spec_tools/evm_tools/__init__.py index cb0e0ed45e..99554d2d4d 100644 --- a/src/ethereum_spec_tools/evm_tools/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/__init__.py @@ -4,15 +4,18 @@ import argparse import subprocess +import sys +from typing import Optional, Sequence, Text, TextIO from ethereum import __version__ from .b11r import B11R, b11r_arguments +from .daemon import Daemon, daemon_arguments +from .statetest import StateTest, state_test_arguments from .t8n import T8N, t8n_arguments from .utils import get_supported_forks -DESCRIPTION = ( - """ +DESCRIPTION = """ This is the EVM tool for execution specs. The EVM tool provides a few useful subcommands to facilitate testing at the EVM layer. @@ -26,14 +29,40 @@ The following forks are supported: -""" - + get_supported_forks() +""" + "\n".join( + get_supported_forks() ) -parser = argparse.ArgumentParser( - description=DESCRIPTION, - formatter_class=argparse.RawDescriptionHelpFormatter, -) + +def create_parser() -> argparse.ArgumentParser: + """ + Create a command-line argument parser for the evm tool. + """ + new_parser = argparse.ArgumentParser( + description=DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + commit_hash = get_git_commit_hash() + + # Add -v option to parser to show the version of the tool + new_parser.add_argument( + "-v", + "--version", + action="version", + version=f"%(prog)s {__version__} (Git commit: {commit_hash})", + help="Show the version of the tool.", + ) + + # Add options to the t8n tool + subparsers = new_parser.add_subparsers(dest="evm_tool") + + daemon_arguments(subparsers) + t8n_arguments(subparsers) + b11r_arguments(subparsers) + state_test_arguments(subparsers) + + return new_parser def get_git_commit_hash() -> str: @@ -59,35 +88,34 @@ def get_git_commit_hash() -> str: return "Error: " + str(e) -commit_hash = get_git_commit_hash() - -# Add -v option to parser to show the version of the tool -parser.add_argument( - "-v", - "--version", - action="version", - version=f"%(prog)s {__version__} (Git commit: {commit_hash})", - help="Show the version of the tool.", -) - - -# Add options to the t8n tool -subparsers = parser.add_subparsers(dest="evm_tool") +def main( + args: Optional[Sequence[Text]] = None, + out_file: Optional[TextIO] = None, + in_file: Optional[TextIO] = None, +) -> int: + """Run the tools based on the given options.""" + parser = create_parser() + options, _ = parser.parse_known_args(args) -def main() -> int: - """Run the tools based on the given options.""" - t8n_arguments(subparsers) - b11r_arguments(subparsers) + if out_file is None: + out_file = sys.stdout - options, _ = parser.parse_known_args() + if in_file is None: + in_file = sys.stdin if options.evm_tool == "t8n": - t8n_tool = T8N(options) + t8n_tool = T8N(options, out_file, in_file) return t8n_tool.run() elif options.evm_tool == "b11r": - b11r_tool = B11R(options) + b11r_tool = B11R(options, out_file, in_file) return b11r_tool.run() + elif options.evm_tool == "daemon": + daemon = Daemon(options) + return daemon.run() + elif options.evm_tool == "statetest": + state_test = StateTest(options, out_file, in_file) + return state_test.run() else: - parser.print_help() + parser.print_help(file=out_file) return 0 diff --git a/src/ethereum_spec_tools/evm_tools/b11r/__init__.py b/src/ethereum_spec_tools/evm_tools/b11r/__init__.py index 759f34cd23..1999f437df 100644 --- a/src/ethereum_spec_tools/evm_tools/b11r/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/b11r/__init__.py @@ -4,8 +4,7 @@ import argparse import json -import sys -from typing import Optional +from typing import Optional, TextIO from ethereum import rlp from ethereum.base_types import Bytes32 @@ -65,17 +64,22 @@ class B11R: Creates the b11r tool. """ - def __init__(self, options: argparse.Namespace) -> None: + def __init__( + self, options: argparse.Namespace, out_file: TextIO, in_file: TextIO + ) -> None: """ Initializes the b11r tool. """ self.options = options + self.out_file = out_file + self.in_file = in_file + if "stdin" in ( options.input_header, options.input_ommers, options.input_txs, ): - stdin = json.load(sys.stdin) + stdin = json.load(in_file) else: stdin = None @@ -126,7 +130,7 @@ def run(self) -> int: self.logger.info("Writing the result...") if self.options.output_block == "stdout": - json.dump(result, sys.stdout, indent=4) + json.dump(result, self.out_file, indent=4) else: with open(self.options.output_block, "w") as f: json.dump(result, f, indent=4) diff --git a/src/ethereum_spec_tools/evm_tools/daemon.py b/src/ethereum_spec_tools/evm_tools/daemon.py new file mode 100644 index 0000000000..1182792a0e --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/daemon.py @@ -0,0 +1,122 @@ +""" +Run ethereum-spec-evm as a daemon. +""" + +import argparse +import json +import os.path +import socketserver +import time +from http.server import BaseHTTPRequestHandler +from io import StringIO, TextIOWrapper +from socket import socket +from threading import Thread +from typing import Any, Optional, Tuple, Union + +from platformdirs import user_runtime_dir + + +def daemon_arguments(subparsers: argparse._SubParsersAction) -> None: + """ + Adds the arguments for the daemon tool subparser. + """ + parser = subparsers.add_parser("daemon", help="Spawn t8n as a daemon") + parser.add_argument("--uds", help="Unix domain socket path") + + +class _EvmToolHandler(BaseHTTPRequestHandler): + def do_POST(self) -> None: + from . import main + + content_length = int(self.headers["Content-Length"]) + content_bytes = self.rfile.read(content_length) + content = json.loads(content_bytes) + + input_string = json.dumps(content["input"]) + input = StringIO(input_string) + + args = [ + "t8n", + "--input.env=stdin", + "--input.alloc=stdin", + "--input.txs=stdin", + "--output.result=stdout", + "--output.body=stdout", + "--output.alloc=stdout", + f"--state.fork={content['state']['fork']}", + f"--state.chainid={content['state']['chainid']}", + f"--state.reward={content['state']['reward']}", + ] + + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + + out_wrapper = TextIOWrapper(self.wfile, encoding="utf-8") + main(args=args, out_file=out_wrapper, in_file=input) + out_wrapper.flush() + + +class _UnixSocketHttpServer(socketserver.UnixStreamServer): + last_response: Optional[float] = None + + def get_request(self) -> Tuple[Any, Any]: + request, client_address = super().get_request() + return (request, ["local", 0]) + + def finish_request( + self, request: Union[socket, Tuple[bytes, socket]], client_address: Any + ) -> None: + try: + super().finish_request(request, client_address) + finally: + self.last_response = time.monotonic() + + def check_timeout(self) -> None: + while True: + time.sleep(11.0) + now = time.monotonic() + last_response = self.last_response + if last_response is None: + self.last_response = now + elif now - last_response > 60.0: + self.shutdown() + break + + +class Daemon: + """ + Converts HTTP requests into ethereum-spec-evm calls. + """ + + def __init__(self, options: argparse.Namespace) -> None: + if options.uds is None: + runtime_dir = user_runtime_dir( + appname="ethereum-spec-evm", + appauthor="org.ethereum", + ensure_exists=True, + ) + self.uds = os.path.join(runtime_dir, "daemon.sock") + else: + self.uds = options.uds + + def _run(self) -> int: + try: + os.remove(self.uds) + except IOError: + pass + + with _UnixSocketHttpServer((self.uds), _EvmToolHandler) as server: + server.timeout = 7.0 + timer = Thread(target=server.check_timeout, daemon=True) + timer.start() + + server.serve_forever() + + return 0 + + def run(self) -> int: + """ + Execute the tool. + """ + return self._run() diff --git a/src/ethereum_spec_tools/evm_tools/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/fixture_loader.py deleted file mode 100644 index 4cfe4c2a67..0000000000 --- a/src/ethereum_spec_tools/evm_tools/fixture_loader.py +++ /dev/null @@ -1,421 +0,0 @@ -""" -Defines Load class for loading json fixtures for the evm -tools (t8n, b11r, etc.) as well as the execution specs -testing framework. -""" - -import importlib -from abc import ABC, abstractmethod -from typing import Any, Tuple - -from ethereum import rlp -from ethereum.base_types import U64, U256, Bytes0 -from ethereum.crypto.hash import Hash32 -from ethereum.utils.hexadecimal import ( - hex_to_bytes, - hex_to_bytes8, - hex_to_bytes32, - hex_to_hash, - hex_to_u64, - hex_to_u256, - hex_to_uint, -) -from ethereum_spec_tools.forks import Hardfork - - -class UnsupportedTx(Exception): - """Exception for unsupported transactions""" - - def __init__(self, encoded_params: bytes, error_message: str) -> None: - super().__init__(error_message) - self.encoded_params = encoded_params - self.error_message = error_message - - -class BaseLoad(ABC): - """Base class for loading json fixtures""" - - @property - @abstractmethod - def fork_module(self) -> str: - """Module that contains the fork code""" - pass - - @property - @abstractmethod - def network(self) -> str: - """Network name""" - pass - - @property - @abstractmethod - def proof_of_stake(self) -> bool: - """Whether the fork is proof of stake""" - pass - - @property - @abstractmethod - def Block(self) -> Any: - """Block class of the fork""" - pass - - @property - @abstractmethod - def Environment(self) -> Any: - """Environment class of the fork""" - pass - - @property - @abstractmethod - def LegacyTransaction(self) -> Any: - """Legacy transaction class of the fork""" - pass - - @property - @abstractmethod - def Account(self) -> Any: - """Account class of the fork""" - pass - - @property - @abstractmethod - def State(self) -> Any: - """State class of the fork""" - pass - - @property - @abstractmethod - def set_account(self) -> Any: - """set_account function of the fork""" - pass - - @property - @abstractmethod - def BlockChain(self) -> Any: - """Block chain class of the fork""" - pass - - @property - @abstractmethod - def process_transaction(self) -> Any: - """process_transaction function of the fork""" - pass - - @property - @abstractmethod - def state_transition(self) -> Any: - """state_transition function of the fork""" - pass - - @property - @abstractmethod - def close_state(self) -> Any: - """close_state function of the fork""" - pass - - @abstractmethod - def json_to_header(self, json_data: Any) -> Any: - """Converts json header data to a header object""" - pass - - @abstractmethod - def json_to_state(self, json_data: Any) -> Any: - """Converts json state data to a state object""" - pass - - @abstractmethod - def json_to_block(self, json_data: Any) -> Any: - """Converts json block data to a list of blocks""" - pass - - -class Load(BaseLoad): - """Class for loading json fixtures""" - - _network: str - _fork_module: str - - @property - def fork_module(self) -> str: - """Module that contains the fork code""" - return self._fork_module - - @property - def network(self) -> str: - """Network name""" - return self._network - - @property - def proof_of_stake(self) -> bool: - """Whether the fork is proof of stake""" - forks = Hardfork.discover() - for fork in forks: - if fork.name == "ethereum." + self._fork_module: - return fork.consensus.is_pos() - raise Exception(f"fork {self._fork_module} not discovered") - - @property - def Block(self) -> Any: - """Block class of the fork""" - return self._module("blocks").Block - - @property - def Bloom(self) -> Any: - """Bloom class of the fork""" - return self._module("fork_types").Bloom - - @property - def Header(self) -> Any: - """Header class of the fork""" - return self._module("blocks").Header - - @property - def Environment(self) -> Any: - """Environment class of the fork""" - return self._module("vm").Environment - - @property - def LegacyTransaction(self) -> Any: - """Legacy transaction class of the fork""" - mod = self._module("transactions") - try: - return mod.LegacyTransaction - except AttributeError: - return mod.Transaction - - @property - def Account(self) -> Any: - """Account class of the fork""" - return self._module("fork_types").Account - - @property - def State(self) -> Any: - """State class of the fork""" - return self._module("state").State - - @property - def set_account(self) -> Any: - """set_account function of the fork""" - return self._module("state").set_account - - @property - def state_transition(self) -> Any: - """state_transition function of the fork""" - return self._module("fork").state_transition - - @property - def process_transaction(self) -> Any: - """process_transaction function of the fork""" - return self._module("fork").process_transaction - - @property - def BlockChain(self) -> Any: - """Block chain class of the fork""" - return self._module("fork").BlockChain - - @property - def hex_to_address(self) -> Any: - """hex_to_address function of the fork""" - return self._module("utils.hexadecimal").hex_to_address - - @property - def hex_to_root(self) -> Any: - """hex_to_root function of the fork""" - return self._module("utils.hexadecimal").hex_to_root - - @property - def close_state(self) -> Any: - """close_state function of the fork""" - return self._module("state").close_state - - def __init__(self, network: str, fork_name: str): - self._network = network - self._fork_module = fork_name - - def _module(self, name: str) -> Any: - """Imports a module from the fork""" - return importlib.import_module(f"ethereum.{self._fork_module}.{name}") - - def json_to_state(self, raw: Any) -> Any: - """Converts json state data to a state object""" - state = self.State() - set_storage = self._module("state").set_storage - - for address_hex, account_state in raw.items(): - address = self.hex_to_address(address_hex) - account = self.Account( - nonce=hex_to_uint(account_state.get("nonce", "0x0")), - balance=U256(hex_to_uint(account_state.get("balance", "0x0"))), - code=hex_to_bytes(account_state.get("code", "")), - ) - self.set_account(state, address, account) - - for k, v in account_state.get("storage", {}).items(): - set_storage( - state, - address, - hex_to_bytes32(k), - U256.from_be_bytes(hex_to_bytes32(v)), - ) - return state - - def json_to_access_list(self, raw: Any) -> Any: - """Converts json access list data to a list of access list entries""" - access_list = [] - for sublist in raw: - access_list.append( - ( - self.hex_to_address(sublist.get("address")), - [ - hex_to_bytes32(key) - for key in sublist.get("storageKeys") - ], - ) - ) - return access_list - - def json_to_tx(self, raw: Any) -> Any: - """Converts json transaction data to a transaction object""" - parameters = [ - hex_to_u256(raw.get("nonce")), - hex_to_u256(raw.get("gasLimit")), - Bytes0(b"") - if raw.get("to") == "" - else self.hex_to_address(raw.get("to")), - hex_to_u256(raw.get("value")), - hex_to_bytes(raw.get("data")), - hex_to_u256( - raw.get("y_parity") if "y_parity" in raw else raw.get("v") - ), - hex_to_u256(raw.get("r")), - hex_to_u256(raw.get("s")), - ] - - # London and beyond - if "maxFeePerGas" in raw and "maxPriorityFeePerGas" in raw: - parameters.insert(0, U64(1)) - parameters.insert(2, hex_to_u256(raw.get("maxPriorityFeePerGas"))) - parameters.insert(3, hex_to_u256(raw.get("maxFeePerGas"))) - parameters.insert( - 8, self.json_to_access_list(raw.get("accessList")) - ) - try: - return b"\x02" + rlp.encode( - self._module("transactions").FeeMarketTransaction( - *parameters - ) - ) - except AttributeError as e: - raise UnsupportedTx( - b"\x02" + rlp.encode(parameters), str(e) - ) from e - - parameters.insert(1, hex_to_u256(raw.get("gasPrice"))) - # Access List Transaction - if "accessList" in raw: - parameters.insert(0, U64(1)) - parameters.insert( - 7, self.json_to_access_list(raw.get("accessList")) - ) - try: - return b"\x01" + rlp.encode( - self._module("transactions").AccessListTransaction( - *parameters - ) - ) - except AttributeError as e: - raise UnsupportedTx( - b"\x01" + rlp.encode(parameters), str(e) - ) from e - - # Legacy Transaction - if hasattr(self._module("transactions"), "LegacyTransaction"): - return self._module("transactions").LegacyTransaction(*parameters) - else: - return self._module("transactions").Transaction(*parameters) - - def json_to_withdrawals(self, raw: Any) -> Any: - """Converts json withdrawal data to a withdrawal object""" - parameters = [ - hex_to_u64(raw.get("index")), - hex_to_u64(raw.get("validatorIndex")), - self.hex_to_address(raw.get("address")), - hex_to_u256(raw.get("amount")), - ] - - return self._module("blocks").Withdrawal(*parameters) - - def json_to_block( - self, - json_block: Any, - ) -> Tuple[Any, Hash32, bytes]: - """Converts json block data to a block object""" - if "rlp" in json_block: - # Always decode from rlp - block_rlp = hex_to_bytes(json_block["rlp"]) - block = rlp.decode_to(self.Block, block_rlp) - block_header_hash = rlp.rlp_hash(block.header) - return block, block_header_hash, block_rlp - - header = self.json_to_header(json_block["blockHeader"]) - transactions = tuple( - self.json_to_tx(tx) for tx in json_block["transactions"] - ) - uncles = tuple( - self.json_to_header(uncle) for uncle in json_block["uncleHeaders"] - ) - - parameters = [ - header, - transactions, - uncles, - ] - - if "withdrawals" in json_block: - withdrawals = tuple( - self.json_to_withdrawals(wd) - for wd in json_block["withdrawals"] - ) - parameters.append(withdrawals) - - block = self.Block(*parameters) - block_header_hash = Hash32( - hex_to_bytes(json_block["blockHeader"]["hash"]) - ) - block_rlp = hex_to_bytes(json_block["rlp"]) - - return block, block_header_hash, block_rlp - - def json_to_header(self, raw: Any) -> Any: - """Converts json header data to a header object""" - parameters = [ - hex_to_hash(raw.get("parentHash")), - hex_to_hash(raw.get("uncleHash") or raw.get("sha3Uncles")), - self.hex_to_address(raw.get("coinbase") or raw.get("miner")), - self.hex_to_root(raw.get("stateRoot")), - self.hex_to_root( - raw.get("transactionsTrie") or raw.get("transactionsRoot") - ), - self.hex_to_root( - raw.get("receiptTrie") or raw.get("receiptsRoot") - ), - self.Bloom(hex_to_bytes(raw.get("bloom") or raw.get("logsBloom"))), - hex_to_uint(raw.get("difficulty")), - hex_to_uint(raw.get("number")), - hex_to_uint(raw.get("gasLimit")), - hex_to_uint(raw.get("gasUsed")), - hex_to_u256(raw.get("timestamp")), - hex_to_bytes(raw.get("extraData")), - hex_to_bytes32(raw.get("mixHash")), - hex_to_bytes8(raw.get("nonce")), - ] - - if "baseFeePerGas" in raw: - base_fee_per_gas = hex_to_uint(raw.get("baseFeePerGas")) - parameters.append(base_fee_per_gas) - - if "withdrawalsRoot" in raw: - withdrawals_root = self.hex_to_root(raw.get("withdrawalsRoot")) - parameters.append(withdrawals_root) - - return self.Header(*parameters) diff --git a/src/ethereum_spec_tools/evm_tools/loaders/__init__.py b/src/ethereum_spec_tools/evm_tools/loaders/__init__.py new file mode 100644 index 0000000000..23d7b50f32 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/loaders/__init__.py @@ -0,0 +1,3 @@ +""" +Loaders for json fixtures and fork code. +""" diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py new file mode 100644 index 0000000000..6f426c39b7 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -0,0 +1,180 @@ +""" +Defines Load class for loading json fixtures for the evm +tools (t8n, b11r, etc.) as well as the execution specs +testing framework. +""" + +from abc import ABC, abstractmethod +from typing import Any, Tuple + +from ethereum import rlp +from ethereum.base_types import U256 +from ethereum.crypto.hash import Hash32 +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes8, + hex_to_bytes32, + hex_to_hash, + hex_to_u64, + hex_to_u256, + hex_to_uint, +) + +from .fork_loader import ForkLoad +from .transaction_loader import TransactionLoad + + +class BaseLoad(ABC): + """Base class for loading JSON fixtures""" + + @abstractmethod + def json_to_header(self, json_data: Any) -> Any: + """Converts json header data to a header object""" + raise NotImplementedError() + + @abstractmethod + def json_to_state(self, json_data: Any) -> Any: + """Converts json state data to a state object""" + raise NotImplementedError() + + @abstractmethod + def json_to_block(self, json_data: Any) -> Any: + """Converts json block data to a list of blocks""" + raise NotImplementedError() + + +class Load(BaseLoad): + """Class for loading json fixtures""" + + _network: str + _fork_module: str + fork: ForkLoad + + def __init__(self, network: str, fork_module: str): + self._network = network + self._fork_module = fork_module + self.fork = ForkLoad(fork_module) + + def json_to_state(self, raw: Any) -> Any: + """Converts json state data to a state object""" + state = self.fork.State() + set_storage = self.fork.set_storage + + for address_hex, account_state in raw.items(): + address = self.fork.hex_to_address(address_hex) + account = self.fork.Account( + nonce=hex_to_uint(account_state.get("nonce", "0x0")), + balance=U256(hex_to_uint(account_state.get("balance", "0x0"))), + code=hex_to_bytes(account_state.get("code", "")), + ) + self.fork.set_account(state, address, account) + + for k, v in account_state.get("storage", {}).items(): + set_storage( + state, + address, + hex_to_bytes32(k), + U256.from_be_bytes(hex_to_bytes32(v)), + ) + return state + + def json_to_withdrawals(self, raw: Any) -> Any: + """Converts json withdrawal data to a withdrawal object""" + parameters = [ + hex_to_u64(raw.get("index")), + hex_to_u64(raw.get("validatorIndex")), + self.fork.hex_to_address(raw.get("address")), + hex_to_u256(raw.get("amount")), + ] + + return self.fork.Withdrawal(*parameters) + + def json_to_block( + self, + json_block: Any, + ) -> Tuple[Any, Hash32, bytes]: + """Converts json block data to a block object""" + if "rlp" in json_block: + # Always decode from rlp + block_rlp = hex_to_bytes(json_block["rlp"]) + block = rlp.decode_to(self.fork.Block, block_rlp) + block_header_hash = rlp.rlp_hash(block.header) + return block, block_header_hash, block_rlp + + header = self.json_to_header(json_block["blockHeader"]) + transactions = tuple( + TransactionLoad(tx, self.fork).read() + for tx in json_block["transactions"] + ) + uncles = tuple( + self.json_to_header(uncle) for uncle in json_block["uncleHeaders"] + ) + + parameters = [ + header, + transactions, + uncles, + ] + + if "withdrawals" in json_block: + withdrawals = tuple( + self.json_to_withdrawals(wd) + for wd in json_block["withdrawals"] + ) + parameters.append(withdrawals) + + block = self.fork.Block(*parameters) + block_header_hash = Hash32( + hex_to_bytes(json_block["blockHeader"]["hash"]) + ) + block_rlp = hex_to_bytes(json_block["rlp"]) + + return block, block_header_hash, block_rlp + + def json_to_header(self, raw: Any) -> Any: + """Converts json header data to a header object""" + parameters = [ + hex_to_hash(raw.get("parentHash")), + hex_to_hash(raw.get("uncleHash") or raw.get("sha3Uncles")), + self.fork.hex_to_address(raw.get("coinbase") or raw.get("miner")), + self.fork.hex_to_root(raw.get("stateRoot")), + self.fork.hex_to_root( + raw.get("transactionsTrie") or raw.get("transactionsRoot") + ), + self.fork.hex_to_root( + raw.get("receiptTrie") or raw.get("receiptsRoot") + ), + self.fork.Bloom( + hex_to_bytes(raw.get("bloom") or raw.get("logsBloom")) + ), + hex_to_uint(raw.get("difficulty")), + hex_to_uint(raw.get("number")), + hex_to_uint(raw.get("gasLimit")), + hex_to_uint(raw.get("gasUsed")), + hex_to_u256(raw.get("timestamp")), + hex_to_bytes(raw.get("extraData")), + hex_to_bytes32(raw.get("mixHash")), + hex_to_bytes8(raw.get("nonce")), + ] + + if "baseFeePerGas" in raw: + base_fee_per_gas = hex_to_uint(raw.get("baseFeePerGas")) + parameters.append(base_fee_per_gas) + + if "withdrawalsRoot" in raw: + withdrawals_root = self.fork.hex_to_root( + raw.get("withdrawalsRoot") + ) + parameters.append(withdrawals_root) + + if "excessBlobGas" in raw: + blob_gas_used = hex_to_u64(raw.get("blobGasUsed")) + parameters.append(blob_gas_used) + excess_blob_gas = hex_to_u64(raw.get("excessBlobGas")) + parameters.append(excess_blob_gas) + parent_beacon_block_root = self.fork.hex_to_root( + raw.get("parentBeaconBlockRoot") + ) + parameters.append(parent_beacon_block_root) + + 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 new file mode 100644 index 0000000000..2b4d4e9ca9 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -0,0 +1,303 @@ +""" +Loader for code from the relevant fork. +""" + +import importlib +from typing import Any + +from ethereum_spec_tools.forks import Hardfork + + +class ForkLoad: + """ + Load the functions and classes from the relevant fork. + """ + + _fork_module: str + _forks: Any + + def __init__(self, fork_module: str): + self._fork_module = fork_module + self._forks = Hardfork.discover() + + @property + def fork_module(self) -> str: + """Module that contains the fork code""" + return self._fork_module + + def _module(self, name: str) -> Any: + """Imports a module from the fork""" + return importlib.import_module(f"ethereum.{self._fork_module}.{name}") + + @property + def proof_of_stake(self) -> bool: + """Whether the fork is proof of stake""" + for fork in self._forks: + if fork.name == "ethereum." + self._fork_module: + return fork.consensus.is_pos() + raise Exception(f"fork {self._fork_module} not discovered") + + def is_after_fork(self, target_fork_name: str) -> bool: + """Check if the fork is after the target fork""" + return_value = False + for fork in self._forks: + if fork.name == target_fork_name: + return_value = True + if fork.name == "ethereum." + self._fork_module: + break + return return_value + + @property + def calculate_block_difficulty(self) -> Any: + """calculate_block_difficulty function of the given fork.""" + return self._module("fork").calculate_block_difficulty + + @property + def calculate_base_fee_per_gas(self) -> Any: + """calculate_base_fee_per_gas function of the given fork.""" + return self._module("fork").calculate_base_fee_per_gas + + @property + def logs_bloom(self) -> Any: + """logs_bloom function of the given fork.""" + return self._module("bloom").logs_bloom + + @property + def BlockChain(self) -> Any: + """Block chain class of the fork""" + return self._module("fork").BlockChain + + @property + def state_transition(self) -> Any: + """state_transition function of the fork""" + return self._module("fork").state_transition + + @property + def make_receipt(self) -> Any: + """make_receipt function of the fork""" + return self._module("fork").make_receipt + + @property + def signing_hash(self) -> Any: + """signing_hash function of the fork""" + return self._module("fork").signing_hash + + @property + def signing_hash_pre155(self) -> Any: + """signing_hash_pre155 function of the fork""" + return self._module("fork").signing_hash_pre155 + + @property + def signing_hash_155(self) -> Any: + """signing_hash_155 function of the fork""" + return self._module("fork").signing_hash_155 + + @property + def signing_hash_2930(self) -> Any: + """signing_hash_2930 function of the fork""" + return self._module("fork").signing_hash_2930 + + @property + def signing_hash_1559(self) -> Any: + """signing_hash_1559 function of the fork""" + return self._module("fork").signing_hash_1559 + + @property + def signing_hash_4844(self) -> Any: + """signing_hash_4844 function of the fork""" + return self._module("fork").signing_hash_4844 + + @property + def check_transaction(self) -> Any: + """check_transaction function of the fork""" + return self._module("fork").check_transaction + + @property + def process_transaction(self) -> Any: + """process_transaction function of the fork""" + return self._module("fork").process_transaction + + @property + def MAX_BLOB_GAS_PER_BLOCK(self) -> Any: + """MAX_BLOB_GAS_PER_BLOCK parameter of the fork""" + return self._module("fork").MAX_BLOB_GAS_PER_BLOCK + + @property + def Block(self) -> Any: + """Block class of the fork""" + return self._module("blocks").Block + + @property + def Bloom(self) -> Any: + """Bloom class of the fork""" + return self._module("fork_types").Bloom + + @property + def Header(self) -> Any: + """Header class of the fork""" + return self._module("blocks").Header + + @property + def Account(self) -> Any: + """Account class of the fork""" + return self._module("fork_types").Account + + @property + def Transaction(self) -> Any: + """Transaction class of the fork""" + return self._module("transactions").Transaction + + @property + def LegacyTransaction(self) -> Any: + """Legacytransaction class of the fork""" + return self._module("transactions").LegacyTransaction + + @property + def AccessListTransaction(self) -> Any: + """Access List transaction class of the fork""" + return self._module("transactions").AccessListTransaction + + @property + def FeeMarketTransaction(self) -> Any: + """Fee Market transaction class of the fork""" + return self._module("transactions").FeeMarketTransaction + + @property + def BlobTransaction(self) -> Any: + """Blob transaction class of the fork""" + return self._module("transactions").BlobTransaction + + @property + def Withdrawal(self) -> Any: + """Withdrawal class of the fork""" + return self._module("blocks").Withdrawal + + @property + def encode_transaction(self) -> Any: + """encode_transaction function of the fork""" + return self._module("transactions").encode_transaction + + @property + def decode_transaction(self) -> Any: + """decode_transaction function of the fork""" + return self._module("transactions").decode_transaction + + @property + 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""" + return self._module("state").state_root + + @property + 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""" + return self._module("trie").root + + @property + 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""" + return self._module("utils.hexadecimal").hex_to_address + + @property + def hex_to_root(self) -> Any: + """hex_to_root function of the fork""" + return self._module("utils.hexadecimal").hex_to_root + + @property + def Environment(self) -> Any: + """Environment class of the fork""" + return self._module("vm").Environment + + @property + def Message(self) -> Any: + """Message class of the fork""" + return self._module("vm").Message + + @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""" + return self._module("dao").apply_dao diff --git a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py new file mode 100644 index 0000000000..7784a42540 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py @@ -0,0 +1,181 @@ +""" +Read transaction data from json file and return the +relevant transaction. +""" + +from dataclasses import fields +from typing import Any, List + +from ethereum import rlp +from ethereum.base_types import U64, U256, Bytes, Bytes0, Bytes32, Uint +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes32, + hex_to_hash, + hex_to_u256, + hex_to_uint, +) + + +class UnsupportedTx(Exception): + """Exception for unsupported transactions""" + + def __init__(self, encoded_params: bytes, error_message: str) -> None: + super().__init__(error_message) + self.encoded_params = encoded_params + self.error_message = error_message + + +class TransactionLoad: + """ + Class for loading transaction data from json file + """ + + def __init__(self, raw: Any, fork: Any) -> None: + self.raw = raw + self.fork = fork + + def json_to_chain_id(self) -> U64: + """Get chain ID for the transaction.""" + return U64(1) + + def json_to_nonce(self) -> U256: + """Get the nonce for the transaction.""" + return hex_to_u256(self.raw.get("nonce")) + + def json_to_gas_price(self) -> Uint: + """Get the gas price for the transaction.""" + return hex_to_uint(self.raw.get("gasPrice")) + + def json_to_gas(self) -> Uint: + """Get the gas limit for the transaction.""" + return hex_to_uint(self.raw.get("gasLimit")) + + def json_to_to(self) -> Bytes: + """Get to address for the transaction.""" + if self.raw.get("to") == "": + return Bytes0(b"") + return self.fork.hex_to_address(self.raw.get("to")) + + def json_to_value(self) -> U256: + """Get the value of the transaction.""" + value = self.raw.get("value") + if value == "0x": + return U256(0) + return hex_to_u256(value) + + def json_to_data(self) -> Bytes: + """Get the data of the transaction.""" + return hex_to_bytes(self.raw.get("data")) + + def json_to_access_list(self) -> Any: + """Get the access list of the transaction.""" + access_list = [] + for sublist in self.raw["accessList"]: + access_list.append( + ( + self.fork.hex_to_address(sublist.get("address")), + [ + hex_to_bytes32(key) + for key in sublist.get("storageKeys") + ], + ) + ) + return access_list + + 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")) + + def json_to_max_fee_per_gas(self) -> Uint: + """Get the max fee per gas of the transaction.""" + return hex_to_uint(self.raw.get("maxFeePerGas")) + + def json_to_max_fee_per_blob_gas(self) -> U256: + """ + Get the max priority fee per blobgas of the transaction. + """ + return hex_to_u256(self.raw.get("maxFeePerBlobGas")) + + def json_to_blob_versioned_hashes(self) -> List[Bytes32]: + """Get the blob versioned hashes of the transaction.""" + return [ + hex_to_hash(blob_hash) + for blob_hash in self.raw.get("blobVersionedHashes") + ] + + def json_to_v(self) -> U256: + """Get the v value of the transaction.""" + return hex_to_u256( + self.raw.get("y_parity") + if "y_parity" in self.raw + else self.raw.get("v") + ) + + def json_to_y_parity(self) -> U256: + """Get the y parity of the transaction.""" + return self.json_to_v() + + def json_to_r(self) -> U256: + """Get the r value of the transaction""" + return hex_to_u256(self.raw.get("r")) + + def json_to_s(self) -> U256: + """Get the s value of the transaction""" + return hex_to_u256(self.raw.get("s")) + + def get_parameters(self, tx_cls: Any) -> List: + """ + Extract all the transaction parameters from the json file + """ + parameters = [] + for field in fields(tx_cls): + parameters.append(getattr(self, f"json_to_{field.name}")()) + return parameters + + def get_legacy_transaction(self) -> Any: + """Return the approprtiate class for legacy transactions.""" + if hasattr(self.fork, "LegacyTransaction"): + return self.fork.LegacyTransaction + else: + return self.fork.Transaction + + 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_cls = self.fork.BlobTransaction + tx_byte_prefix = b"\x03" + elif tx_type == "0x2": + tx_cls = self.fork.FeeMarketTransaction + tx_byte_prefix = b"\x02" + elif tx_type == "0x1": + tx_cls = self.fork.AccessListTransaction + tx_byte_prefix = b"\x01" + elif tx_type == "0x0": + 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: + tx_cls = self.fork.BlobTransaction + tx_byte_prefix = b"\x03" + elif "maxFeePerGas" in self.raw: + tx_cls = self.fork.FeeMarketTransaction + tx_byte_prefix = b"\x02" + elif "accessList" in self.raw: + tx_cls = self.fork.AccessListTransaction + tx_byte_prefix = b"\x01" + else: + tx_cls = self.get_legacy_transaction() + tx_byte_prefix = b"" + + parameters = self.get_parameters(tx_cls) + try: + return tx_cls(*parameters) + except Exception as e: + raise UnsupportedTx( + tx_byte_prefix + rlp.encode(parameters), str(e) + ) from e diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py new file mode 100644 index 0000000000..196352655d --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -0,0 +1,290 @@ +""" +Execute state tests. +""" + +import argparse +import json +import logging +import sys +from copy import deepcopy +from dataclasses import dataclass +from io import StringIO +from typing import Any, Dict, Iterable, List, Optional, TextIO + +from ethereum.utils.hexadecimal import hex_to_bytes + +from ..t8n import T8N +from ..t8n.t8n_types import Result +from ..utils import get_supported_forks + + +@dataclass +class TestCase: + """ + A test case derived from the inputs common to all forks and a single + post-state unique to a single fork. + """ + + path: str + key: str + index: int + fork_name: str + post: Dict + pre: Dict + env: Dict + transaction: Dict + + +def read_test_cases(test_file_path: str) -> Iterable[TestCase]: + """ + Given a path to a filled state test in JSON format, return all the + `TestCase`s it contains. + """ + with open(test_file_path) as test_file: + tests = json.load(test_file) + + for key, test in tests.items(): + env = test["env"] + if not isinstance(env, dict): + raise TypeError("env not dict") + + pre = test["pre"] + if not isinstance(pre, dict): + raise TypeError("pre not dict") + + transaction = test["transaction"] + if not isinstance(transaction, dict): + raise TypeError("transaction not dict") + + for fork_name, content in test["post"].items(): + for idx, post in enumerate(content): + if not isinstance(post, dict): + raise TypeError(f'post["{fork_name}"] not dict') + + yield TestCase( + path=test_file_path, + key=key, + index=idx, + fork_name=fork_name, + post=post, + env=env, + pre=pre, + transaction=transaction, + ) + + +def run_test_case( + test_case: TestCase, + t8n_extra: Optional[List[str]] = None, + output_basedir: Optional[str | TextIO] = None, +) -> Result: + """ + Runs a single general state test + """ + from .. import create_parser + + env = deepcopy(test_case.env) + try: + env["blockHashes"] = {"0": env["previousHash"]} + except KeyError: + env["blockHashes"] = {} + env["withdrawals"] = [] + + alloc = deepcopy(test_case.pre) + + post = deepcopy(test_case.post) + d = post["indexes"]["data"] + g = post["indexes"]["gas"] + v = post["indexes"]["value"] + + tx = {} + for k, value in test_case.transaction.items(): + if k == "data": + tx["input"] = value[d] + elif k == "gasLimit": + tx["gas"] = value[g] + elif k == "value": + tx[k] = value[v] + elif k == "accessLists": + if value[d] is not None: + tx["accessList"] = value[d] + else: + tx[k] = value + + txs = [tx] + + in_stream = StringIO( + json.dumps( + { + "env": env, + "alloc": alloc, + "txs": txs, + } + ) + ) + out_stream = StringIO() + + # Run the t8n tool + t8n_args = [ + "t8n", + "--input.alloc", + "stdin", + "--input.env", + "stdin", + "--input.txs", + "stdin", + "--state.fork", + f"{test_case.fork_name}", + ] + + if t8n_extra is not None: + t8n_args.extend(t8n_extra) + + parser = create_parser() + t8n_options = parser.parse_args(t8n_args) + if output_basedir is not None: + t8n_options.output_basedir = output_basedir + + t8n = T8N(t8n_options, out_stream, in_stream) + t8n.apply_body() + return t8n.result + + +def state_test_arguments(subparsers: argparse._SubParsersAction) -> None: + """ + Adds the arguments for the statetest tool subparser. + """ + statetest_parser = subparsers.add_parser( + "statetest", + help="Runs state tests from a file or from the standard input.", + ) + + statetest_parser.add_argument("file", nargs="?", default=None) + statetest_parser.add_argument("--json", action="store_true", default=False) + statetest_parser.add_argument( + "--noreturndata", + dest="return_data", + action="store_false", + default=True, + ) + statetest_parser.add_argument( + "--nostack", dest="stack", action="store_false", default=True + ) + statetest_parser.add_argument( + "--nomemory", dest="memory", action="store_false", default=True + ) + + +class _PrefixFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + output = super().format(record) + return "\n".join("# " + x for x in output.splitlines()) + + +class StateTest: + """ + Run one or more state tests. + """ + + def __init__( + self, options: Any, out_file: TextIO, in_file: TextIO + ) -> None: + self.file = options.file + self.out_file = out_file + self.in_file = in_file + self.supported_forks = tuple( + x.casefold() for x in get_supported_forks() + ) + self.trace: bool = options.json + self.memory: bool = options.memory + self.stack: bool = options.stack + self.return_data: bool = options.return_data + + def run(self) -> int: + """ + Execute the tests. + """ + logger = logging.getLogger("T8N") + logger.setLevel(level=logging.INFO) + stream_handler = logging.StreamHandler() + formatter = _PrefixFormatter("%(levelname)s:%(name)s:%(message)s") + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + if self.file is None: + return self.run_many() + else: + return self.run_one(self.file) + + def run_one(self, path: str) -> int: + """ + Execute state tests from a single file. + """ + results = [] + for test_case in read_test_cases(path): + if test_case.fork_name.casefold() not in self.supported_forks: + continue + + t8n_extra: List[str] = [] + + if self.trace: + t8n_extra.append("--trace") + + if self.memory: + t8n_extra.append("--trace.memory") + else: + t8n_extra.append("--trace.nomemory") + + if not self.stack: + t8n_extra.append("--trace.nostack") + + if self.return_data: + t8n_extra.append("--trace.returndata") + else: + t8n_extra.append("--trace.noreturndata") + + result = run_test_case( + test_case, + t8n_extra=t8n_extra, + output_basedir=sys.stderr, + ) + + # Always output the state root on stderr (even with tracing + # disabled) for the holiman/goevmlab integration. + json.dump( + {"stateRoot": "0x" + result.state_root.hex()}, + sys.stderr, + ) + sys.stderr.write("\n") + + passed = hex_to_bytes(test_case.post["hash"]) == result.state_root + result_dict = { + "stateRoot": "0x" + result.state_root.hex(), + "fork": test_case.fork_name, + "name": test_case.key, + "pass": passed, + } + + if not passed: + actual = result.state_root.hex() + expected = test_case.post["hash"][2:] + result_dict[ + "error" + ] = f"post state root mismatch: got {actual}, want {expected}" + + results.append(result_dict) + + json.dump(results, self.out_file, indent=4) + self.out_file.write("\n") + return 0 + + def run_many(self) -> int: + """ + Execute state tests from a line-delimited list of files provided from + `self.in_file`. + """ + for line in self.in_file: + result = self.run_one(line[:-1]) + if result != 0: + return result + return 0 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 7e022ce1dc..74d5dd95f6 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -5,17 +5,18 @@ import argparse import json import os -import sys from functools import partial -from typing import Any +from typing import Any, TextIO from ethereum import rlp, trace from ethereum.base_types import U64, U256, Uint from ethereum.crypto.hash import keccak256 -from ethereum.exceptions import InvalidBlock +from ethereum.exceptions import EthereumException, InvalidBlock +from ethereum.utils.ensure import ensure from ethereum_spec_tools.forks import Hardfork -from ..fixture_loader import Load +from ..loaders.fixture_loader import Load +from ..loaders.fork_loader import ForkLoad from ..utils import ( FatalException, get_module_name, @@ -75,7 +76,11 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: class T8N(Load): """The class that carries out the transition""" - def __init__(self, options: Any) -> None: + def __init__( + self, options: Any, out_file: TextIO, in_file: TextIO + ) -> None: + self.out_file = out_file + self.in_file = in_file self.options = options self.forks = Hardfork.discover() @@ -84,13 +89,14 @@ def __init__(self, options: Any) -> None: options.input_alloc, options.input_txs, ): - stdin = json.load(sys.stdin) + stdin = json.load(in_file) else: stdin = None fork_module, self.fork_block = get_module_name( self.forks, self.options, stdin ) + self.fork = ForkLoad(fork_module) if self.options.trace: trace_memory = getattr(self.options, "trace.memory", False) @@ -117,45 +123,14 @@ def __init__(self, options: Any) -> None: self.env.block_difficulty, self.env.base_fee_per_gas ) - @property - def fork(self) -> Any: - """The fork module of the given fork.""" - return self._module("fork") - - @property - def transactions(self) -> Any: - """The transactions module of the given fork.""" - return self._module("transactions") - - @property - def blocks(self) -> Any: - """The blocks module of the given fork.""" - return self._module("blocks") - - @property - def fork_types(self) -> Any: - """The fork_types model of the given fork.""" - return self._module("fork_types") - - @property - def state(self) -> Any: - """The state module of the given fork.""" - return self._module("state") - - @property - def trie(self) -> Any: - """The trie module of the given fork.""" - return self._module("trie") - - @property - def bloom(self) -> Any: - """The bloom module of the given fork.""" - return self._module("bloom") - - @property - def vm(self) -> Any: - """The vm module of the given fork.""" - return self._module("vm") + 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: @@ -163,23 +138,13 @@ 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.is_after_fork( + if self.options.state_reward < 0 or self.fork.is_after_fork( "ethereum.paris" ): return None else: return U256(self.options.state_reward) - def is_after_fork(self, target_fork_name: str) -> bool: - """Check if the fork is after the target fork""" - return_value = False - for fork in self.forks: - if fork.name == target_fork_name: - return_value = True - if fork.name == "ethereum." + self._fork_module: - break - return return_value - def check_transaction(self, tx: Any, gas_available: Any) -> Any: """ Implements the check_transaction function of the fork. @@ -187,12 +152,12 @@ def check_transaction(self, tx: Any, gas_available: Any) -> Any: """ arguments = [tx] - if self.is_after_fork("ethereum.london"): + if self.fork.is_after_fork("ethereum.london"): arguments.append(self.env.base_fee_per_gas) arguments.append(gas_available) - if self.is_after_fork("ethereum.spurious_dragon"): + if self.fork.is_after_fork("ethereum.spurious_dragon"): arguments.append(self.chain_id) return self.fork.check_transaction(*arguments) @@ -211,15 +176,30 @@ def environment(self, tx: Any, gas_available: Any) -> Any: "state": self.alloc.state, } - if self.is_after_fork("ethereum.paris"): + 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.is_after_fork("ethereum.istanbul"): + if self.fork.is_after_fork("ethereum.istanbul"): kw_arguments["chain_id"] = self.chain_id - if self.is_after_fork("ethereum.london"): + if self.fork.is_after_fork("ethereum.cancun"): + ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + ) = self.fork.check_transaction( + tx, + self.env.base_fee_per_gas, + gas_available, + self.chain_id, + ) + 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 + elif self.fork.is_after_fork("ethereum.london"): sender_address, effective_gas_price = self.fork.check_transaction( tx, self.env.base_fee_per_gas, @@ -229,7 +209,7 @@ def environment(self, tx: Any, gas_available: Any) -> Any: 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 - elif self.is_after_fork("ethereum.spurious_dragon"): + elif self.fork.is_after_fork("ethereum.spurious_dragon"): sender_address = self.fork.check_transaction( tx, gas_available, self.chain_id ) @@ -242,17 +222,21 @@ def environment(self, tx: Any, gas_available: Any) -> Any: kw_arguments["traces"] = [] - return self.vm.Environment(**kw_arguments) + if self.fork.is_after_fork("ethereum.cancun"): + kw_arguments["excess_blob_gas"] = self.env.excess_blob_gas + kw_arguments["transient_storage"] = self.fork.TransientStorage() + + 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.is_after_fork("ethereum.berlin"): - arguments.append(self.fork_types.encode_transaction(tx)) + if self.fork.is_after_fork("ethereum.berlin"): + arguments.append(self.fork.encode_transaction(tx)) else: arguments.append(tx) - self.trie.trie_set(*arguments) + self.fork.trie_set(*arguments) def make_receipt( self, tx: Any, process_transaction_return: Any, gas_available: Any @@ -260,10 +244,10 @@ def make_receipt( """Create a transaction receipt.""" arguments = [tx] - if self.is_after_fork("ethereum.byzantium"): + if self.fork.is_after_fork("ethereum.byzantium"): arguments.append(process_transaction_return[2]) else: - arguments.append(self.state.state_root(self.alloc.state)) + arguments.append(self.fork.state_root(self.alloc.state)) arguments.append((self.env.block_gas_limit - gas_available)) arguments.append(process_transaction_return[1]) @@ -283,30 +267,30 @@ def pay_rewards(self) -> None: miner_reward = self.BLOCK_REWARD + ( len(ommers) * (self.BLOCK_REWARD // 32) ) - self.state.create_ether(state, coinbase, miner_reward) + 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.state.create_ether(state, ommer.address, ommer_miner_reward) + self.fork.create_ether(state, ommer.address, ommer_miner_reward) touched_accounts.append(ommer.address) - if self.is_after_fork("ethereum.spurious_dragon"): + 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.state.account_exists_and_is_empty(state, account): - self.state.destroy_account(state, account) + if self.fork.account_exists_and_is_empty(state, account): + self.fork.destroy_account(state, account) def backup_state(self) -> None: """Back up the state in order to restore in case of an error.""" state = self.alloc.state self.alloc.state_backup = ( - self.trie.copy_trie(state._main_trie), + self.fork.copy_trie(state._main_trie), { - k: self.trie.copy_trie(t) + k: self.fork.copy_trie(t) for (k, t) in state._storage_tries.items() }, ) @@ -327,9 +311,62 @@ def apply_body(self) -> None: block_gas_limit = self.env.block_gas_limit gas_available = block_gas_limit - transactions_trie = self.trie.Trie(secured=False, default=None) - receipts_trie = self.trie.Trie(secured=False, default=None) + 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, + ) + + system_tx_env = self.fork.Environment( + caller=self.SYSTEM_ADDRESS, + origin=self.SYSTEM_ADDRESS, + block_hashes=self.env.block_hashes, + coinbase=self.env.coinbase, + number=self.env.block_number, + gas_limit=self.env.block_gas_limit, + base_fee_per_gas=self.env.base_fee_per_gas, + gas_price=self.env.base_fee_per_gas, + time=self.env.block_timestamp, + prev_randao=self.env.prev_randao, + state=self.alloc.state, + chain_id=self.chain_id, + traces=[], + excess_blob_gas=self.env.excess_blob_gas, + blob_versioned_hashes=(), + transient_storage=self.fork.TransientStorage(), + ) + + system_tx_output = self.fork.process_message_call( + system_tx_message, system_tx_env + ) + + self.fork.destroy_touched_empty_accounts( + system_tx_env.state, system_tx_output.touched_accounts + ) for i, (tx_idx, tx) in enumerate(self.txs.transactions): # i is the index among valid transactions @@ -340,8 +377,17 @@ def apply_body(self) -> None: try: env = self.environment(tx, gas_available) - process_transaction_return = self.process_transaction(env, tx) - except InvalidBlock as e: + process_transaction_return = self.fork.process_transaction( + env, tx + ) + + if self.fork.is_after_fork("ethereum.cancun"): + blob_gas_used += self.fork.calculate_total_blob_gas(tx) + ensure( + blob_gas_used <= self.fork.MAX_BLOB_GAS_PER_BLOCK, + 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: {str(e)}" @@ -363,7 +409,7 @@ def apply_body(self) -> None: tx, process_transaction_return, gas_available ) - self.trie.trie_set( + self.fork.trie_set( receipts_trie, rlp.encode(Uint(i)), receipt, @@ -380,30 +426,34 @@ def apply_body(self) -> None: block_gas_used = block_gas_limit - gas_available - block_logs_bloom = self.bloom.logs_bloom(block_logs) + block_logs_bloom = self.fork.logs_bloom(block_logs) logs_hash = keccak256(rlp.encode(block_logs)) - if self.is_after_fork("ethereum.shanghai"): - withdrawals_trie = self.trie.Trie(secured=False, default=None) + 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.trie.trie_set( + self.fork.trie_set( withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd) ) - self.state.process_withdrawal(self.alloc.state, wd) + self.fork.process_withdrawal(self.alloc.state, wd) - if self.state.account_exists_and_is_empty( + if self.fork.account_exists_and_is_empty( self.alloc.state, wd.address ): - self.state.destroy_account(self.alloc.state, wd.address) + self.fork.destroy_account(self.alloc.state, wd.address) + + self.result.withdrawals_root = self.fork.root(withdrawals_trie) - self.result.withdrawals_root = self.trie.root(withdrawals_trie) + if self.fork.is_after_fork("ethereum.cancun"): + self.result.blob_gas_used = blob_gas_used + self.result.excess_blob_gas = self.env.excess_blob_gas - self.result.state_root = self.state.state_root(self.alloc.state) - self.result.tx_root = self.trie.root(transactions_trie) - self.result.receipt_root = self.trie.root(receipts_trie) + self.result.state_root = self.fork.state_root(self.alloc.state) + self.result.tx_root = self.fork.root(transactions_trie) + self.result.receipt_root = self.fork.root(receipts_trie) self.result.bloom = block_logs_bloom self.result.logs_hash = logs_hash self.result.rejected = self.txs.rejected_txs @@ -459,6 +509,6 @@ def run(self) -> int: self.logger.info(f"Wrote result to {result_output_path}") if json_output: - json.dump(json_output, sys.stdout, indent=4) + json.dump(json_output, self.out_file, indent=4) return 0 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index d08f322b01..70ace92834 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -3,16 +3,19 @@ """ import json from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional from ethereum import rlp -from ethereum.base_types import U256, Bytes32, Uint +from ethereum.base_types import U64, U256, Bytes32, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.utils.byte import left_pad_zero_bytes from ethereum.utils.hexadecimal import hex_to_bytes from ..utils import parse_hex_or_int +if TYPE_CHECKING: + from ethereum_spec_tools.evm_tools.t8n import T8N + @dataclass class Ommer: @@ -43,8 +46,12 @@ class Env: block_hashes: Optional[List[Any]] parent_ommers_hash: Optional[Hash32] ommers: Any + parent_beacon_block_root: Optional[Hash32] + parent_excess_blob_gas: Optional[U64] + parent_blob_gas_used: Optional[U64] + excess_blob_gas: Optional[U64] - def __init__(self, t8n: Any, stdin: Optional[Dict] = None): + def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): if t8n.options.input_env == "stdin": assert stdin is not None data = stdin["env"] @@ -52,7 +59,7 @@ def __init__(self, t8n: Any, stdin: Optional[Dict] = None): with open(t8n.options.input_env, "r") as f: data = json.load(f) - self.coinbase = t8n.hex_to_address(data["currentCoinbase"]) + self.coinbase = t8n.fork.hex_to_address(data["currentCoinbase"]) self.block_gas_limit = parse_hex_or_int(data["currentGasLimit"], Uint) self.block_number = parse_hex_or_int(data["currentNumber"], Uint) self.block_timestamp = parse_hex_or_int(data["currentTimestamp"], U256) @@ -64,7 +71,54 @@ def __init__(self, t8n: Any, stdin: Optional[Dict] = None): self.read_ommers(data, t8n) self.read_withdrawals(data, t8n) - def read_base_fee_per_gas(self, data: Any, t8n: Any) -> 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 + ) + self.read_excess_blob_gas(data, t8n) + + def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: + """ + Read the excess_blob_gas from the data. If the excess blob gas is + not present, it is calculated from the parent block parameters. + """ + self.parent_blob_gas_used = U64(0) + self.parent_excess_blob_gas = U64(0) + self.excess_blob_gas = None + + if not t8n.fork.is_after_fork("ethereum.cancun"): + return + + if "currentExcessBlobGas" in data: + self.excess_blob_gas = parse_hex_or_int( + data["currentExcessBlobGas"], U64 + ) + return + + if "parentExcessBlobGas" in data: + self.parent_excess_blob_gas = parse_hex_or_int( + data["parentExcessBlobGas"], U64 + ) + + if "parentBlobGasUsed" in data: + self.parent_blob_gas_used = parse_hex_or_int( + data["parentBlobGasUsed"], U64 + ) + + excess_blob_gas = ( + self.parent_excess_blob_gas + self.parent_blob_gas_used + ) + + target_blob_gas_per_block = t8n.fork.TARGET_BLOB_GAS_PER_BLOCK + + self.excess_blob_gas = U64(0) + if excess_blob_gas >= target_blob_gas_per_block: + self.excess_blob_gas = excess_blob_gas - target_blob_gas_per_block + + def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: """ Read the base_fee_per_gas from the data. If the base fee is not present, it is calculated from the parent block parameters. @@ -74,7 +128,7 @@ def read_base_fee_per_gas(self, data: Any, t8n: Any) -> None: self.parent_base_fee_per_gas = None self.base_fee_per_gas = None - if t8n.is_after_fork("ethereum.london"): + if t8n.fork.is_after_fork("ethereum.london"): if "currentBaseFee" in data: self.base_fee_per_gas = parse_hex_or_int( data["currentBaseFee"], Uint @@ -89,7 +143,7 @@ def read_base_fee_per_gas(self, data: Any, t8n: Any) -> None: self.parent_base_fee_per_gas = parse_hex_or_int( data["parentBaseFee"], Uint ) - parameters = [ + parameters: List[object] = [ self.block_gas_limit, self.parent_gas_limit, self.parent_gas_used, @@ -98,19 +152,19 @@ def read_base_fee_per_gas(self, data: Any, t8n: Any) -> None: # TODO: See if this explicit check can be removed. See # https://github.com/ethereum/execution-specs/issues/740 - if t8n.fork_module == "london": + if t8n.fork.fork_module == "london": parameters.append(t8n.fork_block == self.block_number) self.base_fee_per_gas = t8n.fork.calculate_base_fee_per_gas( *parameters ) - def read_randao(self, data: Any, t8n: Any) -> None: + def read_randao(self, data: Any, t8n: "T8N") -> None: """ Read the randao from the data. """ self.prev_randao = None - if t8n.is_after_fork("ethereum.paris"): + if t8n.fork.is_after_fork("ethereum.paris"): # tf tool might not always provide an # even number of nibbles in the randao # This could create issues in the @@ -126,17 +180,17 @@ def read_randao(self, data: Any, t8n: Any) -> None: left_pad_zero_bytes(hex_to_bytes(current_random), 32) ) - def read_withdrawals(self, data: Any, t8n: Any) -> None: + def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ Read the withdrawals from the data. """ self.withdrawals = None - if t8n.is_after_fork("ethereum.shanghai"): + if t8n.fork.is_after_fork("ethereum.shanghai"): self.withdrawals = tuple( t8n.json_to_withdrawals(wd) for wd in data["withdrawals"] ) - def read_block_difficulty(self, data: Any, t8n: Any) -> None: + def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: """ Read the block difficulty from the data. If `currentDifficulty` is present, it is used. Otherwise, @@ -146,7 +200,7 @@ def read_block_difficulty(self, data: Any, t8n: Any) -> None: self.parent_timestamp = None self.parent_difficulty = None self.parent_ommers_hash = None - if t8n.is_after_fork("ethereum.paris"): + if t8n.fork.is_after_fork("ethereum.paris"): return elif "currentDifficulty" in data: self.block_difficulty = parse_hex_or_int( @@ -165,7 +219,7 @@ def read_block_difficulty(self, data: Any, t8n: Any) -> None: self.parent_timestamp, self.parent_difficulty, ] - if t8n.is_after_fork("ethereum.byzantium"): + if t8n.fork.is_after_fork("ethereum.byzantium"): if "parentUncleHash" in data: EMPTY_OMMER_HASH = keccak256(rlp.encode([])) self.parent_ommers_hash = Hash32( @@ -199,7 +253,7 @@ def read_block_hashes(self, data: Any) -> None: self.block_hashes = block_hashes - def read_ommers(self, data: Any, t8n: Any) -> None: + def read_ommers(self, data: Any, t8n: "T8N") -> None: """ Read the ommers. The ommers data might not have all the details needed to obtain the Header. @@ -210,7 +264,7 @@ def read_ommers(self, data: Any, t8n: Any) -> None: ommers.append( Ommer( ommer["delta"], - t8n.hex_to_address(ommer["address"]), + t8n.fork.hex_to_address(ommer["address"]), ) ) self.ommers = ommers 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 f24f08ee47..a8ae33cdf9 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py @@ -3,7 +3,8 @@ """ import json import os -from dataclasses import dataclass, fields +from contextlib import ExitStack +from dataclasses import asdict, dataclass, is_dataclass from typing import List, Optional, Protocol, TextIO, Union, runtime_checkable from ethereum.base_types import U256, Bytes, Uint @@ -30,7 +31,7 @@ class Trace: """ pc: int - op: str + op: Optional[Union[str, int]] gas: str gasCost: str memory: Optional[str] @@ -186,9 +187,12 @@ def evm_trace( last_trace.gasCostTraced = True last_trace.errorTraced = True elif isinstance(event, OpStart): + op = event.op.value + if op == "InvalidOpcode": + op = "Invalid" new_trace = Trace( pc=evm.pc, - op=event.op.value, + op=op, gas=hex(evm.gas_left), gasCost="0x0", memory=memory, @@ -222,9 +226,15 @@ def evm_trace( # two conditions do not cover it. or last_trace.depth == evm.message.depth ): + if not hasattr(event.error, "code"): + name = event.error.__class__.__name__ + raise TypeError( + f"OpException event error type `{name}` does not have code" + ) from event.error + new_trace = Trace( pc=evm.pc, - op="InvalidOpcode", + op=event.error.code, gas=hex(evm.gas_left), gasCost="0x0", memory=memory, @@ -271,20 +281,41 @@ def evm_trace( last_trace.gasCostTraced = True +class _TraceJsonEncoder(json.JSONEncoder): + @staticmethod + def retain(k: str, v: Optional[object]) -> bool: + if v is None: + return False + + if k in EXCLUDE_FROM_OUTPUT: + return False + + if k in ("pc", "gas", "gasCost", "refund"): + if isinstance(v, str) and int(v, 0).bit_length() > 64: + return False + + return True + + def default(self, obj: object) -> object: + if not is_dataclass(obj) or isinstance(obj, type): + return super().default(obj) + + trace = { + k: v + for k, v in asdict(obj).items() + if _TraceJsonEncoder.retain(k, v) + } + + return trace + + def output_op_trace( trace: Union[Trace, FinalTrace], json_file: TextIO ) -> None: """ Output a single trace to a json file. """ - dict_trace = { - field.name: getattr(trace, field.name) - for field in fields(trace) - if field.name not in EXCLUDE_FROM_OUTPUT - and getattr(trace, field.name) is not None - } - - json.dump(dict_trace, json_file, separators=(",", ":")) + json.dump(trace, json_file, separators=(",", ":"), cls=_TraceJsonEncoder) json_file.write("\n") @@ -292,16 +323,24 @@ def output_traces( traces: List[Union[Trace, FinalTrace]], tx_index: int, tx_hash: bytes, - output_basedir: str = ".", + output_basedir: str | TextIO = ".", ) -> None: """ Output the traces to a json file. """ - tx_hash_str = "0x" + tx_hash.hex() - output_path = os.path.join( - output_basedir, f"trace-{tx_index}-{tx_hash_str}.jsonl" - ) - with open(output_path, "w") as json_file: + with ExitStack() as stack: + json_file: TextIO + + if isinstance(output_basedir, str): + tx_hash_str = "0x" + tx_hash.hex() + output_path = os.path.join( + output_basedir, f"trace-{tx_index}-{tx_hash_str}.jsonl" + ) + json_file = open(output_path, "w") + stack.push(json_file) + else: + json_file = output_basedir + for trace in traces: if getattr(trace, "precompile", False): # Traces related to pre-compile are not output. 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 ec78ab8a98..ca84c71d1f 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -10,7 +10,7 @@ from ethereum.crypto.hash import keccak256 from ethereum.utils.hexadecimal import hex_to_bytes, hex_to_u256, hex_to_uint -from ..fixture_loader import UnsupportedTx +from ..loaders.transaction_loader import TransactionLoad, UnsupportedTx from ..utils import FatalException, secp256k1_sign if TYPE_CHECKING: @@ -38,13 +38,13 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): # strings, so we convert them here. for address, account in data.items(): for key, value in account.items(): - if key == "storage": + if key == "storage" or not value: continue elif not value.startswith("0x"): data[address][key] = "0x" + hex(int(value)) state = t8n.json_to_state(data) - if t8n.fork_module == "dao_fork": + if t8n.fork.fork_module == "dao_fork": t8n.fork.apply_dao(state) self.state = state @@ -136,12 +136,9 @@ def transactions(self) -> Iterator[Tuple[int, Any]]: ] = f"Unsupported transaction type: {e.error_message}" self.all_txs.append(e.encoded_params) except Exception as e: - self.t8n.logger.warning( - f"Failed to parse transaction {idx}: {str(e)}" - ) - self.rejected_txs[ - idx - ] = f"Failed to parse transaction {idx}: {str(e)}" + msg = f"Failed to parse transaction {idx}: {str(e)}" + self.t8n.logger.warning(msg, exc_info=e) + self.rejected_txs[idx] = msg def parse_rlp_tx(self, raw_tx: Any) -> Any: """ @@ -150,17 +147,15 @@ def parse_rlp_tx(self, raw_tx: Any) -> Any: t8n = self.t8n tx_rlp = rlp.encode(raw_tx) - if t8n.is_after_fork("ethereum.berlin"): + if t8n.fork.is_after_fork("ethereum.berlin"): if isinstance(raw_tx, Bytes): - transaction = t8n.transactions.decode_transaction(raw_tx) + transaction = t8n.fork.decode_transaction(raw_tx) self.all_txs.append(raw_tx) else: - transaction = rlp.decode_to( - t8n.transactions.LegacyTransaction, tx_rlp - ) + transaction = rlp.decode_to(t8n.fork.LegacyTransaction, tx_rlp) self.all_txs.append(transaction) else: - transaction = rlp.decode_to(t8n.transactions.Transaction, tx_rlp) + transaction = rlp.decode_to(t8n.fork.Transaction, tx_rlp) self.all_txs.append(transaction) return transaction @@ -192,11 +187,11 @@ def parse_json_tx(self, raw_tx: Any) -> Any: if "secretKey" in raw_tx and v == r == s == 0: self.sign_transaction(raw_tx) - tx = t8n.json_to_tx(raw_tx) + tx = TransactionLoad(raw_tx, t8n.fork).read() self.all_txs.append(tx) - if t8n.is_after_fork("ethereum.berlin"): - transaction = t8n.transactions.decode_transaction(tx) + if t8n.fork.is_after_fork("ethereum.berlin"): + transaction = t8n.fork.decode_transaction(tx) else: transaction = tx @@ -206,10 +201,8 @@ def add_transaction(self, tx: Any) -> None: """ Add a transaction to the list of successful transactions. """ - if self.t8n.is_after_fork("ethereum.berlin"): - self.successful_txs.append( - self.t8n.transactions.encode_transaction(tx) - ) + 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) @@ -217,10 +210,10 @@ def get_tx_hash(self, tx: Any) -> bytes: """ Get the transaction hash of a transaction. """ - if self.t8n.is_after_fork("ethereum.berlin") and not isinstance( - tx, self.t8n.transactions.LegacyTransaction + if self.t8n.fork.is_after_fork("ethereum.berlin") and not isinstance( + tx, self.t8n.fork.LegacyTransaction ): - return keccak256(self.t8n.transactions.encode_transaction(tx)) + return keccak256(self.t8n.fork.encode_transaction(tx)) else: return keccak256(rlp.encode(tx)) @@ -246,21 +239,21 @@ def sign_transaction(self, json_tx: Any) -> None: t8n = self.t8n protected = json_tx.get("protected", True) - tx = t8n.json_to_tx(json_tx) + tx = TransactionLoad(json_tx, t8n.fork).read() if isinstance(tx, bytes): - tx_decoded = t8n.transactions.decode_transaction(tx) + tx_decoded = t8n.fork.decode_transaction(tx) else: tx_decoded = tx secret_key = hex_to_uint(json_tx["secretKey"][2:]) - if t8n.is_after_fork("ethereum.berlin"): - Transaction = t8n.transactions.LegacyTransaction + if t8n.fork.is_after_fork("ethereum.berlin"): + Transaction = t8n.fork.LegacyTransaction else: - Transaction = t8n.transactions.Transaction + Transaction = t8n.fork.Transaction if isinstance(tx_decoded, Transaction): - if t8n.is_after_fork("ethereum.spurious_dragon"): + if t8n.fork.is_after_fork("ethereum.spurious_dragon"): if protected: signing_hash = t8n.fork.signing_hash_155( tx_decoded, U64(1) @@ -272,12 +265,15 @@ def sign_transaction(self, json_tx: Any) -> None: else: signing_hash = t8n.fork.signing_hash(tx_decoded) v_addend = 27 - elif isinstance(tx_decoded, t8n.transactions.AccessListTransaction): + elif isinstance(tx_decoded, t8n.fork.AccessListTransaction): signing_hash = t8n.fork.signing_hash_2930(tx_decoded) v_addend = 0 - elif isinstance(tx_decoded, t8n.transactions.FeeMarketTransaction): + elif isinstance(tx_decoded, t8n.fork.FeeMarketTransaction): signing_hash = t8n.fork.signing_hash_1559(tx_decoded) v_addend = 0 + elif isinstance(tx_decoded, t8n.fork.BlobTransaction): + signing_hash = t8n.fork.signing_hash_4844(tx_decoded) + v_addend = 0 else: raise FatalException("Unknown transaction type") @@ -305,6 +301,8 @@ class Result: receipts: Any = None rejected: Any = None gas_used: Any = None + excess_blob_gas: Optional[U64] = None + blob_gas_used: Optional[Uint] = None def to_json(self) -> Any: """Encode the result to JSON""" @@ -328,6 +326,12 @@ def to_json(self) -> Any: else: data["currentBaseFee"] = None + if self.excess_blob_gas is not None: + data["currentExcessBlobGas"] = hex(self.excess_blob_gas) + + if self.blob_gas_used is not None: + data["blobGasUsed"] = hex(self.blob_gas_used) + data["rejected"] = [ {"index": idx, "error": error} for idx, error in self.rejected.items() diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index bb163f4e20..0503f34d2d 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -5,7 +5,7 @@ import json import logging import sys -from typing import Any, Callable, Dict, Optional, Tuple, TypeVar +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar import coincurve @@ -124,7 +124,7 @@ def get_module_name(forks: Any, options: Any, stdin: Any) -> Tuple[str, int]: sys.exit(f"Unsupported state fork: {options.state_fork}") -def get_supported_forks() -> str: +def get_supported_forks() -> List[str]: """ Get the supported forks. """ @@ -142,7 +142,7 @@ def get_supported_forks() -> str: if fork.casefold() not in UNSUPPORTED_FORKS ] - return "\n".join(supported_forks) + return supported_forks def get_stream_logger(name: str) -> Any: diff --git a/src/ethereum_spec_tools/forks.py b/src/ethereum_spec_tools/forks.py index a61394f512..3067af7760 100644 --- a/src/ethereum_spec_tools/forks.py +++ b/src/ethereum_spec_tools/forks.py @@ -85,15 +85,22 @@ def discover(cls: Type[H], base: Optional[PurePath] = None) -> List[H]: forks: List[H] = [] for pkg in modules: + # Use find_spec() to find the module specification. if isinstance(pkg.module_finder, importlib.abc.MetaPathFinder): - found = pkg.module_finder.find_module(pkg.name, None) + found = pkg.module_finder.find_spec(pkg.name, None) else: - found = pkg.module_finder.find_module(pkg.name) - + found = pkg.module_finder.find_spec(pkg.name) if not found: - raise Exception(f"unable to load module {pkg.name}") + raise Exception(f"unable to find module spec for {pkg.name}") + + # Load the module from the spec. + mod = importlib.util.module_from_spec(found) - mod = found.load_module(pkg.name) + # Execute the module in its namespace. + if found.loader: + found.loader.exec_module(mod) + else: + raise Exception(f"No loader found for module {pkg.name}") if hasattr(mod, "FORK_CRITERIA"): forks.append(cls(mod)) diff --git a/src/ethereum_spec_tools/py.typed b/src/ethereum_spec_tools/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/berlin/test_state_transition.py b/tests/berlin/test_state_transition.py index 600a40b989..d20a90759c 100644 --- a/tests/berlin/test_state_transition.py +++ b/tests/berlin/test_state_transition.py @@ -98,24 +98,24 @@ def test_general_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -132,31 +132,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.LegacyTransaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -166,7 +166,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -182,4 +182,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/byzantium/test_state_transition.py b/tests/byzantium/test_state_transition.py index e4753a068b..8cbbd9878d 100644 --- a/tests/byzantium/test_state_transition.py +++ b/tests/byzantium/test_state_transition.py @@ -103,24 +103,24 @@ def test_non_legacy_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -137,31 +137,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -171,7 +171,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -186,4 +186,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/cancun/__init__.py b/tests/cancun/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/cancun/test_evm_tools.py b/tests/cancun/test_evm_tools.py new file mode 100644 index 0000000000..1e74ea0605 --- /dev/null +++ b/tests/cancun/test_evm_tools.py @@ -0,0 +1,43 @@ +import importlib +from functools import partial +from typing import Dict + +import pytest + +from tests.helpers import TEST_FIXTURES +from tests.helpers.load_evm_tools_tests import ( + fetch_evm_tools_tests, + idfn, + load_evm_tools_test, +) + +ETHEREUM_TESTS_PATH = TEST_FIXTURES["ethereum_tests"]["fixture_path"] +TEST_DIR = f"{ETHEREUM_TESTS_PATH}/GeneralStateTests/" +FORK_NAME = "Cancun" + +run_evm_tools_test = partial( + load_evm_tools_test, + fork_name=FORK_NAME, +) + +SLOW_TESTS = ( + "CALLBlake2f_MaxRounds", + "CALLCODEBlake2f", + "CALLBlake2f", + "loopExp", + "loopMul", +) + + +@pytest.mark.evm_tools +@pytest.mark.parametrize( + "test_case", + fetch_evm_tools_tests( + TEST_DIR, + FORK_NAME, + SLOW_TESTS, + ), + ids=idfn, +) +def test_evm_tools(test_case: Dict) -> None: + run_evm_tools_test(test_case) diff --git a/tests/cancun/test_rlp.py b/tests/cancun/test_rlp.py new file mode 100644 index 0000000000..5631234ac3 --- /dev/null +++ b/tests/cancun/test_rlp.py @@ -0,0 +1,164 @@ +import pytest + +import ethereum.rlp as rlp +from ethereum.base_types import U64, U256, Bytes, Bytes0, Bytes8, Bytes32, Uint +from ethereum.cancun.blocks import Block, Header, Log, Receipt, Withdrawal +from ethereum.cancun.transactions import ( + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, + decode_transaction, + encode_transaction, +) +from ethereum.cancun.utils.hexadecimal import hex_to_address +from ethereum.crypto.hash import keccak256 +from ethereum.utils.hexadecimal import hex_to_bytes256 + +hash1 = keccak256(b"foo") +hash2 = keccak256(b"bar") +hash3 = keccak256(b"baz") +hash4 = keccak256(b"foobar") +hash5 = keccak256(b"quux") +hash6 = keccak256(b"foobarbaz") + +address1 = hex_to_address("0x00000000219ab540356cbb839cbe05303d7705fa") +address2 = hex_to_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") +address3 = hex_to_address("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8") + +bloom = hex_to_bytes256( + "0x886480c00200620d84180d0470000c503081160044d05015808" + "0037401107060120040105281100100104500414203040a208003" + "4814200610da1208a638d16e440c024880800301e1004c2b02285" + "0602000084c3249a0c084569c90c2002001586241041e8004035a" + "4400a0100938001e041180083180b0340661372060401428c0200" + "87410402b9484028100049481900c08034864314688d001548c30" + "00828e542284180280006402a28a0264da00ac223004006209609" + "83206603200084040122a4739080501251542082020a4087c0002" + "81c08800898d0900024047380000127038098e090801080000429" + "0c84201661040200201c0004b8490ad588804" +) + +legacy_transaction = LegacyTransaction( + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"foo"), + U256(27), + U256(5), + U256(6), +) + +access_list_transaction = AccessListTransaction( + U64(1), + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ((address1, (hash1, hash2)), (address2, tuple())), + U256(27), + U256(5), + U256(6), +) + +transaction_1559 = FeeMarketTransaction( + U64(1), + U256(1), + Uint(7), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ((address1, (hash1, hash2)), (address2, tuple())), + U256(27), + U256(5), + U256(6), +) + +withdrawal = Withdrawal(U64(0), U64(1), address1, U256(2)) + + +header = Header( + parent_hash=hash1, + ommers_hash=hash2, + coinbase=address1, + state_root=hash3, + transactions_root=hash4, + receipt_root=hash5, + bloom=bloom, + difficulty=Uint(1), + number=Uint(2), + gas_limit=Uint(3), + gas_used=Uint(4), + timestamp=U256(5), + extra_data=Bytes(b"foobar"), + prev_randao=Bytes32(b"1234567890abcdef1234567890abcdef"), + nonce=Bytes8(b"12345678"), + base_fee_per_gas=Uint(6), + withdrawals_root=hash6, + parent_beacon_block_root=Bytes32(b"1234567890abcdef1234567890abcdef"), + blob_gas_used=U64(7), + excess_blob_gas=U64(8), +) + +block = Block( + header=header, + transactions=( + encode_transaction(legacy_transaction), + encode_transaction(access_list_transaction), + encode_transaction(transaction_1559), + ), + ommers=(), + withdrawals=(withdrawal,), +) + +log1 = Log( + address=address1, + topics=(hash1, hash2), + data=Bytes(b"foobar"), +) + +log2 = Log( + address=address1, + topics=(hash1,), + data=Bytes(b"quux"), +) + +receipt = Receipt( + succeeded=True, + cumulative_gas_used=Uint(1), + bloom=bloom, + logs=(log1, log2), +) + + +@pytest.mark.parametrize( + "rlp_object", + [ + legacy_transaction, + access_list_transaction, + transaction_1559, + header, + block, + log1, + log2, + receipt, + withdrawal, + ], +) +def test_cancun_rlp(rlp_object: rlp.RLP) -> 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/cancun/test_state_transition.py b/tests/cancun/test_state_transition.py new file mode 100644 index 0000000000..7c777edcb2 --- /dev/null +++ b/tests/cancun/test_state_transition.py @@ -0,0 +1,118 @@ +from functools import partial +from typing import Dict, Tuple + +import pytest + +from ethereum import rlp +from ethereum.base_types import U256, Bytes, Bytes8, Bytes32, Uint +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import InvalidBlock, RLPDecodingError +from tests.helpers import TEST_FIXTURES +from tests.helpers.load_state_tests import ( + Load, + fetch_state_test_files, + idfn, + run_blockchain_st_test, +) + +fetch_cancun_tests = partial(fetch_state_test_files, network="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/" + +SLOW_TESTS = ( + # GeneralStateTests + "stTimeConsuming/CALLBlake2f_MaxRounds.json", + "stTimeConsuming/static_Call50000_sha256.json", + "vmPerformance/loopExp.json", + "vmPerformance/loopMul.json", + "QuadraticComplexitySolidity_CallDataCopy_d0g1v0_Cancun", + "CALLBlake2f_d9g0v0_Cancun", + "CALLCODEBlake2f_d9g0v0", + # GeneralStateTests + "stRandom/randomStatetest177.json", + "stCreateTest/CreateOOGafterMaxCodesize.json", + # ValidBlockTest + "bcExploitTest/DelegateCallSpam.json", + # InvalidBlockTest + "bcUncleHeaderValidity/nonceWrong.json", + "bcUncleHeaderValidity/wrongMixHash.json", +) + +# These are tests that are considered to be incorrect, +# Please provide an explanation when adding entries +IGNORE_TESTS = ( + # ValidBlockTest + "bcForkStressTest/ForkStressTest.json", + "bcGasPricerTest/RPC_API_Test.json", + "bcMultiChainTest", + "bcTotalDifficultyTest", + # InvalidBlockTest + "bcForgedTest", + "bcMultiChainTest", + "GasLimitHigherThan2p63m1_Cancun", + # TODO: The below tests are being ignored due to a bug in + # upstream repo. They should be removed from the ignore list + # once the bug is resolved + # See: https://github.com/ethereum/execution-spec-tests/pull/134 + "Pyspecs/vm/chain_id.json", + "Pyspecs/vm/dup.json", + "Pyspecs/example/yul.json", + "Pyspecs/eips/warm_coinbase_gas_usage.json", + "Pyspecs/eips/warm_coinbase_call_out_of_gas.json", +) + +# All tests that recursively create a large number of frames (50000) +BIG_MEMORY_TESTS = ( + # GeneralStateTests + "50000_", + "/stQuadraticComplexityTest/", + "/stRandom2/", + "/stRandom/", + "/stSpecialTest/", + "stTimeConsuming/", + "stBadOpcode/", + "stStaticCall/", +) + +fetch_state_tests = partial( + fetch_cancun_tests, + test_dir, + ignore_list=IGNORE_TESTS, + slow_list=SLOW_TESTS, + big_memory_list=BIG_MEMORY_TESTS, +) + + +@pytest.mark.parametrize( + "test_case", + fetch_state_tests(), + ids=idfn, +) +def test_general_state_tests(test_case: Dict) -> None: + run_cancun_blockchain_st_tests(test_case) + + +# Run execution-spec-generated-tests +test_dir = f"{ETHEREUM_SPEC_TESTS_PATH}/fixtures/withdrawals" + + +@pytest.mark.parametrize( + "test_case", + fetch_cancun_tests(test_dir), + ids=idfn, +) +def test_execution_specs_generated_tests(test_case: Dict) -> None: + run_cancun_blockchain_st_tests(test_case) diff --git a/tests/cancun/test_trie.py b/tests/cancun/test_trie.py new file mode 100644 index 0000000000..edc329c73c --- /dev/null +++ b/tests/cancun/test_trie.py @@ -0,0 +1,89 @@ +import json +from typing import Any + +from ethereum.cancun.fork_types import Bytes +from ethereum.cancun.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/constantinople/test_state_transition.py b/tests/constantinople/test_state_transition.py index dead7793da..8c5768505d 100644 --- a/tests/constantinople/test_state_transition.py +++ b/tests/constantinople/test_state_transition.py @@ -106,24 +106,24 @@ def test_non_legacy_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -140,31 +140,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -174,7 +174,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -189,4 +189,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/evm_tools/test_b11r.py b/tests/evm_tools/test_b11r.py index 329546f9ba..9764e25a32 100644 --- a/tests/evm_tools/test_b11r.py +++ b/tests/evm_tools/test_b11r.py @@ -1,19 +1,13 @@ import json import os +import sys from typing import Any, Dict, List import pytest -from ethereum.base_types import U64, U256, Uint -from ethereum.rlp import decode -from ethereum.utils.hexadecimal import ( - Hash32, - hex_to_bytes, - hex_to_u256, - hex_to_uint, -) -from ethereum_spec_tools.evm_tools import parser, subparsers -from ethereum_spec_tools.evm_tools.b11r import B11R, b11r_arguments +from ethereum.utils.hexadecimal import hex_to_bytes +from ethereum_spec_tools.evm_tools import create_parser +from ethereum_spec_tools.evm_tools.b11r import B11R from ethereum_spec_tools.evm_tools.utils import FatalException from tests.helpers import TEST_FIXTURES @@ -21,7 +15,7 @@ IGNORE_TESTS: List[str] = [] -b11r_arguments(subparsers) +parser = create_parser() def find_test_fixtures() -> Any: @@ -55,7 +49,7 @@ def b11r_tool_test(test_case: Dict) -> None: options = parser.parse_args(test_case["args"]) try: - b11r_tool = B11R(options) + b11r_tool = B11R(options, sys.stdout, sys.stdin) b11r_tool.build_block() except Exception as e: raise FatalException(e) diff --git a/tests/evm_tools/test_t8n.py b/tests/evm_tools/test_t8n.py deleted file mode 100644 index 7b1984b2bd..0000000000 --- a/tests/evm_tools/test_t8n.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -import os -from typing import Any, Dict, List - -import pytest - -from ethereum import rlp -from ethereum.utils.hexadecimal import hex_to_bytes, hex_to_u256, hex_to_uint -from ethereum_spec_tools.evm_tools import parser -from ethereum_spec_tools.evm_tools.t8n import T8N -from ethereum_spec_tools.evm_tools.utils import FatalException -from tests.helpers import TEST_FIXTURES - -T8N_TEST_PATH = TEST_FIXTURES["evm_tools_testdata"]["fixture_path"] - -IGNORE_TESTS = [ - "t8n/fixtures/expected/26/Merge.json", -] - - -def find_test_fixtures() -> Any: - with open(os.path.join(T8N_TEST_PATH, "t8n_commands.json")) as f: - data = json.load(f) - - for key, value in data.items(): - final_args = [] - for arg in value["args"]: - final_args.append(arg.replace("__BASEDIR__", T8N_TEST_PATH)) - yield { - "name": key, - "args": final_args, - "expected": os.path.join(T8N_TEST_PATH, key), - "success": value["success"], - } - - -def idfn(test_case: Dict) -> str: - return test_case["name"] - - -def get_rejected_indices(rejected: Dict) -> List[int]: - rejected_indices = [] - for item in rejected: - rejected_indices.append(item["index"]) - return rejected_indices - - -def t8n_tool_test(test_case: Dict) -> None: - options = parser.parse_args(test_case["args"]) - - try: - t8n_tool = T8N(options) - t8n_tool.apply_body() - except Exception as e: - raise FatalException(e) - - json_result = t8n_tool.result.to_json() - with open(test_case["expected"], "r") as f: - data = json.load(f) - - # with open("temp.json", "w") as f: - # json.dump(json_state, f, indent=4) - - if "rejected" in data["result"] and len(data["result"]["rejected"]) != 0: - assert len(json_result["rejected"]) != 0 - - rejected_indices = get_rejected_indices(json_result["rejected"]) - expected_rejected_indices = get_rejected_indices( - data["result"]["rejected"] - ) - - assert sorted(rejected_indices) == sorted(expected_rejected_indices) - else: - assert len(json_result["rejected"]) == 0 - - assert t8n_tool.hex_to_root( - json_result["stateRoot"] - ) == t8n_tool.hex_to_root(data["result"]["stateRoot"]) - assert t8n_tool.hex_to_root(json_result["txRoot"]) == t8n_tool.hex_to_root( - data["result"]["txRoot"] - ) - assert t8n_tool.hex_to_root( - json_result["receiptsRoot"] - ) == t8n_tool.hex_to_root(data["result"]["receiptsRoot"]) - assert t8n_tool.Bloom( - hex_to_bytes(json_result["logsBloom"]) - ) == t8n_tool.Bloom(hex_to_bytes(data["result"]["logsBloom"])) - assert hex_to_u256(json_result["gasUsed"]) == hex_to_u256( - data["result"]["gasUsed"] - ) - assert rlp.encode(t8n_tool.txs.all_txs) == hex_to_bytes(data["txs_rlp"]) - if not t8n_tool.is_after_fork("ethereum.paris"): - assert hex_to_uint(json_result["currentDifficulty"]) == hex_to_uint( - data["result"]["currentDifficulty"] - ) - if t8n_tool.is_after_fork("ethereum.shanghai"): - assert t8n_tool.hex_to_root( - json_result["withdrawalsRoot"] - ) == t8n_tool.hex_to_root(data["result"]["withdrawalsRoot"]) - - if "receipts" in data["result"]: - assert len(json_result["receipts"]) == len(data["result"]["receipts"]) - for t8n_receipt, expected_receipt in zip( - json_result["receipts"], data["result"]["receipts"] - ): - assert t8n_receipt["gasUsed"] == expected_receipt["gasUsed"] - assert ( - t8n_receipt["transactionHash"] - == expected_receipt["transactionHash"] - ) - - -@pytest.mark.evm_tools -@pytest.mark.parametrize( - "test_case", - find_test_fixtures(), - ids=idfn, -) -def test_t8n(test_case: Dict) -> None: - if test_case["name"] in IGNORE_TESTS: - pytest.xfail("Undefined behavior for specs") - elif test_case["success"]: - t8n_tool_test(test_case) - else: - with pytest.raises(FatalException): - t8n_tool_test(test_case) diff --git a/tests/frontier/test_state_transition.py b/tests/frontier/test_state_transition.py index 4da448a560..d2c1d270a6 100644 --- a/tests/frontier/test_state_transition.py +++ b/tests/frontier/test_state_transition.py @@ -97,24 +97,24 @@ def test_non_legacy_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -131,31 +131,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -165,7 +165,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -180,4 +180,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index f11894405c..1b1d813396 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -12,7 +12,7 @@ }, "ethereum_tests": { "url": "https://github.com/ethereum/tests.git", - "commit_hash": "0ec53d0", + "commit_hash": "52ddcbc", "fixture_path": "tests/fixtures/ethereum_tests", }, } diff --git a/tests/helpers/load_evm_tools_tests.py b/tests/helpers/load_evm_tools_tests.py index bcad923e4b..fa93317dc8 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -7,10 +7,11 @@ import pytest from ethereum.utils.hexadecimal import hex_to_bytes -from ethereum_spec_tools.evm_tools import parser, subparsers -from ethereum_spec_tools.evm_tools.t8n import T8N, t8n_arguments +from ethereum_spec_tools.evm_tools import create_parser +from ethereum_spec_tools.evm_tools.statetest import read_test_cases +from ethereum_spec_tools.evm_tools.t8n import T8N -t8n_arguments(subparsers) +parser = create_parser() def fetch_evm_tools_tests( @@ -26,28 +27,25 @@ def fetch_evm_tools_tests( for root, _, files in os.walk(test_dir): for filename in files: - if filename.endswith(".json"): - test_file_path = os.path.join(root, filename) - with open(test_file_path) as test_file: - tests = json.load(test_file) - - for key, test in tests.items(): - slow = True if key in slow_tests else False - if fork_name in test["post"]: - for idx, transition in enumerate( - test["post"][fork_name] - ): - test_case = { - "test_file": test_file_path, - "test_key": key, - "index": idx, - } - if slow: - yield pytest.param( - test_case, marks=pytest.mark.slow - ) - else: - yield test_case + if not filename.endswith(".json"): + continue + + test_file_path = os.path.join(root, filename) + test_cases = read_test_cases(test_file_path) + for test_case in test_cases: + if test_case.fork_name != fork_name: + continue + + test_case_dict = { + "test_file": test_case.path, + "test_key": test_case.key, + "index": test_case.index, + } + + if test_case.key in slow_tests: + yield pytest.param(test_case_dict, marks=pytest.mark.slow) + else: + yield test_case_dict def idfn(test_case: Dict) -> str: @@ -72,7 +70,10 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: tests = json.load(f) env = tests[test_key]["env"] - env["blockHashes"] = {"0": env["previousHash"]} + try: + env["blockHashes"] = {"0": env["previousHash"]} + except KeyError: + env["blockHashes"] = {} env["withdrawals"] = [] alloc = tests[test_key]["pre"] @@ -99,7 +100,7 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: txs = [tx] - sys.stdin = StringIO( + in_stream = StringIO( json.dumps( { "env": env, @@ -123,7 +124,7 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: ] t8n_options = parser.parse_args(t8n_args) - t8n = T8N(t8n_options) + t8n = T8N(t8n_options, sys.stdout, in_stream) t8n.apply_body() 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 f53cfef229..99f704b240 100644 --- a/tests/helpers/load_state_tests.py +++ b/tests/helpers/load_state_tests.py @@ -13,7 +13,7 @@ from ethereum.base_types import U64 from ethereum.exceptions import InvalidBlock from ethereum.utils.hexadecimal import hex_to_bytes -from ethereum_spec_tools.evm_tools.fixture_loader import Load +from ethereum_spec_tools.evm_tools.loaders.fixture_loader import Load class NoTestsFound(Exception): @@ -44,7 +44,7 @@ def run_blockchain_st_test(test_case: Dict, load: Load) -> None: if hasattr(genesis_header, "withdrawals_root"): parameters.append(()) - genesis_block = load.Block(*parameters) + genesis_block = load.fork.Block(*parameters) genesis_header_hash = hex_to_bytes(json_data["genesisBlockHeader"]["hash"]) assert rlp.rlp_hash(genesis_header) == genesis_header_hash @@ -56,13 +56,15 @@ def run_blockchain_st_test(test_case: Dict, load: Load) -> None: # == test_data["genesis_block_rlp"] # ) - chain = load.BlockChain( + chain = load.fork.BlockChain( blocks=[genesis_block], state=load.json_to_state(json_data["pre"]), chain_id=U64(json_data["genesisBlockHeader"].get("chainId", 1)), ) - mock_pow = json_data["sealEngine"] == "NoProof" and not load.proof_of_stake + mock_pow = ( + json_data["sealEngine"] == "NoProof" and not load.fork.proof_of_stake + ) for json_block in json_data["blocks"]: block_exception = None @@ -83,8 +85,8 @@ def run_blockchain_st_test(test_case: Dict, load: Load) -> None: expected_post_state = load.json_to_state(json_data["postState"]) assert chain.state == expected_post_state - load.close_state(chain.state) - load.close_state(expected_post_state) + load.fork.close_state(chain.state) + load.fork.close_state(expected_post_state) def add_block_to_chain( @@ -100,17 +102,17 @@ def add_block_to_chain( assert rlp.encode(cast(rlp.RLP, block)) == block_rlp if not mock_pow: - load.state_transition(chain, block) + load.fork.state_transition(chain, block) else: fork_module = importlib.import_module( - f"ethereum.{load.fork_module}.fork" + f"ethereum.{load.fork.fork_module}.fork" ) with patch.object( fork_module, "validate_proof_of_work", autospec=True, ) as mocked_pow_validator: - load.state_transition(chain, block) + load.fork.state_transition(chain, block) mocked_pow_validator.assert_has_calls( [call(block.header)], any_order=False, diff --git a/tests/homestead/test_state_transition.py b/tests/homestead/test_state_transition.py index 175b2bbde2..aa980419d0 100644 --- a/tests/homestead/test_state_transition.py +++ b/tests/homestead/test_state_transition.py @@ -179,24 +179,24 @@ def test_non_legacy_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -213,31 +213,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -247,7 +247,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -262,4 +262,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/istanbul/test_state_transition.py b/tests/istanbul/test_state_transition.py index f4114a6e22..3bdf7dbc50 100644 --- a/tests/istanbul/test_state_transition.py +++ b/tests/istanbul/test_state_transition.py @@ -99,24 +99,24 @@ def test_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -133,31 +133,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -167,7 +167,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -183,4 +183,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/london/test_state_transition.py b/tests/london/test_state_transition.py index 928b41305b..235daf8ea7 100644 --- a/tests/london/test_state_transition.py +++ b/tests/london/test_state_transition.py @@ -100,24 +100,24 @@ def test_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -135,31 +135,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.LegacyTransaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -169,7 +169,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -186,4 +186,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/paris/test_state_transition.py b/tests/paris/test_state_transition.py index 771597a3d7..1e65985b73 100644 --- a/tests/paris/test_state_transition.py +++ b/tests/paris/test_state_transition.py @@ -100,24 +100,24 @@ def test_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -135,31 +135,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.LegacyTransaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -169,7 +169,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -186,4 +186,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/spurious_dragon/test_state_transition.py b/tests/spurious_dragon/test_state_transition.py index 177ab5a829..f81289bff5 100644 --- a/tests/spurious_dragon/test_state_transition.py +++ b/tests/spurious_dragon/test_state_transition.py @@ -97,24 +97,24 @@ def test_non_legacy_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -131,31 +131,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -165,7 +165,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -180,4 +180,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/tangerine_whistle/test_state_transition.py b/tests/tangerine_whistle/test_state_transition.py index 57b7062332..541d9d7926 100644 --- a/tests/tangerine_whistle/test_state_transition.py +++ b/tests/tangerine_whistle/test_state_transition.py @@ -99,24 +99,24 @@ def test_non_legacy_state_tests(test_case: Dict) -> None: def test_transaction_with_insufficient_balance_for_value() -> None: - genesis_header = FIXTURES_LOADER.Header( + genesis_header = FIXTURES_LOADER.fork.Header( parent_hash=Hash32([0] * 32), ommers_hash=Hash32.fromhex( "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" ), - coinbase=FIXTURES_LOADER.hex_to_address( + coinbase=FIXTURES_LOADER.fork.hex_to_address( "8888f1f195afa192cfee860698584c030f4c9db1" ), - state_root=FIXTURES_LOADER.hex_to_root( + state_root=FIXTURES_LOADER.fork.hex_to_root( "d84598d90e2a72125c111171717f5508fd40ed0d0cd067ceb4e734d4da3a810a" ), - transactions_root=FIXTURES_LOADER.hex_to_root( + transactions_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - receipt_root=FIXTURES_LOADER.hex_to_root( + receipt_root=FIXTURES_LOADER.fork.hex_to_root( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ), - bloom=FIXTURES_LOADER.Bloom([0] * 256), + bloom=FIXTURES_LOADER.fork.Bloom([0] * 256), difficulty=Uint(0x020000), number=Uint(0x00), gas_limit=Uint(0x2FEFD8), @@ -133,31 +133,31 @@ def test_transaction_with_insufficient_balance_for_value() -> None: assert rlp.rlp_hash(genesis_header) == genesis_header_hash - genesis_block = FIXTURES_LOADER.Block( + genesis_block = FIXTURES_LOADER.fork.Block( genesis_header, (), (), ) - state = FIXTURES_LOADER.State() + state = FIXTURES_LOADER.fork.State() - address = FIXTURES_LOADER.hex_to_address( + address = FIXTURES_LOADER.fork.hex_to_address( "a94f5374fce5edbc8e2a8697c15331677e6ebf0b" ) - account = FIXTURES_LOADER.Account( + account = FIXTURES_LOADER.fork.Account( nonce=Uint(0), balance=U256(0x056BC75E2D63100000), code=Bytes(), ) - FIXTURES_LOADER.set_account(state, address, account) + FIXTURES_LOADER.fork.set_account(state, address, account) - tx = FIXTURES_LOADER.LegacyTransaction( + tx = FIXTURES_LOADER.fork.Transaction( nonce=U256(0x00), gas_price=U256(1000), gas=U256(150000), - to=FIXTURES_LOADER.hex_to_address( + to=FIXTURES_LOADER.fork.hex_to_address( "c94f5374fce5edbc8e2a8697c15331677e6ebf0b" ), value=U256(1000000000000000000000), @@ -167,7 +167,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: s=U256(0), ) - env = FIXTURES_LOADER.Environment( + env = FIXTURES_LOADER.fork.Environment( caller=address, origin=address, block_hashes=[genesis_header_hash], @@ -182,4 +182,4 @@ def test_transaction_with_insufficient_balance_for_value() -> None: ) with pytest.raises(InvalidBlock): - FIXTURES_LOADER.process_transaction(env, tx) + FIXTURES_LOADER.fork.process_transaction(env, tx) diff --git a/tests/test_base_types.py b/tests/test_base_types.py index 761142b631..16d9415a27 100644 --- a/tests/test_base_types.py +++ b/tests/test_base_types.py @@ -11,7 +11,7 @@ def test_uint_new() -> None: def test_uint_new_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(-5) @@ -27,7 +27,7 @@ def test_uint_radd() -> None: def test_uint_radd_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) + Uint(5) @@ -44,7 +44,7 @@ def test_uint_add() -> None: def test_uint_add_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) + (-4) @@ -63,7 +63,7 @@ def test_uint_iadd() -> None: def test_uint_iadd_negative() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value += -4 @@ -81,12 +81,12 @@ def test_uint_rsub() -> None: def test_uint_rsub_too_big() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): 6 - Uint(7) def test_uint_rsub_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) - Uint(5) @@ -103,12 +103,12 @@ def test_uint_sub() -> None: def test_uint_sub_too_big() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) - 6 def test_uint_sub_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) - (-4) @@ -127,13 +127,13 @@ def test_uint_isub() -> None: def test_uint_isub_too_big() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value -= 6 def test_uint_isub_negative() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value -= -4 @@ -151,7 +151,7 @@ def test_uint_rmul() -> None: def test_uint_rmul_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) * Uint(5) @@ -168,7 +168,7 @@ def test_uint_mul() -> None: def test_uint_mul_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) * (-4) @@ -187,7 +187,7 @@ def test_uint_imul() -> None: def test_uint_imul_negative() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value *= -4 @@ -205,7 +205,7 @@ def test_uint_floordiv() -> None: def test_uint_floordiv_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) // -2 @@ -222,7 +222,7 @@ def test_uint_rfloordiv() -> None: def test_uint_rfloordiv_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-2) // Uint(5) @@ -241,7 +241,7 @@ def test_uint_ifloordiv() -> None: def test_uint_ifloordiv_negative() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value //= -2 @@ -252,7 +252,7 @@ def test_uint_rmod() -> None: def test_uint_rmod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) % Uint(5) @@ -269,7 +269,7 @@ def test_uint_mod() -> None: def test_uint_mod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(5) % (-4) @@ -288,7 +288,7 @@ def test_uint_imod() -> None: def test_uint_imod_negative() -> None: value = Uint(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value %= -4 @@ -308,7 +308,7 @@ def test_uint_divmod() -> None: def test_uint_divmod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(Uint(5), -2) @@ -329,7 +329,7 @@ def test_uint_rdivmod() -> None: def test_uint_rdivmod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(-5, Uint(2)) @@ -348,7 +348,7 @@ def test_uint_pow() -> None: def test_uint_pow_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(3) ** -2 @@ -359,7 +359,7 @@ def test_uint_pow_modulo() -> None: def test_uint_pow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): pow(Uint(4), 2, -3) @@ -370,7 +370,7 @@ def test_uint_rpow() -> None: def test_uint_rpow_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-3) ** Uint(2) @@ -381,7 +381,7 @@ def test_uint_rpow_modulo() -> None: def test_uint_rpow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint.__rpow__(Uint(2), 4, -3) @@ -394,7 +394,7 @@ def test_uint_ipow() -> None: def test_uint_ipow_negative() -> None: value = Uint(3) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value **= -2 @@ -405,7 +405,7 @@ def test_uint_ipow_modulo() -> None: def test_uint_ipow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): Uint(4).__ipow__(2, -3) @@ -497,7 +497,7 @@ def test_u256_new() -> None: def test_u256_new_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(-5) @@ -513,7 +513,7 @@ def test_u256_new_max_value() -> None: def test_u256_new_too_large() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256) @@ -524,12 +524,12 @@ def test_u256_radd() -> None: def test_u256_radd_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (2**256 - 1) + U256(5) def test_u256_radd_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) + U256(5) @@ -546,12 +546,12 @@ def test_u256_add() -> None: def test_u256_add_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) + (2**256 - 1) def test_u256_add_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) + (-4) @@ -574,7 +574,7 @@ def test_u256_wrapping_add_overflow() -> None: def test_u256_wrapping_add_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5).wrapping_add(-4) @@ -587,7 +587,7 @@ def test_u256_iadd() -> None: def test_u256_iadd_negative() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value += -4 @@ -600,7 +600,7 @@ def test_u256_iadd_float() -> None: def test_u256_iadd_overflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value += 2**256 - 1 @@ -611,12 +611,12 @@ def test_u256_rsub() -> None: def test_u256_rsub_underflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (0) - U256(1) def test_u256_rsub_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) - U256(5) @@ -633,12 +633,12 @@ def test_u256_sub() -> None: def test_u256_sub_underflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) - 6 def test_u256_sub_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) - (-4) @@ -661,7 +661,7 @@ def test_u256_wrapping_sub_underflow() -> None: def test_u256_wrapping_sub_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5).wrapping_sub(-4) @@ -674,7 +674,7 @@ def test_u256_isub() -> None: def test_u256_isub_negative() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value -= -4 @@ -687,7 +687,7 @@ def test_u256_isub_float() -> None: def test_u256_isub_underflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value -= 6 @@ -698,12 +698,12 @@ def test_u256_rmul() -> None: def test_u256_rmul_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (2**256 - 1) * U256(5) def test_u256_rmul_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-4) * U256(5) @@ -720,12 +720,12 @@ def test_u256_mul() -> None: def test_u256_mul_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256.MAX_VALUE * 4 def test_u256_mul_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) * (-4) @@ -751,7 +751,7 @@ def test_u256_wrapping_mul_overflow() -> None: def test_u256_wrapping_mul_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5).wrapping_mul(-4) @@ -764,13 +764,13 @@ def test_u256_imul() -> None: def test_u256_imul_negative() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value *= -4 def test_u256_imul_arg_overflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value *= 2**256 @@ -783,7 +783,7 @@ def test_u256_imul_float() -> None: def test_u256_imul_overflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value *= 2**256 - 1 @@ -794,12 +794,12 @@ def test_u256_floordiv() -> None: def test_u256_floordiv_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) // (2**256) def test_u256_floordiv_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) // -2 @@ -816,12 +816,12 @@ def test_u256_rfloordiv() -> None: def test_u256_rfloordiv_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (2**256) // U256(2) def test_u256_rfloordiv_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-2) // U256(5) @@ -840,13 +840,13 @@ def test_u256_ifloordiv() -> None: def test_u256_ifloordiv_negative() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value //= -2 def test_u256_ifloordiv_overflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value //= 2**256 @@ -869,12 +869,12 @@ def test_u256_mod() -> None: def test_u256_mod_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) % (2**256) def test_u256_mod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(5) % (-4) @@ -893,13 +893,13 @@ def test_u256_imod() -> None: def test_u256_imod_overflow() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value %= 2**256 def test_u256_imod_negative() -> None: value = U256(5) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value %= -4 @@ -919,12 +919,12 @@ def test_u256_divmod() -> None: def test_u256_divmod_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(U256(5), 2**256) def test_u256_divmod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(U256(5), -2) @@ -945,12 +945,12 @@ def test_u256_rdivmod() -> None: def test_u256_rdivmod_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(2**256, U256(2)) def test_u256_rdivmod_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): divmod(-5, U256(2)) @@ -969,12 +969,12 @@ def test_u256_pow() -> None: def test_u256_pow_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(340282366920938463463374607431768211456) ** 3 def test_u256_pow_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(3) ** -2 @@ -985,12 +985,12 @@ def test_u256_pow_modulo() -> None: def test_u256_pow_modulo_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): pow(U256(4), 2, 2**257) def test_u256_pow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): pow(U256(4), 2, -3) @@ -1001,12 +1001,12 @@ def test_u256_rpow() -> None: def test_u256_rpow_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (2**256) ** U256(2) def test_u256_rpow_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): (-3) ** U256(2) @@ -1017,12 +1017,12 @@ def test_u256_rpow_modulo() -> None: def test_u256_rpow_modulo_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256.__rpow__(U256(2), 4, 2**256 + 1) def test_u256_rpow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256.__rpow__(U256(2), 4, -3) @@ -1035,13 +1035,13 @@ def test_u256_ipow() -> None: def test_u256_ipow_overflow() -> None: value = U256(340282366920938463463374607431768211456) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value **= 3 def test_u256_ipow_negative() -> None: value = U256(3) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): value **= -2 @@ -1052,12 +1052,12 @@ def test_u256_ipow_modulo() -> None: def test_u256_ipow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(4).__ipow__(2, -3) def test_u256_ipow_modulo_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(4).__ipow__(2, 2**256 + 1) @@ -1074,7 +1074,7 @@ def test_u256_wrapping_pow_overflow() -> None: def test_u256_wrapping_pow_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(3).wrapping_pow(-2) @@ -1085,12 +1085,12 @@ def test_u256_wrapping_pow_modulo() -> None: def test_u256_wrapping_pow_modulo_overflow() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(4).wrapping_pow(2, 2**256 + 1) def test_u256_wrapping_pow_modulo_negative() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(4).wrapping_pow(2, -3) @@ -1186,11 +1186,11 @@ def test_u256_bitwise_and_successful() -> None: def test_u256_bitwise_and_fails() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(0) & (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) & (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) & -10 @@ -1203,11 +1203,11 @@ def test_u256_bitwise_or_successful() -> None: def test_u256_bitwise_or_failed() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(0) | (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) | (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) | -10 @@ -1220,11 +1220,11 @@ def test_u256_bitwise_xor_successful() -> None: def test_u256_bitwise_xor_failed() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(0) | (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) ^ (2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1) ^ -10 @@ -1239,11 +1239,11 @@ def test_u256_bitwise_rxor_successful() -> None: def test_u256_bitwise_rxor_failed() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(0).__rxor__(2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1).__rxor__(2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1).__rxor__(-10) @@ -1258,11 +1258,11 @@ def test_u256_bitwise_ixor_successful() -> None: def test_u256_bitwise_ixor_failed() -> None: - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(0).__ixor__(2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1).__ixor__(2**256) - with pytest.raises(ValueError): + with pytest.raises(OverflowError): U256(2**256 - 1).__ixor__(-10) diff --git a/tox.ini b/tox.ini index 3140f66e72..647714587b 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ extras = test commands = pytest \ - -m "not slow and not evm_tools" \ + -m "not slow" \ -n auto --maxprocesses 5 \ --cov=ethereum --cov-report=term --cov-report "xml:{toxworkdir}/coverage.xml" \ --ignore-glob='tests/fixtures/*' \ @@ -36,7 +36,7 @@ commands = --tb=no \ --show-capture=no \ --disable-warnings \ - -m "not slow and not evm_tools" \ + -m "not slow" \ -n auto --maxprocesses 2 \ --ignore-glob='tests/fixtures/*' \ --basetemp="{temp_dir}/pytest" diff --git a/whitelist.txt b/whitelist.txt index a8e3e471c5..2d0c8d696f 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -4,6 +4,7 @@ ClassDef ImportFrom radd Hash64 +holiman Bytes20 Bytes4 Bytes32 @@ -27,6 +28,7 @@ exc fmt formatter getsource +goevmlab Hash32 hasher hashimoto @@ -61,6 +63,7 @@ U32 U8 secp256k1 secp256k1n +statetest subclasses iadd ispkg @@ -407,4 +410,24 @@ listable treediff strikethrough setext +mcopy + +tload +tstore + +KZG +BLOBHASH +eth2spec +Bytes48 +BLS +BLOBBASEFEE + +socketserver +qs +platformdirs +rfile +wfile +uds +appname +appauthor orelse