diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 1d8bbcc106..97d4a73d4c 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -191,7 +191,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. @@ -212,6 +216,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: @@ -239,6 +245,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) @@ -736,6 +743,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. @@ -755,6 +763,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 ------- @@ -779,6 +789,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( @@ -1059,3 +1076,62 @@ 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/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index b2a8c5e2b9..caa15c89c4 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -73,6 +73,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) @@ -89,6 +91,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 5e085504cd..e7cce75001 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -150,6 +150,18 @@ 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 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.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index fc09dfd8a3..3a64e1dd7d 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -409,6 +409,14 @@ 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: + self.fork.validate_inclusion_list_transactions( + block_env, + block_output, + 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 07a3071c1f..d278c22274 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -13,6 +13,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) 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: @@ -313,3 +316,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.has_is_inclusion_list_satisfied: + 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 c60c266965..7e6e884b48 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