diff --git a/src/ethereum/osaka/fork.py b/src/ethereum/osaka/fork.py index 22c07e0ae8..3618fccfc7 100644 --- a/src/ethereum/osaka/fork.py +++ b/src/ethereum/osaka/fork.py @@ -189,7 +189,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. @@ -210,6 +214,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 validated. """ if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") @@ -236,6 +242,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: block_env=block_env, transactions=block.transactions, withdrawals=block.withdrawals, + inclusion_list_transactions=inclusion_list_transactions, ) block_state_root = state_root(block_env.state) transactions_root = root(block_output.transactions_trie) @@ -727,6 +734,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. @@ -746,6 +754,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 validated. Returns ------- @@ -769,6 +779,13 @@ def apply_body( for i, tx in enumerate(map(decode_transaction, transactions)): process_transaction(block_env, block_output, tx, Uint(i)) + validate_inclusion_list_transactions( + block_env=block_env, + block_output=block_output, + transactions=transactions, + inclusion_list_transactions=inclusion_list_transactions, + ) + process_withdrawals(block_env, block_output, withdrawals) process_general_purpose_requests( @@ -1053,3 +1070,61 @@ def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: return False return True + + +def validate_inclusion_list_transactions( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + transactions: Tuple[LegacyTransaction | Bytes, ...], + inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...], +) -> None: + """ + Validate whether the block satisfies inclusion list constraints. + + It checks if each inclusion list transaction is present in the block. + For those not included, it checks if there was sufficient gas remaining + to include the transaction and whether the transaction is valid against + the nonce and balance of the sender. If any inclusion list transaction + could have been included but was not, the block is marked as not + satisfying inclusion list constraints. Any inclusion list transaction + that is a blob transaction is ignored. + + Compliance with inclusion list constraints does not affect any other + block outputs. + + [EIP-7805]: https://eips.ethereum.org/EIPS/eip-7805 + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + 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 validated. + """ + for inclusion_list_transaction in inclusion_list_transactions: + # Skip if an inclusion list transaction is present in the block. + if inclusion_list_transaction in transactions: + continue + + tx = decode_transaction(inclusion_list_transaction) + + # Ignore blob transactions. + if isinstance(tx, BlobTransaction): + continue + + try: + # Run the tests of intrinsic validity. + validate_transaction(tx) + check_transaction(block_env, block_output, tx) + except EthereumException: + # This inclusion list transaction could not be included. + continue + else: + # This inclusion list transaction could have been included. + # Mark the block as not satisfying inclusion list constraints. + block_output.is_inclusion_list_satisfied = False + break diff --git a/src/ethereum/osaka/vm/__init__.py b/src/ethereum/osaka/vm/__init__.py index df75f66a6e..972e2bb588 100644 --- a/src/ethereum/osaka/vm/__init__.py +++ b/src/ethereum/osaka/vm/__init__.py @@ -74,6 +74,8 @@ class BlockOutput: Total blob gas used in the block. requests : `Bytes` Hash of all the requests in the block. + is_inclusion_list_satisfied : `bool` + Whether the block satisfies the inclusion list constraints. """ block_gas_used: Uint = Uint(0) @@ -90,6 +92,7 @@ class BlockOutput: ) blob_gas_used: U64 = U64(0) requests: List[Bytes] = 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 deb975b89e..1f193d35ea 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -152,6 +152,11 @@ def process_transaction(self) -> Any: """process_transaction function of the fork""" return self._module("fork").process_transaction + @property + def validate_inclusion_list_transactions(self) -> Any: + """validate_inclusion_list_transactions function of the fork""" + return self._module("fork").validate_inclusion_list_transactions + @property def MAX_BLOB_GAS_PER_BLOCK(self) -> Any: """MAX_BLOB_GAS_PER_BLOCK parameter of the fork""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index d0929ab70d..50299ccd4b 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -255,6 +255,14 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: U256(self.options.state_reward), block_env ) + if self.fork.is_after_fork("ethereum.osaka"): + self.fork.validate_inclusion_list_transactions( + block_env, + block_output, + self.txs.transactions, + self.env.inclusion_list_transactions, + ) + if self.fork.is_after_fork("ethereum.shanghai"): 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 9a9a0bf7d4..56c92788c1 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -12,6 +12,7 @@ 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 ethereum_spec_tools.evm_tools.t8n.t8n_types import Txs from ..utils import parse_hex_or_int @@ -54,6 +55,7 @@ class Env: parent_blob_gas_used: Optional[U64] excess_blob_gas: Optional[U64] requests: Any + inclusion_list_transactions: Any def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): if t8n.options.input_env == "stdin": @@ -74,6 +76,7 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): self.read_block_hashes(data, t8n) 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.is_after_fork("ethereum.cancun"): @@ -328,3 +331,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 lists. + """ + self.inclusion_list_transactions = None + + if not t8n.fork.is_after_fork("ethereum.osaka"): + return + + inclusion_list_transactions = [] + if "inclusionLists" in data: + inclusion_list_transactions = Txs( + t8n, data["inclusionLists"] + ).all_txs + self.inclusion_list_transactions = 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 d99b7782b9..e6a4e39716 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -268,6 +268,7 @@ class Result: requests_hash: Optional[Hash32] = None requests: Optional[List[Bytes]] = None block_exception: Optional[str] = None + is_inclusion_list_satisfied: Optional[bool] = None def get_receipts_from_output( self, @@ -323,6 +324,11 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: self.requests = block_output.requests self.requests_hash = t8n.fork.compute_requests_hash(self.requests) + 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. @@ -390,4 +396,7 @@ def to_json(self) -> Any: if self.block_exception is not None: data["blockException"] = self.block_exception + if self.is_inclusion_list_satisfied is not None: + data["isInclusionListSatisfied"] = self.is_inclusion_list_satisfied + return data