diff --git a/src/ethereum_spec_tools/evm_tools/__init__.py b/src/ethereum_spec_tools/evm_tools/__init__.py index 99554d2d4d..2264f3dc3f 100644 --- a/src/ethereum_spec_tools/evm_tools/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/__init__.py @@ -105,7 +105,7 @@ def main( in_file = sys.stdin if options.evm_tool == "t8n": - t8n_tool = T8N(options, out_file, in_file) + t8n_tool = T8N(options, in_file, out_file) return t8n_tool.run() elif options.evm_tool == "b11r": b11r_tool = B11R(options, out_file, in_file) diff --git a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py index 6ecca69ad5..08621b78db 100644 --- a/src/ethereum_spec_tools/evm_tools/statetest/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/statetest/__init__.py @@ -146,7 +146,7 @@ def run_test_case( if output_basedir is not None: t8n_options.output_basedir = output_basedir - t8n = T8N(t8n_options, out_stream, in_stream) + t8n = T8N(t8n_options, in_stream, out_stream) t8n.run_state_test() return t8n.result diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index d0929ab70d..c34e8dec4c 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -7,20 +7,24 @@ import json import os from functools import partial -from typing import Any, TextIO +from typing import Any, Optional, TextIO, Union from ethereum_rlp import rlp from ethereum_types.numeric import U64, U256, Uint from ethereum import trace from ethereum.exceptions import EthereumException, InvalidBlock +from ethereum_clis.types import TransitionToolRequest from ethereum_spec_tools.forks import Hardfork +from ethereum_clis.types import TransitionToolOutput +from ethereum_clis.clis.execution_specs import ExecutionSpecsExceptionMapper from ..loaders.fixture_loader import Load from ..loaders.fork_loader import ForkLoad from ..utils import ( FatalException, get_module_name, + get_module_name_json_input, get_stream_logger, parse_hex_or_int, ) @@ -80,13 +84,59 @@ class T8N(Load): """The class that carries out the transition""" def __init__( - self, options: Any, out_file: TextIO, in_file: TextIO + self, + options: Any, + in_file: Union[TransitionToolRequest, TextIO], + out_file: Optional[TextIO] = None ) -> None: self.out_file = out_file self.in_file = in_file self.options = options self.forks = Hardfork.discover() + if isinstance(in_file, TransitionToolRequest): + self._init_from_pydantic(options, in_file) + else: + if out_file is None: + raise ValueError("out_file is required for JSON file input") + self._init_from_json(options, in_file, out_file) + + def _init_from_pydantic(self, options: Any, in_file: TransitionToolRequest) -> None: + fork_module, self.fork_block = get_module_name( + self.forks, options, in_file.input.env + ) + self.fork = ForkLoad(fork_module) + self.exception_mapper = ExecutionSpecsExceptionMapper() + + if options.trace: + trace_memory = getattr(options, "trace.memory", False) + trace_stack = not getattr(options, "trace.nostack", False) + trace_return_data = getattr(options, "trace.returndata") + trace.set_evm_trace( + partial( + evm_trace, + trace_memory=trace_memory, + trace_stack=trace_stack, + trace_return_data=trace_return_data, + output_basedir=options.output_basedir, + ) + ) + self.logger = get_stream_logger("T8N") + + super().__init__( + options.state_fork, + fork_module, + ) + + self.chain_id = parse_hex_or_int(options.state_chainid, U64) + self.alloc = Alloc(self, in_file.input.alloc) + self.env = Env(self, in_file.input.env) + self.txs = Txs(self, in_file.input.txs) + self.result = Result( + self.env.block_difficulty, self.env.base_fee_per_gas + ) + + def _init_from_json(self, options: Any, in_file: TextIO, out_file: TextIO) -> None: if "stdin" in ( options.input_env, options.input_alloc, @@ -96,7 +146,7 @@ def __init__( else: stdin = None - fork_module, self.fork_block = get_module_name( + fork_module, self.fork_block = get_module_name_json_input( self.forks, self.options, stdin ) self.fork = ForkLoad(fork_module) @@ -311,7 +361,21 @@ def run(self) -> int: json_state = self.alloc.to_json() json_result = self.result.to_json() + if isinstance(self.in_file, TransitionToolRequest): + json_output = {} + + txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() + json_output["body"] = txs_rlp + json_output["alloc"] = json_state + json_output["result"] = json_result + output: TransitionToolOutput = TransitionToolOutput.model_validate( + json_output, context={"exception_mapper": self.exception_mapper} + ) + return output + else: + return self._write_json_output(json_state, json_result) + def _write_json_output(self, json_state: Any, json_result: Any) -> int: json_output = {} if self.options.output_body == "stdout": diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 9a9a0bf7d4..d363fcde8f 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -3,7 +3,7 @@ """ import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from ethereum_rlp import rlp from ethereum_types.bytes import Bytes32 @@ -11,6 +11,7 @@ from ethereum.crypto.hash import Hash32, keccak256 from ethereum.utils.byte import left_pad_zero_bytes +from ethereum_test_types.block_types import Environment from ethereum.utils.hexadecimal import hex_to_bytes from ..utils import parse_hex_or_int @@ -55,7 +56,37 @@ class Env: excess_blob_gas: Optional[U64] requests: Any - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): + def __init__(self, t8n: "T8N", stdin: Optional[Union[Environment, Dict]] = None): + if isinstance(stdin, Environment): + self._init_from_pydantic(t8n, stdin) + else: + self._init_from_json(t8n, stdin) + + def _init_from_pydantic(self, t8n: "T8N", stdin: Environment) -> None: + self.coinbase = stdin.fee_recipient + self.block_gas_limit = Uint(stdin.gas_limit) + self.block_number = Uint(stdin.number) + self.block_timestamp = U256(stdin.timestamp) + + self.read_block_difficulty_pydantic(stdin, t8n) + self.read_base_fee_per_gas_pydantic(stdin, t8n) + self.read_randao_pydantic(stdin, t8n) + self.read_block_hashes_pydantic(stdin, t8n) + self.read_ommers_pydantic(stdin, t8n) + self.read_withdrawals_pydantic(stdin, t8n) + + self.parent_beacon_block_root = None + if t8n.fork.is_after_fork("ethereum.cancun"): + if not t8n.options.state_test: + parent_beacon_block_root_bytes = stdin.parent_beacon_block_root + self.parent_beacon_block_root = ( + Bytes32(parent_beacon_block_root_bytes) + if parent_beacon_block_root_bytes is not None + else None + ) + self.read_excess_blob_gas_pydantic(stdin, t8n) + + def _init_from_json(self, t8n: "T8N", stdin: Optional[Dict] = None) -> None: if t8n.options.input_env == "stdin": assert stdin is not None data = stdin["env"] @@ -86,6 +117,73 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): ) self.read_excess_blob_gas(data, t8n) + def read_excess_blob_gas_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the excess_blob_gas from the data. If the excess blob gas is + not present, it is calculated from the parent block parameters. + """ + self.parent_blob_gas_used = U64(0) + self.parent_excess_blob_gas = U64(0) + self.excess_blob_gas = None + + if not t8n.fork.is_after_fork("ethereum.cancun"): + return + + if data.excess_blob_gas is not None: + self.excess_blob_gas = U64(data.excess_blob_gas) + + if data.parent_excess_blob_gas is not None: + self.parent_excess_blob_gas = U64(data.parent_excess_blob_gas) + + if data.parent_blob_gas_used is not None: + self.parent_blob_gas_used = U64(data.parent_blob_gas_used) + + if self.excess_blob_gas is not None: + return + + assert self.parent_excess_blob_gas is not None + assert self.parent_blob_gas_used is not None + + parent_blob_gas = ( + self.parent_excess_blob_gas + self.parent_blob_gas_used + ) + + target_blob_gas_per_block = t8n.fork.TARGET_BLOB_GAS_PER_BLOCK + + if parent_blob_gas < target_blob_gas_per_block: + self.excess_blob_gas = U64(0) + else: + self.excess_blob_gas = parent_blob_gas - target_blob_gas_per_block + + if t8n.fork.is_after_fork("ethereum.osaka"): + # Under certain conditions specified in EIP-7918, the + # the excess_blob_gas is calculated differently in osaka + assert self.parent_base_fee_per_gas is not None + + GAS_PER_BLOB = t8n.fork.GAS_PER_BLOB + BLOB_BASE_COST = t8n.fork.BLOB_BASE_COST + BLOB_SCHEDULE_MAX = t8n.fork.BLOB_SCHEDULE_MAX + BLOB_SCHEDULE_TARGET = t8n.fork.BLOB_SCHEDULE_TARGET + + target_blob_gas_price = Uint(GAS_PER_BLOB) + target_blob_gas_price *= t8n.fork.calculate_blob_gas_price( + self.parent_excess_blob_gas + ) + + base_blob_tx_price = ( + BLOB_BASE_COST * self.parent_base_fee_per_gas + ) + if base_blob_tx_price > target_blob_gas_price: + blob_schedule_delta = ( + BLOB_SCHEDULE_MAX - BLOB_SCHEDULE_TARGET + ) + self.excess_blob_gas = ( + self.parent_excess_blob_gas + + self.parent_blob_gas_used + * blob_schedule_delta + // BLOB_SCHEDULE_MAX + ) + def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: """ Read the excess_blob_gas from the data. If the excess blob gas is @@ -159,6 +257,45 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: // BLOB_SCHEDULE_MAX ) + def read_base_fee_per_gas_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the base_fee_per_gas from the data. If the base fee is + not present, it is calculated from the parent block parameters. + """ + self.parent_gas_used = None + self.parent_gas_limit = None + self.parent_base_fee_per_gas = None + self.base_fee_per_gas = None + + if t8n.fork.is_after_fork("ethereum.london"): + if data.base_fee_per_gas is not None: + self.base_fee_per_gas = Uint(data.base_fee_per_gas) + + if data.parent_gas_used is not None: + self.parent_gas_used = Uint(data.parent_gas_used) + + if data.parent_gas_limit is not None: + self.parent_gas_limit = Uint(data.parent_gas_limit) + + if data.parent_base_fee_per_gas is not None: + self.parent_base_fee_per_gas = Uint(data.parent_base_fee_per_gas) + + if self.base_fee_per_gas is None: + assert self.parent_gas_limit is not None + assert self.parent_gas_used is not None + assert self.parent_base_fee_per_gas is not None + + parameters: List[object] = [ + self.block_gas_limit, + self.parent_gas_limit, + self.parent_gas_used, + self.parent_base_fee_per_gas, + ] + + self.base_fee_per_gas = t8n.fork.calculate_base_fee_per_gas( + *parameters + ) + def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: """ Read the base_fee_per_gas from the data. If the base fee is @@ -206,6 +343,14 @@ def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: *parameters ) + def read_randao_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the randao from the data. + """ + self.prev_randao = None + if t8n.fork.is_after_fork("ethereum.paris"): + self.prev_randao = Bytes32(data.prev_randao.to_bytes(32, "big")) + def read_randao(self, data: Any, t8n: "T8N") -> None: """ Read the randao from the data. @@ -227,6 +372,37 @@ def read_randao(self, data: Any, t8n: "T8N") -> None: left_pad_zero_bytes(hex_to_bytes(current_random), 32) ) + def _to_canonical_withdrawal(self, raw, fork): + """Convert raw withdrawal to canonical format.""" + return fork.Withdrawal( + index=U64(raw.index), + validator_index=U64(raw.validator_index), + address=raw.address, + amount=U256(raw.amount), + ) + + def read_withdrawals_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the withdrawals from the data. + """ + self.withdrawals = None + if t8n.fork.is_after_fork("ethereum.shanghai"): + raw_withdrawals = data.withdrawals + if raw_withdrawals: + def to_canonical_withdrawal(raw): + return t8n.fork.Withdrawal( + index=U64(raw.index), + validator_index=U64(raw.validator_index), + address=raw.address, + amount=U256(raw.amount), + ) + self.withdrawals = tuple( + self._to_canonical_withdrawal(wd, t8n.fork) + for wd in raw_withdrawals + ) + else: + self.withdrawals = () + def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ Read the withdrawals from the data. @@ -237,6 +413,43 @@ def read_withdrawals(self, data: Any, t8n: "T8N") -> None: t8n.json_to_withdrawals(wd) for wd in data["withdrawals"] ) + def read_block_difficulty_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the block difficulty from the data. + If `currentDifficulty` is present, it is used. Otherwise, + the difficulty is calculated from the parent block. + """ + self.block_difficulty = None + self.parent_timestamp = None + self.parent_difficulty = None + self.parent_ommers_hash = None + if t8n.fork.is_after_fork("ethereum.paris"): + return + elif data.difficulty is not None: + self.block_difficulty = Uint(data.difficulty) + else: + self.parent_timestamp = U256(data.parent_timestamp) + self.parent_difficulty = Uint(data.parent_difficulty) + args: List[object] = [ + self.block_number, + self.block_timestamp, + self.parent_timestamp, + self.parent_difficulty, + ] + if t8n.fork.is_after_fork("ethereum.byzantium"): + if data.parent_ommers_hash is not None: + EMPTY_OMMER_HASH = keccak256(rlp.encode([])) + self.parent_ommers_hash = Hash32( + data.parent_ommers_hash + ) + parent_has_ommers = ( + self.parent_ommers_hash != EMPTY_OMMER_HASH + ) + args.append(parent_has_ommers) + else: + args.append(False) + self.block_difficulty = t8n.fork.calculate_block_difficulty(*args) + def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: """ Read the block difficulty from the data. @@ -280,6 +493,35 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: args.append(False) self.block_difficulty = t8n.fork.calculate_block_difficulty(*args) + def read_block_hashes_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the block hashes. Returns a maximum of 256 block hashes. + """ + self.parent_hash = None + if ( + t8n.fork.is_after_fork("ethereum.prague") + and not t8n.options.state_test + ): + self.parent_hash = Hash32(data.parent_hash) + + # Read the block hashes + block_hashes: List[Any] = [] + + # The hex key strings provided might not have standard formatting + clean_block_hashes: Dict[int, Hash32] = data.block_hashes + + # Store a maximum of 256 block hashes. + max_blockhash_count = min(Uint(256), self.block_number) + for number in range( + self.block_number - max_blockhash_count, self.block_number + ): + if number in clean_block_hashes.keys(): + block_hashes.append(Hash32(clean_block_hashes[number])) + else: + block_hashes.append(None) + + self.block_hashes = block_hashes + def read_block_hashes(self, data: Any, t8n: "T8N") -> None: """ Read the block hashes. Returns a maximum of 256 block hashes. @@ -313,6 +555,22 @@ def read_block_hashes(self, data: Any, t8n: "T8N") -> None: self.block_hashes = block_hashes + def read_ommers_pydantic(self, data: Any, t8n: "T8N") -> None: + """ + Read the ommers. The ommers data might not have all the details + needed to obtain the Header. + """ + ommers = [] + if data.ommers is not None: + for ommer in data.ommers: + ommers.append( + Ommer( + ommer.delta, + ommer.address, + ) + ) + self.ommers = ommers + def read_ommers(self, data: Any, t8n: "T8N") -> None: """ Read the ommers. The ommers data might not have all the details diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index d99b7782b9..178ed6c293 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -3,14 +3,16 @@ """ import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from ethereum_rlp import Simple, rlp -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.bytes import Bytes, Bytes20, Bytes0 +from ethereum.exceptions import StateWithEmptyAccount +from ethereum_types.numeric import U8, U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.utils.hexadecimal import hex_to_bytes, hex_to_u256, hex_to_uint +from ethereum_test_types import Transaction, Alloc as PydanticAlloc from ..loaders.transaction_loader import TransactionLoad, UnsupportedTx from ..utils import FatalException, encode_to_hex, secp256k1_sign @@ -27,8 +29,52 @@ class Alloc: state: Any state_backup: Any - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): + def __init__( + self, + t8n: "T8N", + stdin: Optional[Union[PydanticAlloc, Dict]] = None, + ): """Read the alloc file and return the state.""" + + if isinstance(stdin, PydanticAlloc): + self._init_from_pydantic(t8n, stdin) + else: + self._init_from_json(t8n, stdin) + + def _init_from_pydantic(self, t8n: "T8N", stdin: PydanticAlloc) -> None: + state = t8n.fork.State() + set_storage = t8n.fork.set_storage + EMPTY_ACCOUNT = t8n.fork.EMPTY_ACCOUNT + + for addr_bytes, account in stdin.root.items(): + canonical_account = t8n.fork.Account( + nonce=Uint(account.nonce), + balance=U256(account.balance), + code=Bytes(account.code), + ) + + if t8n.fork.proof_of_stake and canonical_account == EMPTY_ACCOUNT: + raise StateWithEmptyAccount( + f"Empty account at {addr_bytes.hex()}." + ) + t8n.fork.set_account(state, addr_bytes, canonical_account) + + if account.storage and account.storage.root: + for storage_key, storage_value in account.storage.root.items(): + storage_key_bytes = storage_key.to_bytes( + 32, byteorder='big' + ) + set_storage( + state, + addr_bytes, + storage_key_bytes, + U256(storage_value) + ) + if t8n.fork.fork_module == "dao_fork": + t8n.fork.apply_dao(state) + self.state = state + + def _init_from_json(self, t8n: "T8N", stdin: Optional[Dict] = None) -> None: if t8n.options.input_alloc == "stdin": assert stdin is not None data = stdin["alloc"] @@ -48,7 +94,6 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): state = t8n.json_to_state(data) if t8n.fork.fork_module == "dao_fork": t8n.fork.apply_dao(state) - self.state = state def to_json(self) -> Any: @@ -74,7 +119,10 @@ def to_json(self) -> Any: ]._data.items() } - data["0x" + address.hex()] = account_data + key = address.hex() + if not key.startswith("0x"): + key = "0x" + key + data[key] = account_data return data @@ -85,7 +133,7 @@ class Txs: return a list of transactions. """ - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): + def __init__(self, t8n: "T8N", stdin: Optional[Union[List[Transaction], Dict]] = None): self.t8n = t8n self.successfully_parsed: List[int] = [] self.transactions: List[Tuple[Uint, Any]] = [] @@ -93,6 +141,38 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.rlp_input = False self.all_txs = [] + if isinstance(stdin, list) and all(isinstance(tx, Transaction) for tx in stdin): + self._init_from_pydantic(stdin) + else: + self._init_from_json(t8n, stdin) + + def _init_from_pydantic(self, stdin: List[Transaction]) -> None: + self.data = stdin + + for idx, raw_tx in enumerate(self.data): + try: + fork_tx = convert_pydantic_tx_to_canonical( + raw_tx, + self.t8n.fork, + ) + self.transactions.append(fork_tx) + self.successfully_parsed.append(idx) + self.all_txs.append(fork_tx) + except UnsupportedTx as e: + self.t8n.logger.warning( + f"Unsupported transaction type {idx}: " + f"{e.error_message}" + ) + self.rejected_txs[ + idx + ] = f"Unsupported transaction type: {e.error_message}" + self.all_txs.append(e.encoded_params) + except Exception as e: + msg = f"Failed to parse transaction {idx}: {str(e)}" + self.t8n.logger.warning(msg, exc_info=e) + self.rejected_txs[idx] = msg + + def _init_from_json(self, t8n: "T8N", stdin: Optional[Dict] = None) -> None: if t8n.options.input_txs == "stdin": assert stdin is not None data = stdin["txs"] @@ -248,6 +328,143 @@ def sign_transaction(self, json_tx: Any) -> None: json_tx["y_parity"] = json_tx["v"] +def _convert_access_list(access_list, fork): + if not access_list: + return [] + AccessCls = getattr(fork, "Access", None) + return [ + AccessCls( + account=entry.address, + slots=tuple(entry.storage_keys), + ) + for entry in access_list + ] + + +def _convert_authorizations(auth_list, fork): + if not auth_list: + return [] + AuthorizationCls = getattr(fork, "Authorization", None) + result = [] + for entry in auth_list: + d = entry.model_dump() + result.append( + AuthorizationCls( + chain_id=U256(d.get("chain_id", 0)), + address=d["address"], + nonce=U64(d.get("nonce", 0)), + y_parity=U8(d.get("v", d.get("y_parity", 0))), + r=U256(d["r"]), + s=U256(d["s"]), + ) + ) + return result + + +def _to_bytes20(val): + if val is None: + return Bytes0() + return Bytes20(val) + + +def convert_pydantic_tx_to_canonical(tx, fork): + """ + Convert a Pydantic Transaction to the canonical transaction class for the given fork. + """ + tx_type = tx.ty or 0 + + # SetCodeTransaction (Type 4) + if hasattr(fork, "SetCodeTransaction") and tx_type == 4: + return fork.SetCodeTransaction( + chain_id=U64(tx.chain_id), + nonce=U256(tx.nonce), + max_priority_fee_per_gas=Uint(tx.max_priority_fee_per_gas or 0), + max_fee_per_gas=Uint(tx.max_fee_per_gas or 0), + gas=Uint(tx.gas_limit), + to=_to_bytes20(tx.to), + value=U256(tx.value), + data=tx.data, + access_list=_convert_access_list(tx.access_list, fork), + authorizations=_convert_authorizations( + tx.authorization_list or [], + fork, + ), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + + # BlobTransaction (Type 3) + elif hasattr(fork, "BlobTransaction") and tx_type == 3: + return fork.BlobTransaction( + chain_id=U64(tx.chain_id), + nonce=U256(tx.nonce), + max_priority_fee_per_gas=Uint(tx.max_priority_fee_per_gas or 0), + max_fee_per_gas=Uint(tx.max_fee_per_gas or 0), + gas=Uint(tx.gas_limit), + to=_to_bytes20(tx.to), + value=U256(tx.value), + data=tx.data, + access_list=_convert_access_list(tx.access_list, fork), + max_fee_per_blob_gas=Uint(tx.max_fee_per_blob_gas or 0), + blob_versioned_hashes=tx.blob_versioned_hashes or (), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + + # FeeMarketTransaction (Type 2) + elif hasattr(fork, "FeeMarketTransaction") and tx_type == 2: + return fork.FeeMarketTransaction( + chain_id=U64(tx.chain_id), + nonce=U256(tx.nonce), + max_priority_fee_per_gas=Uint(tx.max_priority_fee_per_gas or 0), + max_fee_per_gas=Uint(tx.max_fee_per_gas or 0), + gas=Uint(tx.gas_limit), + to=_to_bytes20(tx.to), + value=U256(tx.value), + data=tx.data, + access_list=_convert_access_list(tx.access_list, fork), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + + # AccessListTransaction (Type 1) + elif hasattr(fork, "AccessListTransaction") and tx_type == 1: + return fork.AccessListTransaction( + chain_id=U64(tx.chain_id), + nonce=U256(tx.nonce), + gas_price=Uint(tx.gas_price or 0), + gas=Uint(tx.gas_limit), + to=_to_bytes20(tx.to), + value=U256(tx.value), + data=tx.data, + access_list=_convert_access_list(tx.access_list, fork), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + + # Legacy Transaction (Type 0) + else: + tx_cls = getattr(fork, "LegacyTransaction", None) + if tx_cls is None: + tx_cls = getattr(fork, "Transaction") + + return tx_cls( + nonce=U256(tx.nonce), + gas_price=Uint(tx.gas_price or 0), + gas=Uint(tx.gas_limit), + to=_to_bytes20(tx.to), + value=U256(tx.value), + data=tx.data, + v=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + + @dataclass class Result: """Type that represents the result of a transition execution""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py b/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py index c84503cfb0..95fbfc77ee 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py @@ -2,7 +2,6 @@ Implementation of the EELS T8N for execution-spec-tests. """ -import json import tempfile from io import StringIO from typing import Any, Dict, Optional @@ -86,17 +85,10 @@ def evaluate( parser = create_parser() t8n_options = parser.parse_args(t8n_args) - out_stream = StringIO() + in_stream = request_data - in_stream = StringIO(json.dumps(request_data_json["input"])) - - t8n = T8N(t8n_options, out_stream, in_stream) - t8n.run() - - output_dict = json.loads(out_stream.getvalue()) - output: TransitionToolOutput = TransitionToolOutput.model_validate( - output_dict, context={"exception_mapper": self.exception_mapper} - ) + t8n = T8N(t8n_options, in_stream) + output = t8n.run() if debug_output_path: dump_files_to_directory( diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index fe1225300b..9508c11718 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -82,6 +82,43 @@ class FatalException(Exception): def get_module_name( + forks: Sequence[Hardfork], options: Any, env: Any +) -> Tuple[str, int]: + """ + Get the module name and the fork block for the given state fork. + """ + if options.state_fork.casefold() in UNSUPPORTED_FORKS: + sys.exit(f"Unsupported state fork: {options.state_fork}") + # If the state fork is an exception, use the exception config. + exception_config: Optional[Dict[str, Any]] = None + try: + exception_config = EXCEPTION_MAPS[options.state_fork] + except KeyError: + pass + + if exception_config: + block_number = parse_hex_or_int(env.number, Uint) + + for fork, fork_block in exception_config["fork_blocks"]: + if block_number >= Uint(fork_block): + current_fork_module = fork + current_fork_block = fork_block + + return current_fork_module, current_fork_block + + # If the state fork is not an exception, use the fork name. + for fork in forks: + fork_module = fork.name.split(".")[-1] + key = "".join(x.title() for x in fork_module.split("_")) + + if key == options.state_fork: + return fork_module, 0 + + # Neither in exception nor a standard fork name. + sys.exit(f"Unsupported state fork: {options.state_fork}") + + +def get_module_name_json_input( forks: Sequence[Hardfork], options: Any, stdin: Any ) -> Tuple[str, int]: """ diff --git a/tests/helpers/load_evm_tools_tests.py b/tests/helpers/load_evm_tools_tests.py index afbbd14f89..0233181bab 100644 --- a/tests/helpers/load_evm_tools_tests.py +++ b/tests/helpers/load_evm_tools_tests.py @@ -125,7 +125,7 @@ def load_evm_tools_test(test_case: Dict[str, str], fork_name: str) -> None: t8n_options = parser.parse_args(t8n_args) try: - t8n = T8N(t8n_options, sys.stdout, in_stream) + t8n = T8N(t8n_options, in_stream, sys.stdout) except StateWithEmptyAccount as e: pytest.xfail(str(e))