From 82b79566d3c2ace28e09789e98898920110352be Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Fri, 1 Aug 2025 15:30:08 +0200 Subject: [PATCH 1/8] utilize pydantic models from eest on t8n --- .../evm_tools/t8n/__init__.py | 72 ++---- src/ethereum_spec_tools/evm_tools/t8n/env.py | 150 +++++------- .../evm_tools/t8n/t8n_types.py | 221 ++++++++++++++---- .../evm_tools/t8n/transition_tool.py | 13 +- src/ethereum_spec_tools/evm_tools/utils.py | 11 +- 5 files changed, 261 insertions(+), 206 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index d0929ab70d..e3ca569f9e 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -4,7 +4,6 @@ import argparse import fnmatch -import json import os from functools import partial from typing import Any, TextIO @@ -14,7 +13,10 @@ 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 Result as TransitionToolOutput +from ethereum_clis.clis.execution_specs import ExecutionSpecsExceptionMapper from ..loaders.fixture_loader import Load from ..loaders.fork_loader import ForkLoad @@ -80,24 +82,15 @@ class T8N(Load): """The class that carries out the transition""" def __init__( - self, options: Any, out_file: TextIO, in_file: TextIO + self, options: Any, out_file: TextIO, in_file: TransitionToolRequest ) -> None: self.out_file = out_file self.in_file = in_file self.options = options self.forks = Hardfork.discover() - if "stdin" in ( - options.input_env, - options.input_alloc, - options.input_txs, - ): - stdin = json.load(in_file) - else: - stdin = None - fork_module, self.fork_block = get_module_name( - self.forks, self.options, stdin + self.forks, self.options, in_file.input.env ) self.fork = ForkLoad(fork_module) @@ -115,6 +108,7 @@ def __init__( ) ) self.logger = get_stream_logger("T8N") + self.exception_mapper = ExecutionSpecsExceptionMapper() super().__init__( self.options.state_fork, @@ -122,9 +116,9 @@ def __init__( ) self.chain_id = parse_hex_or_int(self.options.state_chainid, U64) - self.alloc = Alloc(self, stdin) - self.env = Env(self, stdin) - self.txs = Txs(self, stdin) + 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 ) @@ -311,45 +305,13 @@ def run(self) -> int: json_state = self.alloc.to_json() json_result = self.result.to_json() - json_output = {} - if self.options.output_body == "stdout": - txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() - json_output["body"] = txs_rlp - elif self.options.output_body is not None: - txs_rlp_path = os.path.join( - self.options.output_basedir, - self.options.output_body, - ) - txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() - with open(txs_rlp_path, "w") as f: - json.dump(txs_rlp, f) - self.logger.info(f"Wrote transaction rlp to {txs_rlp_path}") - - if self.options.output_alloc == "stdout": - json_output["alloc"] = json_state - else: - alloc_output_path = os.path.join( - self.options.output_basedir, - self.options.output_alloc, - ) - with open(alloc_output_path, "w") as f: - json.dump(json_state, f, indent=4) - self.logger.info(f"Wrote alloc to {alloc_output_path}") - - if self.options.output_result == "stdout": - json_output["result"] = json_result - else: - result_output_path = os.path.join( - self.options.output_basedir, - self.options.output_result, - ) - with open(result_output_path, "w") as f: - json.dump(json_result, f, indent=4) - self.logger.info(f"Wrote result to {result_output_path}") - - if json_output: - json.dump(json_output, self.out_file, indent=4) - - return 0 + 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 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 9a9a0bf7d4..fee0b024f7 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -10,10 +10,7 @@ from ethereum_types.numeric import U64, U256, Uint from ethereum.crypto.hash import Hash32, keccak256 -from ethereum.utils.byte import left_pad_zero_bytes -from ethereum.utils.hexadecimal import hex_to_bytes - -from ..utils import parse_hex_or_int +from ethereum_test_types.block_types import Environment if TYPE_CHECKING: from ethereum_spec_tools.evm_tools.t8n import T8N @@ -55,36 +52,29 @@ class Env: excess_blob_gas: Optional[U64] requests: Any - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): - if t8n.options.input_env == "stdin": - assert stdin is not None - data = stdin["env"] - else: - with open(t8n.options.input_env, "r") as f: - data = json.load(f) - - self.coinbase = t8n.fork.hex_to_address(data["currentCoinbase"]) - self.block_gas_limit = parse_hex_or_int(data["currentGasLimit"], Uint) - self.block_number = parse_hex_or_int(data["currentNumber"], Uint) - self.block_timestamp = parse_hex_or_int(data["currentTimestamp"], U256) + def __init__(self, t8n: "T8N", stdin: Environment): + 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(data, t8n) - self.read_base_fee_per_gas(data, t8n) - self.read_randao(data, t8n) - self.read_block_hashes(data, t8n) - self.read_ommers(data, t8n) - self.read_withdrawals(data, t8n) + self.read_block_difficulty(stdin, t8n) + self.read_base_fee_per_gas(stdin, t8n) + self.read_randao(stdin, t8n) + self.read_block_hashes(stdin, t8n) + self.read_ommers(stdin, t8n) + self.read_withdrawals(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_hex = data["parentBeaconBlockRoot"] + parent_beacon_block_root_bytes = stdin.parent_beacon_block_root self.parent_beacon_block_root = ( - Bytes32(hex_to_bytes(parent_beacon_block_root_hex)) - if parent_beacon_block_root_hex is not None + Bytes32(parent_beacon_block_root_bytes) + if parent_beacon_block_root_bytes is not None else None ) - self.read_excess_blob_gas(data, t8n) + self.read_excess_blob_gas(stdin, t8n) def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: """ @@ -98,20 +88,14 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: if not t8n.fork.is_after_fork("ethereum.cancun"): return - if "currentExcessBlobGas" in data: - self.excess_blob_gas = parse_hex_or_int( - data["currentExcessBlobGas"], U64 - ) + if hasattr(data, "excess_blob_gas") and data.excess_blob_gas is not None: + self.excess_blob_gas = U64(data.excess_blob_gas) - if "parentExcessBlobGas" in data: - self.parent_excess_blob_gas = parse_hex_or_int( - data["parentExcessBlobGas"], U64 - ) + if hasattr(data, "parent_excess_blob_gas") and data.parent_excess_blob_gas is not None: + self.parent_excess_blob_gas = U64(data.parent_excess_blob_gas) - if "parentBlobGasUsed" in data: - self.parent_blob_gas_used = parse_hex_or_int( - data["parentBlobGasUsed"], U64 - ) + if hasattr(data, "parent_blob_gas_used") and 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 @@ -170,25 +154,17 @@ def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: self.base_fee_per_gas = None if t8n.fork.is_after_fork("ethereum.london"): - if "currentBaseFee" in data: - self.base_fee_per_gas = parse_hex_or_int( - data["currentBaseFee"], Uint - ) + if hasattr(data, "base_fee_per_gas") and data.base_fee_per_gas is not None: + self.base_fee_per_gas = Uint(data.base_fee_per_gas) - if "parentGasUsed" in data: - self.parent_gas_used = parse_hex_or_int( - data["parentGasUsed"], Uint - ) + if hasattr(data, "parent_gas_used") and data.parent_gas_used is not None: + self.parent_gas_used = Uint(data.parent_gas_used) - if "parentGasLimit" in data: - self.parent_gas_limit = parse_hex_or_int( - data["parentGasLimit"], Uint - ) + if hasattr(data, "parent_gas_limit") and data.parent_gas_limit is not None: + self.parent_gas_limit = Uint(data.parent_gas_limit) - if "parentBaseFee" in data: - self.parent_base_fee_per_gas = parse_hex_or_int( - data["parentBaseFee"], Uint - ) + if hasattr(data, "parent_base_fee_per_gas") and 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 @@ -212,20 +188,7 @@ def read_randao(self, data: Any, t8n: "T8N") -> None: """ self.prev_randao = None if t8n.fork.is_after_fork("ethereum.paris"): - # tf tool might not always provide an - # even number of nibbles in the randao - # This could create issues in the - # hex_to_bytes function - current_random = data["currentRandom"] - if current_random.startswith("0x"): - current_random = current_random[2:] - - if len(current_random) % 2 == 1: - current_random = "0" + current_random - - self.prev_randao = Bytes32( - left_pad_zero_bytes(hex_to_bytes(current_random), 32) - ) + self.prev_randao = Bytes32(data.prev_randao.to_bytes(32, "big")) def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ @@ -233,9 +196,18 @@ def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ self.withdrawals = None if t8n.fork.is_after_fork("ethereum.shanghai"): - self.withdrawals = tuple( - t8n.json_to_withdrawals(wd) for wd in data["withdrawals"] - ) + raw_withdrawals = getattr(data, "withdrawals", None) + 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(to_canonical_withdrawal(wd) for wd in raw_withdrawals) + else: + self.withdrawals = () def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: """ @@ -249,17 +221,11 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: self.parent_ommers_hash = None if t8n.fork.is_after_fork("ethereum.paris"): return - elif "currentDifficulty" in data: - self.block_difficulty = parse_hex_or_int( - data["currentDifficulty"], Uint - ) + elif hasattr(data, "difficulty") and data.difficulty is not None: + self.block_difficulty = Uint(data.difficulty) else: - self.parent_timestamp = parse_hex_or_int( - data["parentTimestamp"], U256 - ) - self.parent_difficulty = parse_hex_or_int( - data["parentDifficulty"], Uint - ) + self.parent_timestamp = U256(data.parent_timestamp) + self.parent_difficulty = Uint(data.parent_difficulty) args: List[object] = [ self.block_number, self.block_timestamp, @@ -267,10 +233,10 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: self.parent_difficulty, ] if t8n.fork.is_after_fork("ethereum.byzantium"): - if "parentUncleHash" in data: + if hasattr(data, "parent_ommers_hash") and data.parent_ommers_hash is not None: EMPTY_OMMER_HASH = keccak256(rlp.encode([])) self.parent_ommers_hash = Hash32( - hex_to_bytes(data["parentUncleHash"]) + data.parent_ommers_hash ) parent_has_ommers = ( self.parent_ommers_hash != EMPTY_OMMER_HASH @@ -289,17 +255,13 @@ def read_block_hashes(self, data: Any, t8n: "T8N") -> None: t8n.fork.is_after_fork("ethereum.prague") and not t8n.options.state_test ): - self.parent_hash = Hash32(hex_to_bytes(data["parentHash"])) + 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] = {} - if "blockHashes" in data: - for key, value in data["blockHashes"].items(): - int_key = int(key, 16) - clean_block_hashes[int_key] = Hash32(hex_to_bytes(value)) + 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) @@ -307,7 +269,7 @@ def read_block_hashes(self, data: Any, t8n: "T8N") -> None: self.block_number - max_blockhash_count, self.block_number ): if number in clean_block_hashes.keys(): - block_hashes.append(clean_block_hashes[number]) + block_hashes.append(Hash32(clean_block_hashes[number])) else: block_hashes.append(None) @@ -319,12 +281,12 @@ def read_ommers(self, data: Any, t8n: "T8N") -> None: needed to obtain the Header. """ ommers = [] - if "ommers" in data: - for ommer in data["ommers"]: + if hasattr(data, "ommers") and data.ommers is not None: + for ommer in data.ommers: ommers.append( Ommer( - ommer["delta"], - t8n.fork.hex_to_address(ommer["address"]), + ommer.delta, + ommer.address, ) ) self.ommers = ommers 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..1cb86b77f6 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -1,16 +1,16 @@ """ Define the types used by the t8n tool. """ -import json from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple 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_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.utils.hexadecimal import hex_to_u256, hex_to_uint +from ethereum_test_types import Transaction from ..loaders.transaction_loader import TransactionLoad, UnsupportedTx from ..utils import FatalException, encode_to_hex, secp256k1_sign @@ -29,23 +29,20 @@ class Alloc: def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): """Read the alloc file and return the state.""" - if t8n.options.input_alloc == "stdin": - assert stdin is not None - data = stdin["alloc"] - else: - with open(t8n.options.input_alloc, "r") as f: - data = json.load(f) - - # The json_to_state functions expects the values to hex - # strings, so we convert them here. - for address, account in data.items(): - for key, value in account.items(): - if key == "storage" or not value: - continue - elif not value.startswith("0x"): - data[address][key] = "0x" + hex(int(value)) - - state = t8n.json_to_state(data) + # TODO: simplify without having to convert to JSON and then json to + # state + alloc_json = { + addr.hex(): acc.model_dump(mode="json") if acc is not None else None + for addr, acc in stdin.root.items() + } + for acc in alloc_json.values(): + if acc is not None and "storage" in acc and acc["storage"] is not None: + # Ensure all storage values are hex strings with 0x prefix + acc["storage"] = { + k: v if (isinstance(v, str) and v.startswith("0x")) else hex(v) + for k, v in acc["storage"].items() + } + state = t8n.json_to_state(alloc_json) if t8n.fork.fork_module == "dao_fork": t8n.fork.apply_dao(state) @@ -74,7 +71,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 +85,7 @@ class Txs: return a list of transactions. """ - def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): + def __init__(self, t8n: "T8N", stdin: List[Transaction] = None): self.t8n = t8n self.successfully_parsed: List[int] = [] self.transactions: List[Tuple[Uint, Any]] = [] @@ -93,29 +93,17 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.rlp_input = False self.all_txs = [] - if t8n.options.input_txs == "stdin": - assert stdin is not None - data = stdin["txs"] - else: - with open(t8n.options.input_txs, "r") as f: - data = json.load(f) - - if data is None: + if stdin is None: self.data: Simple = [] - elif isinstance(data, str): - self.rlp_input = True - self.data = rlp.decode(hex_to_bytes(data)) else: - self.data = data + self.data = stdin for idx, raw_tx in enumerate(self.data): try: - if self.rlp_input: - self.transactions.append(self.parse_rlp_tx(raw_tx)) - self.successfully_parsed.append(idx) - else: - self.transactions.append(self.parse_json_tx(raw_tx)) - self.successfully_parsed.append(idx) + fork_tx = self.pydantic_to_fork_transaction(raw_tx) + 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}: " @@ -187,6 +175,9 @@ def parse_json_tx(self, raw_tx: Any) -> Any: return transaction + def pydantic_to_fork_transaction(self, tx_model): + return convert_pydantic_tx_to_canonical(tx_model, self.t8n.fork) + def sign_transaction(self, json_tx: Any) -> None: """ Sign a transaction. This function will be invoked if a `secretKey` @@ -248,6 +239,154 @@ def sign_transaction(self, json_tx: Any) -> None: json_tx["y_parity"] = json_tx["v"] +def convert_pydantic_tx_to_canonical(tx, fork): + """ + Convert a Pydantic Transaction to the canonical transaction class for the given fork. + fork: The fork object (e.g., self.fork from ForkLoad) + """ + if isinstance(tx, list): + return [convert_pydantic_tx_to_canonical(t, fork) for t in tx] + + def convert_access_list(access_list): + if not access_list: + return [] + AccessCls = getattr(fork, "Access", None) + if AccessCls is None: + from ethereum.arrow_glacier.transactions import Access as AccessCls + return [ + AccessCls( + account=entry.address, + slots=tuple(entry.storage_keys), + ) + for entry in access_list + ] + + def convert_authorizations(auth_list): + if not auth_list: + return [] + AuthorizationCls = getattr(fork, "Authorization", None) + if AuthorizationCls is None: + from ethereum.osaka.fork_types import Authorization as AuthorizationCls + result = [] + for entry in auth_list: + d = entry.dict() if hasattr(entry, "dict") else dict(entry) + 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 + + # determine tx type + tx_type = getattr(tx, "ty", 0) + if hasattr(fork, "SetCodeTransaction") and tx_type == 4: + tx_cls = fork.SetCodeTransaction + elif hasattr(fork, "BlobTransaction") and tx_type == 3: + tx_cls = fork.BlobTransaction + elif hasattr(fork, "FeeMarketTransaction") and tx_type == 2: + tx_cls = fork.FeeMarketTransaction + elif hasattr(fork, "AccessListTransaction") and tx_type == 1: + tx_cls = fork.AccessListTransaction + else: + tx_cls = getattr(fork, "LegacyTransaction", None) + if tx_cls is None: + tx_cls = getattr(fork, "Transaction") + + def to_bytes20(val): + if val is None: + return Bytes0() + if isinstance(val, Bytes20): + return val + if isinstance(val, (bytes, bytearray)) and len(val) == 20: + return Bytes20(val) + if isinstance(val, (bytes, bytearray)) and len(val) == 0: + return Bytes20(b"\x00" * 20) + if isinstance(val, (bytes, bytearray)): + return Bytes20(val.rjust(20, b"\x00")) + return Bytes20(bytes(val).rjust(20, b"\x00")) + + # build the canonical transaction + if tx_cls.__name__ == "FeeMarketTransaction": + return tx_cls( + 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), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + elif tx_cls.__name__ == "AccessListTransaction": + return tx_cls( + 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), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + elif tx_cls.__name__ == "BlobTransaction": + return tx_cls( + 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), + max_fee_per_blob_gas=Uint(getattr(tx, "max_fee_per_blob_gas", 0)), + blob_versioned_hashes=getattr(tx, "blob_versioned_hashes", ()), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + elif tx_cls.__name__ == "SetCodeTransaction": + return tx_cls( + 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), + authorizations=convert_authorizations(getattr(tx, "authorization_list", ())), + y_parity=U256(tx.v), + r=U256(tx.r), + s=U256(tx.s), + ) + else: + 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..0e15341636 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 @@ -88,15 +87,15 @@ def evaluate( out_stream = StringIO() - in_stream = StringIO(json.dumps(request_data_json["input"])) + in_stream = request_data t8n = T8N(t8n_options, out_stream, in_stream) - t8n.run() + output = t8n.run() - output_dict = json.loads(out_stream.getvalue()) - output: TransitionToolOutput = TransitionToolOutput.model_validate( - output_dict, context={"exception_mapper": self.exception_mapper} - ) + # output_dict = json.loads(out_stream.getvalue()) + # output: TransitionToolOutput = TransitionToolOutput.model_validate( + # output_dict, context={"exception_mapper": self.exception_mapper} + # ) 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..3d81947772 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -82,7 +82,7 @@ class FatalException(Exception): def get_module_name( - forks: Sequence[Hardfork], options: Any, stdin: Any + forks: Sequence[Hardfork], options: Any, env: Any ) -> Tuple[str, int]: """ Get the module name and the fork block for the given state fork. @@ -97,14 +97,7 @@ def get_module_name( pass if exception_config: - if options.input_env == "stdin": - assert stdin is not None - data = stdin["env"] - else: - with open(options.input_env, "r") as f: - data = json.load(f) - - block_number = parse_hex_or_int(data["currentNumber"], Uint) + block_number = parse_hex_or_int(env.number, Uint) for fork, fork_block in exception_config["fork_blocks"]: if block_number >= Uint(fork_block): From 1537b0192fbb3cfb3aa4677659e425519a781431 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Fri, 1 Aug 2025 15:52:26 +0200 Subject: [PATCH 2/8] fix: imports --- src/ethereum_spec_tools/evm_tools/t8n/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index e3ca569f9e..896455c9c5 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -15,7 +15,7 @@ from ethereum.exceptions import EthereumException, InvalidBlock from ethereum_clis.types import TransitionToolRequest from ethereum_spec_tools.forks import Hardfork -from ethereum_clis.types import Result as TransitionToolOutput +from ethereum_clis.types import TransitionToolOutput from ethereum_clis.clis.execution_specs import ExecutionSpecsExceptionMapper from ..loaders.fixture_loader import Load @@ -93,6 +93,7 @@ def __init__( self.forks, self.options, in_file.input.env ) self.fork = ForkLoad(fork_module) + self.exception_mapper = ExecutionSpecsExceptionMapper() if self.options.trace: trace_memory = getattr(self.options, "trace.memory", False) @@ -108,7 +109,6 @@ def __init__( ) ) self.logger = get_stream_logger("T8N") - self.exception_mapper = ExecutionSpecsExceptionMapper() super().__init__( self.options.state_fork, From 1f51eaf199b98e02fc4153a057855f4e4514bb93 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Thu, 14 Aug 2025 17:00:19 +0200 Subject: [PATCH 3/8] convert alloc directly --- .../evm_tools/t8n/t8n_types.py | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) 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 1cb86b77f6..ba015e2c2c 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -6,6 +6,7 @@ from ethereum_rlp import Simple, rlp 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 @@ -29,20 +30,37 @@ class Alloc: def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): """Read the alloc file and return the state.""" - # TODO: simplify without having to convert to JSON and then json to - # state - alloc_json = { - addr.hex(): acc.model_dump(mode="json") if acc is not None else None - for addr, acc in stdin.root.items() - } - for acc in alloc_json.values(): - if acc is not None and "storage" in acc and acc["storage"] is not None: - # Ensure all storage values are hex strings with 0x prefix - acc["storage"] = { - k: v if (isinstance(v, str) and v.startswith("0x")) else hex(v) - for k, v in acc["storage"].items() - } - state = t8n.json_to_state(alloc_json) + 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: + addr_hex = addr_bytes.hex() if isinstance(addr_bytes, bytes) else str(addr_bytes) + raise StateWithEmptyAccount(f"Empty account at {addr_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(): + if isinstance(storage_key, int): + storage_key_bytes = storage_key.to_bytes(32, byteorder='big') + else: + storage_key_bytes = storage_key + + set_storage( + state, + addr_bytes, + storage_key_bytes, + U256(storage_value) + ) + if t8n.fork.fork_module == "dao_fork": t8n.fork.apply_dao(state) From 7292acddadcf59172b12cf03dd11641e8bb44d42 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Tue, 19 Aug 2025 11:51:12 +0200 Subject: [PATCH 4/8] rem unnecessary checks --- src/ethereum_spec_tools/evm_tools/t8n/env.py | 20 +++++------ .../evm_tools/t8n/t8n_types.py | 33 +++++-------------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index fee0b024f7..ed4cad0088 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -88,13 +88,13 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: if not t8n.fork.is_after_fork("ethereum.cancun"): return - if hasattr(data, "excess_blob_gas") and data.excess_blob_gas is not None: + if data.excess_blob_gas is not None: self.excess_blob_gas = U64(data.excess_blob_gas) - if hasattr(data, "parent_excess_blob_gas") and data.parent_excess_blob_gas is not None: + if data.parent_excess_blob_gas is not None: self.parent_excess_blob_gas = U64(data.parent_excess_blob_gas) - if hasattr(data, "parent_blob_gas_used") and data.parent_blob_gas_used is not None: + 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: @@ -154,16 +154,16 @@ def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: self.base_fee_per_gas = None if t8n.fork.is_after_fork("ethereum.london"): - if hasattr(data, "base_fee_per_gas") and data.base_fee_per_gas is not None: + if data.base_fee_per_gas is not None: self.base_fee_per_gas = Uint(data.base_fee_per_gas) - if hasattr(data, "parent_gas_used") and data.parent_gas_used is not None: + if data.parent_gas_used is not None: self.parent_gas_used = Uint(data.parent_gas_used) - if hasattr(data, "parent_gas_limit") and data.parent_gas_limit is not None: + if data.parent_gas_limit is not None: self.parent_gas_limit = Uint(data.parent_gas_limit) - if hasattr(data, "parent_base_fee_per_gas") and data.parent_base_fee_per_gas is not None: + 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: @@ -221,7 +221,7 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: self.parent_ommers_hash = None if t8n.fork.is_after_fork("ethereum.paris"): return - elif hasattr(data, "difficulty") and data.difficulty is not None: + elif data.difficulty is not None: self.block_difficulty = Uint(data.difficulty) else: self.parent_timestamp = U256(data.parent_timestamp) @@ -233,7 +233,7 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: self.parent_difficulty, ] if t8n.fork.is_after_fork("ethereum.byzantium"): - if hasattr(data, "parent_ommers_hash") and data.parent_ommers_hash is not None: + if data.parent_ommers_hash is not None: EMPTY_OMMER_HASH = keccak256(rlp.encode([])) self.parent_ommers_hash = Hash32( data.parent_ommers_hash @@ -281,7 +281,7 @@ def read_ommers(self, data: Any, t8n: "T8N") -> None: needed to obtain the Header. """ ommers = [] - if hasattr(data, "ommers") and data.ommers is not None: + if data.ommers is not None: for ommer in data.ommers: ommers.append( Ommer( 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 ba015e2c2c..28e35c45d3 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -42,18 +42,16 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): ) if t8n.fork.proof_of_stake and canonical_account == EMPTY_ACCOUNT: - addr_hex = addr_bytes.hex() if isinstance(addr_bytes, bytes) else str(addr_bytes) - raise StateWithEmptyAccount(f"Empty account at {addr_hex}.") - + 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(): - if isinstance(storage_key, int): - storage_key_bytes = storage_key.to_bytes(32, byteorder='big') - else: - storage_key_bytes = storage_key - + storage_key_bytes = storage_key.to_bytes( + 32, byteorder='big' + ) set_storage( state, addr_bytes, @@ -260,17 +258,12 @@ def sign_transaction(self, json_tx: Any) -> None: def convert_pydantic_tx_to_canonical(tx, fork): """ Convert a Pydantic Transaction to the canonical transaction class for the given fork. - fork: The fork object (e.g., self.fork from ForkLoad) """ - if isinstance(tx, list): - return [convert_pydantic_tx_to_canonical(t, fork) for t in tx] def convert_access_list(access_list): if not access_list: return [] AccessCls = getattr(fork, "Access", None) - if AccessCls is None: - from ethereum.arrow_glacier.transactions import Access as AccessCls return [ AccessCls( account=entry.address, @@ -283,11 +276,9 @@ def convert_authorizations(auth_list): if not auth_list: return [] AuthorizationCls = getattr(fork, "Authorization", None) - if AuthorizationCls is None: - from ethereum.osaka.fork_types import Authorization as AuthorizationCls result = [] for entry in auth_list: - d = entry.dict() if hasattr(entry, "dict") else dict(entry) + d = entry.model_dump() result.append( AuthorizationCls( chain_id=U256(d.get("chain_id", 0)), @@ -318,15 +309,7 @@ def convert_authorizations(auth_list): def to_bytes20(val): if val is None: return Bytes0() - if isinstance(val, Bytes20): - return val - if isinstance(val, (bytes, bytearray)) and len(val) == 20: - return Bytes20(val) - if isinstance(val, (bytes, bytearray)) and len(val) == 0: - return Bytes20(b"\x00" * 20) - if isinstance(val, (bytes, bytearray)): - return Bytes20(val.rjust(20, b"\x00")) - return Bytes20(bytes(val).rjust(20, b"\x00")) + return Bytes20(val) # build the canonical transaction if tx_cls.__name__ == "FeeMarketTransaction": From bb826c6c58c61a83c690ab82b3c7ebfbda8ae2b6 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Tue, 19 Aug 2025 13:00:11 +0200 Subject: [PATCH 5/8] simplify txs --- src/ethereum_spec_tools/evm_tools/t8n/env.py | 2 +- .../evm_tools/t8n/t8n_types.py | 71 +++++++++---------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index ed4cad0088..cbd5184378 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -196,7 +196,7 @@ def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ self.withdrawals = None if t8n.fork.is_after_fork("ethereum.shanghai"): - raw_withdrawals = getattr(data, "withdrawals", None) + raw_withdrawals = data.withdrawals if raw_withdrawals: def to_canonical_withdrawal(raw): return t8n.fork.Withdrawal( 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 28e35c45d3..941693e512 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple -from ethereum_rlp import Simple, rlp +from ethereum_rlp import rlp from ethereum_types.bytes import Bytes, Bytes20, Bytes0 from ethereum.exceptions import StateWithEmptyAccount from ethereum_types.numeric import U8, U64, U256, Uint @@ -110,13 +110,16 @@ def __init__(self, t8n: "T8N", stdin: List[Transaction] = None): self.all_txs = [] if stdin is None: - self.data: Simple = [] + self.data: List[Transaction] = [] else: self.data = stdin for idx, raw_tx in enumerate(self.data): try: - fork_tx = self.pydantic_to_fork_transaction(raw_tx) + 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) @@ -191,9 +194,6 @@ def parse_json_tx(self, raw_tx: Any) -> Any: return transaction - def pydantic_to_fork_transaction(self, tx_model): - return convert_pydantic_tx_to_canonical(tx_model, self.t8n.fork) - def sign_transaction(self, json_tx: Any) -> None: """ Sign a transaction. This function will be invoked if a `secretKey` @@ -291,29 +291,16 @@ def convert_authorizations(auth_list): ) return result - # determine tx type - tx_type = getattr(tx, "ty", 0) - if hasattr(fork, "SetCodeTransaction") and tx_type == 4: - tx_cls = fork.SetCodeTransaction - elif hasattr(fork, "BlobTransaction") and tx_type == 3: - tx_cls = fork.BlobTransaction - elif hasattr(fork, "FeeMarketTransaction") and tx_type == 2: - tx_cls = fork.FeeMarketTransaction - elif hasattr(fork, "AccessListTransaction") and tx_type == 1: - tx_cls = fork.AccessListTransaction - else: - tx_cls = getattr(fork, "LegacyTransaction", None) - if tx_cls is None: - tx_cls = getattr(fork, "Transaction") - def to_bytes20(val): if val is None: return Bytes0() return Bytes20(val) - # build the canonical transaction - if tx_cls.__name__ == "FeeMarketTransaction": - return tx_cls( + 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), @@ -323,26 +310,34 @@ def to_bytes20(val): value=U256(tx.value), data=tx.data, access_list=convert_access_list(tx.access_list), + authorizations=convert_authorizations(tx.authorization_list or []), y_parity=U256(tx.v), r=U256(tx.r), s=U256(tx.s), ) - elif tx_cls.__name__ == "AccessListTransaction": - return tx_cls( + + # BlobTransaction (Type 3) + elif hasattr(fork, "BlobTransaction") and tx_type == 3: + return fork.BlobTransaction( chain_id=U64(tx.chain_id), nonce=U256(tx.nonce), - gas_price=Uint(tx.gas_price or 0), + 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), + 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), ) - elif tx_cls.__name__ == "BlobTransaction": - return tx_cls( + + # 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), @@ -352,29 +347,33 @@ def to_bytes20(val): value=U256(tx.value), data=tx.data, access_list=convert_access_list(tx.access_list), - max_fee_per_blob_gas=Uint(getattr(tx, "max_fee_per_blob_gas", 0)), - blob_versioned_hashes=getattr(tx, "blob_versioned_hashes", ()), y_parity=U256(tx.v), r=U256(tx.r), s=U256(tx.s), ) - elif tx_cls.__name__ == "SetCodeTransaction": - return tx_cls( + + # AccessListTransaction (Type 1) + elif hasattr(fork, "AccessListTransaction") and tx_type == 1: + return fork.AccessListTransaction( 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_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), - authorizations=convert_authorizations(getattr(tx, "authorization_list", ())), 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), From dc31f962bfa8cc35d11b8615a52dbac70cead535 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Wed, 20 Aug 2025 00:47:10 +0200 Subject: [PATCH 6/8] add dual input option --- .../evm_tools/t8n/__init__.py | 130 ++++++- src/ethereum_spec_tools/evm_tools/t8n/env.py | 316 +++++++++++++++++- .../evm_tools/t8n/t8n_types.py | 93 +++++- .../evm_tools/t8n/transition_tool.py | 9 +- src/ethereum_spec_tools/evm_tools/utils.py | 44 +++ 5 files changed, 544 insertions(+), 48 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 896455c9c5..c34e8dec4c 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -4,9 +4,10 @@ import argparse import fnmatch +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 @@ -23,6 +24,7 @@ from ..utils import ( FatalException, get_module_name, + get_module_name_json_input, get_stream_logger, parse_hex_or_int, ) @@ -82,19 +84,73 @@ class T8N(Load): """The class that carries out the transition""" def __init__( - self, options: Any, out_file: TextIO, in_file: TransitionToolRequest + 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, self.options, in_file.input.env + 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, + options.input_txs, + ): + stdin = json.load(in_file) + else: + stdin = None + + fork_module, self.fork_block = get_module_name_json_input( + self.forks, self.options, stdin + ) + self.fork = ForkLoad(fork_module) + if self.options.trace: trace_memory = getattr(self.options, "trace.memory", False) trace_stack = not getattr(self.options, "trace.nostack", False) @@ -116,9 +172,9 @@ def __init__( ) self.chain_id = parse_hex_or_int(self.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.alloc = Alloc(self, stdin) + self.env = Env(self, stdin) + self.txs = Txs(self, stdin) self.result = Result( self.env.block_difficulty, self.env.base_fee_per_gas ) @@ -305,13 +361,59 @@ 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 = {} - 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 + if self.options.output_body == "stdout": + txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() + json_output["body"] = txs_rlp + elif self.options.output_body is not None: + txs_rlp_path = os.path.join( + self.options.output_basedir, + self.options.output_body, + ) + txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex() + with open(txs_rlp_path, "w") as f: + json.dump(txs_rlp, f) + self.logger.info(f"Wrote transaction rlp to {txs_rlp_path}") + + if self.options.output_alloc == "stdout": + json_output["alloc"] = json_state + else: + alloc_output_path = os.path.join( + self.options.output_basedir, + self.options.output_alloc, + ) + with open(alloc_output_path, "w") as f: + json.dump(json_state, f, indent=4) + self.logger.info(f"Wrote alloc to {alloc_output_path}") + + if self.options.output_result == "stdout": + json_output["result"] = json_result + else: + result_output_path = os.path.join( + self.options.output_basedir, + self.options.output_result, + ) + with open(result_output_path, "w") as f: + json.dump(json_result, f, indent=4) + self.logger.info(f"Wrote result to {result_output_path}") + + if json_output: + json.dump(json_output, self.out_file, indent=4) + + return 0 diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index cbd5184378..28b6b0705a 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -3,14 +3,18 @@ """ 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 from ethereum_types.numeric import U64, U256, Uint 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 if TYPE_CHECKING: from ethereum_spec_tools.evm_tools.t8n import T8N @@ -52,18 +56,24 @@ class Env: excess_blob_gas: Optional[U64] requests: Any - def __init__(self, t8n: "T8N", stdin: Environment): + 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(stdin, t8n) - self.read_base_fee_per_gas(stdin, t8n) - self.read_randao(stdin, t8n) - self.read_block_hashes(stdin, t8n) - self.read_ommers(stdin, t8n) - self.read_withdrawals(stdin, t8n) + 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"): @@ -74,9 +84,40 @@ def __init__(self, t8n: "T8N", stdin: Environment): if parent_beacon_block_root_bytes is not None else None ) - self.read_excess_blob_gas(stdin, t8n) + self.read_excess_blob_gas_pydantic(stdin, t8n) - def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: + 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"] + else: + with open(t8n.options.input_env, "r") as f: + data = json.load(f) + + self.coinbase = t8n.fork.hex_to_address(data["currentCoinbase"]) + self.block_gas_limit = parse_hex_or_int(data["currentGasLimit"], Uint) + self.block_number = parse_hex_or_int(data["currentNumber"], Uint) + self.block_timestamp = parse_hex_or_int(data["currentTimestamp"], U256) + + self.read_block_difficulty(data, t8n) + self.read_base_fee_per_gas(data, t8n) + self.read_randao(data, t8n) + self.read_block_hashes(data, t8n) + self.read_ommers(data, t8n) + self.read_withdrawals(data, t8n) + + self.parent_beacon_block_root = None + if t8n.fork.is_after_fork("ethereum.cancun"): + if not t8n.options.state_test: + parent_beacon_block_root_hex = data["parentBeaconBlockRoot"] + self.parent_beacon_block_root = ( + Bytes32(hex_to_bytes(parent_beacon_block_root_hex)) + if parent_beacon_block_root_hex is not None + else None + ) + self.read_excess_blob_gas(data, t8n) + + def read_excess_blob_gas_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. @@ -143,7 +184,80 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: // BLOB_SCHEDULE_MAX ) - def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: + def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: + """ + Read the excess_blob_gas from the data. If the excess blob gas is + not present, it is calculated from the parent block parameters. + """ + self.parent_blob_gas_used = U64(0) + self.parent_excess_blob_gas = U64(0) + self.excess_blob_gas = None + + if not t8n.fork.is_after_fork("ethereum.cancun"): + return + + if "currentExcessBlobGas" in data: + self.excess_blob_gas = parse_hex_or_int( + data["currentExcessBlobGas"], U64 + ) + + if "parentExcessBlobGas" in data: + self.parent_excess_blob_gas = parse_hex_or_int( + data["parentExcessBlobGas"], U64 + ) + + if "parentBlobGasUsed" in data: + self.parent_blob_gas_used = parse_hex_or_int( + data["parentBlobGasUsed"], U64 + ) + + 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_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. @@ -182,7 +296,54 @@ def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: *parameters ) - def read_randao(self, data: Any, t8n: "T8N") -> None: + def read_base_fee_per_gas(self, data: Any, t8n: "T8N") -> None: + """ + Read the base_fee_per_gas from the data. If the base fee is + not present, it is calculated from the parent block parameters. + """ + 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 "currentBaseFee" in data: + self.base_fee_per_gas = parse_hex_or_int( + data["currentBaseFee"], Uint + ) + + if "parentGasUsed" in data: + self.parent_gas_used = parse_hex_or_int( + data["parentGasUsed"], Uint + ) + + if "parentGasLimit" in data: + self.parent_gas_limit = parse_hex_or_int( + data["parentGasLimit"], Uint + ) + + if "parentBaseFee" in data: + self.parent_base_fee_per_gas = parse_hex_or_int( + data["parentBaseFee"], Uint + ) + + 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_randao_pydantic(self, data: Any, t8n: "T8N") -> None: """ Read the randao from the data. """ @@ -190,7 +351,28 @@ def read_randao(self, data: Any, t8n: "T8N") -> None: if t8n.fork.is_after_fork("ethereum.paris"): self.prev_randao = Bytes32(data.prev_randao.to_bytes(32, "big")) - def read_withdrawals(self, data: Any, t8n: "T8N") -> None: + def read_randao(self, data: Any, t8n: "T8N") -> None: + """ + Read the randao from the data. + """ + self.prev_randao = None + if t8n.fork.is_after_fork("ethereum.paris"): + # tf tool might not always provide an + # even number of nibbles in the randao + # This could create issues in the + # hex_to_bytes function + current_random = data["currentRandom"] + if current_random.startswith("0x"): + current_random = current_random[2:] + + if len(current_random) % 2 == 1: + current_random = "0" + current_random + + self.prev_randao = Bytes32( + left_pad_zero_bytes(hex_to_bytes(current_random), 32) + ) + + def read_withdrawals_pydantic(self, data: Any, t8n: "T8N") -> None: """ Read the withdrawals from the data. """ @@ -209,7 +391,17 @@ def to_canonical_withdrawal(raw): else: self.withdrawals = () - def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: + def read_withdrawals(self, data: Any, t8n: "T8N") -> None: + """ + Read the withdrawals from the data. + """ + self.withdrawals = None + if t8n.fork.is_after_fork("ethereum.shanghai"): + self.withdrawals = tuple( + t8n.json_to_withdrawals(wd) for wd in data["withdrawals"] + ) + + def read_block_difficulty_pydantic(self, data: Any, t8n: "T8N") -> None: """ Read the block difficulty from the data. If `currentDifficulty` is present, it is used. Otherwise, @@ -246,7 +438,50 @@ def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: args.append(False) self.block_difficulty = t8n.fork.calculate_block_difficulty(*args) - def read_block_hashes(self, data: Any, t8n: "T8N") -> None: + def read_block_difficulty(self, data: Any, t8n: "T8N") -> None: + """ + Read the block difficulty from the data. + If `currentDifficulty` is present, it is used. Otherwise, + 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 "currentDifficulty" in data: + self.block_difficulty = parse_hex_or_int( + data["currentDifficulty"], Uint + ) + else: + self.parent_timestamp = parse_hex_or_int( + data["parentTimestamp"], U256 + ) + self.parent_difficulty = parse_hex_or_int( + data["parentDifficulty"], Uint + ) + args: List[object] = [ + self.block_number, + self.block_timestamp, + self.parent_timestamp, + self.parent_difficulty, + ] + if t8n.fork.is_after_fork("ethereum.byzantium"): + if "parentUncleHash" in data: + EMPTY_OMMER_HASH = keccak256(rlp.encode([])) + self.parent_ommers_hash = Hash32( + hex_to_bytes(data["parentUncleHash"]) + ) + 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_hashes_pydantic(self, data: Any, t8n: "T8N") -> None: """ Read the block hashes. Returns a maximum of 256 block hashes. """ @@ -275,7 +510,40 @@ def read_block_hashes(self, data: Any, t8n: "T8N") -> None: self.block_hashes = block_hashes - def read_ommers(self, data: Any, t8n: "T8N") -> None: + def read_block_hashes(self, data: Any, t8n: "T8N") -> None: + """ + Read the block hashes. Returns a maximum of 256 block hashes. + """ + self.parent_hash = None + if ( + t8n.fork.is_after_fork("ethereum.prague") + and not t8n.options.state_test + ): + self.parent_hash = Hash32(hex_to_bytes(data["parentHash"])) + + # Read the block hashes + block_hashes: List[Any] = [] + + # The hex key strings provided might not have standard formatting + clean_block_hashes: Dict[int, Hash32] = {} + if "blockHashes" in data: + for key, value in data["blockHashes"].items(): + int_key = int(key, 16) + clean_block_hashes[int_key] = Hash32(hex_to_bytes(value)) + + # 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(clean_block_hashes[number]) + else: + block_hashes.append(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. @@ -290,3 +558,19 @@ def read_ommers(self, data: Any, t8n: "T8N") -> None: ) ) self.ommers = ommers + + def read_ommers(self, data: Any, t8n: "T8N") -> None: + """ + Read the ommers. The ommers data might not have all the details + needed to obtain the Header. + """ + ommers = [] + if "ommers" in data: + for ommer in data["ommers"]: + ommers.append( + Ommer( + ommer["delta"], + t8n.fork.hex_to_address(ommer["address"]), + ) + ) + self.ommers = ommers 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 941693e512..5bb5af05c6 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -1,17 +1,18 @@ """ Define the types used by the t8n tool. """ +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 rlp +from ethereum_rlp import Simple, rlp 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_u256, hex_to_uint -from ethereum_test_types import Transaction +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 @@ -28,8 +29,19 @@ 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 @@ -58,10 +70,30 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): 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"] + else: + with open(t8n.options.input_alloc, "r") as f: + data = json.load(f) + + # The json_to_state functions expects the values to hex + # strings, so we convert them here. + for address, account in data.items(): + for key, value in account.items(): + if key == "storage" or not value: + continue + elif not value.startswith("0x"): + data[address][key] = "0x" + hex(int(value)) + + state = t8n.json_to_state(data) + if t8n.fork.fork_module == "dao_fork": + t8n.fork.apply_dao(state) self.state = state def to_json(self) -> Any: @@ -101,7 +133,7 @@ class Txs: return a list of transactions. """ - def __init__(self, t8n: "T8N", stdin: List[Transaction] = 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]] = [] @@ -109,10 +141,13 @@ def __init__(self, t8n: "T8N", stdin: List[Transaction] = None): self.rlp_input = False self.all_txs = [] - if stdin is None: - self.data: List[Transaction] = [] + if isinstance(stdin, list) and all(isinstance(tx, Transaction) for tx in stdin): + self._init_from_pydantic(stdin) else: - self.data = stdin + 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: @@ -137,6 +172,44 @@ def __init__(self, t8n: "T8N", stdin: List[Transaction] = None): 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"] + else: + with open(t8n.options.input_txs, "r") as f: + data = json.load(f) + + if data is None: + self.data: Simple = [] + elif isinstance(data, str): + self.rlp_input = True + self.data = rlp.decode(hex_to_bytes(data)) + else: + self.data = data + + for idx, raw_tx in enumerate(self.data): + try: + if self.rlp_input: + self.transactions.append(self.parse_rlp_tx(raw_tx)) + self.successfully_parsed.append(idx) + else: + self.transactions.append(self.parse_json_tx(raw_tx)) + self.successfully_parsed.append(idx) + except UnsupportedTx as e: + self.t8n.logger.warning( + f"Unsupported transaction type {idx}: " + 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 parse_rlp_tx(self, raw_tx: Any) -> Any: """ Read transactions from RLP. 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 0e15341636..95fbfc77ee 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/transition_tool.py @@ -85,18 +85,11 @@ def evaluate( parser = create_parser() t8n_options = parser.parse_args(t8n_args) - out_stream = StringIO() - in_stream = request_data - t8n = T8N(t8n_options, out_stream, in_stream) + t8n = T8N(t8n_options, in_stream) output = t8n.run() - # output_dict = json.loads(out_stream.getvalue()) - # output: TransitionToolOutput = TransitionToolOutput.model_validate( - # output_dict, context={"exception_mapper": self.exception_mapper} - # ) - if debug_output_path: dump_files_to_directory( debug_output_path, diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index 3d81947772..9508c11718 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -118,6 +118,50 @@ def get_module_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]: + """ + 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: + if options.input_env == "stdin": + assert stdin is not None + data = stdin["env"] + else: + with open(options.input_env, "r") as f: + data = json.load(f) + + block_number = parse_hex_or_int(data["currentNumber"], 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_supported_forks() -> List[str]: """ Get the supported forks. From e1dc6a3ea63fa5f090620fdd01e84870bdafea49 Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Wed, 20 Aug 2025 00:51:58 +0200 Subject: [PATCH 7/8] update t8n interface --- src/ethereum_spec_tools/evm_tools/__init__.py | 2 +- src/ethereum_spec_tools/evm_tools/statetest/__init__.py | 2 +- tests/helpers/load_evm_tools_tests.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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)) From 5a17bd95d86850cdd868dcde9270574254e1420d Mon Sep 17 00:00:00 2001 From: souradeep-das Date: Thu, 21 Aug 2025 14:25:17 +0200 Subject: [PATCH 8/8] review fixes --- src/ethereum_spec_tools/evm_tools/t8n/env.py | 14 ++- .../evm_tools/t8n/t8n_types.py | 99 ++++++++++--------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 28b6b0705a..d363fcde8f 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -372,6 +372,15 @@ 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. @@ -387,7 +396,10 @@ def to_canonical_withdrawal(raw): address=raw.address, amount=U256(raw.amount), ) - self.withdrawals = tuple(to_canonical_withdrawal(wd) for wd in raw_withdrawals) + self.withdrawals = tuple( + self._to_canonical_withdrawal(wd, t8n.fork) + for wd in raw_withdrawals + ) else: self.withdrawals = () 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 5bb5af05c6..178ed6c293 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -328,47 +328,49 @@ def sign_transaction(self, json_tx: Any) -> None: json_tx["y_parity"] = json_tx["v"] -def convert_pydantic_tx_to_canonical(tx, fork): - """ - Convert a Pydantic Transaction to the canonical transaction class for the given fork. - """ - - def convert_access_list(access_list): - if not access_list: - return [] - AccessCls = getattr(fork, "Access", None) - return [ - AccessCls( - account=entry.address, - slots=tuple(entry.storage_keys), +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"]), ) - for entry in access_list - ] + ) + return result - def convert_authorizations(auth_list): - 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 _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) @@ -379,11 +381,14 @@ def to_bytes20(val): 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), + to=_to_bytes20(tx.to), value=U256(tx.value), data=tx.data, - access_list=convert_access_list(tx.access_list), - authorizations=convert_authorizations(tx.authorization_list or []), + 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), @@ -397,10 +402,10 @@ def to_bytes20(val): 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), + to=_to_bytes20(tx.to), value=U256(tx.value), data=tx.data, - access_list=convert_access_list(tx.access_list), + 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), @@ -416,10 +421,10 @@ def to_bytes20(val): 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), + to=_to_bytes20(tx.to), value=U256(tx.value), data=tx.data, - access_list=convert_access_list(tx.access_list), + access_list=_convert_access_list(tx.access_list, fork), y_parity=U256(tx.v), r=U256(tx.r), s=U256(tx.s), @@ -432,10 +437,10 @@ def to_bytes20(val): nonce=U256(tx.nonce), gas_price=Uint(tx.gas_price or 0), gas=Uint(tx.gas_limit), - to=to_bytes20(tx.to), + to=_to_bytes20(tx.to), value=U256(tx.value), data=tx.data, - access_list=convert_access_list(tx.access_list), + access_list=_convert_access_list(tx.access_list, fork), y_parity=U256(tx.v), r=U256(tx.r), s=U256(tx.s), @@ -451,7 +456,7 @@ def to_bytes20(val): nonce=U256(tx.nonce), gas_price=Uint(tx.gas_price or 0), gas=Uint(tx.gas_limit), - to=to_bytes20(tx.to), + to=_to_bytes20(tx.to), value=U256(tx.value), data=tx.data, v=U256(tx.v),