diff --git a/setup.cfg b/setup.cfg index f959e85a39..cc672a35cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,12 @@ packages = ethereum/tangerine_whistle/vm/instructions ethereum/tangerine_whistle/vm/precompiled_contracts ethereum_optimized/tangerine_whistle + ethereum/spurious_dragon + ethereum/spurious_dragon/utils + ethereum/spurious_dragon/vm + ethereum/spurious_dragon/vm/instructions + ethereum/spurious_dragon/vm/precompiled_contracts + ethereum_optimized/spurious_dragon package_dir = =src diff --git a/src/ethereum/dao_fork/__init__.py b/src/ethereum/dao_fork/__init__.py index b67bb39323..6d71c75471 100644 --- a/src/ethereum/dao_fork/__init__.py +++ b/src/ethereum/dao_fork/__init__.py @@ -6,3 +6,4 @@ """ MAINNET_FORK_BLOCK = 1920000 +CHAIN_ID = 1 diff --git a/src/ethereum/dao_fork/state.py b/src/ethereum/dao_fork/state.py index 15b8865900..33de0c2cea 100644 --- a/src/ethereum/dao_fork/state.py +++ b/src/ethereum/dao_fork/state.py @@ -152,8 +152,33 @@ 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()`). + Set the `Account` object at an address. + + You may delete an account with this function even if it has storage. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + if account is None: + destroy_account(state, address) + else: + set_account_internal(state, address, account) + + +def set_account_internal( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. + + You must not set an account to `None` with this function if it has non-zero + storage keys (use `destroy_account()`). Parameters ---------- @@ -184,7 +209,7 @@ def destroy_account(state: State, address: Address) -> None: """ if address in state._storage_tries: del state._storage_tries[address] - set_account(state, address, None) + set_account_internal(state, address, None) def get_storage(state: State, address: Address, key: Bytes) -> U256: diff --git a/src/ethereum/dao_fork/vm/gas.py b/src/ethereum/dao_fork/vm/gas.py index 12477dd56c..f19b1eaa7c 100644 --- a/src/ethereum/dao_fork/vm/gas.py +++ b/src/ethereum/dao_fork/vm/gas.py @@ -30,6 +30,7 @@ GAS_MID = U256(8) GAS_HIGH = U256(10) GAS_EXPONENTIATION = U256(10) +GAS_EXPONENTIATION_PER_BYTE = U256(10) GAS_MEMORY = U256(3) GAS_KECCAK256 = U256(30) GAS_KECCAK256_WORD = U256(6) diff --git a/src/ethereum/dao_fork/vm/instructions/arithmetic.py b/src/ethereum/dao_fork/vm/instructions/arithmetic.py index f2383ac85e..743189403c 100644 --- a/src/ethereum/dao_fork/vm/instructions/arithmetic.py +++ b/src/ethereum/dao_fork/vm/instructions/arithmetic.py @@ -18,6 +18,7 @@ from .. import Evm from ..gas import ( GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, GAS_LOW, GAS_MID, GAS_VERY_LOW, @@ -329,7 +330,7 @@ def exp(evm: Evm) -> None: # function is inaccurate leading to wrong results. exponent_bits = exponent.bit_length() exponent_bytes = (exponent_bits + 7) // 8 - gas_used += GAS_EXPONENTIATION * exponent_bytes + gas_used += GAS_EXPONENTIATION_PER_BYTE * exponent_bytes evm.gas_left = subtract_gas(evm.gas_left, gas_used) result = U256(pow(base, exponent, U256_CEIL_VALUE)) diff --git a/src/ethereum/frontier/__init__.py b/src/ethereum/frontier/__init__.py index 097e89bded..4b0088ded3 100644 --- a/src/ethereum/frontier/__init__.py +++ b/src/ethereum/frontier/__init__.py @@ -6,3 +6,4 @@ """ MAINNET_FORK_BLOCK = 0 +CHAIN_ID = 1 diff --git a/src/ethereum/frontier/state.py b/src/ethereum/frontier/state.py index 15b8865900..33de0c2cea 100644 --- a/src/ethereum/frontier/state.py +++ b/src/ethereum/frontier/state.py @@ -152,8 +152,33 @@ 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()`). + Set the `Account` object at an address. + + You may delete an account with this function even if it has storage. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + if account is None: + destroy_account(state, address) + else: + set_account_internal(state, address, account) + + +def set_account_internal( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. + + You must not set an account to `None` with this function if it has non-zero + storage keys (use `destroy_account()`). Parameters ---------- @@ -184,7 +209,7 @@ def destroy_account(state: State, address: Address) -> None: """ if address in state._storage_tries: del state._storage_tries[address] - set_account(state, address, None) + set_account_internal(state, address, None) def get_storage(state: State, address: Address, key: Bytes) -> U256: diff --git a/src/ethereum/frontier/vm/gas.py b/src/ethereum/frontier/vm/gas.py index f1341b56f5..0d2c0fd428 100644 --- a/src/ethereum/frontier/vm/gas.py +++ b/src/ethereum/frontier/vm/gas.py @@ -30,6 +30,7 @@ GAS_MID = U256(8) GAS_HIGH = U256(10) GAS_EXPONENTIATION = U256(10) +GAS_EXPONENTIATION_PER_BYTE = U256(10) GAS_MEMORY = U256(3) GAS_KECCAK256 = U256(30) GAS_KECCAK256_WORD = U256(6) diff --git a/src/ethereum/frontier/vm/instructions/arithmetic.py b/src/ethereum/frontier/vm/instructions/arithmetic.py index c26639b8e6..b20bfb2b8b 100644 --- a/src/ethereum/frontier/vm/instructions/arithmetic.py +++ b/src/ethereum/frontier/vm/instructions/arithmetic.py @@ -18,6 +18,7 @@ from .. import Evm from ..gas import ( GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, GAS_LOW, GAS_MID, GAS_VERY_LOW, @@ -329,7 +330,7 @@ def exp(evm: Evm) -> None: # function is inaccurate leading to wrong results. exponent_bits = exponent.bit_length() exponent_bytes = (exponent_bits + 7) // 8 - gas_used += GAS_EXPONENTIATION * exponent_bytes + gas_used += GAS_EXPONENTIATION_PER_BYTE * exponent_bytes evm.gas_left = subtract_gas(evm.gas_left, gas_used) result = U256(pow(base, exponent, U256_CEIL_VALUE)) diff --git a/src/ethereum/homestead/__init__.py b/src/ethereum/homestead/__init__.py index 143745815e..c030ab6661 100644 --- a/src/ethereum/homestead/__init__.py +++ b/src/ethereum/homestead/__init__.py @@ -6,3 +6,4 @@ """ MAINNET_FORK_BLOCK = 1150000 +CHAIN_ID = 1 diff --git a/src/ethereum/homestead/state.py b/src/ethereum/homestead/state.py index 15b8865900..33de0c2cea 100644 --- a/src/ethereum/homestead/state.py +++ b/src/ethereum/homestead/state.py @@ -152,8 +152,33 @@ 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()`). + Set the `Account` object at an address. + + You may delete an account with this function even if it has storage. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + if account is None: + destroy_account(state, address) + else: + set_account_internal(state, address, account) + + +def set_account_internal( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. + + You must not set an account to `None` with this function if it has non-zero + storage keys (use `destroy_account()`). Parameters ---------- @@ -184,7 +209,7 @@ def destroy_account(state: State, address: Address) -> None: """ if address in state._storage_tries: del state._storage_tries[address] - set_account(state, address, None) + set_account_internal(state, address, None) def get_storage(state: State, address: Address, key: Bytes) -> U256: diff --git a/src/ethereum/homestead/vm/gas.py b/src/ethereum/homestead/vm/gas.py index 552e97345a..95218b5b2f 100644 --- a/src/ethereum/homestead/vm/gas.py +++ b/src/ethereum/homestead/vm/gas.py @@ -30,6 +30,7 @@ GAS_MID = U256(8) GAS_HIGH = U256(10) GAS_EXPONENTIATION = U256(10) +GAS_EXPONENTIATION_PER_BYTE = U256(10) GAS_MEMORY = U256(3) GAS_KECCAK256 = U256(30) GAS_KECCAK256_WORD = U256(6) diff --git a/src/ethereum/homestead/vm/instructions/arithmetic.py b/src/ethereum/homestead/vm/instructions/arithmetic.py index adff42671e..cac3332828 100644 --- a/src/ethereum/homestead/vm/instructions/arithmetic.py +++ b/src/ethereum/homestead/vm/instructions/arithmetic.py @@ -18,6 +18,7 @@ from .. import Evm from ..gas import ( GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, GAS_LOW, GAS_MID, GAS_VERY_LOW, @@ -329,7 +330,7 @@ def exp(evm: Evm) -> None: # function is inaccurate leading to wrong results. exponent_bits = exponent.bit_length() exponent_bytes = (exponent_bits + 7) // 8 - gas_used += GAS_EXPONENTIATION * exponent_bytes + gas_used += GAS_EXPONENTIATION_PER_BYTE * exponent_bytes evm.gas_left = subtract_gas(evm.gas_left, gas_used) result = U256(pow(base, exponent, U256_CEIL_VALUE)) diff --git a/src/ethereum/spurious_dragon/__init__.py b/src/ethereum/spurious_dragon/__init__.py new file mode 100644 index 0000000000..f62b0887e2 --- /dev/null +++ b/src/ethereum/spurious_dragon/__init__.py @@ -0,0 +1,9 @@ +""" +Ethereum Spurious Dragon Hardfork +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The fourth Ethereum hardfork. +""" + +MAINNET_FORK_BLOCK = 2675000 +CHAIN_ID = 1 diff --git a/src/ethereum/spurious_dragon/bloom.py b/src/ethereum/spurious_dragon/bloom.py new file mode 100644 index 0000000000..77b2c19a31 --- /dev/null +++ b/src/ethereum/spurious_dragon/bloom.py @@ -0,0 +1,76 @@ +""" +Ethereum Logs Bloom +^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Logs Bloom related functionalities used in Ethereum. +""" + +from typing import Tuple + +from ethereum.base_types import Uint +from ethereum.crypto import keccak256 + +from .eth_types import Bloom, Log + + +def add_to_bloom(bloom: bytearray, bloom_entry: bytes) -> None: + """ + Add a bloom entry to the bloom filter (`bloom`). + + Parameters + ---------- + bloom : + The bloom filter. + bloom_entry : + An entry which is to be added to bloom filter. + """ + # TODO: This functionality hasn't been tested rigorously yet. + 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. + + 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. + """ + # TODO: Logs bloom functionality hasn't been tested rigorously yet. The + # required test cases need `CALL` opcode to be implemented. + 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/spurious_dragon/eth_types.py b/src/ethereum/spurious_dragon/eth_types.py new file mode 100644 index 0000000000..2d669ed497 --- /dev/null +++ b/src/ethereum/spurious_dragon/eth_types.py @@ -0,0 +1,155 @@ +""" +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 typing import Tuple, Union + +from .. import rlp +from ..base_types import ( + U256, + Bytes, + Bytes0, + Bytes8, + Bytes20, + Bytes32, + Bytes256, + Uint, + slotted_freezable, +) +from ..crypto import Hash32, keccak256 + +Address = Bytes20 +Root = Hash32 + +Bloom = Bytes256 + +TX_BASE_COST = 21000 +TX_DATA_COST_PER_NON_ZERO = 68 +TX_DATA_COST_PER_ZERO = 4 +TX_CREATE_COST = 32000 + + +@slotted_freezable +@dataclass +class Transaction: + """ + Atomic operation performed on the block chain. + """ + + nonce: U256 + gas_price: U256 + gas: U256 + to: Union[Bytes0, Address] + value: U256 + data: Bytes + v: U256 + r: U256 + s: U256 + + +@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), + ) + ) + + +@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 + mix_digest: Bytes32 + nonce: Bytes8 + + +@slotted_freezable +@dataclass +class Block: + """ + A complete block. + """ + + header: Header + transactions: Tuple[Transaction, ...] + ommers: Tuple[Header, ...] + + +@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. + """ + + post_state: Root + cumulative_gas_used: Uint + bloom: Bloom + logs: Tuple[Log, ...] diff --git a/src/ethereum/spurious_dragon/spec.py b/src/ethereum/spurious_dragon/spec.py new file mode 100644 index 0000000000..5684dcb005 --- /dev/null +++ b/src/ethereum/spurious_dragon/spec.py @@ -0,0 +1,820 @@ +""" +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, Set, Tuple + +from ethereum.base_types import Bytes0 +from ethereum.crypto import SECP256K1N +from ethereum.ethash import dataset_size, generate_cache, hashimoto_light +from ethereum.spurious_dragon.eth_types import TX_CREATE_COST +from ethereum.utils.ensure import ensure + +from .. import crypto, rlp +from ..base_types import U256, U256_CEIL_VALUE, Bytes, Uint +from . import CHAIN_ID, vm +from .bloom import logs_bloom +from .eth_types import ( + TX_BASE_COST, + TX_DATA_COST_PER_NON_ZERO, + TX_DATA_COST_PER_ZERO, + Address, + Block, + Bloom, + Hash32, + Header, + Log, + Receipt, + Root, + Transaction, +) +from .state import ( + State, + account_exists, + create_ether, + destroy_account, + get_account, + increment_nonce, + is_account_empty, + set_account_balance, + state_root, +) +from .trie import Trie, root, trie_set +from .utils.message import prepare_message +from .vm.interpreter import process_message_call + +BLOCK_REWARD = U256(5 * 10 ** 18) +GAS_LIMIT_ADJUSTMENT_FACTOR = 1024 +GAS_LIMIT_MINIMUM = 5000 +GENESIS_DIFFICULTY = Uint(131072) +MAX_OMMER_DEPTH = 6 + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + + +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. + + 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. + + 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 = crypto.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. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + """ + parent_header = chain.blocks[-1].header + validate_header(block.header, parent_header) + validate_ommers(block.ommers, block.header, chain) + ( + gas_used, + transactions_root, + receipt_root, + block_logs_bloom, + state, + ) = apply_body( + chain.state, + get_last_256_block_hashes(chain), + block.header.coinbase, + block.header.number, + block.header.gas_limit, + block.header.timestamp, + block.header.difficulty, + block.transactions, + block.ommers, + ) + ensure(gas_used == block.header.gas_used) + ensure(transactions_root == block.header.transactions_root) + ensure(state_root(state) == block.header.state_root) + ensure(receipt_root == block.header.receipt_root) + ensure(block_logs_bloom == block.header.bloom) + + 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 validate_header(header: Header, parent_header: Header) -> None: + """ + Verifies a block header. + + Parameters + ---------- + header : + Header to check for correctness. + parent_header : + Parent Header of the header to check for correctness + """ + block_difficulty = calculate_block_difficulty( + parent_header.number, + header.timestamp, + parent_header.timestamp, + parent_header.difficulty, + ) + + block_parent_hash = crypto.keccak256(rlp.encode(parent_header)) + + ensure(header.parent_hash == block_parent_hash) + ensure(header.difficulty == block_difficulty) + ensure(header.number == parent_header.number + 1) + ensure(check_gas_limit(header.gas_limit, parent_header.gas_limit)) + ensure(header.timestamp > parent_header.timestamp) + ensure(len(header.extra_data) <= 32) + + validate_proof_of_work(header) + + +def generate_header_hash_for_pow(header: Header) -> Hash32: + """ + Generate rlp hash of the header which is to be used for Proof-of-Work + verification. This hash is generated with the following header fields: + + * `parent_hash` + * `ommers_hash` + * `coinbase` + * `state_root` + * `transactions_root` + * `receipt_root` + * `bloom` + * `difficulty` + * `number` + * `gas_limit` + * `gas_used` + * `timestamp` + * `extra_data` + + In other words, the PoW artefacts `mix_digest` and `nonce` are ignored + while calculating this hash. + + Parameters + ---------- + header : + The header object for which the hash is to be generated. + + Returns + ------- + hash : `Hash32` + The PoW valid rlp hash of the passed in header. + """ + header_data_without_pow_artefacts = [ + header.parent_hash, + header.ommers_hash, + header.coinbase, + header.state_root, + header.transactions_root, + header.receipt_root, + header.bloom, + header.difficulty, + header.number, + header.gas_limit, + header.gas_used, + header.timestamp, + header.extra_data, + ] + + return rlp.rlp_hash(header_data_without_pow_artefacts) + + +def validate_proof_of_work(header: Header) -> None: + """ + Validates the Proof of Work constraints. + + Parameters + ---------- + header : + Header of interest. + """ + header_hash = generate_header_hash_for_pow(header) + # TODO: Memoize this somewhere and read from that data instead of + # calculating cache for every block validation. + cache = generate_cache(header.number) + mix_digest, result = hashimoto_light( + header_hash, header.nonce, cache, dataset_size(header.number) + ) + + ensure(mix_digest == header.mix_digest) + ensure( + Uint.from_be_bytes(result) <= (U256_CEIL_VALUE // header.difficulty) + ) + + +def apply_body( + state: State, + block_hashes: List[Hash32], + coinbase: Address, + block_number: Uint, + block_gas_limit: Uint, + block_time: U256, + block_difficulty: Uint, + transactions: Tuple[Transaction, ...], + ommers: Tuple[Header, ...], +) -> Tuple[Uint, Root, Root, Bloom, State]: + """ + Executes a block. + + Parameters + ---------- + state : + Current account state. + block_hashes : + List of hashes of the previous 256 blocks in the order of + increasing block number. + coinbase : + Address of account which receives block reward and transaction fees. + block_number : + Position of the block within the chain. + block_gas_limit : + Initial amount of gas available for execution in this block. + block_time : + Time the block was produced, measured in seconds since the epoch. + block_difficulty : + Difficulty of the block. + transactions : + Transactions included in the block. + ommers : + Headers of ancestor blocks which are not direct parents (formerly + uncles.) + + Returns + ------- + gas_available : `eth1spec.base_types.Uint` + Remaining gas after all transactions have been executed. + transactions_root : `eth1spec.eth_types.Root` + Trie root of all the transactions in the block. + receipt_root : `eth1spec.eth_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 : `eth1spec.eth_types.State` + State after all transactions have been executed. + """ + gas_available = block_gas_limit + transactions_trie: Trie[Bytes, Optional[Transaction]] = Trie( + secured=False, default=None + ) + receipts_trie: Trie[Bytes, Optional[Receipt]] = Trie( + secured=False, default=None + ) + block_logs: Tuple[Log, ...] = () + + for i, tx in enumerate(transactions): + trie_set(transactions_trie, rlp.encode(Uint(i)), tx) + + ensure(tx.gas <= gas_available) + sender_address = recover_sender(tx) + + env = vm.Environment( + caller=sender_address, + origin=sender_address, + block_hashes=block_hashes, + coinbase=coinbase, + number=block_number, + gas_limit=block_gas_limit, + gas_price=tx.gas_price, + time=block_time, + difficulty=block_difficulty, + state=state, + ) + + gas_used, logs = process_transaction(env, tx) + gas_available -= gas_used + + trie_set( + receipts_trie, + rlp.encode(Uint(i)), + Receipt( + post_state=state_root(state), + cumulative_gas_used=(block_gas_limit - gas_available), + bloom=logs_bloom(logs), + logs=logs, + ), + ) + block_logs += logs + + pay_rewards(state, block_number, coinbase, ommers) + + gas_remaining = block_gas_limit - gas_available + + block_logs_bloom = logs_bloom(block_logs) + + return ( + gas_remaining, + root(transactions_trie), + root(receipts_trie), + block_logs_bloom, + state, + ) + + +def validate_ommers( + ommers: Tuple[Header, ...], block_header: Header, chain: BlockChain +) -> None: + """ + Validates the ommers mentioned in the block. + + Parameters + ---------- + ommers : + List of ommers mentioned in the current block. + block_header: + The header of current block. + chain : + History and current state. + """ + block_hash = rlp.rlp_hash(block_header) + + ensure(rlp.rlp_hash(ommers) == block_header.ommers_hash) + + if len(ommers) == 0: + # Nothing to validate + return + + # Check that each ommer satisfies the constraints of a header + for ommer in ommers: + ensure(1 <= ommer.number < block_header.number) + ommer_parent_header = chain.blocks[ + -(block_header.number - ommer.number) - 1 + ].header + validate_header(ommer, ommer_parent_header) + + # Check that there can be only at most 2 ommers for a block. + ensure(len(ommers) <= 2) + + ommers_hashes = [rlp.rlp_hash(ommer) for ommer in ommers] + # Check that there are no duplicates in the ommers of current block + ensure(len(ommers_hashes) == len(set(ommers_hashes))) + + recent_canonical_blocks = chain.blocks[-(MAX_OMMER_DEPTH + 1) :] + recent_canonical_block_hashes = { + rlp.rlp_hash(block.header) for block in recent_canonical_blocks + } + recent_ommers_hashes: Set[Hash32] = set() + for block in recent_canonical_blocks: + recent_ommers_hashes = recent_ommers_hashes.union( + {rlp.rlp_hash(ommer) for ommer in block.ommers} + ) + + for ommer_index, ommer in enumerate(ommers): + ommer_hash = ommers_hashes[ommer_index] + # The current block shouldn't be the ommer + ensure(ommer_hash != block_hash) + + # Ommer shouldn't be one of the recent canonical blocks + ensure(ommer_hash not in recent_canonical_block_hashes) + + # Ommer shouldn't be one of the uncles mentioned in the recent + # canonical blocks + ensure(ommer_hash not in recent_ommers_hashes) + + # Ommer age with respect to the current block. For example, an age of + # 1 indicates that the ommer is a sibling of previous block. + ommer_age = block_header.number - ommer.number + ensure(1 <= ommer_age <= MAX_OMMER_DEPTH) + + ensure(ommer.parent_hash in recent_canonical_block_hashes) + ensure(ommer.parent_hash != block_header.parent_hash) + + +def pay_rewards( + state: State, + block_number: Uint, + coinbase: Address, + ommers: Tuple[Header, ...], +) -> None: + """ + Pay rewards to the block miner as well as the ommers miners. + + Parameters + ---------- + state : + Current account state. + block_number : + Position of the block within the chain. + coinbase : + Address of account which receives block reward and transaction fees. + ommers : + List of ommers mentioned in the current block. + """ + miner_reward = BLOCK_REWARD + (len(ommers) * (BLOCK_REWARD // 32)) + create_ether(state, coinbase, miner_reward) + + for ommer in ommers: + # Ommer age with respect to the current block. + ommer_age = U256(block_number - ommer.number) + ommer_miner_reward = ((8 - ommer_age) * BLOCK_REWARD) // 8 + create_ether(state, ommer.coinbase, ommer_miner_reward) + + +def process_transaction( + env: vm.Environment, tx: Transaction +) -> Tuple[U256, Tuple[Log, ...]]: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + env : + Environment for the Ethereum Virtual Machine. + tx : + Transaction to execute. + + Returns + ------- + gas_left : `eth1spec.base_types.U256` + Remaining gas after execution. + logs : `Tuple[eth1spec.eth_types.Log, ...]` + Logs generated during execution. + """ + ensure(validate_transaction(tx)) + + sender = env.origin + sender_account = get_account(env.state, sender) + gas_fee = tx.gas * tx.gas_price + ensure(sender_account.nonce == tx.nonce) + ensure(sender_account.balance >= gas_fee) + + gas = tx.gas - calculate_intrinsic_cost(tx) + increment_nonce(env.state, sender) + sender_balance_after_gas_fee = sender_account.balance - gas_fee + set_account_balance(env.state, sender, sender_balance_after_gas_fee) + + message = prepare_message( + sender, + tx.to, + tx.value, + tx.data, + gas, + env, + ) + + ( + gas_left, + refund_counter, + logs, + accounts_to_delete, + touched_accounts, + has_erred, + ) = process_message_call(message, env) + + gas_used = tx.gas - gas_left + gas_refund = min(gas_used // 2, refund_counter) + gas_refund_amount = (gas_left + gas_refund) * tx.gas_price + transaction_fee = (tx.gas - gas_left - gas_refund) * tx.gas_price + 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 + ) + set_account_balance( + env.state, env.coinbase, coinbase_balance_after_mining_fee + ) + + for address in accounts_to_delete: + destroy_account(env.state, address) + + for address in touched_accounts: + should_delete = account_exists( + env.state, address + ) and is_account_empty(env.state, address) + if should_delete: + destroy_account(env.state, address) + + return total_gas_used, logs + + +def validate_transaction(tx: Transaction) -> bool: + """ + Verifies a transaction. + + Parameters + ---------- + tx : + Transaction to validate. + + Returns + ------- + verified : `bool` + True if the transaction can be executed, or False otherwise. + """ + return calculate_intrinsic_cost(tx) <= tx.gas + + +def calculate_intrinsic_cost(tx: Transaction) -> Uint: + """ + Calculates the intrinsic cost of the transaction that is charged before + execution is instantiated. + + Parameters + ---------- + tx : + Transaction to compute the intrinsic cost of. + + Returns + ------- + verified : `eth1spec.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 + else: + create_cost = 0 + + return Uint(TX_BASE_COST + data_cost + create_cost) + + +def recover_sender(tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + sender : `eth1spec.eth_types.Address` + The address of the account that signed the transaction. + """ + v, r, s = tx.v, tx.r, tx.s + + ensure(0 < r and r < SECP256K1N) + ensure(0 < s and s <= SECP256K1N // 2) + + if v == 27 or v == 28: + public_key = crypto.secp256k1_recover( + r, s, v - 27, signing_hash_legacy(tx) + ) + else: + ensure(v == 35 + CHAIN_ID * 2 or v == 36 + CHAIN_ID * 2) + public_key = crypto.secp256k1_recover( + r, s, v - 35 - CHAIN_ID * 2, signing_hash_155(tx) + ) + return Address(crypto.keccak256(public_key)[12:32]) + + +def signing_hash_legacy(tx: Transaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre EIP 155) signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Hash of the transaction. + """ + return crypto.keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: Transaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP 155 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Hash of the transaction. + """ + return crypto.keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + Uint(1), + Uint(0), + Uint(0), + ) + ) + ) + + +def compute_header_hash(header: Header) -> Hash32: + """ + Computes the hash of a block header. + + Parameters + ---------- + header : + Header of interest. + + Returns + ------- + hash : `ethereum.eth_types.Hash32` + Hash of the header. + """ + return crypto.keccak256(rlp.encode(header)) + + +def get_block_header_by_hash(hash: Hash32, chain: BlockChain) -> Header: + """ + Fetches the block header with the corresponding hash. + + Parameters + ---------- + hash : + Hash of the header of interest. + + chain : + History and current state. + + Returns + ------- + Header : `ethereum.eth_types.Header` + Block header found by its hash. + """ + for block in chain.blocks: + if compute_header_hash(block.header) == hash: + return block.header + else: + raise ValueError(f"Could not find header with hash={hash.hex()}") + + +def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: + """ + Validates the gas limit for 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 + + +def calculate_block_difficulty( + parent_block_number: Uint, + timestamp: U256, + parent_timestamp: U256, + parent_difficulty: Uint, +) -> Uint: + """ + Computes difficulty of a block using its header and parent header. + + Parameters + ---------- + parent_block_number : + Block number of the parent block. + timestamp : + Timestamp of the block. + parent_timestamp : + Timestamp of the parent block. + parent_difficulty : + difficulty of the parent block. + + Returns + ------- + difficulty : `ethereum.base_types.Uint` + Computed difficulty for a block. + """ + offset = ( + int(parent_difficulty) + // 2048 + * max(1 - int(timestamp - parent_timestamp) // 10, -99) + ) + difficulty = int(parent_difficulty) + offset + # Historical Note: The difficulty bomb was not present in Ethereum at the + # start of Frontier, but was added shortly after launch. However since the + # bomb has no effect prior to block 200000 we pretend it existed from + # genesis. + # See https://github.com/ethereum/go-ethereum/pull/1588 + num_bomb_periods = ((int(parent_block_number) + 1) // 100000) - 2 + if num_bomb_periods >= 0: + return Uint( + max(difficulty + 2 ** num_bomb_periods, GENESIS_DIFFICULTY) + ) + else: + return Uint(max(difficulty, GENESIS_DIFFICULTY)) diff --git a/src/ethereum/spurious_dragon/state.py b/src/ethereum/spurious_dragon/state.py new file mode 100644 index 0000000000..715746c68a --- /dev/null +++ b/src/ethereum/spurious_dragon/state.py @@ -0,0 +1,483 @@ +""" +State +^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state contains all information that is preserved between transactions. + +It consists of a main account trie and storage tries for each contract. + +There is a distinction between an account that does not exist and +`EMPTY_ACCOUNT`. +""" +from dataclasses import dataclass, field +from typing import Callable, Dict, List, Optional, Tuple + +from ethereum.base_types import U256, Bytes, Uint, modify +from ethereum.utils.ensure import ensure + +from .eth_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) + + +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 + + +def begin_transaction(state: State) -> 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. + """ + state._snapshots.append( + ( + copy_trie(state._main_trie), + {k: copy_trie(t) for (k, t) in state._storage_tries.items()}, + ) + ) + + +def commit_transaction(state: State) -> None: + """ + Commit a state transaction. + + Parameters + ---------- + state : State + The state. + """ + state._snapshots.pop() + + +def rollback_transaction(state: State) -> None: + """ + Rollback a state transaction, resetting the state to the point when the + corresponding `start_transaction()` call was made. + + Parameters + ---------- + state : State + The state. + """ + state._main_trie, state._storage_tries = state._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 = trie_get(state._main_trie, 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. + """ + if address in state._storage_tries: + del state._storage_tries[address] + set_account(state, address, None) + + +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 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 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 non zero nonce, non empty code and non 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 non zero nonce, non empty code and + non zero balance, False otherwise. + """ + return ( + not account_has_code_or_nonce(state, address) + and get_account(state, address).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) + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(state, sender_address, reduce_sender_balance) + modify_state(state, recipient_address, increase_recipient_balance) + + +def set_account_balance(state: State, address: Address, amount: U256) -> None: + """ + Sets the balance of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + + amount: + The amount that needs to set in balance. + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(state, address, set_balance) + + +def touch_account(state: State, address: Address) -> None: + """ + Initializes an account to state. + + Parameters + ---------- + state: + The current state. + + address: + The address of the account that need to initialised. + """ + if not account_exists(state, address): + set_account(state, address, EMPTY_ACCOUNT) + + +def increment_nonce(state: State, address: Address) -> None: + """ + Increments the nonce of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += 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 create_ether(state: State, address: Address, amount: U256) -> None: + """ + Add newly created ether to an account. + + Parameters + ---------- + state: + The current state. + address: + Address of the account to which ether is added. + amount: + The amount of ether to be added to the account of interest. + """ + + def increase_balance(account: Account) -> None: + account.balance += amount + + modify_state(state, address, increase_balance) diff --git a/src/ethereum/spurious_dragon/trie.py b/src/ethereum/spurious_dragon/trie.py new file mode 100644 index 0000000000..eb73c73a7a --- /dev/null +++ b/src/ethereum/spurious_dragon/trie.py @@ -0,0 +1,468 @@ +""" +State Trie +^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`eth1spec.eth_types.Account` objects. +""" + +import copy +from dataclasses import dataclass, field +from typing import ( + Callable, + Dict, + Generic, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + TypeVar, + Union, + cast, +) + +import ethereum.tangerine_whistle.trie +from ethereum.utils.ensure import ensure +from ethereum.utils.hexadecimal import hex_to_bytes + +from .. import crypto, rlp +from ..base_types import U256, Bytes, Uint, slotted_freezable +from .eth_types import ( + Account, + Address, + Receipt, + Root, + Transaction, + encode_account, +) + +# note: an empty trie (regardless of whether it is secured) has root: +# +# crypto.keccak256(RLP(b'')) +# == +# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 # noqa: E501,SC10 +# +# also: +# +# crypto.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, Transaction, Receipt, Uint, U256, None] +K = TypeVar("K", bound=Bytes) +V = TypeVar( + "V", + Optional[Account], + Optional[Bytes], + Bytes, + Optional[Transaction], + Optional[Receipt], + 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 Exception(f"Invalid internal node type {type(node)}!") + + encoded = rlp.encode(unencoded) + if len(encoded) < 32: + return unencoded + else: + return crypto.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, (Transaction, Receipt, U256)): + return rlp.encode(cast(rlp.RLP, node)) + elif isinstance(node, Bytes): + return node + else: + return ethereum.tangerine_whistle.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: 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[eth1spec.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"") + key: Bytes + if trie.secured: + # "secure" tries hash keys once before construction + key = crypto.keccak256(preimage) + else: + key = preimage + mapped[bytes_to_nibble_list(key)] = encoded_value + + return mapped + + +def root( + trie: Trie[K, V], + get_storage_root: 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 : `eth1spec.eth_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 crypto.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 : `eth1spec.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 TypeError() + 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/spurious_dragon/utils/__init__.py b/src/ethereum/spurious_dragon/utils/__init__.py new file mode 100644 index 0000000000..df93f57810 --- /dev/null +++ b/src/ethereum/spurious_dragon/utils/__init__.py @@ -0,0 +1,13 @@ +""" +Spurious Dragon Utility Functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Utility functions used in this spurious dragon version of specification. +""" diff --git a/src/ethereum/spurious_dragon/utils/address.py b/src/ethereum/spurious_dragon/utils/address.py new file mode 100644 index 0000000000..c1c4545aa3 --- /dev/null +++ b/src/ethereum/spurious_dragon/utils/address.py @@ -0,0 +1,61 @@ +""" +Spurious Dragon Utility Functions For Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this spurious dragon version of +specification. +""" +from typing import Union + +from ethereum.base_types import U256, Uint +from ethereum.crypto import keccak256 + +from ... import rlp +from ..eth_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: `ethereum.spurious_dragon.eth_types.Address` + The computed address of the new account. + """ + computed_address = keccak256(rlp.encode([address, nonce])) + canonical_address = computed_address[-20:] + padded_address = canonical_address.rjust(20, b"\x00") + return Address(padded_address) diff --git a/src/ethereum/spurious_dragon/utils/hexadecimal.py b/src/ethereum/spurious_dragon/utils/hexadecimal.py new file mode 100644 index 0000000000..d9f39a5719 --- /dev/null +++ b/src/ethereum/spurious_dragon/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 +Spurious Dragon types. +""" +from ethereum.utils.hexadecimal import remove_hex_prefix + +from ..eth_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/spurious_dragon/utils/json.py b/src/ethereum/spurious_dragon/utils/json.py new file mode 100644 index 0000000000..c67e8a1e6d --- /dev/null +++ b/src/ethereum/spurious_dragon/utils/json.py @@ -0,0 +1,126 @@ +""" +Spurious Dragon Utilities Json +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Json specific utilities used in this spurious dragon version of +specification. +""" +from typing import Any, Dict, Tuple + +from ethereum.base_types import Bytes0 +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes8, + hex_to_bytes32, + hex_to_hash, + hex_to_u256, + hex_to_uint, +) + +from ..eth_types import Block, Header, Transaction +from ..utils.hexadecimal import hex_to_address, hex_to_bloom, hex_to_root + + +def json_to_transactions(json_data: Dict[Any, Any]) -> Tuple[Transaction, ...]: + """ + Convert json data to tuple of transaction objects. + + Parameters + ---------- + json_data : + The transactions data where the values are hexadecimals. + + Returns + ------- + transactions : `Tuple[Transaction, ...]` + The transaction objects obtained from the json data. + """ + transactions = [] + for transaction in json_data["transactions"]: + tx = Transaction( + nonce=hex_to_u256(transaction["nonce"]), + gas_price=hex_to_u256(transaction["gasPrice"]), + gas=hex_to_u256(transaction["gas"]), + to=( + Bytes0(b"") + if transaction["to"] == "" + else hex_to_address(transaction["to"]) + ), + value=hex_to_u256(transaction["value"]), + data=hex_to_bytes(transaction["input"]), + v=hex_to_u256(transaction["v"]), + r=hex_to_u256(transaction["r"]), + s=hex_to_u256(transaction["s"]), + ) + transactions.append(tx) + + return tuple(transactions) + + +def json_to_header(json_data: Dict[Any, Any]) -> Header: + """ + Convert json data to block header. + + Parameters + ---------- + json_data : + The header data where the values are hexadecimals. + + Returns + ------- + header : `Header` + The header object obtained from the json data. + """ + return Header( + parent_hash=hex_to_hash(json_data["parentHash"]), + ommers_hash=hex_to_hash(json_data["sha3Uncles"]), + coinbase=hex_to_address(json_data["miner"]), + state_root=hex_to_root(json_data["stateRoot"]), + transactions_root=hex_to_root(json_data["transactionsRoot"]), + receipt_root=hex_to_root(json_data["receiptsRoot"]), + bloom=hex_to_bloom(json_data["logsBloom"]), + difficulty=hex_to_uint(json_data["difficulty"]), + number=hex_to_uint(json_data["number"]), + gas_limit=hex_to_uint(json_data["gasLimit"]), + gas_used=hex_to_uint(json_data["gasUsed"]), + timestamp=hex_to_u256(json_data["timestamp"]), + extra_data=hex_to_bytes(json_data["extraData"]), + mix_digest=hex_to_bytes32(json_data["mixHash"]), + nonce=hex_to_bytes8(json_data["nonce"]), + ) + + +def json_to_block( + block_json_data: Dict[Any, Any], ommers: Tuple[Header, ...] +) -> Block: + """ + Convert json data to a block object with the help of ommer objects. + + Parameters + ---------- + block_json_data : + The block json data where the values are hexadecimals, which is used + to derive the header and the transactions. + ommers: + The ommer headers required to form the current block object. + + Returns + ------- + header : `Header` + The header object obtained from the json data. + """ + header = json_to_header(block_json_data) + transactions = json_to_transactions(block_json_data) + + return Block( + header=header, + transactions=transactions, + ommers=ommers, + ) diff --git a/src/ethereum/spurious_dragon/utils/message.py b/src/ethereum/spurious_dragon/utils/message.py new file mode 100644 index 0000000000..abb92d01a7 --- /dev/null +++ b/src/ethereum/spurious_dragon/utils/message.py @@ -0,0 +1,91 @@ +""" +Spurious Dragon Utility Functions For The Message Data-structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this spurious dragon version of +specification. +""" +from typing import Optional, Union + +from ethereum.base_types import U256, Bytes, Bytes0, Uint + +from ..eth_types import Address +from ..state import get_account +from ..vm import Environment, Message +from .address import compute_contract_address + + +def prepare_message( + caller: Address, + target: Union[Bytes0, Address], + value: U256, + data: Bytes, + gas: U256, + env: Environment, + code_address: Optional[Address] = None, + should_transfer_value: bool = True, +) -> 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. + + Returns + ------- + message: `ethereum.spurious_dragon.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 TypeError() + + 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, + ) diff --git a/src/ethereum/spurious_dragon/vm/__init__.py b/src/ethereum/spurious_dragon/vm/__init__.py new file mode 100644 index 0000000000..a4bceab256 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/__init__.py @@ -0,0 +1,82 @@ +""" +Ethereum Virtual Machine (EVM) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`eth1spec.eth_types.Account`. +""" + +from dataclasses import dataclass +from typing import Dict, List, Optional, Set, Tuple, Union + +from ethereum.base_types import U256, Bytes, Bytes0, Uint +from ethereum.crypto import Hash32 + +from ..eth_types import Address, Log +from ..state import State + +__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 + gas_limit: Uint + gas_price: U256 + time: U256 + difficulty: Uint + state: State + + +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + caller: Address + target: Union[Bytes0, Address] + current_target: Address + gas: U256 + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: U256 + env: Environment + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: U256 + running: bool + message: Message + output: Bytes + accounts_to_delete: Dict[Address, Address] + has_erred: bool + children: List["Evm"] diff --git a/src/ethereum/spurious_dragon/vm/error.py b/src/ethereum/spurious_dragon/vm/error.py new file mode 100644 index 0000000000..5893ea32fa --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/error.py @@ -0,0 +1,75 @@ +""" +Ethereum Virtual Machine (EVM) Errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Errors which cause the EVM to halt exceptionally. +""" + + +class StackUnderflowError(Exception): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(Exception): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(Exception): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(Exception): + """ + Raised when an invalid opcode is encountered. + """ + + pass + + +class InvalidJumpDestError(Exception): + """ + 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(Exception): + """ + Raised when the message depth is greater than `1024` + """ + + pass + + +class InsufficientFunds(Exception): + """ + Raised when an account has insufficient funds to transfer the + requested value. + """ + + pass diff --git a/src/ethereum/spurious_dragon/vm/gas.py b/src/ethereum/spurious_dragon/vm/gas.py new file mode 100644 index 0000000000..1a3bdf2f62 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/gas.py @@ -0,0 +1,234 @@ +""" +Ethereum Virtual Machine (EVM) Gas +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" +from ethereum.base_types import U256, Uint +from ethereum.utils.numeric import ceil32 +from ethereum.utils.safe_arithmetic import u256_safe_add + +from .error import OutOfGasError + +GAS_JUMPDEST = U256(1) +GAS_BASE = U256(2) +GAS_VERY_LOW = U256(3) +GAS_SLOAD = U256(200) +GAS_STORAGE_SET = U256(20000) +GAS_STORAGE_UPDATE = U256(5000) +GAS_STORAGE_CLEAR_REFUND = U256(15000) +GAS_LOW = U256(5) +GAS_MID = U256(8) +GAS_HIGH = U256(10) +GAS_EXPONENTIATION = U256(10) +GAS_EXPONENTIATION_PER_BYTE = U256(50) +GAS_MEMORY = U256(3) +GAS_KECCAK256 = U256(30) +GAS_KECCAK256_WORD = U256(6) +GAS_COPY = U256(3) +GAS_BLOCK_HASH = U256(20) +GAS_EXTERNAL = U256(700) +GAS_BALANCE = U256(400) +GAS_LOG = U256(375) +GAS_LOG_DATA = U256(8) +GAS_LOG_TOPIC = U256(375) +GAS_CREATE = U256(32000) +GAS_CODE_DEPOSIT = U256(200) +GAS_ZERO = U256(0) +GAS_CALL = U256(700) +GAS_NEW_ACCOUNT = U256(25000) +GAS_CALL_VALUE = U256(9000) +GAS_CALL_STIPEND = U256(2300) +GAS_SELF_DESTRUCT = U256(5000) +GAS_SELF_DESTRUCT_NEW_ACCOUNT = U256(25000) +REFUND_SELF_DESTRUCT = U256(24000) +GAS_ECRECOVER = U256(3000) +GAS_SHA256 = U256(60) +GAS_SHA256_WORD = U256(12) +GAS_RIPEMD160 = U256(600) +GAS_RIPEMD160_WORD = U256(120) +GAS_IDENTITY = U256(15) +GAS_IDENTITY_WORD = U256(3) + + +def subtract_gas(gas_left: U256, amount: U256) -> U256: + """ + Subtracts `amount` from `gas_left`. + + Parameters + ---------- + gas_left : + The amount of gas left in the current frame. + amount : + The amount of gas the current operation requires. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `gas_left` is less than `amount`. + """ + if gas_left < amount: + raise OutOfGasError + + return gas_left - amount + + +def calculate_memory_gas_cost(size_in_bytes: Uint) -> U256: + """ + 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.U256` + 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 U256(total_gas_cost) + except ValueError: + raise OutOfGasError + + +def calculate_gas_extend_memory( + memory: bytearray, start_position: Uint, size: U256 +) -> U256: + """ + Calculates the gas amount to extend memory + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size: + Amount of bytes by which the memory needs to be extended. + + Returns + ------- + to_be_paid : `ethereum.base_types.U256` + returns `0` if size=0 or if the + size after extending memory is less than the size before extending + else it returns the amount that needs to be paid for extendinng memory. + """ + if size == 0: + return U256(0) + memory_size = Uint(len(memory)) + before_size = ceil32(memory_size) + after_size = ceil32(start_position + size) + if after_size <= before_size: + return U256(0) + already_paid = calculate_memory_gas_cost(before_size) + total_cost = calculate_memory_gas_cost(after_size) + to_be_paid = total_cost - already_paid + return to_be_paid + + +def calculate_call_gas_cost( + gas: U256, gas_left: U256, extra_gas: U256 +) -> U256: + """ + Calculates the gas amount for executing Opcodes `CALL` and `CALLCODE`. + + Parameters + ---------- + gas : + The amount of gas provided to the message-call. + gas_left : + The amount of gas left in the current frame. + extra_gas : + The amount of gas needed for transferring value + creating a new + account inside a message call. + + Returns + ------- + call_gas_cost: `ethereum.base_types.U256` + The total gas amount for executing Opcodes `CALL` and `CALLCODE`. + """ + if gas_left < extra_gas: + raise OutOfGasError + + gas = min(gas, max_message_call_gas(gas_left - extra_gas)) + + return u256_safe_add( + gas, + extra_gas, + exception_type=OutOfGasError, + ) + + +def calculate_message_call_gas_stipend( + value: U256, + gas: U256, + gas_left: U256, + extra_gas: U256, + call_stipend: U256 = GAS_CALL_STIPEND, +) -> U256: + """ + Calculates the gas stipend for making the message call + with the given value. + + 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. + 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_stipend : `ethereum.base_types.U256` + The gas stipend for making the message-call. + """ + if gas_left < extra_gas: + raise OutOfGasError + + gas = min(gas, max_message_call_gas(gas_left - extra_gas)) + call_stipend = U256(0) if value == 0 else call_stipend + return u256_safe_add( + gas, + call_stipend, + exception_type=OutOfGasError, + ) + + +def max_message_call_gas(gas: U256) -> U256: + """ + 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.U256` + The maximum gas allowed for making the message-call. + """ + return gas - (gas // 64) diff --git a/src/ethereum/spurious_dragon/vm/instructions/__init__.py b/src/ethereum/spurious_dragon/vm/instructions/__init__.py new file mode 100644 index 0000000000..963a218a3f --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/__init__.py @@ -0,0 +1,330 @@ +""" +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 + + # 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 + + # Block Ops + BLOCKHASH = 0x40 + COINBASE = 0x41 + TIMESTAMP = 0x42 + NUMBER = 0x43 + DIFFICULTY = 0x44 + GASLIMIT = 0x45 + + # Control Flow Ops + STOP = 0x00 + JUMP = 0x56 + JUMPI = 0x57 + PC = 0x58 + GAS = 0x5A + JUMPDEST = 0x5B + + # Storage Ops + SLOAD = 0x54 + SSTORE = 0x55 + + # Pop Operation + POP = 0x50 + + # Push Operations + 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 + + # Log Operations + LOG0 = 0xA0 + LOG1 = 0xA1 + LOG2 = 0xA2 + LOG3 = 0xA3 + LOG4 = 0xA4 + + # System Operations + CREATE = 0xF0 + RETURN = 0xF3 + CALL = 0xF1 + CALLCODE = 0xF2 + DELEGATECALL = 0xF4 + SELFDESTRUCT = 0xFF + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: control_flow_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.LT: comparison_instructions.less_than, + Ops.GT: comparison_instructions.greater_than, + Ops.SLT: comparison_instructions.signed_less_than, + Ops.SGT: comparison_instructions.signed_greater_than, + Ops.EQ: comparison_instructions.equal, + Ops.ISZERO: comparison_instructions.is_zero, + Ops.AND: bitwise_instructions.bitwise_and, + Ops.OR: bitwise_instructions.bitwise_or, + Ops.XOR: bitwise_instructions.bitwise_xor, + Ops.NOT: bitwise_instructions.bitwise_not, + Ops.BYTE: bitwise_instructions.get_byte, + Ops.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.DIFFICULTY: block_instructions.difficulty, + Ops.GASLIMIT: block_instructions.gas_limit, + Ops.MLOAD: memory_instructions.mload, + Ops.MSTORE: memory_instructions.mstore, + Ops.MSTORE8: memory_instructions.mstore8, + Ops.MSIZE: memory_instructions.msize, + 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.SSTORE: storage_instructions.sstore, + 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.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, +} diff --git a/src/ethereum/spurious_dragon/vm/instructions/arithmetic.py b/src/ethereum/spurious_dragon/vm/instructions/arithmetic.py new file mode 100644 index 0000000000..1ef88b96a6 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/arithmetic.py @@ -0,0 +1,386 @@ +""" +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, + subtract_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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + result = x.wrapping_add(y) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + result = x.wrapping_sub(y) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + result = x.wrapping_mul(y) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + dividend = pop(evm.stack) + divisor = pop(evm.stack) + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + 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)) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `3`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `8`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_MID) + + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + if z == 0: + result = U256(0) + else: + result = U256((x + y) % z) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `3`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `8`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_MID) + + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + if z == 0: + result = U256(0) + else: + result = U256((x * y) % z) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + """ + base = Uint(pop(evm.stack)) + exponent = Uint(pop(evm.stack)) + + gas_used = GAS_EXPONENTIATION + if exponent != 0: + # 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 + gas_used += GAS_EXPONENTIATION_PER_BYTE * exponent_bytes + evm.gas_left = subtract_gas(evm.gas_left, gas_used) + + result = U256(pow(base, exponent, U256_CEIL_VALUE)) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `5`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + # byte_num would be 0-indexed when inserted to the stack. + byte_num = pop(evm.stack) + value = pop(evm.stack) + + if byte_num > 31: + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead b'\x00'. # noqa: SC100 + 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) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/bitwise.py b/src/ethereum/spurious_dragon/vm/instructions/bitwise.py new file mode 100644 index 0000000000..cd64333ce7 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/bitwise.py @@ -0,0 +1,157 @@ +""" +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 + +from .. import Evm +from ..gas import GAS_VERY_LOW, subtract_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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + x = pop(evm.stack) + y = pop(evm.stack) + push(evm.stack, x & y) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + x = pop(evm.stack) + y = pop(evm.stack) + push(evm.stack, x | y) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + x = pop(evm.stack) + y = pop(evm.stack) + push(evm.stack, x ^ y) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + x = pop(evm.stack) + push(evm.stack, ~x) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + # 0-indexed from left (most significant) to right (least significant) + # in "Big Endian" representation. + byte_index = pop(evm.stack) + word = pop(evm.stack) + + 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) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/block.py b/src/ethereum/spurious_dragon/vm/instructions/block.py new file mode 100644 index 0000000000..5af475a6ae --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/block.py @@ -0,0 +1,180 @@ +""" +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, subtract_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 + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `20`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BLOCK_HASH) + + block_number = pop(evm.stack) + + 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)) + + 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 + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equal to `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256.from_be_bytes(evm.env.coinbase)) + + 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 + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equal to `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, evm.env.time) + + 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 + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equal to `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(evm.env.number)) + + evm.pc += 1 + + +def difficulty(evm: Evm) -> None: + """ + Push the current block's difficulty 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 + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equal to `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(evm.env.difficulty)) + + 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 + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equal to `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(evm.env.gas_limit)) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/comparison.py b/src/ethereum/spurious_dragon/vm/instructions/comparison.py new file mode 100644 index 0000000000..e264ee1334 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/comparison.py @@ -0,0 +1,184 @@ +""" +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, subtract_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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + left = pop(evm.stack) + right = pop(evm.stack) + result = U256(left < right) + + push(evm.stack, result) + + evm.pc += 1 + + +def signed_less_than(evm: Evm) -> None: + """ + Signed less-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + result = U256(left < right) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + left = pop(evm.stack) + right = pop(evm.stack) + result = U256(left > right) + + push(evm.stack, result) + + evm.pc += 1 + + +def signed_greater_than(evm: Evm) -> None: + """ + Signed greater-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + result = U256(left > right) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + left = pop(evm.stack) + right = pop(evm.stack) + result = U256(left == right) + + push(evm.stack, result) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + x = pop(evm.stack) + result = U256(x == 0) + + push(evm.stack, result) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/control_flow.py b/src/ethereum/spurious_dragon/vm/instructions/control_flow.py new file mode 100644 index 0000000000..99f1ad677f --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/control_flow.py @@ -0,0 +1,167 @@ +""" +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.error import InvalidJumpDestError +from ...vm.gas import GAS_BASE, GAS_HIGH, GAS_JUMPDEST, GAS_MID, subtract_gas +from .. import Evm +from ..stack import pop, push + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + evm.running = False + + +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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.InvalidJumpDestError + If the jump destination 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. + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `8`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_MID) + jump_dest = pop(evm.stack) + + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.InvalidJumpDestError + If the jump destination 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. + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `10`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_HIGH) + + jump_dest = pop(evm.stack) + conditional_value = pop(evm.stack) + + if conditional_value == 0: + evm.pc += 1 + return + + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + evm.pc = Uint(jump_dest) + + +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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is more than `1023`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(evm.pc)) + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is more than `1023`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, evm.gas_left) + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `1`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_JUMPDEST) + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/environment.py b/src/ethereum/spurious_dragon/vm/instructions/environment.py new file mode 100644 index 0000000000..fd279616a4 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/environment.py @@ -0,0 +1,434 @@ +""" +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, Uint +from ethereum.utils.numeric import ceil32 +from ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...state import get_account +from ...utils.address import to_address +from ...vm.error import OutOfGasError +from ...vm.memory import extend_memory, memory_write +from .. import Evm +from ..gas import ( + GAS_BALANCE, + GAS_BASE, + GAS_COPY, + GAS_EXTERNAL, + GAS_VERY_LOW, + calculate_gas_extend_memory, + subtract_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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + + evm.pc += 1 + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `20`. + """ + # TODO: There are no test cases against this function. Need to write + # custom test cases. + evm.gas_left = subtract_gas(evm.gas_left, GAS_BALANCE) + + address = to_address(pop(evm.stack)) + + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.env.state, address).balance + + push(evm.stack, balance) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256.from_be_bytes(evm.env.origin)) + + evm.pc += 1 + + +def caller(evm: Evm) -> None: + """ + Pushes the address of the caller onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256.from_be_bytes(evm.message.caller)) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, evm.message.value) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + # Converting start_index to Uint from U256 as start_index + 32 can + # overflow U256. + start_index = Uint(pop(evm.stack)) + value = evm.message.data[start_index : start_index + 32] + # Right pad with 0 so that there are overall 32 bytes. + value = value.ljust(32, b"\x00") + + push(evm.stack, U256.from_be_bytes(value)) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(len(evm.message.data))) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `3`. + """ + # Converting below to Uint as though the start indices may belong to U256, + # the ending indices may overflow U256. + memory_start_index = Uint(pop(evm.stack)) + data_start_index = Uint(pop(evm.stack)) + size = pop(evm.stack) + + words = ceil32(Uint(size)) // 32 + copy_gas_cost = u256_safe_multiply( + GAS_COPY, + words, + exception_type=OutOfGasError, + ) + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, memory_start_index, size + ) + total_gas_cost = u256_safe_add( + GAS_VERY_LOW, + copy_gas_cost, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + evm.pc += 1 + + if size == 0: + return + + extend_memory(evm.memory, memory_start_index, size) + + value = evm.message.data[data_start_index : data_start_index + size] + # But it is possible that data_start_index + size won't exist in evm.data + # in which case we need to right pad the above obtained bytes with 0. + value = value.ljust(size, b"\x00") + + memory_write(evm.memory, memory_start_index, value) + + +def codesize(evm: Evm) -> None: + """ + Push the size of code running in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, U256(len(evm.code))) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `3`. + """ + # Converting below to Uint as though the start indices may belong to U256, + # the ending indices may not belong to U256. + memory_start_index = Uint(pop(evm.stack)) + code_start_index = Uint(pop(evm.stack)) + size = pop(evm.stack) + + words = ceil32(Uint(size)) // 32 + copy_gas_cost = u256_safe_multiply( + GAS_COPY, + words, + exception_type=OutOfGasError, + ) + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, memory_start_index, size + ) + total_gas_cost = u256_safe_add( + GAS_VERY_LOW, + copy_gas_cost, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + evm.pc += 1 + + if size == 0: + return + + extend_memory(evm.memory, memory_start_index, size) + + value = evm.code[code_start_index : code_start_index + size] + # But it is possible that code_start_index + size - 1 won't exist in + # evm.code in which case we need to right pad the above obtained bytes + # with 0. + value = value.ljust(size, b"\x00") + + memory_write(evm.memory, memory_start_index, value) + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + push(evm.stack, evm.env.gas_price) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `20`. + """ + # TODO: There are no test cases against this function. Need to write + # custom test cases. + evm.gas_left = subtract_gas(evm.gas_left, GAS_EXTERNAL) + + address = to_address(pop(evm.stack)) + + # 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) + + evm.pc += 1 + + +def extcodecopy(evm: Evm) -> None: + """ + Copy a portion of an account's code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `4`. + """ + # TODO: There are no test cases against this function. Need to write + # custom test cases. + + address = to_address(pop(evm.stack)) + + memory_start_index = Uint(pop(evm.stack)) + code_start_index = Uint(pop(evm.stack)) + size = pop(evm.stack) + + words = ceil32(Uint(size)) // 32 + copy_gas_cost = u256_safe_multiply( + GAS_COPY, + words, + exception_type=OutOfGasError, + ) + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, memory_start_index, size + ) + total_gas_cost = u256_safe_add( + GAS_EXTERNAL, + copy_gas_cost, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + evm.pc += 1 + + if size == 0: + return + + # Non-existent accounts default to EMPTY_ACCOUNT, which has empty code. + code = get_account(evm.env.state, address).code + + extend_memory(evm.memory, memory_start_index, size) + + value = code[code_start_index : code_start_index + size] + # But it is possible that code_start_index + size won't exist in evm.code + # in which case we need to right pad the above obtained bytes with 0. + value = value.ljust(size, b"\x00") + + memory_write(evm.memory, memory_start_index, value) diff --git a/src/ethereum/spurious_dragon/vm/instructions/keccak.py b/src/ethereum/spurious_dragon/vm/instructions/keccak.py new file mode 100644 index 0000000000..1736b6eefd --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/keccak.py @@ -0,0 +1,78 @@ +""" +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 import keccak256 +from ethereum.utils.numeric import ceil32 +from ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...vm.error import OutOfGasError +from .. import Evm +from ..gas import ( + GAS_KECCAK256, + GAS_KECCAK256_WORD, + calculate_gas_extend_memory, + subtract_gas, +) +from ..memory import extend_memory, 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + """ + # Converting memory_start_index to Uint as memory_end_index can + # overflow U256. + memory_start_index = Uint(pop(evm.stack)) + size = pop(evm.stack) + + words = ceil32(Uint(size)) // 32 + word_gas_cost = u256_safe_multiply( + GAS_KECCAK256_WORD, + words, + exception_type=OutOfGasError, + ) + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, memory_start_index, size + ) + total_gas_cost = u256_safe_add( + GAS_KECCAK256, + word_gas_cost, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + extend_memory(evm.memory, memory_start_index, size) + + data = memory_read_bytes(evm.memory, memory_start_index, size) + hash = keccak256(data) + + push(evm.stack, U256.from_be_bytes(hash)) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/log.py b/src/ethereum/spurious_dragon/vm/instructions/log.py new file mode 100644 index 0000000000..43e0e3b46a --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/log.py @@ -0,0 +1,97 @@ +""" +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, Uint +from ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...eth_types import Log +from ...vm.error import OutOfGasError +from .. import Evm +from ..gas import ( + GAS_LOG, + GAS_LOG_DATA, + GAS_LOG_TOPIC, + calculate_gas_extend_memory, + subtract_gas, +) +from ..memory import extend_memory, 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2 + num_topics`. + """ + # Converting memory_start_index to Uint as memory_start_index + size - 1 + # can overflow U256. + memory_start_index = Uint(pop(evm.stack)) + size = pop(evm.stack) + + gas_cost_log_data = u256_safe_multiply( + GAS_LOG_DATA, size, exception_type=OutOfGasError + ) + gas_cost_log_topic = u256_safe_multiply( + GAS_LOG_TOPIC, num_topics, exception_type=OutOfGasError + ) + gas_cost_memory_extend = calculate_gas_extend_memory( + evm.memory, memory_start_index, size + ) + gas_cost = u256_safe_add( + GAS_LOG, + gas_cost_log_data, + gas_cost_log_topic, + gas_cost_memory_extend, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_cost) + + extend_memory(evm.memory, memory_start_index, size) + + topics = [] + for _ in range(num_topics): + topic = pop(evm.stack).to_be_bytes32() + topics.append(topic) + + 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,) + + 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/spurious_dragon/vm/instructions/memory.py b/src/ethereum/spurious_dragon/vm/instructions/memory.py new file mode 100644 index 0000000000..d69a60b124 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/memory.py @@ -0,0 +1,169 @@ +""" +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 U8_MAX_VALUE, U256, Uint +from ethereum.utils.safe_arithmetic import u256_safe_add + +from ...vm.error import OutOfGasError +from .. import Evm +from ..gas import ( + GAS_BASE, + GAS_VERY_LOW, + calculate_gas_extend_memory, + subtract_gas, +) +from ..memory import extend_memory, 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than + `3` + gas needed to extend memeory. + """ + # convert to Uint as start_position + size_to_extend can overflow. + start_position = Uint(pop(evm.stack)) + value = pop(evm.stack).to_be_bytes32() + + gas_cost_memory_extend = calculate_gas_extend_memory( + evm.memory, start_position, U256(32) + ) + total_gas_cost = u256_safe_add( + GAS_VERY_LOW, + gas_cost_memory_extend, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + # extend memory and subtract gas for allocating 32 bytes of memory + extend_memory(evm.memory, start_position, U256(32)) + memory_write(evm.memory, start_position, value) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than + `3` + gas needed to extend memory. + """ + # convert to Uint as start_position + size_to_extend can overflow. + start_position = Uint(pop(evm.stack)) + value = pop(evm.stack) + # make sure that value doesn't exceed 1 byte + normalized_bytes_value = (value & U8_MAX_VALUE).to_be_bytes() + + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, start_position, U256(1) + ) + total_gas_cost = u256_safe_add( + GAS_VERY_LOW, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + # extend memory and subtract gas for allocating 32 bytes of memory + extend_memory(evm.memory, start_position, U256(1)) + memory_write(evm.memory, start_position, normalized_bytes_value) + + evm.pc += 1 + + +def mload(evm: Evm) -> None: + """ + Load word from memory. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than + `3` + gas needed to extend memory. + """ + # convert to Uint as start_position + size_to_extend can overflow. + start_position = Uint(pop(evm.stack)) + + memory_extend_gas_cost = calculate_gas_extend_memory( + evm.memory, start_position, U256(32) + ) + total_gas_cost = u256_safe_add( + GAS_VERY_LOW, + memory_extend_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + + # extend memory and subtract gas for allocating 32 bytes of memory + extend_memory(evm.memory, start_position, U256(32)) + value = U256.from_be_bytes( + memory_read_bytes(evm.memory, start_position, U256(32)) + ) + push(evm.stack, value) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2` + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + memory_size = U256(len(evm.memory)) + push(evm.stack, memory_size) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/stack.py b/src/ethereum/spurious_dragon/vm/instructions/stack.py new file mode 100644 index 0000000000..fc1ca938b7 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/stack.py @@ -0,0 +1,204 @@ +""" +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.spurious_dragon.vm.error import StackUnderflowError +from ethereum.utils.ensure import ensure + +from .. import Evm, stack +from ..gas import GAS_BASE, GAS_VERY_LOW, subtract_gas + + +def pop(evm: Evm) -> None: + """ + Remove item from stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `2`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_BASE) + stack.pop(evm.stack) + + evm.pc += 1 + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes a N-byte immediate onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is equals `1024`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + data_to_push = U256.from_be_bytes( + evm.code[evm.pc + 1 : evm.pc + num_bytes + 1] + ) + stack.push(evm.stack, data_to_push) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + ensure(item_number < len(evm.stack), exception_class=StackUnderflowError) + + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `3`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + ensure(item_number < len(evm.stack), exception_class=StackUnderflowError) + + top_element_idx = len(evm.stack) - 1 + nth_element_idx = len(evm.stack) - 1 - item_number + evm.stack[top_element_idx], evm.stack[nth_element_idx] = ( + evm.stack[nth_element_idx], + evm.stack[top_element_idx], + ) + + evm.pc += 1 + + +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/spurious_dragon/vm/instructions/storage.py b/src/ethereum/spurious_dragon/vm/instructions/storage.py new file mode 100644 index 0000000000..24d132d6b5 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/storage.py @@ -0,0 +1,91 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" + +from ...state import get_storage, set_storage +from .. import Evm +from ..gas import ( + GAS_SLOAD, + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + subtract_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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `1`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `50`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_SLOAD) + + key = pop(evm.stack).to_be_bytes32() + value = get_storage(evm.env.state, evm.message.current_target, key) + + push(evm.stack, value) + + 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `len(stack)` is less than `2`. + ethereum.spurious_dragon.vm.error.OutOfGasError + If `evm.gas_left` is less than `20000`. + """ + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + current_value = get_storage(evm.env.state, evm.message.current_target, key) + + # TODO: SSTORE gas usage hasn't been tested yet. Testing this needs + # other opcodes to be implemented. + # Calculating the gas needed for the storage + if new_value != 0 and current_value == 0: + gas_cost = GAS_STORAGE_SET + else: + gas_cost = GAS_STORAGE_UPDATE + + evm.gas_left = subtract_gas(evm.gas_left, gas_cost) + + # TODO: Refund counter hasn't been tested yet. Testing this needs other + # Opcodes to be implemented + if new_value == 0 and current_value != 0: + evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + + set_storage(evm.env.state, evm.message.current_target, key, new_value) + + evm.pc += 1 diff --git a/src/ethereum/spurious_dragon/vm/instructions/system.py b/src/ethereum/spurious_dragon/vm/instructions/system.py new file mode 100644 index 0000000000..a871b6cdf1 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/instructions/system.py @@ -0,0 +1,474 @@ +""" +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.spurious_dragon.state import account_exists +from ethereum.spurious_dragon.vm.gas import ( + GAS_CALL, + GAS_CALL_VALUE, + GAS_NEW_ACCOUNT, + GAS_SELF_DESTRUCT, + GAS_SELF_DESTRUCT_NEW_ACCOUNT, + max_message_call_gas, +) +from ethereum.utils.safe_arithmetic import u256_safe_add + +from ...state import ( + account_has_code_or_nonce, + get_account, + increment_nonce, + is_account_empty, + set_account_balance, +) +from ...utils.address import compute_contract_address, to_address +from ...vm.error import OutOfGasError +from .. import Evm, Message +from ..gas import ( + GAS_CREATE, + GAS_ZERO, + calculate_call_gas_cost, + calculate_gas_extend_memory, + calculate_message_call_gas_stipend, + subtract_gas, +) +from ..memory import extend_memory, memory_read_bytes, memory_write +from ..stack import pop, push + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message + + endowment = pop(evm.stack) + memory_start_position = Uint(pop(evm.stack)) + memory_size = pop(evm.stack) + + extend_memory_gas_cost = calculate_gas_extend_memory( + evm.memory, memory_start_position, memory_size + ) + total_gas_cost = u256_safe_add( + GAS_CREATE, + extend_memory_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + extend_memory(evm.memory, memory_start_position, memory_size) + sender_address = evm.message.current_target + sender = get_account(evm.env.state, sender_address) + + evm.pc += 1 + + if sender.balance < endowment: + push(evm.stack, U256(0)) + return None + + if evm.message.depth + 1 > STACK_DEPTH_LIMIT: + push(evm.stack, U256(0)) + return None + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + increment_nonce(evm.env.state, evm.message.current_target) + + create_message_gas = max_message_call_gas(evm.gas_left) + evm.gas_left = subtract_gas(evm.gas_left, create_message_gas) + + contract_address = compute_contract_address( + evm.message.current_target, + get_account(evm.env.state, evm.message.current_target).nonce - U256(1), + ) + is_collision = account_has_code_or_nonce(evm.env.state, contract_address) + if is_collision: + push(evm.stack, U256(0)) + return + + 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, + ) + child_evm = process_create_message(child_message, evm.env) + evm.children.append(child_evm) + if child_evm.has_erred: + push(evm.stack, U256(0)) + else: + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + evm.gas_left += child_evm.gas_left + evm.refund_counter += child_evm.refund_counter + evm.logs += child_evm.logs + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + """ + memory_start_position = Uint(pop(evm.stack)) + memory_size = pop(evm.stack) + gas_cost = GAS_ZERO + calculate_gas_extend_memory( + evm.memory, memory_start_position, memory_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_cost) + extend_memory(evm.memory, memory_start_position, memory_size) + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + # HALT the execution + evm.running = False + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + """ + from ethereum.spurious_dragon.vm.interpreter import ( + STACK_DEPTH_LIMIT, + process_message, + ) + + evm.gas_left = subtract_gas(evm.gas_left, GAS_CALL) + + gas = pop(evm.stack) + to = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = Uint(pop(evm.stack)) + memory_input_size = pop(evm.stack) + memory_output_start_position = Uint(pop(evm.stack)) + memory_output_size = pop(evm.stack) + + gas_input_memory = calculate_gas_extend_memory( + evm.memory, memory_input_start_position, memory_input_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_input_memory) + extend_memory(evm.memory, memory_input_start_position, memory_input_size) + gas_output_memory = calculate_gas_extend_memory( + evm.memory, memory_output_start_position, memory_output_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_output_memory) + extend_memory(evm.memory, memory_output_start_position, memory_output_size) + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + is_account_alive = account_exists( + evm.env.state, to + ) and not is_account_empty(evm.env.state, to) + create_gas_cost = ( + U256(0) if is_account_alive or value == 0 else GAS_NEW_ACCOUNT + ) + transfer_gas_cost = U256(0) if value == 0 else GAS_CALL_VALUE + extra_gas = u256_safe_add( + create_gas_cost, + transfer_gas_cost, + exception_type=OutOfGasError, + ) + call_gas_fee = calculate_call_gas_cost(gas, evm.gas_left, extra_gas) + message_call_gas_fee = calculate_message_call_gas_stipend( + value, gas, evm.gas_left, extra_gas + ) + + evm.gas_left = subtract_gas(evm.gas_left, call_gas_fee) + sender_balance = get_account( + evm.env.state, evm.message.current_target + ).balance + + evm.pc += 1 + + if sender_balance < value: + push(evm.stack, U256(0)) + evm.gas_left += message_call_gas_fee + return None + if evm.message.depth + 1 > STACK_DEPTH_LIMIT: + push(evm.stack, U256(0)) + evm.gas_left += message_call_gas_fee + return None + + code = get_account(evm.env.state, to).code + child_message = Message( + caller=evm.message.current_target, + target=to, + gas=message_call_gas_fee, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + 1, + code_address=to, + should_transfer_value=True, + ) + child_evm = process_message(child_message, evm.env) + evm.children.append(child_evm) + + if child_evm.has_erred: + push(evm.stack, U256(0)) + else: + 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], + ) + evm.gas_left += child_evm.gas_left + evm.refund_counter += child_evm.refund_counter + evm.logs += child_evm.logs + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account’s code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + from ethereum.spurious_dragon.vm.interpreter import ( + STACK_DEPTH_LIMIT, + process_message, + ) + + evm.gas_left = subtract_gas(evm.gas_left, GAS_CALL) + + gas = pop(evm.stack) + code_address = to_address(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = Uint(pop(evm.stack)) + memory_input_size = pop(evm.stack) + memory_output_start_position = Uint(pop(evm.stack)) + memory_output_size = pop(evm.stack) + to = evm.message.current_target + + gas_input_memory = calculate_gas_extend_memory( + evm.memory, memory_input_start_position, memory_input_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_input_memory) + extend_memory(evm.memory, memory_input_start_position, memory_input_size) + gas_output_memory = calculate_gas_extend_memory( + evm.memory, memory_output_start_position, memory_output_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_output_memory) + extend_memory(evm.memory, memory_output_start_position, memory_output_size) + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + transfer_gas_cost = U256(0) if value == 0 else GAS_CALL_VALUE + extra_gas = transfer_gas_cost + call_gas_fee = calculate_call_gas_cost(gas, evm.gas_left, extra_gas) + message_call_gas_fee = calculate_message_call_gas_stipend( + value, gas, evm.gas_left, extra_gas + ) + + evm.gas_left = subtract_gas(evm.gas_left, call_gas_fee) + + sender_balance = get_account( + evm.env.state, evm.message.current_target + ).balance + + evm.pc += 1 + + if sender_balance < value: + push(evm.stack, U256(0)) + evm.gas_left += message_call_gas_fee + return None + if evm.message.depth + 1 > STACK_DEPTH_LIMIT: + push(evm.stack, U256(0)) + evm.gas_left += message_call_gas_fee + return None + + code = get_account(evm.env.state, code_address).code + child_message = Message( + caller=evm.message.current_target, + target=to, + gas=message_call_gas_fee, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + 1, + code_address=code_address, + should_transfer_value=True, + ) + + child_evm = process_message(child_message, evm.env) + evm.children.append(child_evm) + if child_evm.has_erred: + push(evm.stack, U256(0)) + else: + 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], + ) + evm.gas_left += child_evm.gas_left + evm.refund_counter += child_evm.refund_counter + evm.logs += child_evm.logs + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_SELF_DESTRUCT) + beneficiary = to_address(pop(evm.stack)) + + originator = evm.message.current_target + beneficiary_balance = get_account(evm.env.state, beneficiary).balance + originator_balance = get_account(evm.env.state, originator).balance + + is_dead_account = not account_exists( + evm.env.state, beneficiary + ) or is_account_empty(evm.env.state, beneficiary) + + if is_dead_account and originator_balance != 0: + evm.gas_left = subtract_gas( + evm.gas_left, GAS_SELF_DESTRUCT_NEW_ACCOUNT + ) + + # First Transfer to beneficiary + # This call will trigger state clearing if the beneficiary is EMPTY_ACCOUNT + set_account_balance( + evm.env.state, beneficiary, beneficiary_balance + originator_balance + ) + # Next, Zero the balance of the address being deleted (must come after + # sending to beneficiary in case the contract named itself as the + # beneficiary). + set_account_balance(evm.env.state, originator, U256(0)) + + # register account for deletion + evm.accounts_to_delete[originator] = beneficiary + + # HALT the execution + evm.running = False + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into this account with an alternative account’s code, + but persisting the current values for sender. + + Parameters + ---------- + evm : + The current EVM frame. + """ + from ethereum.spurious_dragon.vm.interpreter import ( + STACK_DEPTH_LIMIT, + process_message, + ) + + evm.gas_left = subtract_gas(evm.gas_left, GAS_CALL) + + gas = pop(evm.stack) + code_address = to_address(pop(evm.stack)) + memory_input_start_position = Uint(pop(evm.stack)) + memory_input_size = pop(evm.stack) + memory_output_start_position = Uint(pop(evm.stack)) + memory_output_size = pop(evm.stack) + value = evm.message.value + to = evm.message.current_target + + gas_input_memory = calculate_gas_extend_memory( + evm.memory, memory_input_start_position, memory_input_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_input_memory) + extend_memory(evm.memory, memory_input_start_position, memory_input_size) + gas_output_memory = calculate_gas_extend_memory( + evm.memory, memory_output_start_position, memory_output_size + ) + evm.gas_left = subtract_gas(evm.gas_left, gas_output_memory) + extend_memory(evm.memory, memory_output_start_position, memory_output_size) + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + extra_gas = U256(0) + call_gas_fee = calculate_call_gas_cost(gas, evm.gas_left, extra_gas) + message_call_gas_fee = calculate_message_call_gas_stipend( + value, gas, evm.gas_left, extra_gas, call_stipend=U256(0) + ) + + evm.gas_left = subtract_gas(evm.gas_left, call_gas_fee) + + evm.pc += 1 + + if evm.message.depth + 1 > STACK_DEPTH_LIMIT: + push(evm.stack, U256(0)) + evm.gas_left += message_call_gas_fee + return None + + code = get_account(evm.env.state, code_address).code + child_message = Message( + caller=evm.message.caller, + target=to, + gas=message_call_gas_fee, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + 1, + code_address=code_address, + should_transfer_value=False, + ) + + child_evm = process_message(child_message, evm.env) + evm.children.append(child_evm) + if child_evm.has_erred: + push(evm.stack, U256(0)) + else: + 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], + ) + evm.gas_left += child_evm.gas_left + evm.refund_counter += child_evm.refund_counter + evm.logs += child_evm.logs diff --git a/src/ethereum/spurious_dragon/vm/interpreter.py b/src/ethereum/spurious_dragon/vm/interpreter.py new file mode 100644 index 0000000000..33b3a5b3ef --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/interpreter.py @@ -0,0 +1,357 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" +from typing import Iterable, Set, Tuple, Union + +from ethereum.base_types import U256, Bytes0, Uint +from ethereum.utils.ensure import EnsureError, ensure + +from ..eth_types import Address, Log +from ..state import ( + account_has_code_or_nonce, + begin_transaction, + commit_transaction, + get_account, + increment_nonce, + move_ether, + rollback_transaction, + set_code, + touch_account, +) +from ..utils.address import to_address +from ..vm import Message +from ..vm.error import ( + InsufficientFunds, + InvalidJumpDestError, + InvalidOpcode, + OutOfGasError, + StackDepthLimitError, + StackOverflowError, + StackUnderflowError, +) +from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, subtract_gas +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import Environment, Evm +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = U256(1024) +MAX_CODE_SIZE = 0x6000 +THREE = to_address(Uint(3)) + + +def process_message_call( + message: Message, env: Environment +) -> Tuple[ + U256, + U256, + Union[Tuple[()], Tuple[Log, ...]], + Set[Address], + Iterable[Address], + bool, +]: + """ + 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 : `Tuple` + A tuple of 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. `has_erred`: True if execution has caused an error. + """ + if message.target == Bytes0(b""): + is_collision = account_has_code_or_nonce( + env.state, message.current_target + ) + if is_collision: + return U256(0), U256(0), tuple(), set(), set(), True + else: + evm = process_create_message(message, env) + else: + evm = process_message(message, env) + + accounts_to_delete = collect_accounts_to_delete(evm, set()) + evm.refund_counter += len(accounts_to_delete) * REFUND_SELF_DESTRUCT + + return ( + evm.gas_left, + evm.refund_counter, + evm.logs, + accounts_to_delete, + collect_touched_accounts(evm), + evm.has_erred, + ) + + +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: `ethereum.spurious_dragon.vm.Evm` + Items containing execution specific objects. + """ + # take snapshot of state before processing the message + begin_transaction(env.state) + + increment_nonce(env.state, message.current_target) + evm = process_message(message, env) + if not evm.has_erred: + contract_code = evm.output + contract_code_gas = len(contract_code) * GAS_CODE_DEPOSIT + try: + evm.gas_left = subtract_gas(evm.gas_left, contract_code_gas) + ensure(len(contract_code) <= MAX_CODE_SIZE, OutOfGasError) + except OutOfGasError: + rollback_transaction(env.state) + evm.gas_left = U256(0) + evm.logs = () + evm.accounts_to_delete = dict() + evm.refund_counter = U256(0) + evm.has_erred = True + else: + set_code(env.state, message.current_target, contract_code) + commit_transaction(env.state) + else: + rollback_transaction(env.state) + 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: `ethereum.spurious_dragon.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) + + touch_account(env.state, message.current_target) + + sender_balance = get_account(env.state, message.caller).balance + + if message.should_transfer_value and message.value != 0: + if sender_balance < message.value: + rollback_transaction(env.state) + raise InsufficientFunds( + f"Insufficient funds: {sender_balance} < {message.value}" + ) + move_ether( + env.state, message.caller, message.current_target, message.value + ) + + evm = execute_code(message, env) + if evm.has_erred: + # revert state to the last saved checkpoint + # since the message call resulted in an error + rollback_transaction(env.state) + else: + commit_transaction(env.state) + 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=U256(0), + running=True, + message=message, + output=b"", + accounts_to_delete=dict(), + has_erred=False, + children=[], + ) + try: + + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + 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]) + + op_implementation[op](evm) + + except ( + OutOfGasError, + InvalidOpcode, + InvalidJumpDestError, + InsufficientFunds, + StackOverflowError, + StackUnderflowError, + StackDepthLimitError, + ): + evm.gas_left = U256(0) + evm.logs = () + evm.accounts_to_delete = dict() + evm.refund_counter = U256(0) + evm.has_erred = True + except ( + EnsureError, + ValueError, + ): + evm.has_erred = True + finally: + return evm + + +def collect_touched_accounts( + evm: Evm, ancestor_had_error: bool = False +) -> Iterable[Address]: + """ + Collect all of the accounts that *may* need to be deleted based on + `EIP-161 `_. + Checking whether they *do* need to be deleted happens in the caller. + See also: https://github.com/ethereum/EIPs/issues/716 + + Parameters + ---------- + evm : + The current EVM frame. + ancestor_had_error : + True if the ancestors of the evm object erred else False + + Returns + ------- + touched_accounts: `typing.Iterable` + returns all the accounts that were touched and may need to be deleted. + """ + # collect the coinbase account if it was touched via zero-fee transfer + if (evm.message.caller == evm.env.origin) and evm.env.gas_price == 0: + yield evm.env.coinbase + + # collect those explicitly marked for deletion + # ("beneficiary" is of SELFDESTRUCT) + for beneficiary in sorted(set(evm.accounts_to_delete.values())): + if evm.has_erred or ancestor_had_error: + # Special case to account for geth+parity bug + # https://github.com/ethereum/EIPs/issues/716 + if beneficiary == THREE: + yield beneficiary + continue + else: + yield beneficiary + + # collect account directly addressed + if evm.message.target != Bytes0(b""): + if evm.has_erred or ancestor_had_error: + # collect RIPEMD160 precompile even if ancestor evm had error. + # otherwise, skip collection from children of erred-out evm objects + if evm.message.target == THREE: + # mypy is a little dumb; + # Although we have evm.message.target != Bytes0(b""), + # my expects target to be Union[Bytes0, Address] + yield evm.message.target # type: ignore + else: + # mypy is a little dumb; + # Although we have evm.message.target != Bytes0(b""), + # my expects target to be Union[Bytes0, Address] + yield evm.message.target # type: ignore + + # recurse into nested computations + # (even erred ones, since looking for RIPEMD160) + for child in evm.children: + yield from collect_touched_accounts( + child, ancestor_had_error=(evm.has_erred or ancestor_had_error) + ) + + +def collect_accounts_to_delete( + evm: Evm, accounts_to_delete: Set[Address] +) -> Set[Address]: + """ + Collects all the accounts that need to deleted from the `evm` object and + its children + Parameters + ---------- + evm : + The current EVM frame. + accounts_to_delete : + list of accounts that need to be deleted. + Note: An empty set should be passed to this parameter. This set + is used to store the results obtained by recursively iterating over the + child evm objects + + Returns + ------- + touched_accounts: `set` + returns all the accounts that were touched and may need to be deleted. + """ + if not evm.has_erred: + for address in evm.accounts_to_delete.keys(): + accounts_to_delete.add(address) + for child in evm.children: + collect_accounts_to_delete(child, accounts_to_delete) + return accounts_to_delete diff --git a/src/ethereum/spurious_dragon/vm/memory.py b/src/ethereum/spurious_dragon/vm/memory.py new file mode 100644 index 0000000000..fea7282f00 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/memory.py @@ -0,0 +1,83 @@ +""" +Ethereum Virtual Machine (EVM) Memory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" +from ethereum.utils.numeric import ceil32 + +from ...base_types import U256, Bytes, Uint + + +def extend_memory(memory: bytearray, start_position: Uint, size: U256) -> None: + """ + Extends the size of the memory and + substracts the gas amount to extend memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Amount of bytes by which the memory needs to be extended. + """ + if size == 0: + return None + memory_size = Uint(len(memory)) + before_size = ceil32(memory_size) + after_size = ceil32(start_position + size) + if after_size <= before_size: + return None + size_to_extend = after_size - memory_size + memory += b"\x00" * size_to_extend + + +def memory_write( + memory: bytearray, start_position: Uint, 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. + """ + for idx, byte in enumerate(value): + memory[start_position + idx] = byte + + +def memory_read_bytes( + memory: bytearray, start_position: Uint, 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 : start_position + size] diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/__init__.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/__init__.py new file mode 100644 index 0000000000..f46049a0bc --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/__init__.py @@ -0,0 +1,14 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..086d0e11d7 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,59 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ECRECOVER precompiled contract. +""" +from ethereum import crypto +from ethereum.base_types import U256 +from ethereum.crypto import SECP256K1N, Hash32 +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GAS_ECRECOVER, subtract_gas + + +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. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_ECRECOVER) + data = evm.message.data + message_hash_bytes = left_pad_zero_bytes(data[:32], 32) + message_hash = Hash32(message_hash_bytes) + v_bytes = left_pad_zero_bytes(data[32:64], 32) + v = U256.from_be_bytes(v_bytes) + r_bytes = left_pad_zero_bytes(data[64:96], 32) + r = U256.from_be_bytes(r_bytes) + s_bytes = left_pad_zero_bytes(data[96:128], 32) + s = U256.from_be_bytes(s_bytes) + + if v != 27 and v != 28: + return + if 0 >= r or r >= SECP256K1N: + return + if 0 >= s or s >= SECP256K1N: + return + + try: + public_key = crypto.secp256k1_recover(r, s, v - 27, message_hash) + except ValueError: + # unable to extract public key + return + + address = crypto.keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/identity.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/identity.py new file mode 100644 index 0000000000..30bf149beb --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/identity.py @@ -0,0 +1,45 @@ +""" +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 ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...vm import Evm +from ...vm.error import OutOfGasError +from ...vm.gas import GAS_IDENTITY, GAS_IDENTITY_WORD, subtract_gas + + +def identity(evm: Evm) -> None: + """ + Writes the message data to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + word_count = ceil32(Uint(len(data))) // 32 + word_count_gas_cost = u256_safe_multiply( + word_count, + GAS_IDENTITY_WORD, + exception_type=OutOfGasError, + ) + total_gas_cost = u256_safe_add( + GAS_IDENTITY, + word_count_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + evm.output = data diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/mapping.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/mapping.py new file mode 100644 index 0000000000..0ee1db796d --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/mapping.py @@ -0,0 +1,28 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts their implementations. +""" +from typing import Callable, Dict + +from ...eth_types import Address +from ...utils.hexadecimal import hex_to_address +from .ecrecover import ecrecover +from .identity import identity +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + hex_to_address("0x01"): ecrecover, + hex_to_address("0x02"): sha256, + hex_to_address("0x03"): ripemd160, + hex_to_address("0x04"): identity, +} diff --git a/src/ethereum/spurious_dragon/vm/precompiled_contracts/ripemd160.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 0000000000..0f78c31249 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,50 @@ +""" +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 ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...vm import Evm +from ...vm.error import OutOfGasError +from ...vm.gas import GAS_RIPEMD160, GAS_RIPEMD160_WORD, subtract_gas + + +def ripemd160(evm: Evm) -> None: + """ + Writes the ripemd160 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + word_count = ceil32(Uint(len(data))) // 32 + word_count_gas_cost = u256_safe_multiply( + word_count, + GAS_RIPEMD160_WORD, + exception_type=OutOfGasError, + ) + total_gas_cost = u256_safe_add( + GAS_RIPEMD160, + word_count_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + 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/spurious_dragon/vm/precompiled_contracts/sha256.py b/src/ethereum/spurious_dragon/vm/precompiled_contracts/sha256.py new file mode 100644 index 0000000000..de4be25eda --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/precompiled_contracts/sha256.py @@ -0,0 +1,47 @@ +""" +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 ethereum.utils.safe_arithmetic import u256_safe_add, u256_safe_multiply + +from ...vm import Evm +from ...vm.error import OutOfGasError +from ...vm.gas import GAS_SHA256, GAS_SHA256_WORD, subtract_gas + + +def sha256(evm: Evm) -> None: + """ + Writes the sha256 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + """ + data = evm.message.data + word_count = ceil32(Uint(len(data))) // 32 + word_count_gas_cost = u256_safe_multiply( + word_count, + GAS_SHA256_WORD, + exception_type=OutOfGasError, + ) + total_gas_cost = u256_safe_add( + GAS_SHA256, + word_count_gas_cost, + exception_type=OutOfGasError, + ) + evm.gas_left = subtract_gas(evm.gas_left, total_gas_cost) + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/spurious_dragon/vm/runtime.py b/src/ethereum/spurious_dragon/vm/runtime.py new file mode 100644 index 0000000000..67a7cda453 --- /dev/null +++ b/src/ethereum/spurious_dragon/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/spurious_dragon/vm/stack.py b/src/ethereum/spurious_dragon/vm/stack.py new file mode 100644 index 0000000000..d5ca46d413 --- /dev/null +++ b/src/ethereum/spurious_dragon/vm/stack.py @@ -0,0 +1,67 @@ +""" +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 .error 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. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackUnderflowError + If `stack` is empty. + """ + 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`. + + Raises + ------ + ethereum.spurious_dragon.vm.error.StackOverflowError + If `len(stack)` is `1024`. + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/src/ethereum/tangerine_whistle/__init__.py b/src/ethereum/tangerine_whistle/__init__.py index 7878ea8443..e491104d0c 100644 --- a/src/ethereum/tangerine_whistle/__init__.py +++ b/src/ethereum/tangerine_whistle/__init__.py @@ -6,3 +6,4 @@ """ MAINNET_FORK_BLOCK = 2463000 +CHAIN_ID = 1 diff --git a/src/ethereum/tangerine_whistle/state.py b/src/ethereum/tangerine_whistle/state.py index 15b8865900..33de0c2cea 100644 --- a/src/ethereum/tangerine_whistle/state.py +++ b/src/ethereum/tangerine_whistle/state.py @@ -152,8 +152,33 @@ 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()`). + Set the `Account` object at an address. + + You may delete an account with this function even if it has storage. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + if account is None: + destroy_account(state, address) + else: + set_account_internal(state, address, account) + + +def set_account_internal( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. + + You must not set an account to `None` with this function if it has non-zero + storage keys (use `destroy_account()`). Parameters ---------- @@ -184,7 +209,7 @@ def destroy_account(state: State, address: Address) -> None: """ if address in state._storage_tries: del state._storage_tries[address] - set_account(state, address, None) + set_account_internal(state, address, None) def get_storage(state: State, address: Address, key: Bytes) -> U256: diff --git a/src/ethereum/tangerine_whistle/vm/gas.py b/src/ethereum/tangerine_whistle/vm/gas.py index ca41052243..d5173911b3 100644 --- a/src/ethereum/tangerine_whistle/vm/gas.py +++ b/src/ethereum/tangerine_whistle/vm/gas.py @@ -28,6 +28,7 @@ GAS_MID = U256(8) GAS_HIGH = U256(10) GAS_EXPONENTIATION = U256(10) +GAS_EXPONENTIATION_PER_BYTE = U256(10) GAS_MEMORY = U256(3) GAS_KECCAK256 = U256(30) GAS_KECCAK256_WORD = U256(6) diff --git a/src/ethereum/tangerine_whistle/vm/instructions/arithmetic.py b/src/ethereum/tangerine_whistle/vm/instructions/arithmetic.py index 50446fed87..aa2b7db3f5 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/arithmetic.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/arithmetic.py @@ -18,6 +18,7 @@ from .. import Evm from ..gas import ( GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, GAS_LOW, GAS_MID, GAS_VERY_LOW, @@ -329,7 +330,7 @@ def exp(evm: Evm) -> None: # function is inaccurate leading to wrong results. exponent_bits = exponent.bit_length() exponent_bytes = (exponent_bits + 7) // 8 - gas_used += GAS_EXPONENTIATION * exponent_bytes + gas_used += GAS_EXPONENTIATION_PER_BYTE * exponent_bytes evm.gas_left = subtract_gas(evm.gas_left, gas_used) result = U256(pow(base, exponent, U256_CEIL_VALUE)) diff --git a/src/ethereum_optimized/__init__.py b/src/ethereum_optimized/__init__.py index 51fce56e2d..9017dc8039 100644 --- a/src/ethereum_optimized/__init__.py +++ b/src/ethereum_optimized/__init__.py @@ -23,9 +23,16 @@ def monkey_patch(state_path: Optional[str]) -> None: """ Apply all monkey patches to the specification. """ - from . import dao_fork, frontier, homestead, tangerine_whistle + from . import ( + dao_fork, + frontier, + homestead, + spurious_dragon, + tangerine_whistle, + ) frontier.monkey_patch(state_path) homestead.monkey_patch(state_path) dao_fork.monkey_patch(state_path) tangerine_whistle.monkey_patch(state_path) + spurious_dragon.monkey_patch(state_path) diff --git a/src/ethereum_optimized/dao_fork/__init__.py b/src/ethereum_optimized/dao_fork/__init__.py index 7fd6f845f3..9aaa214ff8 100644 --- a/src/ethereum_optimized/dao_fork/__init__.py +++ b/src/ethereum_optimized/dao_fork/__init__.py @@ -26,7 +26,7 @@ def monkey_patch_optimized_state_db(state_path: Optional[str]) -> None: "State": fast_state.State, "get_account": fast_state.get_account, "get_account_optional": fast_state.get_account_optional, - "set_account": fast_state.set_account, + "set_account_internal": fast_state.set_account_internal, "destroy_account": fast_state.destroy_account, "get_storage": fast_state.get_storage, "set_storage": fast_state.set_storage, diff --git a/src/ethereum_optimized/dao_fork/state_db.py b/src/ethereum_optimized/dao_fork/state_db.py index 34faedd55c..bf882294e8 100644 --- a/src/ethereum_optimized/dao_fork/state_db.py +++ b/src/ethereum_optimized/dao_fork/state_db.py @@ -322,7 +322,7 @@ def get_account(state: State, address: Address) -> Account: return res -def set_account( +def set_account_internal( state: State, address: Address, account: Optional[Account] ) -> None: """ diff --git a/src/ethereum_optimized/frontier/__init__.py b/src/ethereum_optimized/frontier/__init__.py index 53af44855e..f73c090e94 100644 --- a/src/ethereum_optimized/frontier/__init__.py +++ b/src/ethereum_optimized/frontier/__init__.py @@ -26,7 +26,7 @@ def monkey_patch_optimized_state_db(state_path: Optional[str]) -> None: "State": fast_state.State, "get_account": fast_state.get_account, "get_account_optional": fast_state.get_account_optional, - "set_account": fast_state.set_account, + "set_account_internal": fast_state.set_account_internal, "destroy_account": fast_state.destroy_account, "get_storage": fast_state.get_storage, "set_storage": fast_state.set_storage, diff --git a/src/ethereum_optimized/frontier/state_db.py b/src/ethereum_optimized/frontier/state_db.py index 4d7b4c925d..45dc65c0c5 100644 --- a/src/ethereum_optimized/frontier/state_db.py +++ b/src/ethereum_optimized/frontier/state_db.py @@ -323,7 +323,7 @@ def get_account(state: State, address: Address) -> Account: return res -def set_account( +def set_account_internal( state: State, address: Address, account: Optional[Account] ) -> None: """ diff --git a/src/ethereum_optimized/homestead/__init__.py b/src/ethereum_optimized/homestead/__init__.py index 1787d3885a..1d97db3eea 100644 --- a/src/ethereum_optimized/homestead/__init__.py +++ b/src/ethereum_optimized/homestead/__init__.py @@ -26,7 +26,7 @@ def monkey_patch_optimized_state_db(state_path: Optional[str]) -> None: "State": fast_state.State, "get_account": fast_state.get_account, "get_account_optional": fast_state.get_account_optional, - "set_account": fast_state.set_account, + "set_account_internal": fast_state.set_account_internal, "destroy_account": fast_state.destroy_account, "get_storage": fast_state.get_storage, "set_storage": fast_state.set_storage, diff --git a/src/ethereum_optimized/homestead/state_db.py b/src/ethereum_optimized/homestead/state_db.py index f019877505..2dc3751a25 100644 --- a/src/ethereum_optimized/homestead/state_db.py +++ b/src/ethereum_optimized/homestead/state_db.py @@ -322,7 +322,7 @@ def get_account(state: State, address: Address) -> Account: return res -def set_account( +def set_account_internal( state: State, address: Address, account: Optional[Account] ) -> None: """ diff --git a/src/ethereum_optimized/spurious_dragon/__init__.py b/src/ethereum_optimized/spurious_dragon/__init__.py new file mode 100644 index 0000000000..71583e3e73 --- /dev/null +++ b/src/ethereum_optimized/spurious_dragon/__init__.py @@ -0,0 +1,71 @@ +""" +Optimized Implementations (Tangerine Whistle) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: +""" + +from typing import Optional + + +def monkey_patch_optimized_state_db(state_path: Optional[str]) -> None: + """ + Replace the state interface with one that supports high performance + updates and storing state in a database. + + This function must be called before the state interface is imported + anywhere. + """ + import ethereum.spurious_dragon.state as slow_state + + from . import state_db as fast_state + + optimized_state_db_patches = { + "State": fast_state.State, + "get_account": fast_state.get_account, + "get_account_optional": fast_state.get_account_optional, + "set_account": fast_state.set_account, + "destroy_account": fast_state.destroy_account, + "get_storage": fast_state.get_storage, + "set_storage": fast_state.set_storage, + "state_root": fast_state.state_root, + "storage_root": fast_state.storage_root, + "begin_transaction": fast_state.begin_transaction, + "rollback_transaction": fast_state.rollback_transaction, + "commit_transaction": fast_state.commit_transaction, + "close_state": fast_state.close_state, + } + + for (name, value) in optimized_state_db_patches.items(): + setattr(slow_state, name, value) + + if state_path is not None: + fast_state.State.default_path = state_path + + +def monkey_patch_optimized_spec() -> None: + """ + Replace the ethash implementation with one that supports higher + performance. + + This function must be called before the spec interface is imported + anywhere. + """ + import ethereum.spurious_dragon.spec as slow_spec + + from . import spec as fast_spec + + slow_spec.validate_proof_of_work = fast_spec.validate_proof_of_work + + +def monkey_patch(state_path: Optional[str]) -> None: + """ + Apply all monkey patches to swap in high performance implementations. + + This function must be called before any of the ethereum modules are + imported anywhere. + """ + monkey_patch_optimized_state_db(state_path) + monkey_patch_optimized_spec() diff --git a/src/ethereum_optimized/spurious_dragon/spec.py b/src/ethereum_optimized/spurious_dragon/spec.py new file mode 100644 index 0000000000..74383658d5 --- /dev/null +++ b/src/ethereum_optimized/spurious_dragon/spec.py @@ -0,0 +1,46 @@ +""" +Optimized Spec +^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This module contains functions can be monkey patched into +`ethereum.spurious_dragon.spec` to use alternate optimized implementations. +""" +from ethereum.base_types import U256_CEIL_VALUE +from ethereum.ethash import epoch +from ethereum.spurious_dragon.eth_types import Header +from ethereum.spurious_dragon.spec import generate_header_hash_for_pow +from ethereum.utils.ensure import ensure + +try: + import ethash +except ImportError as e: + # Add a message, but keep it an ImportError. + raise e from Exception( + "Install with `pip install 'ethereum[optimized]'` to enable this " + "package" + ) + + +def validate_proof_of_work(header: Header) -> None: + """ + See `ethereum.spurious_dragon.spec.validate_proof_of_work`. + """ + epoch_number = epoch(header.number) + header_hash = generate_header_hash_for_pow(header) + + result = ethash.verify( + int(epoch_number), + header_hash, + header.mix_digest, + int.from_bytes(header.nonce, "big"), + (U256_CEIL_VALUE // header.difficulty).to_be_bytes32(), + ) + + ensure(result) diff --git a/src/ethereum_optimized/spurious_dragon/state_db.py b/src/ethereum_optimized/spurious_dragon/state_db.py new file mode 100644 index 0000000000..53aa1378c2 --- /dev/null +++ b/src/ethereum_optimized/spurious_dragon/state_db.py @@ -0,0 +1,864 @@ +""" +Optimized State +^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This module contains functions can be monkey patched into +`ethereum.spurious_dragon.state` to use an optimized database backed state. +""" +import logging +from dataclasses import dataclass +from tempfile import TemporaryDirectory +from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple + +from ethereum import crypto, rlp +from ethereum.base_types import U256, Bytes, Uint +from ethereum.spurious_dragon.eth_types import ( + EMPTY_ACCOUNT, + Account, + Address, + Root, + encode_account, +) +from ethereum.spurious_dragon.trie import ( + EMPTY_TRIE_ROOT, + BranchNode, + ExtensionNode, + InternalNode, + LeafNode, + Node, + bytes_to_nibble_list, + common_prefix_length, + encode_internal_node, +) + +from .trie_utils import decode_to_internal_node, encode_internal_node_nohash + +try: + import lmdb +except ImportError as e: + # Add a message, but keep it an ImportError. + raise e from Exception( + "Install with `pip install 'ethereum[optimized]'` to enable this " + "package" + ) + +# 0x0 : Metadata +# 0x1 : Accounts and storage +# 0x2 : Internal Nodes + +DB_VERSION = b"1" + + +@dataclass +class State: + """ + The State, backed by a LMDB database. + + When created with `State()` store the db in a temporary directory. When + created with `State(path)` open or create the db located at `path`. + """ + + default_path: ClassVar[Optional[str]] = None + + _db: Any + _current_tx: Any + _tx_stack: List[Any] + _dirty_accounts: List[Dict[Address, Optional[Account]]] + _dirty_storage: List[Dict[Address, Dict[Bytes, Optional[U256]]]] + _destroyed_accounts: List[Set[Address]] + _root: Root + + def __init__(self, path: Optional[str] = None) -> None: + if path is None: + path = State.default_path + + if path is None: + # Reference kept so directory won't be deleted until State is + self._tempdir = TemporaryDirectory() + path = self._tempdir.name + + logging.info("using optimized state db at %s", path) + + self._db = lmdb.open(path, map_size=2 ** 40) + self._current_tx = None + self._tx_stack = [] + self._dirty_accounts = [{}] + self._dirty_storage = [{}] + self._destroyed_accounts = [set()] + begin_db_transaction(self) + version = get_metadata(self, b"version") + if version is None: + if self._db.stat()["entries"] != 0: + raise Exception("State DB is missing version") + else: + set_metadata(self, b"version", DB_VERSION) + elif version != DB_VERSION: + raise Exception( + f"State DB version mismatch" + f" (expected: {DB_VERSION.decode('ascii')}," + f" got: {version.decode('ascii')})" + ) + + def __eq__(self, other: object) -> bool: + """ + Test for equality by comparing state roots. + """ + if not isinstance(other, State): + return NotImplemented + return state_root(self) == state_root(other) + + def __enter__(self) -> "State": + """Support with statements""" + # This is actually noop, but call it anyway for correctness + self._db.__enter__() + return self + + def __exit__(self, *args: Any) -> bool: + """Support with statements""" + return self._db.__exit__(*args) + + +def close_state(state: State) -> None: + """Close a state, releasing all resources it holds""" + state._db.close() + del state._current_tx + del state._tx_stack + del state._dirty_accounts + del state._dirty_storage + del state._destroyed_accounts + + +def get_metadata(state: State, key: Bytes) -> Optional[Bytes]: + """Get a piece of metadata""" + return state._current_tx.get(b"\x00" + key) + + +def set_metadata(state: State, key: Bytes, value: Bytes) -> None: + """Set a piece of metadata""" + return state._current_tx.put(b"\x00" + key, value) + + +def begin_db_transaction(state: State) -> None: + """ + Start a database transaction. A transaction is automatically started when a + `State` is created and the entire stack of transactions must be committed + for any permanent changes to be made to the database. + + Database transactions are more expensive than normal transactions, but the + state root can be calculated while in them. + + No operations are supported when not in a transaction. + """ + if state._tx_stack == []: + state._tx_stack.append(lmdb.Transaction(state._db, write=True)) + elif state._tx_stack[-1] is None: + raise Exception( + "Non db transactions cannot have db transactions as children" + ) + else: + state_root(state) + state._tx_stack.append( + lmdb.Transaction(state._db, parent=state._tx_stack[-1], write=True) + ) + state._current_tx = state._tx_stack[-1] + + +def commit_db_transaction(state: State) -> None: + """ + Commit the current database transaction. + """ + if state._tx_stack[-1] is None: + raise Exception("Current transaction is not a db transaction") + state_root(state) + state._tx_stack.pop().commit() + if state._tx_stack != []: + state._current_tx = state._tx_stack[-1] + else: + state._current_tx = None + + +def rollback_db_transaction(state: State) -> None: + """ + Rollback the current database transaction. + """ + if state._tx_stack[-1] is None: + raise Exception("Current transaction is not a db transaction") + state._tx_stack.pop().abort() + state._dirty_accounts = [{}] + state._dirty_storage = [{}] + state._destroyed_accounts = [set()] + if state._tx_stack != []: + state._current_tx = state._tx_stack[-1] + else: + state._current_tx = None + + +def begin_transaction(state: State) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + if state._tx_stack == []: + raise Exception("First transaction must be a db transaction") + state._tx_stack.append(None) + state._dirty_accounts.append({}) + state._dirty_storage.append({}) + state._destroyed_accounts.append(set()) + + +def commit_transaction(state: State) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + if state._tx_stack[-1] is not None: + raise Exception("Current transaction is a db transaction") + for (address, account) in state._dirty_accounts.pop().items(): + state._dirty_accounts[-1][address] = account + state._destroyed_accounts[-2] |= state._destroyed_accounts[-1] + for address in state._destroyed_accounts.pop(): + state._dirty_storage[-2].pop(address) + for (address, cache) in state._dirty_storage.pop().items(): + if address not in state._dirty_storage[-1]: + state._dirty_storage[-1][address] = {} + for (key, value) in cache.items(): + state._dirty_storage[-1][address][key] = value + state._tx_stack.pop() + + +def rollback_transaction(state: State) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + if state._tx_stack[-1] is not None: + raise Exception("Current transaction is a db transaction") + state._tx_stack.pop() + state._dirty_accounts.pop() + state._dirty_storage.pop() + state._destroyed_accounts.pop() + + +def get_internal_key(key: Bytes) -> Bytes: + """ + Convert a key to the form used internally inside the trie. + """ + return bytes_to_nibble_list(crypto.keccak256(key)) + + +def get_storage(state: State, address: Address, key: Bytes) -> U256: + """ + See `ethereum.spurious_dragon.state`. + """ + for i in range(len(state._tx_stack) - 1, -1, -1): + if address in state._dirty_storage[i]: + if key in state._dirty_storage[i][address]: + cached_res = state._dirty_storage[i][address][key] + if cached_res is None: + return U256(0) + else: + return cached_res + if key in state._destroyed_accounts[i]: + # Higher levels refer to the account prior to destruction + return U256(0) + res = state._current_tx.get(b"\x01" + address + b"\x00" + key) + if res is None: + return U256(0) + else: + res = rlp.decode(res) + assert isinstance(res, bytes) + return U256.from_be_bytes(res) + + +def set_storage( + state: State, address: Address, key: Bytes, value: U256 +) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + if address not in state._dirty_accounts[-1]: + state._dirty_accounts[-1][address] = get_account_optional( + state, address + ) + if address not in state._dirty_storage[-1]: + state._dirty_storage[-1][address] = {} + if value == 0: + state._dirty_storage[-1][address][key] = None + else: + state._dirty_storage[-1][address][key] = value + + +def get_account_optional(state: State, address: Address) -> Optional[Account]: + """ + See `ethereum.spurious_dragon.state`. + """ + for cache in reversed(state._dirty_accounts): + if address in cache: + return cache[address] + internal_address = get_internal_key(address) + res = state._current_tx.get(b"\x01" + internal_address) + if res is None: + return None + else: + data = rlp.decode(res) + assert isinstance(data, list) + return Account( + Uint.from_be_bytes(data[0]), U256.from_be_bytes(data[1]), data[2] + ) + + +def get_account(state: State, address: Address) -> Account: + """ + See `ethereum.spurious_dragon.state`. + """ + res = get_account_optional(state, address) + if res is None: + return EMPTY_ACCOUNT + else: + return res + + +def set_account( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + if account is None: + state._dirty_accounts[-1][address] = None + else: + state._dirty_accounts[-1][address] = account + + +def destroy_account(state: State, address: Address) -> None: + """ + See `ethereum.spurious_dragon.state`. + """ + state._destroyed_accounts[-1].add(address) + state._dirty_storage[-1].pop(address, None) + state._dirty_accounts[-1][address] = None + + +def clear_destroyed_account(state: State, address: Bytes) -> None: + """ + Remove every storage key associated to a destroyed account from the + database. + """ + internal_address = get_internal_key(address) + cursor = state._current_tx.cursor() + cursor.set_range(b"\x01" + address + b"\x00") + while cursor.key().startswith(b"\x01" + address): + cursor.delete() + cursor.set_range(b"\x02" + internal_address + b"\x00") + while cursor.key().startswith(b"\x02" + internal_address): + cursor.delete() + + +def make_node( + state: State, node_key: Bytes, value: Node, cursor: Any +) -> rlp.RLP: + """ + Given a node, get its `RLP` representation. This function calculates the + storage root if the node is an `Account`. + """ + if isinstance(value, Account): + res = cursor.get(b"\x02" + node_key + b"\x00") + if res is None: + account_storage_root = EMPTY_TRIE_ROOT + else: + account_storage_root = crypto.keccak256(res) + return encode_account(value, account_storage_root) + elif isinstance(value, Bytes): + return value + else: + assert value is not None + return rlp.encode(value) + + +def write_internal_node( + cursor: Any, + trie_prefix: Bytes, + node_key: Bytes, + node: Optional[InternalNode], +) -> None: + """ + Write an internal node into the database. + """ + if node is None: + if cursor.set_key(b"\x02" + trie_prefix + node_key): + cursor.delete() + else: + cursor.put( + b"\x02" + trie_prefix + node_key, + encode_internal_node_nohash(node), + ) + + +def state_root( + state: State, +) -> Root: + """ + Calculate the state root. + """ + if state._current_tx is None: + raise Exception("Cannot compute state root inside non db transaction") + for address, account in state._dirty_accounts[-1].items(): + internal_address = get_internal_key(address) + if account is None: + state._current_tx.delete(b"\x01" + internal_address) + elif isinstance(account, Bytes): # Testing only + state._current_tx.put( + b"\x01" + internal_address, + account, + ) + elif isinstance(account, Account): + state._current_tx.put( + b"\x01" + internal_address, + rlp.encode([account.nonce, account.balance, account.code]), + ) + else: + raise Exception( + f"Invalid object of type {type(account)} stored in state" + ) + for address in state._destroyed_accounts[-1]: + clear_destroyed_account(state, address) + state._destroyed_accounts[-1] = set() + for address in list(state._dirty_storage[-1]): + storage_root(state, address) + dirty_list: List[Tuple[Bytes, Node]] = list( + sorted( + ( + (get_internal_key(address), account) + for address, account in state._dirty_accounts[-1].items() + ), + reverse=True, + ) + ) + root_node = walk( + state, + b"", + b"", + dirty_list, + state._current_tx.cursor(), + ) + write_internal_node(state._current_tx.cursor(), b"", b"", root_node) + state._dirty_accounts[-1] = {} + if root_node is None: + return EMPTY_TRIE_ROOT + else: + root = encode_internal_node(root_node) + if isinstance(root, Bytes): + return Root(root) + else: + return crypto.keccak256(rlp.encode(root)) + + +def storage_root(state: State, address: Address) -> Root: + """ + Calculate the storage root. + """ + if state._current_tx is None: + raise Exception( + "Cannot compute storage root inside non db transaction" + ) + return _storage_root(state, address, state._current_tx.cursor()) + + +def _storage_root(state: State, address: Address, cursor: Any) -> Root: + """ + Calculate the storage root. + """ + dirty_storage = state._dirty_storage[-1].pop(address, {}).items() + for key, value in dirty_storage: + if value is None: + state._current_tx.delete(b"\x01" + address + b"\x00" + key) + else: + state._current_tx.put( + b"\x01" + address + b"\x00" + key, + rlp.encode(value), + ) + + internal_address = get_internal_key(address) + storage_prefix = internal_address + b"\x00" + dirty_list: List[Tuple[Bytes, Node]] = list( + sorted( + ( + (get_internal_key(key), account) + for key, account in dirty_storage + ), + reverse=True, + ) + ) + root_node = walk( + state, + storage_prefix, + b"", + dirty_list, + cursor, + ) + write_internal_node(cursor, storage_prefix, b"", root_node) + if root_node is None: + return EMPTY_TRIE_ROOT + else: + root = encode_internal_node(root_node) + if isinstance(root, Bytes): + return Root(root) + else: + return crypto.keccak256(rlp.encode(root)) + + +def walk( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Visit the internal node at `node_key` and update all its subnodes as + required by `dirty_list`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + res = cursor.get(b"\x02" + trie_prefix + node_key) + if res is None: + current_node = None + else: + current_node = decode_to_internal_node(res) + while dirty_list and dirty_list[-1][0].startswith(node_key): + if current_node is None: + current_node = walk_empty( + state, trie_prefix, node_key, dirty_list, cursor + ) + elif isinstance(current_node, LeafNode): + current_node = walk_leaf( + state, + trie_prefix, + node_key, + current_node, + dirty_list, + cursor, + ) + elif isinstance(current_node, ExtensionNode): + current_node = walk_extension( + state, + trie_prefix, + node_key, + current_node, + dirty_list, + cursor, + ) + elif isinstance(current_node, BranchNode): + current_node = walk_branch( + state, + trie_prefix, + node_key, + current_node, + dirty_list, + cursor, + ) + else: + assert False # Invalid internal node type + return current_node + + +def walk_empty( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Consume the last element of `dirty_list` and create a `LeafNode` pointing + to it at `node_key`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + key, value = dirty_list.pop() + if value is not None: + return LeafNode( + key[len(node_key) :], make_node(state, key, value, cursor) + ) + else: + return None + + +def walk_leaf( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + leaf_node: LeafNode, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Consume the last element of `dirty_list` and update the `LeafNode` at + `node_key`, potentially turning it into `ExtensionNode` -> `BranchNode` + -> `LeafNode`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + key, value = dirty_list[-1] + if key[len(node_key) :] == leaf_node.rest_of_key: + dirty_list.pop() + if value is None: + return None + else: + return LeafNode( + leaf_node.rest_of_key, + make_node(state, key, value, cursor), + ) + else: + prefix_length = common_prefix_length( + leaf_node.rest_of_key, key[len(node_key) :] + ) + prefix = leaf_node.rest_of_key[:prefix_length] + assert ( + len(leaf_node.rest_of_key) != prefix_length + ) # Keys must be same length + new_leaf_node = LeafNode( + leaf_node.rest_of_key[prefix_length + 1 :], + leaf_node.value, + ) + write_internal_node( + cursor, + trie_prefix, + node_key + leaf_node.rest_of_key[: prefix_length + 1], + LeafNode( + leaf_node.rest_of_key[prefix_length + 1 :], + leaf_node.value, + ), + ) + current_node = split_branch( + state, + trie_prefix, + node_key + prefix, + leaf_node.rest_of_key[prefix_length], + encode_internal_node(new_leaf_node), + dirty_list, + cursor, + ) + if prefix_length != 0: + return make_extension_node( + state, + trie_prefix, + node_key, + node_key + prefix, + current_node, + cursor, + ) + else: + return current_node + + +def walk_extension( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + extension_node: ExtensionNode, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Consume the last element of `dirty_list` and update the `ExtensionNode` at + `node_key`, potentially turning it into `ExtensionNode` -> `BranchNode` + -> `ExtensionNode`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + key, value = dirty_list[-1] + if key[len(node_key) :].startswith(extension_node.key_segment): + target_node = walk( + state, + trie_prefix, + node_key + extension_node.key_segment, + dirty_list, + cursor, + ) + return make_extension_node( + state, + trie_prefix, + node_key, + node_key + extension_node.key_segment, + target_node, + cursor, + ) + prefix_length = common_prefix_length( + extension_node.key_segment, key[len(node_key) :] + ) + prefix = extension_node.key_segment[:prefix_length] + if prefix_length != len(extension_node.key_segment) - 1: + new_extension_node = ExtensionNode( + extension_node.key_segment[prefix_length + 1 :], + extension_node.subnode, + ) + write_internal_node( + cursor, + trie_prefix, + node_key + extension_node.key_segment[: prefix_length + 1], + new_extension_node, + ) + encoded_new_extension_node = encode_internal_node(new_extension_node) + else: + encoded_new_extension_node = extension_node.subnode + node = split_branch( + state, + trie_prefix, + node_key + prefix, + extension_node.key_segment[prefix_length], + encoded_new_extension_node, + dirty_list, + cursor, + ) + if prefix_length != 0: + return make_extension_node( + state, trie_prefix, node_key, node_key + prefix, node, cursor + ) + else: + return node + + +def walk_branch( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + current_node: BranchNode, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Consume the last element of `dirty_list` and update the `BranchNode` at + `node_key`, potentially turning it into `ExtensionNode` or a `LeafNode`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + assert dirty_list[-1][0] != node_key # All keys must be the same length + + # The copy here is probably unnecessary, but the optimization isn't worth + # the risk. + encoded_subnodes = current_node.subnodes[:] + if dirty_list[-1][0].startswith(node_key): + for i in range(16): + if ( + dirty_list + and dirty_list[-1][0].startswith(node_key) + and dirty_list[-1][0][len(node_key)] == i + ): + subnode = walk( + state, + trie_prefix, + node_key + bytes([i]), + dirty_list, + cursor, + ) + write_internal_node( + cursor, trie_prefix, node_key + bytes([i]), subnode + ) + encoded_subnodes[i] = encode_internal_node(subnode) + + number_of_subnodes = 16 - encoded_subnodes.count(b"") + + if number_of_subnodes == 0: + return None + elif number_of_subnodes == 1: + for i in range(16): + if encoded_subnodes[i] != b"": + subnode_index = i + break + subnode = decode_to_internal_node( + cursor.get( + b"\x02" + trie_prefix + node_key + bytes([subnode_index]) + ) + ) + return make_extension_node( + state, + trie_prefix, + node_key, + node_key + bytes([subnode_index]), + subnode, + cursor, + ) + else: + return BranchNode(encoded_subnodes, b"") + + +def make_extension_node( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + target_key: Bytes, + target_node: Optional[InternalNode], + cursor: Any, +) -> Optional[InternalNode]: + """ + Make an extension node at `node_key` pointing at `target_key`. This + function will correctly replace `ExtensionNode -> LeafNode` with `LeafNode` + and `ExtensionNode -> ExtensionNode` with `ExtensionNode`. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + assert node_key != target_key + if target_node is None: + write_internal_node(cursor, trie_prefix, target_key, None) + return None + elif isinstance(target_node, LeafNode): + write_internal_node(cursor, trie_prefix, target_key, None) + return LeafNode( + target_key[len(node_key) :] + target_node.rest_of_key, + target_node.value, + ) + elif isinstance(target_node, ExtensionNode): + write_internal_node(cursor, trie_prefix, target_key, None) + return ExtensionNode( + target_key[len(node_key) :] + target_node.key_segment, + target_node.subnode, + ) + elif isinstance(target_node, BranchNode): + write_internal_node(cursor, trie_prefix, target_key, target_node) + return ExtensionNode( + target_key[len(node_key) :], encode_internal_node(target_node) + ) + else: + assert False # Invalid internal node type + + +def split_branch( + state: State, + trie_prefix: Bytes, + node_key: Bytes, + key: int, + encoded_subnode: rlp.RLP, + dirty_list: List[Tuple[Bytes, Node]], + cursor: Any, +) -> Optional[InternalNode]: + """ + Make a branch node with `encoded_subnode` as its only child and then + consume all `dirty_list` elements under it. + + This function takes a `encoded_node` to avoid a database read in some + situations. + + This function returns the new value of the visited node, but does not write + it to the database. + """ + encoded_subnodes: List[rlp.RLP] = [b""] * 16 + encoded_subnodes[key] = encoded_subnode + return walk_branch( + state, + trie_prefix, + node_key, + BranchNode(encoded_subnodes, b""), + dirty_list, + cursor, + ) diff --git a/src/ethereum_optimized/spurious_dragon/trie_utils.py b/src/ethereum_optimized/spurious_dragon/trie_utils.py new file mode 100644 index 0000000000..ffd311e524 --- /dev/null +++ b/src/ethereum_optimized/spurious_dragon/trie_utils.py @@ -0,0 +1,83 @@ +""" +Optimized Trie Utility Functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +..contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This module contains utility functions needed by the optimized state. +""" +from typing import Tuple + +from ethereum import rlp +from ethereum.base_types import Bytes +from ethereum.spurious_dragon.trie import ( + BranchNode, + ExtensionNode, + InternalNode, + LeafNode, + nibble_list_to_compact, +) +from ethereum.utils.ensure import ensure + + +def compact_to_nibble_list(bytes: Bytes) -> Tuple[Bytes, bool]: + """ + Performs the reverse of + `ethereum.spurious_dragon.trie.nibble_list_to_compact`. + """ + is_leaf = bool(bytes[0] & 0x20) + parity = bool(bytes[0] & 0x10) + nibble_list = bytearray() + if parity: + nibble_list.append(bytes[0] & 0x0F) + for byte in bytes[1:]: + nibble_list.append(byte >> 4) + nibble_list.append(byte & 0x0F) + return (Bytes(nibble_list), is_leaf) + + +def encode_internal_node_nohash(node: InternalNode) -> Bytes: + """ + Perform an `ethereum.spurious_dragon.trie.encode_internal_node`, but skip + the hashing step. + """ + if isinstance(node, LeafNode): + return rlp.encode( + [ + nibble_list_to_compact(node.rest_of_key, True), + node.value, + ] + ) + elif isinstance(node, ExtensionNode): + return rlp.encode( + [ + nibble_list_to_compact(node.key_segment, False), + node.subnode, + ] + ) + elif isinstance(node, BranchNode): + return rlp.encode(node.subnodes + [node.value]) + else: + raise Exception(f"Invalid internal node type {type(node)}!") + + +def decode_to_internal_node(data_in: Bytes) -> InternalNode: + """ + Decode an `InternalNode` from it's `RLP` representation. + """ + data = rlp.decode(data_in) + assert isinstance(data, list) + if len(data) == 2: + key_segment, is_leaf = compact_to_nibble_list(data[0]) + if is_leaf: + return LeafNode(key_segment, data[1]) + else: + return ExtensionNode(key_segment, data[1]) + else: + ensure(len(data) == 17) + return BranchNode(data[:-1], data[-1]) diff --git a/src/ethereum_optimized/tangerine_whistle/__init__.py b/src/ethereum_optimized/tangerine_whistle/__init__.py index 1dc524dfb5..1e6ffec411 100644 --- a/src/ethereum_optimized/tangerine_whistle/__init__.py +++ b/src/ethereum_optimized/tangerine_whistle/__init__.py @@ -26,7 +26,7 @@ def monkey_patch_optimized_state_db(state_path: Optional[str]) -> None: "State": fast_state.State, "get_account": fast_state.get_account, "get_account_optional": fast_state.get_account_optional, - "set_account": fast_state.set_account, + "set_account_internal": fast_state.set_account_internal, "destroy_account": fast_state.destroy_account, "get_storage": fast_state.get_storage, "set_storage": fast_state.set_storage, diff --git a/src/ethereum_optimized/tangerine_whistle/state_db.py b/src/ethereum_optimized/tangerine_whistle/state_db.py index 8e60e85ff4..66eec8d4ef 100644 --- a/src/ethereum_optimized/tangerine_whistle/state_db.py +++ b/src/ethereum_optimized/tangerine_whistle/state_db.py @@ -322,7 +322,7 @@ def get_account(state: State, address: Address) -> Account: return res -def set_account( +def set_account_internal( state: State, address: Address, account: Optional[Account] ) -> None: """ diff --git a/src/ethereum_spec_tools/sync.py b/src/ethereum_spec_tools/sync.py index cce323287a..4b9231c3b6 100644 --- a/src/ethereum_spec_tools/sync.py +++ b/src/ethereum_spec_tools/sync.py @@ -706,7 +706,17 @@ def process_blocks(self) -> None: end - start, ) - if block_number % 1000 == 0: + if block_number > 2220000 and block_number > 2463000: + # Excessive DB load due to the Shanghai DOS attacks, requires + # more regular DB commits + if block_number % 100 == 0: + self.persist() + if block_number > 2675000 and block_number < 2700598: + # Excessive DB load due to state clearing, requires more + # regular DB commits + if block_number % 100 == 0: + self.persist() + elif block_number % 1000 == 0: self.persist() diff --git a/tests/frontier/blockchain_st_test_helpers.py b/tests/frontier/blockchain_st_test_helpers.py index 0589ea4ea6..704e6ee5f0 100644 --- a/tests/frontier/blockchain_st_test_helpers.py +++ b/tests/frontier/blockchain_st_test_helpers.py @@ -18,7 +18,7 @@ from ethereum.frontier.state import ( State, close_state, - set_account, + set_account_internal, set_storage, ) from ethereum.frontier.utils.hexadecimal import hex_to_address, hex_to_root @@ -195,7 +195,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( diff --git a/tests/frontier/optimized/test_state_db.py b/tests/frontier/optimized/test_state_db.py index 7fbc6f2581..97605e4396 100644 --- a/tests/frontier/optimized/test_state_db.py +++ b/tests/frontier/optimized/test_state_db.py @@ -64,7 +64,7 @@ def test_trie() -> None: for insert_list in operations: for (key, value) in insert_list: normal_trie.trie_set(trie_normal, key, value) - state_db.set_account(state, key, value) # type: ignore + state_db.set_account_internal(state, key, value) # type: ignore root = normal_trie.root(trie_normal) assert root == state_db.state_root(state) state_db.get_internal_key = backup_get_internal_key @@ -77,7 +77,7 @@ def test_trie() -> None: def test_storage_key() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) return obj @@ -99,12 +99,12 @@ def actions(impl: Any) -> Any: def test_resurrection() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) impl.destroy_account(obj, ADDRESS_FOO) impl.state_root(obj) - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) return obj state_normal = actions(state) diff --git a/tests/frontier/vm/vm_test_helpers.py b/tests/frontier/vm/vm_test_helpers.py index 17deb3066b..331f8620db 100644 --- a/tests/frontier/vm/vm_test_helpers.py +++ b/tests/frontier/vm/vm_test_helpers.py @@ -10,7 +10,7 @@ from ethereum.frontier.state import ( State, close_state, - set_account, + set_account_internal, set_storage, storage_root, ) @@ -126,7 +126,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( @@ -136,7 +136,7 @@ def json_to_state(raw: Any) -> State: U256.from_be_bytes(hex_to_bytes32(v)), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) return state diff --git a/tests/homestead/blockchain_st_test_helpers.py b/tests/homestead/blockchain_st_test_helpers.py index a2a984f8d7..47202cc009 100644 --- a/tests/homestead/blockchain_st_test_helpers.py +++ b/tests/homestead/blockchain_st_test_helpers.py @@ -18,7 +18,7 @@ from ethereum.homestead.state import ( State, close_state, - set_account, + set_account_internal, set_storage, ) from ethereum.homestead.utils.hexadecimal import hex_to_address, hex_to_root @@ -195,7 +195,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( diff --git a/tests/homestead/optimized/test_state_db.py b/tests/homestead/optimized/test_state_db.py index cc9c8f3668..17cafb8c0b 100644 --- a/tests/homestead/optimized/test_state_db.py +++ b/tests/homestead/optimized/test_state_db.py @@ -64,7 +64,7 @@ def test_trie() -> None: for insert_list in operations: for (key, value) in insert_list: normal_trie.trie_set(trie_normal, key, value) - state_db.set_account(state, key, value) # type: ignore + state_db.set_account_internal(state, key, value) # type: ignore root = normal_trie.root(trie_normal) assert root == state_db.state_root(state) state_db.get_internal_key = backup_get_internal_key @@ -77,7 +77,7 @@ def test_trie() -> None: def test_storage_key() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) return obj @@ -99,12 +99,12 @@ def actions(impl: Any) -> Any: def test_resurrection() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) impl.destroy_account(obj, ADDRESS_FOO) impl.state_root(obj) - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) return obj state_normal = actions(state) diff --git a/tests/homestead/vm/vm_test_helpers.py b/tests/homestead/vm/vm_test_helpers.py index fbf6f20724..80df68d4fb 100644 --- a/tests/homestead/vm/vm_test_helpers.py +++ b/tests/homestead/vm/vm_test_helpers.py @@ -10,7 +10,7 @@ from ethereum.homestead.state import ( State, close_state, - set_account, + set_account_internal, set_storage, storage_root, ) @@ -126,7 +126,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( @@ -136,7 +136,7 @@ def json_to_state(raw: Any) -> State: U256.from_be_bytes(hex_to_bytes32(v)), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) return state diff --git a/tests/spurious_dragon/__init__.py b/tests/spurious_dragon/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/spurious_dragon/blockchain_st_test_helpers.py b/tests/spurious_dragon/blockchain_st_test_helpers.py new file mode 100644 index 0000000000..c0f32639f9 --- /dev/null +++ b/tests/spurious_dragon/blockchain_st_test_helpers.py @@ -0,0 +1,216 @@ +import json +import os +from functools import partial +from typing import Any, Dict, List, Tuple, cast +from unittest.mock import call, patch + +from ethereum import rlp +from ethereum.base_types import U256, Bytes0 +from ethereum.crypto import Hash32 +from ethereum.rlp import rlp_hash +from ethereum.spurious_dragon.eth_types import ( + Account, + Block, + Bloom, + Header, + Transaction, +) +from ethereum.spurious_dragon.spec import BlockChain, state_transition +from ethereum.spurious_dragon.state import ( + State, + close_state, + set_account, + set_storage, +) +from ethereum.spurious_dragon.utils.hexadecimal import ( + hex_to_address, + hex_to_root, +) +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes8, + hex_to_bytes32, + hex_to_hash, + hex_to_u256, + hex_to_uint, +) + + +def run_blockchain_st_test( + test_dir: str, test_file: str, network: str +) -> None: + test_data = load_test(test_dir, test_file, network) + + genesis_header = test_data["genesis_header"] + genesis_block = Block( + genesis_header, + (), + (), + ) + + assert rlp_hash(genesis_header) == test_data["genesis_header_hash"] + assert ( + rlp.encode(cast(rlp.RLP, genesis_block)) + == test_data["genesis_block_rlp"] + ) + + chain = BlockChain( + blocks=[genesis_block], + state=test_data["pre_state"], + ) + + if not test_data["ignore_pow_validation"]: + add_blocks_to_chain(chain, test_data) + else: + with patch( + "ethereum.spurious_dragon.spec.validate_proof_of_work", + autospec=True, + ) as mocked_pow_validator: + add_blocks_to_chain(chain, test_data) + mocked_pow_validator.assert_has_calls( + [call(block.header) for block in test_data["blocks"]], + any_order=False, + ) + + assert rlp_hash(chain.blocks[-1].header) == test_data["last_block_hash"] + assert chain.state == test_data["expected_post_state"] + close_state(chain.state) + close_state(test_data["expected_post_state"]) + + +def add_blocks_to_chain(chain: BlockChain, test_data: Dict[str, Any]) -> None: + for idx, block in enumerate(test_data["blocks"]): + assert rlp_hash(block.header) == test_data["block_header_hashes"][idx] + assert rlp.encode(cast(rlp.RLP, block)) == test_data["block_rlps"][idx] + state_transition(chain, block) + + +def load_test(test_dir: str, test_file: str, network: str) -> Dict[str, Any]: + # Extract the pure basename of the file without the path to the file. + # Ex: Extract "world.json" from "path/to/file/world.json" + pure_test_file = os.path.basename(test_file) + # Extract the filename without the extension. Ex: Extract "world" from + # "world.json" + test_name = os.path.splitext(pure_test_file)[0] + path = os.path.join(test_dir, test_file) + with open(path, "r") as fp: + json_data = json.load(fp)[f"{test_name}_{network}"] + + blocks, block_header_hashes, block_rlps = json_to_blocks( + json_data["blocks"] + ) + + return { + "genesis_header": json_to_header(json_data["genesisBlockHeader"]), + "genesis_header_hash": hex_to_bytes( + json_data["genesisBlockHeader"]["hash"] + ), + "genesis_block_rlp": hex_to_bytes(json_data["genesisRLP"]), + "last_block_hash": hex_to_bytes(json_data["lastblockhash"]), + "pre_state": json_to_state(json_data["pre"]), + "expected_post_state": json_to_state(json_data["postState"]), + "blocks": blocks, + "block_header_hashes": block_header_hashes, + "block_rlps": block_rlps, + "ignore_pow_validation": json_data["sealEngine"] == "NoProof", + } + + +def json_to_blocks( + json_blocks: Any, +) -> Tuple[List[Block], List[Hash32], List[bytes]]: + blocks = [] + block_header_hashes = [] + block_rlps = [] + + for json_block in json_blocks: + if "blockHeader" not in json_block and "rlp" in json_block: + # Some blocks are represented by only the RLP and not the block details + block_rlp = hex_to_bytes(json_block["rlp"]) + block = rlp.decode_to(Block, block_rlp) + blocks.append(block) + block_header_hashes.append(rlp.rlp_hash(block.header)) + block_rlps.append(block_rlp) + continue + + header = json_to_header(json_block["blockHeader"]) + transactions = tuple( + json_to_tx(tx) for tx in json_block["transactions"] + ) + uncles = tuple( + json_to_header(uncle) for uncle in json_block["uncleHeaders"] + ) + + blocks.append( + Block( + header, + transactions, + uncles, + ) + ) + block_header_hashes.append( + Hash32(hex_to_bytes(json_block["blockHeader"]["hash"])) + ) + block_rlps.append(hex_to_bytes(json_block["rlp"])) + + return blocks, block_header_hashes, block_rlps + + +def json_to_header(raw: Any) -> Header: + return Header( + hex_to_hash(raw.get("parentHash")), + hex_to_hash(raw.get("uncleHash")), + hex_to_address(raw.get("coinbase")), + hex_to_root(raw.get("stateRoot")), + hex_to_root(raw.get("transactionsTrie")), + hex_to_root(raw.get("receiptTrie")), + Bloom(hex_to_bytes(raw.get("bloom"))), + 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")), + ) + + +def json_to_tx(raw: Any) -> Transaction: + return Transaction( + hex_to_u256(raw.get("nonce")), + hex_to_u256(raw.get("gasPrice")), + hex_to_u256(raw.get("gasLimit")), + Bytes0(b"") if raw.get("to") == "" else hex_to_address(raw.get("to")), + hex_to_u256(raw.get("value")), + hex_to_bytes(raw.get("data")), + hex_to_u256(raw.get("v")), + hex_to_u256(raw.get("r")), + hex_to_u256(raw.get("s")), + ) + + +def json_to_state(raw: Any) -> State: + state = State() + for (addr_hex, acc_state) in raw.items(): + addr = hex_to_address(addr_hex) + account = Account( + nonce=hex_to_uint(acc_state.get("nonce", "0x0")), + balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), + code=hex_to_bytes(acc_state.get("code", "")), + ) + set_account(state, addr, account) + + for (k, v) in acc_state.get("storage", {}).items(): + set_storage( + state, + addr, + hex_to_bytes32(k), + U256.from_be_bytes(hex_to_bytes32(v)), + ) + return state + + +run_spurious_dragon_blockchain_st_tests = partial( + run_blockchain_st_test, network="EIP158" +) diff --git a/tests/spurious_dragon/optimized/__init__.py b/tests/spurious_dragon/optimized/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/spurious_dragon/optimized/test_state_db.py b/tests/spurious_dragon/optimized/test_state_db.py new file mode 100644 index 0000000000..a13199a8d5 --- /dev/null +++ b/tests/spurious_dragon/optimized/test_state_db.py @@ -0,0 +1,117 @@ +import sys +from typing import Any, List, Optional, Tuple + +import pytest + +import ethereum.spurious_dragon.state as state +import ethereum.spurious_dragon.trie as normal_trie +from ethereum.base_types import U256, Bytes +from ethereum.spurious_dragon.eth_types import EMPTY_ACCOUNT +from ethereum.spurious_dragon.utils.hexadecimal import hex_to_address + +try: + import ethereum_optimized.spurious_dragon.state_db as state_db +except ImportError: + pass + + +ADDRESS_FOO = hex_to_address("0x00000000219ab540356cbb839cbe05303d7705fa") + + +operations: List[List[Tuple[Bytes, Optional[Bytes]]]] = [ + [], + [(b"001234", b"foo")], + [(b"001234", b"bar")], + [(b"001234", None)], + [(b"abcdeg", b"baz"), (b"abcdef", b"foobar")], + [(b"ab1234", b"bar")], + [(b"abcdeg", None)], + [(b"abcde1", b"foo"), (b"abcde2", b"foo")], + [ + (b"abcde1", None), + (b"abcde2", None), + (b"abcdeg", None), + (b"abcdef", None), + ], + [(b"ab\x00\x00\x00\x00", b"zero"), (b"ab\x00\x00\x00\x01", b"one")], + [(b"ab\x00\x00\x00\x10", b"foo")], + [(b"Ab1234", b"foo")], + [(b"123456", b"foo"), (b"12345a", b"foo"), (b"123a56", b"foo")], + [(b"123a56", None)], +] + + +def fake_get_internal_key(key: Bytes) -> Bytes: + """ + Replacing `state_db.get_internal_key()` with this function switches + `state_db` to a unsecured trie which is necessary for some tests. + """ + return normal_trie.bytes_to_nibble_list(key) + + +@pytest.mark.skipif( + "ethereum_optimized.tangerine_whistle.state_db" not in sys.modules, + reason="missing dependency (use `pip install 'ethereum[optimized]'`)", +) +def test_trie() -> None: + trie_normal: normal_trie.Trie[Bytes, Optional[Bytes]] = normal_trie.Trie( + False, None + ) + backup_get_internal_key = state_db.get_internal_key + state_db.get_internal_key = fake_get_internal_key + with state_db.State() as state: + state_db.begin_db_transaction(state) + for insert_list in operations: + for (key, value) in insert_list: + normal_trie.trie_set(trie_normal, key, value) + state_db.set_account(state, key, value) # type: ignore + root = normal_trie.root(trie_normal) + assert root == state_db.state_root(state) + state_db.get_internal_key = backup_get_internal_key + + +@pytest.mark.skipif( + "ethereum_optimized.tangerine_whistle.state_db" not in sys.modules, + reason="missing dependency (use `pip install 'ethereum[optimized]'`)", +) +def test_storage_key() -> None: + def actions(impl: Any) -> Any: + obj = impl.State() + impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) + impl.state_root(obj) + return obj + + state_normal = actions(state) + state_optimized = actions(state_db) + assert state.get_storage( + state_normal, ADDRESS_FOO, b"" + ) == state_db.get_storage(state_optimized, ADDRESS_FOO, b"") + assert state.state_root(state_normal) == state_db.state_root( + state_optimized + ) + + +@pytest.mark.skipif( + "ethereum_optimized.tangerine_whistle.state_db" not in sys.modules, + reason="missing dependency (use `pip install 'ethereum[optimized]'`)", +) +def test_resurrection() -> None: + def actions(impl: Any) -> Any: + obj = impl.State() + impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) + impl.state_root(obj) + impl.destroy_account(obj, ADDRESS_FOO) + impl.state_root(obj) + impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + return obj + + state_normal = actions(state) + state_optimized = actions(state_db) + assert state.get_storage( + state_normal, ADDRESS_FOO, b"" + ) == state_db.get_storage(state_optimized, ADDRESS_FOO, b"") + assert state.state_root(state_normal) == state_db.state_root( + state_optimized + ) diff --git a/tests/spurious_dragon/optimized/test_trie_utils.py b/tests/spurious_dragon/optimized/test_trie_utils.py new file mode 100644 index 0000000000..5cf5e1df01 --- /dev/null +++ b/tests/spurious_dragon/optimized/test_trie_utils.py @@ -0,0 +1,22 @@ +from ethereum.spurious_dragon.trie import nibble_list_to_compact +from ethereum_optimized.tangerine_whistle.trie_utils import ( + compact_to_nibble_list, +) + + +def test_compact_to_nibble_list() -> None: + nibble_lists = [ + b"\x00\x01\x0f\x05", + b"\x00\x01\x0f", + b"\x04\x02\x0a", + b"\x01\x02\x0f\x01", + ] + for x in nibble_lists: + assert compact_to_nibble_list(nibble_list_to_compact(x, True)) == ( + x, + True, + ) + assert compact_to_nibble_list(nibble_list_to_compact(x, False)) == ( + x, + False, + ) diff --git a/tests/spurious_dragon/test_ethash.py b/tests/spurious_dragon/test_ethash.py new file mode 100644 index 0000000000..70b1cc62a5 --- /dev/null +++ b/tests/spurious_dragon/test_ethash.py @@ -0,0 +1,392 @@ +import json +import pkgutil +import shutil +import subprocess +from random import randint +from typing import Any, Dict, List, Tuple, cast + +import pytest + +from ethereum import crypto, rlp +from ethereum.base_types import U256_CEIL_VALUE, Uint +from ethereum.crypto import keccak256 +from ethereum.ethash import ( + EPOCH_SIZE, + HASH_BYTES, + MIX_BYTES, + cache_size, + dataset_size, + epoch, + generate_cache, + generate_dataset_item, + generate_seed, + hashimoto_light, +) +from ethereum.spurious_dragon.eth_types import Header +from ethereum.spurious_dragon.spec import ( + generate_header_hash_for_pow, + validate_proof_of_work, +) +from ethereum.spurious_dragon.utils.json import json_to_header +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes8, + hex_to_bytes32, +) +from ethereum.utils.numeric import is_prime, le_uint32_sequence_to_bytes + + +@pytest.mark.parametrize( + "block_number, expected_epoch", + [ + (Uint(0), Uint(0)), + (Uint(29999), Uint(0)), + (Uint(30000), Uint(1)), + ], +) +def test_epoch(block_number: Uint, expected_epoch: Uint) -> None: + assert epoch(block_number) == expected_epoch + + +def test_epoch_start_and_end_blocks_have_same_epoch() -> None: + for _ in range(100): + block_number = Uint(randint(10 ** 9, 2 * (10 ** 9))) + epoch_start_block_number = (block_number // EPOCH_SIZE) * EPOCH_SIZE + epoch_end_block_number = epoch_start_block_number + EPOCH_SIZE - 1 + + assert ( + epoch(block_number) + == epoch(epoch_start_block_number) + == epoch(epoch_end_block_number) + ) + + +def test_cache_size_1st_epoch() -> None: + assert ( + cache_size(Uint(0)) == cache_size(Uint(0) + EPOCH_SIZE - 1) == 16776896 + ) + assert is_prime(cache_size(Uint(0)) // HASH_BYTES) + + +def test_cache_size_2048_epochs() -> None: + cache_size_2048_epochs = json.loads( + cast( + bytes, + pkgutil.get_data( + "ethereum", "assets/cache_sizes_2048_epochs.json" + ), + ).decode() + ) + assert len(cache_size_2048_epochs) == 2048 + + for epoch_number in range(2048): + assert ( + cache_size(Uint(epoch_number * EPOCH_SIZE)) + == cache_size_2048_epochs[epoch_number] + ) + + +def test_epoch_start_and_end_blocks_have_same_cache_size() -> None: + for _ in range(100): + block_number = Uint(randint(10 ** 9, 2 * (10 ** 9))) + epoch_start_block_number = (block_number // EPOCH_SIZE) * EPOCH_SIZE + epoch_end_block_number = epoch_start_block_number + EPOCH_SIZE - 1 + + assert ( + cache_size(block_number) + == cache_size(epoch_start_block_number) + == cache_size(epoch_end_block_number) + ) + + +def test_dataset_size_1st_epoch() -> None: + assert ( + dataset_size(Uint(0)) + == dataset_size(Uint(0 + EPOCH_SIZE - 1)) + == 1073739904 + ) + assert is_prime(dataset_size(Uint(0)) // MIX_BYTES) + + +def test_dataset_size_2048_epochs() -> None: + dataset_size_2048_epochs = json.loads( + cast( + bytes, + pkgutil.get_data( + "ethereum", "assets/dataset_sizes_2048_epochs.json" + ), + ).decode() + ) + assert len(dataset_size_2048_epochs) == 2048 + + for epoch_number in range(2048): + assert ( + dataset_size(Uint(epoch_number * EPOCH_SIZE)) + == dataset_size_2048_epochs[epoch_number] + ) + + +def test_epoch_start_and_end_blocks_have_same_dataset_size() -> None: + for _ in range(100): + block_number = Uint(randint(10 ** 9, 2 * (10 ** 9))) + epoch_start_block_number = (block_number // EPOCH_SIZE) * EPOCH_SIZE + epoch_end_block_number = epoch_start_block_number + EPOCH_SIZE - 1 + + assert ( + dataset_size(block_number) + == dataset_size(epoch_start_block_number) + == dataset_size(epoch_end_block_number) + ) + + +def test_seed() -> None: + assert ( + generate_seed(Uint(0)) + == generate_seed(Uint(0 + EPOCH_SIZE - 1)) + == b"\x00" * 32 + ) + assert ( + generate_seed(Uint(EPOCH_SIZE)) + == generate_seed(Uint(2 * EPOCH_SIZE - 1)) + == keccak256(b"\x00" * 32) + ) + # NOTE: The below bytes value was obtained by obtaining the seed for the same block number from Geth. + assert ( + generate_seed(Uint(12345678)) + == b"[\x8c\xa5\xaaC\x05\xae\xed<\x87\x1d\xbc\xabQBGj\xfd;\x9cJ\x98\xf6Dq\\z\xaao\x1c\xf7\x03" + ) + + +def test_epoch_start_and_end_blocks_have_same_seed() -> None: + for _ in range(100): + block_number = Uint(randint(10000, 20000)) + epoch_start_block_number = (block_number // EPOCH_SIZE) * EPOCH_SIZE + epoch_end_block_number = epoch_start_block_number + EPOCH_SIZE - 1 + + assert ( + generate_seed(epoch_start_block_number) + == generate_seed(block_number) + == generate_seed(epoch_end_block_number) + ) + + +def test_ethtest_fixtures() -> None: + ethereum_tests = load_pow_test_fixtures() + for test in ethereum_tests: + header = test["header"] + assert header.nonce == test["nonce"] + assert header.mix_digest == test["mix_digest"] + assert generate_seed(header.number) == test["seed"] + assert cache_size(header.number) == test["cache_size"] + assert dataset_size(header.number) == test["dataset_size"] + + header_hash = generate_header_hash_for_pow(header) + assert header_hash == test["header_hash"] + + cache = generate_cache(header.number) + cache_hash = crypto.keccak256( + b"".join( + le_uint32_sequence_to_bytes(cache_item) for cache_item in cache + ) + ) + assert cache_hash == test["cache_hash"] + + mix_digest, result = hashimoto_light( + header_hash, header.nonce, cache, dataset_size(header.number) + ) + assert mix_digest == test["mix_digest"] + assert result == test["result"] + + +def load_pow_test_fixtures() -> List[Dict[str, Any]]: + with open( + "tests/fixtures/PoWTests/ethash_tests.json" + ) as pow_test_file_handler: + return [ + { + "nonce": hex_to_bytes8(raw_fixture["nonce"]), + "mix_digest": hex_to_bytes32(raw_fixture["mixHash"]), + "header": rlp.decode_to( + Header, hex_to_bytes(raw_fixture["header"]) + ), + "seed": hex_to_bytes32(raw_fixture["seed"]), + "result": hex_to_bytes32(raw_fixture["result"]), + "cache_size": Uint(raw_fixture["cache_size"]), + "dataset_size": Uint(raw_fixture["full_size"]), + "header_hash": hex_to_bytes32(raw_fixture["header_hash"]), + "cache_hash": hex_to_bytes32(raw_fixture["cache_hash"]), + } + for raw_fixture in json.load(pow_test_file_handler).values() + ] + + +@pytest.mark.slow +@pytest.mark.parametrize( + "block_number, block_difficulty, header_hash, nonce, expected_mix_digest, expected_result", + [ + [ + Uint(1), + Uint(17171480576), + "0x85913a3057ea8bec78cd916871ca73802e77724e014dda65add3405d02240eb7", + "0x539bd4979fef1ec4", + "0x969b900de27b6ac6a67742365dd65f55a0526c41fd18e1b16f1a1215c2e66f59", + "0x000000002bc095dd4de049873e6302c3f14a7f2e5b5a1f60cdf1f1798164d610", + ], + [ + Uint(5), + Uint(17154711556), + "0xfe557bbc2346abe74c4e66b1843df7a884f83e3594a210d96594c455c32d33c1", + "0xfba9d0cff9dc5cf3", + "0x17b85b5ec310c4868249fa2f378c83b4f330e2d897e5373a8195946c71d1d19e", + "0x000000000767f35d1d21220cb5c53e060afd84fadd622db784f0d4b0541c034a", + ], + [ + Uint(123456), + Uint(4505282870523), + "0xad896938ef53ff923b4336d03573d52c69097dabf8734d71b9546d31db603121", + "0xf4b883fed83092b2", + "0x84d4162717b039a996ffaf59a54158443c62201b76170b02dbad626cca3226d5", + "0x00000000000fb25dfcfe2fcdc9a63c892ce795aba4380513a9705489bf247b07", + ], + [ + Uint(1000865), + Uint(12652630789208), + "0xcc868f6114e4cadc3876e4ca4e0705b2bcb76955f459bb019a80d72a512eefdb", + "0xc6613bcf40e716d6", + "0xce47e0609103ac85d56bf1637e51afd28e29431f47c11df47db80a63d95efbae", + "0x000000000015de37404be3c9beda75e12ae41ef7c937dcd52130cfc3b389bf42", + ], + ], +) +def test_pow_random_blocks( + block_number: Uint, + block_difficulty: Uint, + header_hash: str, + nonce: str, + expected_mix_digest: str, + expected_result: str, +) -> None: + mix_digest, result = hashimoto_light( + hex_to_bytes32(header_hash), + hex_to_bytes8(nonce), + generate_cache(block_number), + dataset_size(block_number), + ) + + assert mix_digest == hex_to_bytes32(expected_mix_digest) + assert result == hex_to_bytes(expected_result) + assert Uint.from_be_bytes(result) <= U256_CEIL_VALUE // (block_difficulty) + + +@pytest.mark.slow +@pytest.mark.parametrize( + "block_file_name", + [ + "block_1.json", + "block_1234567.json", + "block_12964999.json", + ], +) +def test_pow_validation_block_headers(block_file_name: str) -> None: + block_str_data = cast( + bytes, pkgutil.get_data("ethereum", f"assets/blocks/{block_file_name}") + ).decode() + block_json_data = json.loads(block_str_data) + + header: Header = json_to_header(block_json_data) + validate_proof_of_work(header) + + +# TODO: Once there is a method to download blocks, test the proof-of-work +# validation for the following blocks in each hardfork (except London as the +# current PoW algo won't work from London): +# * Start of hardfork +# * two random blocks inside the hardfork +# * End of hardfork + + +# +# Geth DAG related functionalities for fuzz testing +# + + +def generate_dag_via_geth( + geth_path: str, block_number: Uint, dag_dump_dir: str +) -> None: + subprocess.call([geth_path, "makedag", str(block_number), dag_dump_dir]) + + +def fetch_dag_data(dag_dump_dir: str, epoch_seed: bytes) -> Tuple[bytes, ...]: + dag_file_path = f"{dag_dump_dir}/full-R23-{epoch_seed.hex()[:16]}" + with open(dag_file_path, "rb") as fp: + dag_dataset = fp.read() + # The first 8 bytes are Magic Bytes and can be ignored. + dag_dataset = dag_dataset[8:] + + dag_dataset_items = [] + for i in range(0, len(dag_dataset), HASH_BYTES): + dag_dataset_items.append(dag_dataset[i : i + HASH_BYTES]) + + return tuple(dag_dataset_items) + + +GETH_MISSING = """geth binary not found. + +Some tests require a copy of the go-ethereum client binary to generate required +data. + +The tool `scripts/download_geth_linux.py` can fetch the appropriate version, or +you can download geth from: + + https://geth.ethereum.org/downloads/ + +Make sure you add the directory containing `geth` to your PATH, then try +running the tests again. +""" + + +@pytest.mark.slow +def test_dataset_generation_random_epoch(tmpdir: str) -> None: + """ + Generate a random epoch and obtain the DAG for that epoch from geth. + Then ensure the following 2 test scenarios: + 1. The first 100 dataset indices are same when the python + implementation is compared with the DAG dataset. + 2. Randomly take 500 indices between + [101, `dataset size in words` - 1] and ensure that the values are + same between python implementation and DAG dataset. + + NOTE - For this test case to run, it is mandatory for Geth to be + installed and accessible + """ + geth_path = shutil.which("geth") + if geth_path is None: + raise Exception(GETH_MISSING) + + epoch_number = Uint(randint(0, 100)) + block_number = epoch_number * EPOCH_SIZE + randint(0, EPOCH_SIZE - 1) + generate_dag_via_geth(geth_path, block_number, f"{tmpdir}/.ethash") + seed = generate_seed(block_number) + dag_dataset = fetch_dag_data(f"{tmpdir}/.ethash", seed) + + cache = generate_cache(block_number) + dataset_size_bytes = dataset_size(block_number) + dataset_size_words = dataset_size_bytes // HASH_BYTES + + assert len(dag_dataset) == dataset_size_words + + assert generate_dataset_item(cache, Uint(0)) == dag_dataset[0] + + for i in range(100): + assert generate_dataset_item(cache, Uint(i)) == dag_dataset[i] + + # Then for this dataset randomly take 5000 indices and check the + # data obtained from our implementation with geth DAG + for _ in range(500): + index = Uint(randint(101, dataset_size_words - 1)) + dataset_item = generate_dataset_item(cache, index) + assert dataset_item == dag_dataset[index], index + + # Manually forcing the dataset out of the memory incase the gc + # doesn't kick in immediately + del dag_dataset diff --git a/tests/spurious_dragon/test_rlp.py b/tests/spurious_dragon/test_rlp.py new file mode 100644 index 0000000000..73aa6abcc9 --- /dev/null +++ b/tests/spurious_dragon/test_rlp.py @@ -0,0 +1,114 @@ +import pytest + +import ethereum.rlp as rlp +from ethereum.base_types import U256, Bytes, Bytes0, Bytes8, Uint +from ethereum.crypto import keccak256 +from ethereum.spurious_dragon.eth_types import ( + Block, + Header, + Log, + Receipt, + Transaction, +) +from ethereum.spurious_dragon.utils.hexadecimal import hex_to_address +from ethereum.utils.hexadecimal import hex_to_bytes256 + +hash1 = keccak256(b"foo") +hash2 = keccak256(b"bar") +hash3 = keccak256(b"baz") +hash4 = keccak256(b"foobar") +hash5 = keccak256(b"quux") +hash6 = keccak256(b"foobarbaz") + +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" +) + +transaction1 = Transaction( + U256(1), + U256(2), + U256(3), + Bytes0(), + U256(4), + Bytes(b"foo"), + U256(27), + U256(5), + U256(6), +) + +transaction2 = Transaction( + U256(1), + U256(2), + U256(3), + Bytes0(), + U256(4), + Bytes(b"foo"), + U256(27), + U256(5), + U256(6), +) + +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"), + mix_digest=hash6, + nonce=Bytes8(b"12345678"), +) + +block = Block( + header=header, + transactions=(transaction1, transaction2), + ommers=(header,), +) + +log1 = Log( + address=address1, + topics=(hash1, hash2), + data=Bytes(b"foobar"), +) + +log2 = Log( + address=address1, + topics=(hash1,), + data=Bytes(b"quux"), +) + +receipt = Receipt( + post_state=hash1, + cumulative_gas_used=Uint(1), + bloom=bloom, + logs=(log1, log2), +) + + +@pytest.mark.parametrize( + "rlp_object", + [transaction1, transaction2, header, block, log1, log2, receipt], +) +def test_spurious_dragon_rlp(rlp_object: rlp.RLP) -> None: + encoded = rlp.encode(rlp_object) + assert rlp.decode_to(type(rlp_object), encoded) == rlp_object diff --git a/tests/spurious_dragon/test_state_transition.py b/tests/spurious_dragon/test_state_transition.py new file mode 100644 index 0000000000..6d3a05d05b --- /dev/null +++ b/tests/spurious_dragon/test_state_transition.py @@ -0,0 +1,43 @@ +import os +from functools import partial +from typing import Generator + +import pytest + +from tests.spurious_dragon.blockchain_st_test_helpers import ( + run_spurious_dragon_blockchain_st_tests, +) + +test_dir = ( + "tests/fixtures/LegacyTests/Constantinople/BlockchainTests/" + "GeneralStateTests/" +) + +run_general_state_tests = partial( + run_spurious_dragon_blockchain_st_tests, test_dir +) + +# Every test below takes more than 60s to run and +# hence they've been marked as slow +SLOW_TESTS = () + + +def get_test_files() -> Generator[str, None, None]: + for idx, _dir in enumerate(os.listdir(test_dir)): + test_file_path = os.path.join(test_dir, _dir) + for _file in os.listdir(test_file_path): + _test_file = os.path.join(_dir, _file) + # TODO: provide a way to run slow tests + if _test_file in SLOW_TESTS: + continue + else: + yield _test_file + + +@pytest.mark.parametrize("test_file", get_test_files()) +def test_general_state_tests(test_file: str) -> None: + try: + run_general_state_tests(test_file) + except KeyError: + # KeyError is raised when a test_file has no tests for spurious_dragon + raise pytest.skip(f"{test_file} has no tests for spurious_dragon") diff --git a/tests/spurious_dragon/test_trie.py b/tests/spurious_dragon/test_trie.py new file mode 100644 index 0000000000..a65e7f58d7 --- /dev/null +++ b/tests/spurious_dragon/test_trie.py @@ -0,0 +1,86 @@ +import json +from typing import Any + +from ethereum.spurious_dragon.eth_types import Bytes +from ethereum.spurious_dragon.trie import Trie, root, trie_set +from ethereum.utils.hexadecimal import ( + has_hex_prefix, + hex_to_bytes, + remove_hex_prefix, +) + + +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("tests/fixtures/TrieTests/" + path) as f: + tests = json.load(f) + + return tests diff --git a/tests/spurious_dragon/vm/__init__.py b/tests/spurious_dragon/vm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/spurious_dragon/vm/test_arithmetic_operations.py b/tests/spurious_dragon/vm/test_arithmetic_operations.py new file mode 100644 index 0000000000..b8b39241e1 --- /dev/null +++ b/tests/spurious_dragon/vm/test_arithmetic_operations.py @@ -0,0 +1,237 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_arithmetic_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmArithmeticTest", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "add0.json", + "add1.json", + "add2.json", + "add3.json", + "add4.json", + ], +) +def test_add(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "sub0.json", + "sub1.json", + "sub2.json", + "sub3.json", + "sub4.json", + ], +) +def test_sub(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mul0.json", + "mul1.json", + "mul2.json", + "mul3.json", + "mul4.json", + "mul5.json", + "mul6.json", + "mul7.json", + ], +) +def test_mul(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "div1.json", + "divBoostBug.json", + "divByNonZero0.json", + "divByNonZero1.json", + "divByNonZero2.json", + "divByNonZero3.json", + "divByZero.json", + "divByZero_2.json", + ], +) +def test_div(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "sdiv0.json", + "sdiv1.json", + "sdiv2.json", + "sdiv3.json", + "sdiv4.json", + "sdiv5.json", + "sdiv6.json", + "sdiv7.json", + "sdiv8.json", + "sdiv9.json", + "sdivByZero0.json", + "sdivByZero1.json", + "sdivByZero2.json", + "sdiv_i256min.json", + "sdiv_i256min2.json", + "sdiv_i256min3.json", + "sdiv_dejavu.json", + ], +) +def test_sdiv(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mod0.json", + "mod1.json", + "mod2.json", + "mod3.json", + "mod4.json", + "modByZero.json", + ], +) +def test_mod(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "smod0.json", + "smod1.json", + "smod2.json", + "smod3.json", + "smod4.json", + "smod5.json", + "smod6.json", + "smod7.json", + "smod8_byZero.json", + "smod_i256min1.json", + "smod_i256min2.json", + ], +) +def test_smod(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "addmod0.json", + "addmod1.json", + "addmod1_overflow2.json", + "addmod1_overflow3.json", + "addmod1_overflow4.json", + "addmod1_overflowDiff.json", + "addmod2.json", + "addmod2_0.json", + "addmod2_1.json", + "addmod3.json", + "addmod3_0.json", + ], +) +def test_addmod(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mulmod0.json", + "mulmod1.json", + "mulmod1_overflow.json", + "mulmod1_overflow2.json", + "mulmod1_overflow3.json", + "mulmod1_overflow4.json", + "mulmod2.json", + "mulmod2_0.json", + "mulmod2_1.json", + "mulmod3.json", + "mulmod3_0.json", + "mulmod4.json", + ], +) +def test_mulmod(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "exp0.json", + "exp1.json", + "exp2.json", + "exp3.json", + "exp4.json", + "exp5.json", + "exp6.json", + "exp7.json", + "exp8.json", + "expXY.json", + "expXY_success.json", + ], +) +def test_exp(test_file: str) -> None: + run_arithmetic_vm_test(test_file, check_gas_left=False) + + +@pytest.mark.parametrize("exponent", ([2, 4, 8, 16, 32, 64, 128, 256])) +def test_exp_power_2(exponent: int) -> None: + run_arithmetic_vm_test( + f"expPowerOf2_{exponent}.json", check_gas_left=False + ) + + +def test_exp_power_256() -> None: + for i in range(1, 34): + run_arithmetic_vm_test(f"expPowerOf256_{i}.json", check_gas_left=False) + + for i in range(34): + run_arithmetic_vm_test( + f"expPowerOf256Of256_{i}.json", check_gas_left=False + ) + + +@pytest.mark.parametrize( + "test_file", + [ + "signextend_0_BigByte.json", + "signextend_00.json", + "signextend_AlmostBiggestByte.json", + "signextend_BigByte_0.json", + "signextend_BigByteBigByte.json", + "signextend_BigBytePlus1_2.json", + "signextend_bigBytePlus1.json", + "signextend_BitIsNotSet.json", + "signextend_BitIsNotSetInHigherByte.json", + "signextend_bitIsSet.json", + "signextend_BitIsSetInHigherByte.json", + "signextend_Overflow_dj42.json", + "signextendInvalidByteNumber.json", + ], +) +def test_signextend(test_file: str) -> None: + run_arithmetic_vm_test(test_file) + + +def test_stop() -> None: + run_arithmetic_vm_test("stop.json") diff --git a/tests/spurious_dragon/vm/test_bitwise_logic_operations.py b/tests/spurious_dragon/vm/test_bitwise_logic_operations.py new file mode 100644 index 0000000000..7fced0dd41 --- /dev/null +++ b/tests/spurious_dragon/vm/test_bitwise_logic_operations.py @@ -0,0 +1,170 @@ +from functools import partial + +import pytest + +from .vm_test_helpers import run_test + +run_bitwise_ops_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmBitwiseLogicOperation", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "lt0.json", + "lt1.json", + "lt2.json", + "lt3.json", + ], +) +def test_lt(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "gt0.json", + "gt1.json", + "gt2.json", + "gt3.json", + ], +) +def test_gt(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "slt0.json", + "slt1.json", + "slt2.json", + "slt3.json", + "slt4.json", + ], +) +def test_slt(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "sgt0.json", + "sgt1.json", + "sgt2.json", + "sgt3.json", + "sgt4.json", + ], +) +def test_sgt(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "eq0.json", + "eq1.json", + "eq2.json", + ], +) +def test_eq(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "iszero0.json", + "iszero1.json", + "iszeo2.json", + ], +) +def test_iszero(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "and0.json", + "and1.json", + "and2.json", + "and3.json", + "and4.json", + "and5.json", + ], +) +def test_and(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "or0.json", + "or1.json", + "or2.json", + "or3.json", + "or4.json", + "or5.json", + ], +) +def test_or(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "xor0.json", + "xor1.json", + "xor2.json", + "xor3.json", + "xor4.json", + "xor5.json", + ], +) +def test_xor(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "not0.json", + "not1.json", + "not2.json", + "not3.json", + "not4.json", + "not5.json", + ], +) +def test_not(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "byte0.json", + "byte1.json", + "byte2.json", + "byte3.json", + "byte4.json", + "byte5.json", + "byte6.json", + "byte7.json", + "byte8.json", + "byte9.json", + "byte10.json", + "byte11.json", + "byteBN.json", + ], +) +def test_byte(test_file: str) -> None: + run_bitwise_ops_vm_test(test_file) diff --git a/tests/spurious_dragon/vm/test_block_operations.py b/tests/spurious_dragon/vm/test_block_operations.py new file mode 100644 index 0000000000..03b02814e1 --- /dev/null +++ b/tests/spurious_dragon/vm/test_block_operations.py @@ -0,0 +1,28 @@ +from functools import partial + +from ..vm.vm_test_helpers import run_test + +run_block_ops_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmBlockInfoTest", +) + + +def test_coinbase() -> None: + run_block_ops_vm_test("coinbase.json") + + +def test_timestamp() -> None: + run_block_ops_vm_test("timestamp.json") + + +def test_number() -> None: + run_block_ops_vm_test("number.json") + + +def test_difficulty() -> None: + run_block_ops_vm_test("difficulty.json") + + +def test_gas_limit() -> None: + run_block_ops_vm_test("gaslimit.json") diff --git a/tests/spurious_dragon/vm/test_control_flow_operations.py b/tests/spurious_dragon/vm/test_control_flow_operations.py new file mode 100644 index 0000000000..86c42dba43 --- /dev/null +++ b/tests/spurious_dragon/vm/test_control_flow_operations.py @@ -0,0 +1,179 @@ +from functools import partial + +import pytest + +from .vm_test_helpers import run_test + +run_control_flow_ops_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmIOandFlowOperations", +) + + +@pytest.mark.parametrize( + "test_file, check_gas_left", + [ + ("jump0_jumpdest0.json", True), + ("jump0_jumpdest2.json", True), + ("jumpAfterStop.json", True), + ("jumpdestBigList.json", True), + ("jumpTo1InstructionafterJump.json", True), + ("jumpDynamicJumpSameDest.json", True), + ("indirect_jump1.json", True), + ("indirect_jump2.json", True), + ("indirect_jump3.json", True), + ("DynamicJump_value1.json", True), + ("DynamicJump_value2.json", True), + ("DynamicJump_value3.json", True), + ("stackjump1.json", True), + ("indirect_jump4.json", True), + ("JDfromStorageDynamicJump0_jumpdest0.json", False), + ("JDfromStorageDynamicJump0_jumpdest2.json", False), + ("DynamicJump0_jumpdest0.json", True), + ("DynamicJump0_jumpdest2.json", True), + ("DynamicJumpAfterStop.json", True), + ("DynamicJumpJD_DependsOnJumps1.json", True), + ("DynamicJumpPathologicalTest0.json", True), + ("DynamicJumpStartWithJumpDest.json", True), + ("BlockNumberDynamicJump0_jumpdest0.json", True), + ("BlockNumberDynamicJump0_jumpdest2.json", True), + ("bad_indirect_jump1.json", True), + ("bad_indirect_jump2.json", True), + ("jump0_AfterJumpdest.json", True), + ("jump0_AfterJumpdest3.json", True), + ("jump0_outOfBoundary.json", True), + ("jump0_withoutJumpdest.json", True), + ("jump1.json", True), + ("jumpHigh.json", True), + ("jumpInsidePushWithJumpDest.json", True), + ("jumpInsidePushWithoutJumpDest.json", True), + ("jumpTo1InstructionafterJump_jumpdestFirstInstruction.json", True), + ("jumpTo1InstructionafterJump_noJumpDest.json", True), + ("jumpToUint64maxPlus1.json", True), + ("jumpToUintmaxPlus1.json", True), + ("JDfromStorageDynamicJump0_AfterJumpdest.json", True), + ("JDfromStorageDynamicJump0_AfterJumpdest3.json", True), + ("JDfromStorageDynamicJump0_withoutJumpdest.json", True), + ("JDfromStorageDynamicJump1.json", True), + ("JDfromStorageDynamicJumpInsidePushWithJumpDest.json", True), + ("JDfromStorageDynamicJumpInsidePushWithoutJumpDest.json", True), + ("DyanmicJump0_outOfBoundary.json", True), + ("DynamicJump0_AfterJumpdest.json", True), + ("DynamicJump0_AfterJumpdest3.json", True), + ("DynamicJump0_withoutJumpdest.json", True), + ("DynamicJump1.json", True), + ("DynamicJumpInsidePushWithJumpDest.json", True), + ("DynamicJumpInsidePushWithoutJumpDest.json", True), + ("DynamicJumpJD_DependsOnJumps0.json", True), + ("DynamicJumpPathologicalTest1.json", True), + ("DynamicJumpPathologicalTest2.json", True), + ("DynamicJumpPathologicalTest3.json", True), + ("BlockNumberDynamicJump0_AfterJumpdest.json", True), + ("BlockNumberDynamicJump0_AfterJumpdest3.json", True), + ("BlockNumberDynamicJump0_withoutJumpdest.json", True), + ("BlockNumberDynamicJump1.json", True), + ("BlockNumberDynamicJumpInsidePushWithJumpDest.json", True), + ("BlockNumberDynamicJumpInsidePushWithoutJumpDest.json", True), + ("jump0_foreverOutOfGas.json", True), + ("JDfromStorageDynamicJump0_foreverOutOfGas.json", True), + ("DynamicJump0_foreverOutOfGas.json", True), + ("BlockNumberDynamicJump0_foreverOutOfGas.json", True), + ("jumpOntoJump.json", True), + ("DynamicJump_valueUnderflow.json", True), + ("stack_loop.json", True), + ], +) +def test_jump(test_file: str, check_gas_left: bool) -> None: + run_control_flow_ops_vm_test(test_file, check_gas_left=check_gas_left) + + +@pytest.mark.parametrize( + "test_file, check_gas_left", + [ + ("jumpi1.json", True), + ("jumpiAfterStop.json", True), + ("jumpi_at_the_end.json", True), + ("JDfromStorageDynamicJumpi1.json", False), + ("JDfromStorageDynamicJumpiAfterStop.json", False), + ("DynamicJumpi1.json", True), + ("DynamicJumpiAfterStop.json", True), + ("BlockNumberDynamicJumpi1.json", True), + ("BlockNumberDynamicJumpiAfterStop.json", True), + ("jumpi0.json", True), + ("jumpi1_jumpdest.json", True), + ("jumpifInsidePushWithJumpDest.json", True), + ("jumpifInsidePushWithoutJumpDest.json", True), + ("jumpiOutsideBoundary.json", True), + ("jumpiToUint64maxPlus1.json", True), + ("jumpiToUintmaxPlus1.json", True), + ("JDfromStorageDynamicJumpi0.json", True), + ("JDfromStorageDynamicJumpi1_jumpdest.json", True), + ("JDfromStorageDynamicJumpifInsidePushWithJumpDest.json", True), + ("JDfromStorageDynamicJumpifInsidePushWithoutJumpDest.json", True), + ("JDfromStorageDynamicJumpiOutsideBoundary.json", True), + ("DynamicJumpi0.json", True), + ("DynamicJumpi1_jumpdest.json", True), + ("DynamicJumpifInsidePushWithJumpDest.json", True), + ("DynamicJumpifInsidePushWithoutJumpDest.json", True), + ("DynamicJumpiOutsideBoundary.json", True), + ("BlockNumberDynamicJumpi0.json", True), + ("BlockNumberDynamicJumpi1_jumpdest.json", True), + ("BlockNumberDynamicJumpifInsidePushWithJumpDest.json", True), + ("BlockNumberDynamicJumpifInsidePushWithoutJumpDest.json", True), + ("BlockNumberDynamicJumpiOutsideBoundary.json", True), + ], +) +def test_jumpi(test_file: str, check_gas_left: bool) -> None: + run_control_flow_ops_vm_test(test_file, check_gas_left=check_gas_left) + + +@pytest.mark.parametrize( + "test_file", + [ + "pc0.json", + "pc1.json", + ], +) +def test_pc(test_file: str) -> None: + run_control_flow_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + ["gas0.json", "gas1.json", "gasOverFlow.json"], +) +def test_gas(test_file: str) -> None: + run_control_flow_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "for_loop1.json", + "for_loop2.json", + "loop_stacklimit_1020.json", + "loop_stacklimit_1021.json", + ], +) +def test_loop(test_file: str) -> None: + run_control_flow_ops_vm_test(test_file) + + +def test_when() -> None: + run_control_flow_ops_vm_test("when.json") + + +@pytest.mark.parametrize( + "test_file", + [ + "byte1.json", + "calldatacopyMemExp.json", + "codecopyMemExp.json", + "deadCode_1.json", + "dupAt51becameMload.json", + "swapAt52becameMstore.json", + "log1MemExp.json", + ], +) +def test_miscellaneous(test_file: str) -> None: + run_control_flow_ops_vm_test(test_file) diff --git a/tests/spurious_dragon/vm/test_environmental_operations.py b/tests/spurious_dragon/vm/test_environmental_operations.py new file mode 100644 index 0000000000..df0cdb7583 --- /dev/null +++ b/tests/spurious_dragon/vm/test_environmental_operations.py @@ -0,0 +1,103 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_environmental_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmEnvironmentalInfo", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "address0.json", + "address1.json", + ], +) +def test_address(test_file: str) -> None: + run_environmental_vm_test(test_file) + + +def test_origin() -> None: + run_environmental_vm_test("origin.json") + + +def test_caller() -> None: + run_environmental_vm_test("caller.json") + + +def test_callvalue() -> None: + run_environmental_vm_test("callvalue.json") + + +@pytest.mark.parametrize( + "test_file", + [ + "calldataload0.json", + "calldataload1.json", + "calldataload2.json", + "calldataload_BigOffset.json", + "calldataloadSizeTooHigh.json", + "calldataloadSizeTooHighPartial.json", + ], +) +def test_calldataload(test_file: str) -> None: + run_environmental_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "calldatasize0.json", + "calldatasize1.json", + "calldatasize2.json", + ], +) +def test_calldatasize(test_file: str) -> None: + run_environmental_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "calldatacopy0.json", + "calldatacopy1.json", + "calldatacopy2.json", + "calldatacopyZeroMemExpansion.json", + "calldatacopy_DataIndexTooHigh.json", + "calldatacopy_DataIndexTooHigh2.json", + "calldatacopy_sec.json", + "calldatacopyUnderFlow.json", + "calldatacopy0_return.json", + "calldatacopy1_return.json", + "calldatacopy2_return.json", + "calldatacopyZeroMemExpansion_return.json", + "calldatacopy_DataIndexTooHigh_return.json", + "calldatacopy_DataIndexTooHigh2_return.json", + ], +) +def test_calldatacopy(test_file: str) -> None: + run_environmental_vm_test(test_file) + + +def test_codesize() -> None: + run_environmental_vm_test("codesize.json") + + +@pytest.mark.parametrize( + "test_file", + [ + "codecopy0.json", + "codecopyZeroMemExpansion.json", + "codecopy_DataIndexTooHigh.json", + ], +) +def test_codecopy(test_file: str) -> None: + run_environmental_vm_test(test_file) + + +def test_gasprice() -> None: + run_environmental_vm_test("gasprice.json") diff --git a/tests/spurious_dragon/vm/test_keccak.py b/tests/spurious_dragon/vm/test_keccak.py new file mode 100644 index 0000000000..0fc04b7a4c --- /dev/null +++ b/tests/spurious_dragon/vm/test_keccak.py @@ -0,0 +1,45 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_sha3_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmSha3Test", +) +run_special_sha3_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmIOandFlowOperations", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "sha3_0.json", + "sha3_1.json", + "sha3_2.json", + "sha3_bigOffset2.json", + "sha3_memSizeNoQuadraticCost31.json", + "sha3_memSizeQuadraticCost32.json", + "sha3_memSizeQuadraticCost32_zeroSize.json", + "sha3_memSizeQuadraticCost33.json", + "sha3_memSizeQuadraticCost63.json", + "sha3_memSizeQuadraticCost64.json", + "sha3_memSizeQuadraticCost64_2.json", + "sha3_memSizeQuadraticCost65.json", + "sha3_3.json", + "sha3_4.json", + "sha3_5.json", + "sha3_6.json", + "sha3_bigOffset.json", + "sha3_bigSize.json", + ], +) +def test_sha3_succeeds(test_file: str) -> None: + run_sha3_vm_test(test_file) + + +def test_sha3_fails_out_of_gas_memory_expansion() -> None: + run_special_sha3_vm_test("sha3MemExp.json") diff --git a/tests/spurious_dragon/vm/test_logging_operations.py b/tests/spurious_dragon/vm/test_logging_operations.py new file mode 100644 index 0000000000..11597498f6 --- /dev/null +++ b/tests/spurious_dragon/vm/test_logging_operations.py @@ -0,0 +1,100 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_logging_ops_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmLogTest", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "log0_emptyMem.json", + "log0_logMemsizeZero.json", + "log0_nonEmptyMem.json", + "log0_nonEmptyMem_logMemSize1.json", + "log0_nonEmptyMem_logMemSize1_logMemStart31.json", + "log0_logMemsizeTooHigh.json", + "log0_logMemStartTooHigh.json", + ], +) +def test_log0(test_file: str) -> None: + run_logging_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "log1_Caller.json", + "log1_emptyMem.json", + "log1_logMemsizeZero.json", + "log1_MaxTopic.json", + "log1_nonEmptyMem.json", + "log1_nonEmptyMem_logMemSize1.json", + "log1_nonEmptyMem_logMemSize1_logMemStart31.json", + "log1_logMemsizeTooHigh.json", + "log1_logMemStartTooHigh.json", + ], +) +def test_log1(test_file: str) -> None: + run_logging_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "log2_Caller.json", + "log2_emptyMem.json", + "log2_logMemsizeZero.json", + "log2_MaxTopic.json", + "log2_nonEmptyMem.json", + "log2_nonEmptyMem_logMemSize1.json", + "log2_nonEmptyMem_logMemSize1_logMemStart31.json", + "log2_logMemsizeTooHigh.json", + "log2_logMemStartTooHigh.json", + ], +) +def test_log2(test_file: str) -> None: + run_logging_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "log3_Caller.json", + "log3_emptyMem.json", + "log3_logMemsizeZero.json", + "log3_MaxTopic.json", + "log3_nonEmptyMem.json", + "log3_nonEmptyMem_logMemSize1.json", + "log3_nonEmptyMem_logMemSize1_logMemStart31.json", + "log3_PC.json", + "log3_logMemsizeTooHigh.json", + "log3_logMemStartTooHigh.json", + ], +) +def test_log3(test_file: str) -> None: + run_logging_ops_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "log4_Caller.json", + "log4_emptyMem.json", + "log4_logMemsizeZero.json", + "log4_MaxTopic.json", + "log4_nonEmptyMem.json", + "log4_nonEmptyMem_logMemSize1.json", + "log4_nonEmptyMem_logMemSize1_logMemStart31.json", + "log4_PC.json", + "log4_logMemsizeTooHigh.json", + "log4_logMemStartTooHigh.json", + ], +) +def test_log4(test_file: str) -> None: + run_logging_ops_vm_test(test_file) diff --git a/tests/spurious_dragon/vm/test_memory_operations.py b/tests/spurious_dragon/vm/test_memory_operations.py new file mode 100644 index 0000000000..7c0e8fe0e4 --- /dev/null +++ b/tests/spurious_dragon/vm/test_memory_operations.py @@ -0,0 +1,71 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_memory_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmIOandFlowOperations/", +) + + +@pytest.mark.parametrize( + "test_file", + [ + "mstore0.json", + "mstore1.json", + "mstoreMemExp.json", + ], +) +def test_mstore(test_file: str) -> None: + run_memory_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mstore8_0.json", + "mstore8_1.json", + "mstore8WordToBigError.json", + "mstore8MemExp.json", + ], +) +def test_mstore8(test_file: str) -> None: + run_memory_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mloadError0.json", + "mloadError1.json", + "mstore_mload0.json", + "mloadOutOfGasError2.json", + ], +) +def test_mload(test_file: str) -> None: + run_memory_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mstore_mload0.json", + ], +) +def test_mstore_mload(test_file: str) -> None: + run_memory_vm_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "msize0.json", + "msize1.json", + "msize2.json", + "msize3.json", + ], +) +def test_msize(test_file: str) -> None: + run_memory_vm_test(test_file) diff --git a/tests/spurious_dragon/vm/test_stack_operations.py b/tests/spurious_dragon/vm/test_stack_operations.py new file mode 100644 index 0000000000..a11f619d0e --- /dev/null +++ b/tests/spurious_dragon/vm/test_stack_operations.py @@ -0,0 +1,70 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_push_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmPushDupSwapTest", +) +run_pop_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmIOandFlowOperations", +) +run_dup_vm_test = run_swap_vm_test = run_push_vm_test + + +@pytest.mark.parametrize( + "test_file, check_gas_left", + [(f"push{i}.json", True) for i in range(1, 34)] + + [ + ("push32Undefined2.json", True), + ("push32AndSuicide.json", False), + ], +) +def test_push_successfully(test_file: str, check_gas_left: bool) -> None: + run_push_vm_test(test_file, check_gas_left=check_gas_left) + + +@pytest.mark.parametrize( + "test_file", + [ + "push1_missingStack.json", + "push32Undefined.json", + "push32Undefined3.json", + "push32FillUpInputWithZerosAtTheEnd.json", + ], +) +def test_push_failed(test_file: str) -> None: + run_push_vm_test(test_file) + + +def test_dup() -> None: + for i in range(1, 17): + run_dup_vm_test(f"dup{i}.json") + + +def test_dup_error() -> None: + run_dup_vm_test("dup2error.json") + + +def test_swap() -> None: + for i in range(1, 17): + run_swap_vm_test(f"swap{i}.json") + + +def test_swap_jump() -> None: + run_swap_vm_test("swapjump1.json") + + +def test_swap_error() -> None: + run_swap_vm_test("swap2error.json") + + +def test_pop() -> None: + run_pop_vm_test("pop0.json") + + +def test_pop_fails_when_stack_underflowed() -> None: + run_pop_vm_test("pop1.json") diff --git a/tests/spurious_dragon/vm/test_storage_operations.py b/tests/spurious_dragon/vm/test_storage_operations.py new file mode 100644 index 0000000000..494d7f8d94 --- /dev/null +++ b/tests/spurious_dragon/vm/test_storage_operations.py @@ -0,0 +1,24 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_storage_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmIOandFlowOperations", +) + + +@pytest.mark.parametrize( + "test_file, check_gas_left", + [ + ("sstore_load_0.json", False), + ("sstore_load_1.json", False), + ("sstore_load_2.json", False), + ("sstore_underflow.json", True), + ("kv1.json", True), + ], +) +def test_sstore_and_sload(test_file: str, check_gas_left: bool) -> None: + run_storage_vm_test(test_file, check_gas_left=check_gas_left) diff --git a/tests/spurious_dragon/vm/test_system_operations.py b/tests/spurious_dragon/vm/test_system_operations.py new file mode 100644 index 0000000000..c135f21ed3 --- /dev/null +++ b/tests/spurious_dragon/vm/test_system_operations.py @@ -0,0 +1,43 @@ +from functools import partial + +import pytest + +from ..vm.vm_test_helpers import run_test + +run_system_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmSystemOperations", +) + +run_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmTests", +) + + +@pytest.mark.parametrize( + "test_file, check_gas_left", + [ + ("suicide0.json", False), + ("suicideNotExistingAccount.json", False), + ("suicideSendEtherToMe.json", False), + ], +) +def test_seldestruct(test_file: str, check_gas_left: bool) -> None: + run_system_vm_test(test_file, check_gas_left=check_gas_left) + + +def test_seldestruct_vm_test() -> None: + run_vm_test("suicide.json", check_gas_left=False) + + +@pytest.mark.parametrize( + "test_file", + [ + "return0.json", + "return1.json", + "return2.json", + ], +) +def test_return(test_file: str) -> None: + run_system_vm_test(test_file) diff --git a/tests/spurious_dragon/vm/vm_test_helpers.py b/tests/spurious_dragon/vm/vm_test_helpers.py new file mode 100644 index 0000000000..0f8fe4b5ea --- /dev/null +++ b/tests/spurious_dragon/vm/vm_test_helpers.py @@ -0,0 +1,165 @@ +import json +import os +from typing import Any, List + +from ethereum import rlp +from ethereum.base_types import U256, Uint +from ethereum.crypto import keccak256 +from ethereum.spurious_dragon.eth_types import Account, Address +from ethereum.spurious_dragon.spec import BlockChain, get_last_256_block_hashes +from ethereum.spurious_dragon.state import ( + State, + close_state, + set_account, + set_storage, + storage_root, +) +from ethereum.spurious_dragon.utils.hexadecimal import hex_to_address +from ethereum.spurious_dragon.utils.message import prepare_message +from ethereum.spurious_dragon.vm import Environment +from ethereum.spurious_dragon.vm.interpreter import process_message_call +from ethereum.utils.hexadecimal import ( + hex_to_bytes, + hex_to_bytes32, + hex_to_u256, + hex_to_uint, +) + + +def run_test( + test_dir: str, test_file: str, check_gas_left: bool = True +) -> None: + test_data = load_test(test_dir, test_file) + target = test_data["target"] + env = test_data["env"] + message = prepare_message( + caller=test_data["caller"], + target=target, + value=test_data["value"], + data=test_data["data"], + gas=test_data["gas"], + env=env, + ) + + ( + gas_left, + refund_counter, + logs, + accounts_to_delete, + touched_accounts, + has_erred, + ) = process_message_call(message=message, env=env) + + if test_data["has_post_state"]: + if check_gas_left: + assert gas_left == test_data["expected_gas_left"] + assert keccak256(rlp.encode(logs)) == test_data["expected_logs_hash"] + # We are checking only the storage here and not the whole state, as the + # balances in the testcases don't change even though some value is + # transferred along with code invocation. But our evm execution transfers + # the value as well as executing the code + for addr in test_data["post_state_addresses"]: + assert storage_root( + test_data["expected_post_state"], addr + ) == storage_root(env.state, addr) + else: + assert has_erred is True + close_state(env.state) + close_state(test_data["expected_post_state"]) + + +def load_test(test_dir: str, test_file: str) -> Any: + test_name = os.path.splitext(test_file)[0] + path = os.path.join(test_dir, test_file) + with open(path, "r") as fp: + json_data = json.load(fp)[test_name] + + env = json_to_env(json_data) + + return { + "caller": hex_to_address(json_data["exec"]["caller"]), + "target": hex_to_address(json_data["exec"]["address"]), + "data": hex_to_bytes(json_data["exec"]["data"]), + "value": hex_to_u256(json_data["exec"]["value"]), + "gas": hex_to_u256(json_data["exec"]["gas"]), + "depth": Uint(0), + "env": env, + "expected_gas_left": hex_to_u256(json_data.get("gas", "0x64")), + "expected_logs_hash": hex_to_bytes(json_data.get("logs", "0x00")), + "expected_post_state": json_to_state(json_data.get("post", {})), + "post_state_addresses": json_to_addrs(json_data.get("post", {})), + "has_post_state": bool(json_data.get("post", {})), + } + + +def json_to_env(json_data: Any) -> Environment: + caller_hex_address = json_data["exec"]["caller"] + # Some tests don't have the caller state defined in the test case. Hence + # creating a dummy caller state. + if caller_hex_address not in json_data["pre"]: + value = json_data["exec"]["value"] + json_data["pre"][caller_hex_address] = get_dummy_account_state(value) + + current_state = json_to_state(json_data["pre"]) + + chain = BlockChain( + blocks=[], + state=current_state, + ) + + return Environment( + caller=hex_to_address(json_data["exec"]["caller"]), + origin=hex_to_address(json_data["exec"]["origin"]), + block_hashes=get_last_256_block_hashes(chain), + coinbase=hex_to_address(json_data["env"]["currentCoinbase"]), + number=hex_to_uint(json_data["env"]["currentNumber"]), + gas_limit=hex_to_uint(json_data["env"]["currentGasLimit"]), + gas_price=hex_to_u256(json_data["exec"]["gasPrice"]), + time=hex_to_u256(json_data["env"]["currentTimestamp"]), + difficulty=hex_to_uint(json_data["env"]["currentDifficulty"]), + state=current_state, + ) + + +def json_to_state(raw: Any) -> State: + state = State() + for (addr_hex, acc_state) in raw.items(): + addr = hex_to_address(addr_hex) + account = Account( + nonce=hex_to_uint(acc_state.get("nonce", "0x0")), + balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), + code=hex_to_bytes(acc_state.get("code", "")), + ) + set_account(state, addr, account) + + for (k, v) in acc_state.get("storage", {}).items(): + set_storage( + state, + addr, + hex_to_bytes32(k), + U256.from_be_bytes(hex_to_bytes32(v)), + ) + + set_account(state, addr, account) + + return state + + +def json_to_addrs(raw: Any) -> List[Address]: + addrs = [] + for addr_hex in raw: + addrs.append(hex_to_address(addr_hex)) + return addrs + + +def get_dummy_account_state(min_balance: str) -> Any: + # dummy account balance is the min balance needed plus 1 eth for gas + # cost + account_balance = hex_to_uint(min_balance) + (10 ** 18) + + return { + "balance": hex(account_balance), + "code": "", + "nonce": "0x00", + "storage": {}, + } diff --git a/tests/tangerine_whistle/blockchain_st_test_helpers.py b/tests/tangerine_whistle/blockchain_st_test_helpers.py index c3f61c0418..a2525c30fa 100644 --- a/tests/tangerine_whistle/blockchain_st_test_helpers.py +++ b/tests/tangerine_whistle/blockchain_st_test_helpers.py @@ -19,7 +19,7 @@ from ethereum.tangerine_whistle.state import ( State, close_state, - set_account, + set_account_internal, set_storage, ) from ethereum.tangerine_whistle.utils.hexadecimal import ( @@ -199,7 +199,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( diff --git a/tests/tangerine_whistle/optimized/test_state_db.py b/tests/tangerine_whistle/optimized/test_state_db.py index 2aafd3d890..05455d2925 100644 --- a/tests/tangerine_whistle/optimized/test_state_db.py +++ b/tests/tangerine_whistle/optimized/test_state_db.py @@ -64,7 +64,7 @@ def test_trie() -> None: for insert_list in operations: for (key, value) in insert_list: normal_trie.trie_set(trie_normal, key, value) - state_db.set_account(state, key, value) # type: ignore + state_db.set_account_internal(state, key, value) # type: ignore root = normal_trie.root(trie_normal) assert root == state_db.state_root(state) state_db.get_internal_key = backup_get_internal_key @@ -77,7 +77,7 @@ def test_trie() -> None: def test_storage_key() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) return obj @@ -99,12 +99,12 @@ def actions(impl: Any) -> Any: def test_resurrection() -> None: def actions(impl: Any) -> Any: obj = impl.State() - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) impl.set_storage(obj, ADDRESS_FOO, b"", U256(42)) impl.state_root(obj) impl.destroy_account(obj, ADDRESS_FOO) impl.state_root(obj) - impl.set_account(obj, ADDRESS_FOO, EMPTY_ACCOUNT) + impl.set_account_internal(obj, ADDRESS_FOO, EMPTY_ACCOUNT) return obj state_normal = actions(state) diff --git a/tests/tangerine_whistle/vm/vm_test_helpers.py b/tests/tangerine_whistle/vm/vm_test_helpers.py index a89ddb4d8c..a31328a32a 100644 --- a/tests/tangerine_whistle/vm/vm_test_helpers.py +++ b/tests/tangerine_whistle/vm/vm_test_helpers.py @@ -13,7 +13,7 @@ from ethereum.tangerine_whistle.state import ( State, close_state, - set_account, + set_account_internal, set_storage, storage_root, ) @@ -132,7 +132,7 @@ def json_to_state(raw: Any) -> State: balance=U256(hex_to_uint(acc_state.get("balance", "0x0"))), code=hex_to_bytes(acc_state.get("code", "")), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) for (k, v) in acc_state.get("storage", {}).items(): set_storage( @@ -142,7 +142,7 @@ def json_to_state(raw: Any) -> State: U256.from_be_bytes(hex_to_bytes32(v)), ) - set_account(state, addr, account) + set_account_internal(state, addr, account) return state diff --git a/whitelist.txt b/whitelist.txt index cf1b830fb3..7fe85d0af0 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -291,3 +291,5 @@ astuple base64 dao +precompile +recurse