diff --git a/packages/testing/src/execution_testing/client_clis/cli_types.py b/packages/testing/src/execution_testing/client_clis/cli_types.py index 07dff0d72ae..9b42191ba90 100644 --- a/packages/testing/src/execution_testing/client_clis/cli_types.py +++ b/packages/testing/src/execution_testing/client_clis/cli_types.py @@ -288,6 +288,7 @@ class Result(CamelModel): requests: List[Bytes] | None = None block_access_list: Bytes | None = None block_access_list_hash: Hash | None = None + is_inclusion_list_satisfied: bool | None = None block_exception: Annotated[ BlockExceptionWithMessage | UndefinedException | None, ExceptionMapperValidator, diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 03afd604692..da60cd4e959 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -225,7 +225,11 @@ def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: return recent_block_hashes -def state_transition(chain: BlockChain, block: Block) -> None: +def state_transition( + chain: BlockChain, + block: Block, + inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...] = (), +) -> None: """ Attempts to apply a block to an existing block chain. @@ -246,6 +250,8 @@ def state_transition(chain: BlockChain, block: Block) -> None: History and current state. block : Block to apply to `chain`. + inclusion_list_transactions : + Inclusion list transactions against which the block will be checked. """ chain_context = ChainContext( @@ -254,7 +260,9 @@ def state_transition(chain: BlockChain, block: Block) -> None: parent_header=chain.blocks[-1].header, ) - block_diff = execute_block(block, chain.state, chain_context) + block_diff = execute_block( + block, chain.state, chain_context, inclusion_list_transactions + ) apply_changes_to_state(chain.state, block_diff) chain.blocks.append(block) @@ -268,6 +276,7 @@ def execute_block( block: Block, pre_state: PreState, chain_context: ChainContext, + inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...] = (), ) -> BlockDiff: """ Execute a block and validate the resulting roots against the header. @@ -282,6 +291,8 @@ def execute_block( Pre-execution state provider. chain_context : Chain context that the block may need during execution. + inclusion_list_transactions : + Inclusion list transactions against which the block will be checked. Returns ------- @@ -319,6 +330,7 @@ def execute_block( block_env=block_env, transactions=block.transactions, withdrawals=block.withdrawals, + inclusion_list_transactions=inclusion_list_transactions, ) block_diff = extract_block_diff(block_state) block_state_root, _ = pre_state.compute_state_root_and_trie_changes( @@ -809,6 +821,7 @@ def apply_body( block_env: vm.BlockEnvironment, transactions: Tuple[LegacyTransaction | Bytes, ...], withdrawals: Tuple[Withdrawal, ...], + inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...], ) -> vm.BlockOutput: """ Executes a block. @@ -828,6 +841,8 @@ def apply_body( Transactions included in the block. withdrawals : Withdrawals to be processed in the current block. + inclusion_list_transactions : + Inclusion list transactions against which the block will be checked. Returns ------- @@ -852,6 +867,16 @@ def apply_body( for i, tx in enumerate(map(decode_transaction, transactions)): process_transaction(block_env, block_output, tx, Uint(i)) + # Check if the block satisfies the inclusion list constraints (EIP-7805) + block_output.is_inclusion_list_satisfied = ( + check_inclusion_list_transactions( + block_env=block_env, + block_output=block_output, + transactions=transactions, + inclusion_list_transactions=inclusion_list_transactions, + ) + ) + # EIP-7928: Post-execution operations use index N+1 block_env.block_access_list_builder.block_access_index = BlockAccessIndex( Uint(len(transactions)) + Uint(1) @@ -1155,3 +1180,61 @@ def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: return False return True + + +def check_inclusion_list_transactions( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + transactions: Tuple[LegacyTransaction | Bytes, ...], + inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...], +) -> bool: + """ + Check whether the block satisfies the inclusion list constraints. + + For each inclusion list transaction not present in the block, + check whether it could have been validly appended to the end of the block. + Blob transactions are excluded from this check. If any such transaction + could have been appended, the block fails the inclusion list check. + + The inclusion list compliance does not affect any other block outputs. + + Parameters + ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. + transactions : + Transactions included in the block. + inclusion_list_transactions : + Inclusion list transactions against which the block will be checked. + + Returns + ------- + is_inclusion_list_satisfied : `bool` + True if the block passes the inclusion list check, False otherwise. + + """ + tx_hashes = {get_transaction_hash(raw_tx) for raw_tx in transactions} + tx_state = TransactionState(parent=block_env.state) + + for raw_tx in inclusion_list_transactions: + if get_transaction_hash(raw_tx) in tx_hashes: + continue + + tx = decode_transaction(raw_tx) + + # Ignore blob transactions. + if isinstance(tx, BlobTransaction): + continue + + try: + validate_transaction(tx) + check_transaction(block_env, block_output, tx, tx_state) + except EthereumException: + continue + else: + # This inclusion list transaction could have been included. + return False + + return True diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 5d6b2fbfb72..4c9fe5a6d36 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -78,6 +78,8 @@ class BlockOutput: Hash of all the requests in the block. block_access_list: `BlockAccessList` The block access list for the block. + is_inclusion_list_satisfied : `bool` + Whether the block satisfies the inclusion list constraints. """ block_gas_used: Uint = Uint(0) @@ -95,6 +97,7 @@ class BlockOutput: blob_gas_used: U64 = U64(0) requests: List[Bytes] = field(default_factory=list) block_access_list: BlockAccessList = field(default_factory=list) + is_inclusion_list_satisfied: bool = True @dataclass diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index 2abf93fb99e..6b695930ad6 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -190,6 +190,18 @@ def process_transaction(self) -> Any: """process_transaction function of the fork.""" return self._module("fork").process_transaction + @property + def check_inclusion_list_transactions(self) -> Any: + """check_inclusion_list_transactions function of the fork.""" + return self._module("fork").check_inclusion_list_transactions + + @property + def has_is_inclusion_list_satisfied(self) -> bool: + """ + Check if the block output has an `is_inclusion_list_satisfied` field. + """ + return hasattr(self.BlockOutput, "is_inclusion_list_satisfied") + @property def Block(self) -> Any: """Block class of the fork.""" @@ -297,6 +309,16 @@ def has_decode_transaction(self) -> bool: """Check if this fork has a `decode_transaction`.""" return hasattr(self._module("transactions"), "decode_transaction") + @property + def encode_transaction(self) -> Any: + """encode_transaction function of the fork.""" + return self._module("transactions").encode_transaction + + @property + def has_encode_transaction(self) -> bool: + """Check if this fork has an `encode_transaction`.""" + return hasattr(self._module("transactions"), "encode_transaction") + @property def has_block_state(self) -> bool: """Check if the fork uses BlockState instead of State.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 396737c25e9..10a35896536 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -447,6 +447,19 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: U256(self.options.state_reward), block_env ) + if self.fork.has_is_inclusion_list_satisfied: + block_output.is_inclusion_list_satisfied = ( + self.fork.check_inclusion_list_transactions( + block_env, + block_output, + tuple( + self.fork.encode_transaction(tx) + for tx in self.txs.transactions + ), + self.env.inclusion_list_transactions, + ) + ) + if self.fork.has_withdrawal: self.fork.process_withdrawals( block_env, block_output, self.env.withdrawals diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index f76c0caaf57..3e367c1b34f 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -4,7 +4,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, Tuple from ethereum_rlp import rlp from ethereum_types.bytes import Bytes8, Bytes20, Bytes32, Bytes256 @@ -54,6 +54,7 @@ class Env: parent_blob_gas_used: Optional[U64] excess_blob_gas: Optional[U64] requests: Any + inclusion_list_transactions: Optional[Tuple[Any, ...]] def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): if t8n.options.input_env == "stdin": @@ -74,6 +75,7 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.read_block_hashes(data) self.read_ommers(data, t8n) self.read_withdrawals(data, t8n) + self.read_inclusion_list_transactions(data, t8n) self.parent_beacon_block_root = None if t8n.fork.has_beacon_roots_address: @@ -316,3 +318,19 @@ def read_ommers(self, data: Any, t8n: "T8N") -> None: ) ) self.ommers = ommers + + def read_inclusion_list_transactions(self, data: Any, t8n: "T8N") -> None: + """ + Read the inclusion list transactions. + """ + self.inclusion_list_transactions = None + + if not t8n.fork.has_is_inclusion_list_satisfied: + return + + inclusion_list_transactions = [] + if "inclusionListTransactions" in data: + for tx in data["inclusionListTransactions"]: + inclusion_list_transactions.append(hex_to_bytes(tx)) + + self.inclusion_list_transactions = tuple(inclusion_list_transactions) 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 ec78bd09719..c6ca3e2fff9 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -275,6 +275,7 @@ class Result: block_exception: Optional[str] = None block_access_list: Optional[Any] = None block_access_list_hash: Optional[Hash32] = None + is_inclusion_list_satisfied: Optional[bool] = None def get_receipts_from_output( self, @@ -354,6 +355,11 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: block_output.block_access_list ) + if hasattr(block_output, "is_inclusion_list_satisfied"): + self.is_inclusion_list_satisfied = ( + block_output.is_inclusion_list_satisfied + ) + def json_encode_receipts(self) -> Any: """ Encode receipts to JSON. @@ -446,4 +452,7 @@ def to_json(self) -> Any: self.block_access_list_hash ) + if self.is_inclusion_list_satisfied is not None: + data["isInclusionListSatisfied"] = self.is_inclusion_list_satisfied + return data