diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/single_test_client.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/single_test_client.py index 2d22fbb2e80..67984186bd2 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/single_test_client.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/single_test_client.py @@ -34,6 +34,9 @@ def client_genesis(fixture: BlockchainFixtureCommon) -> dict: alloc = to_json(fixture.pre) # NOTE: nethermind requires account keys without '0x' prefix genesis["alloc"] = {k.replace("0x", ""): v for k, v in alloc.items()} + # NOTE: geth expects slotNumber as plain integer, not hex string + if "slotNumber" in genesis: + genesis["slotNumber"] = int(genesis["slotNumber"], 16) return genesis diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py index 11373581f8c..890e35cd09f 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py @@ -156,6 +156,9 @@ def _payload_attributes( if next_fork.engine_payload_attribute_max_blobs_per_block() else None ), + slot_number=( + 0 if next_fork.engine_payload_attribute_slot_number() else None + ), ) def _finalize_payload( diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index 26209fc1b07..f83cf587350 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -211,6 +211,10 @@ class FixtureHeader(CamelModel): block_access_list_hash: ( Annotated[Hash, HeaderForkRequirement("bal_hash")] | None ) = Field(None, alias="blockAccessListHash") + slot_number: ( + Annotated[ZeroPaddedHexNumber, HeaderForkRequirement("slot_number")] + | None + ) = Field(None) fork: Fork | None = Field(None, exclude=True) @@ -349,7 +353,7 @@ def get_default_from_annotation( def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: """Get the genesis header for the given fork.""" environment_values = env.model_dump( - exclude_none=True, exclude={"withdrawals"} + exclude_none=True, exclude={"withdrawals", "slot_number"} ) if env.withdrawals is not None: environment_values["withdrawals_root"] = Withdrawal.list_root( @@ -366,6 +370,7 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: if fork.header_bal_hash_required() else None ), + "slot_number": 0 if fork.header_slot_number_required() else None, "fork": fork, } return cls(**environment_values, **extras) @@ -406,6 +411,7 @@ class FixtureExecutionPayload(CamelModel): block_access_list: Bytes | None = Field( None, description="RLP-serialized EIP-7928 Block Access List" ) + slot_number: HexNumber | None = Field(None) @classmethod def from_fixture_header( diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index a36f55ae1c1..250808fba96 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -451,6 +451,12 @@ def empty_block_bal_item_count(cls) -> int: """ pass + @classmethod + @abstractmethod + def header_slot_number_required(cls) -> bool: + """Return true if the header must contain slot number (EIP-7843).""" + pass + # Gas related abstract methods @classmethod @@ -896,6 +902,14 @@ def engine_payload_attribute_max_blobs_per_block(cls) -> bool: """ pass + @classmethod + @abstractmethod + def engine_payload_attribute_slot_number(cls) -> bool: + """ + Return true if the payload attributes include the slot number. + """ + pass + # Engine API method versions @classmethod def engine_new_payload_version(cls) -> Optional[int]: diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7843.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7843.py new file mode 100644 index 00000000000..d701bef1cc5 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7843.py @@ -0,0 +1,54 @@ +""" +EIP-7843: SLOTNUM opcode. + +Opcode to get the current slot number. + +https://eips.ethereum.org/EIPS/eip-7843 +""" + +from typing import Callable, Dict, List + +from execution_testing.vm import ( + OpcodeBase, + Opcodes, +) + +from ....base_fork import BaseFork + + +class EIP7843( + BaseFork, + # Engine API method version bumps + # New field `slotNumber` in ExecutionPayload + engine_new_payload_version_bump=True, + engine_get_payload_version_bump=True, + engine_forkchoice_updated_version_bump=True, +): + """EIP-7843 class.""" + + @classmethod + def header_slot_number_required(cls) -> bool: + """Slot number in header required.""" + return True + + @classmethod + def engine_payload_attribute_slot_number(cls) -> bool: + """Payload attributes include the slot number.""" + return True + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add SLOTNUM opcode gas cost.""" + gas_costs = cls.gas_costs() + base_map = super(EIP7843, cls).opcode_gas_map() + return { + **base_map, + Opcodes.SLOTNUM: gas_costs.BASE, + } + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """Add SLOTNUM opcode.""" + return [Opcodes.SLOTNUM] + super(EIP7843, cls).valid_opcodes() diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index d1d6cd06143..b1e7dd15495 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -924,6 +924,11 @@ def header_beacon_root_required(cls) -> bool: """At genesis, header must not contain parent beacon block root.""" return False + @classmethod + def header_slot_number_required(cls) -> bool: + """At genesis, header must not contain slot number (EIP-7843).""" + return False + @classmethod def engine_new_payload_blob_hashes(cls) -> bool: """At genesis, payloads do not have blob hashes.""" @@ -964,6 +969,13 @@ def engine_payload_attribute_max_blobs_per_block(cls) -> bool: """ return False + @classmethod + def engine_payload_attribute_slot_number(cls) -> bool: + """ + At genesis, payload attributes do not include the slot number. + """ + return False + @classmethod def get_reward(cls) -> int: """ diff --git a/packages/testing/src/execution_testing/rpc/rpc_types.py b/packages/testing/src/execution_testing/rpc/rpc_types.py index b0668583f0d..7aab1ecf51d 100644 --- a/packages/testing/src/execution_testing/rpc/rpc_types.py +++ b/packages/testing/src/execution_testing/rpc/rpc_types.py @@ -212,6 +212,7 @@ class PayloadAttributes(CamelModel): parent_beacon_block_root: Hash | None = None target_blobs_per_block: HexNumber | None = None max_blobs_per_block: HexNumber | None = None + slot_number: HexNumber | None = None class BlobsBundle(CamelModel): diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 61ab40360fd..59500a157f9 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -30,6 +30,7 @@ HeaderNonce, HexNumber, Number, + ZeroPaddedHexNumber, ) from execution_testing.client_clis import ( BlockExceptionWithMessage, @@ -165,6 +166,7 @@ class Header(CamelModel): parent_beacon_block_root: Removable | Hash | None = None requests_hash: Removable | Hash | None = None block_access_list_hash: Removable | Hash | None = None + slot_number: Removable | HexNumber | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """ @@ -349,6 +351,8 @@ def set_environment(self, env: Environment) -> Environment: and self.block_access_list is not None ): new_env_values["block_access_list"] = self.block_access_list + if not isinstance(self.slot_number, Removable): + new_env_values["slot_number"] = self.slot_number """ These values are required, but they depend on the previous environment, so they can be calculated here. @@ -670,22 +674,37 @@ def generate_block_data( if (blob_gas_per_blob := fork.blob_gas_per_blob()) > 0: blob_gas_used = blob_gas_per_blob * count_blobs(txs) + # Prepare slot_number for header initialization + slot_number_value: ZeroPaddedHexNumber | None = None + if fork.header_slot_number_required(): + slot_number_value = ZeroPaddedHexNumber( + int(env.slot_number) if env.slot_number is not None else 0 + ) + header = FixtureHeader( **( transition_tool_output.result.model_dump( exclude_none=True, exclude={"blob_gas_used", "transactions_trie"}, ) - | env.model_dump(exclude_none=True, exclude={"blob_gas_used"}) + | env.model_dump( + exclude_none=True, + exclude={"blob_gas_used", "slot_number"}, + ) ), blob_gas_used=blob_gas_used, transactions_trie=Transaction.list_root(txs), extra_data=block.extra_data if block.extra_data is not None else b"", + slot_number=slot_number_value, fork=fork, ) + # Clear block_access_list_hash if the fork doesn't require it + if not fork.header_bal_hash_required(): + header.block_access_list_hash = None + if block.header_verify is not None: # Verify the header after transition tool processing. try: @@ -766,8 +785,9 @@ def generate_block_data( bal = block.expected_block_access_list.modify_if_invalid_test( t8n_bal ) - if bal != t8n_bal: - # If the BAL was modified, update the header hash + if bal != t8n_bal and fork.header_bal_hash_required(): + # If the BAL was modified and the fork requires it, update the + # header hash header.block_access_list_hash = Hash(bal.rlp.keccak256()) built_block = BuiltBlock( diff --git a/packages/testing/src/execution_testing/specs/state.py b/packages/testing/src/execution_testing/specs/state.py index bead9b2a932..b0c456a3590 100644 --- a/packages/testing/src/execution_testing/specs/state.py +++ b/packages/testing/src/execution_testing/specs/state.py @@ -318,6 +318,7 @@ def _generate_blockchain_blocks(self) -> List[Block]: "extra_data": self.env.extra_data, "withdrawals": self.env.withdrawals, "parent_beacon_block_root": self.env.parent_beacon_block_root, + "slot_number": self.env.slot_number, "txs": [self.tx], "ommers": [], "header_verify": self.blockchain_test_header_verify, diff --git a/packages/testing/src/execution_testing/specs/static_state/environment.py b/packages/testing/src/execution_testing/specs/static_state/environment.py index 32cd3b4e6db..89b42cbe45b 100644 --- a/packages/testing/src/execution_testing/specs/static_state/environment.py +++ b/packages/testing/src/execution_testing/specs/static_state/environment.py @@ -33,6 +33,7 @@ class EnvironmentInStateTestFiller(BaseModel): current_excess_blob_gas: ValueInFiller | None = Field( None, alias="currentExcessBlobGas" ) + current_slot_number: ValueInFiller | None = Field(None, alias="slotNumber") model_config = ConfigDict(extra="forbid") @@ -72,4 +73,6 @@ def get_environment(self, tags: TagDict) -> Environment: kwargs["base_fee_per_gas"] = self.current_base_fee if self.current_excess_blob_gas is not None: kwargs["excess_blob_gas"] = self.current_excess_blob_gas + if self.current_slot_number is not None: + kwargs["slot_number"] = self.current_slot_number return Environment(**kwargs) diff --git a/packages/testing/src/execution_testing/test_types/block_types.py b/packages/testing/src/execution_testing/test_types/block_types.py index 4d95a4e43f7..56d367e2e68 100644 --- a/packages/testing/src/execution_testing/test_types/block_types.py +++ b/packages/testing/src/execution_testing/test_types/block_types.py @@ -101,6 +101,7 @@ class EnvironmentGeneric(CamelModel, Generic[NumberBoundTypeVar]): excess_blob_gas: NumberBoundTypeVar | None = Field( None, alias="currentExcessBlobGas" ) + slot_number: NumberBoundTypeVar | None = Field(None, alias="slotNumber") parent_difficulty: NumberBoundTypeVar | None = Field(None) parent_timestamp: NumberBoundTypeVar | None = Field(None) @@ -200,6 +201,9 @@ def set_fork_requirements(self, fork: Fork) -> "Environment": ): updated_values["parent_beacon_block_root"] = 0 + if fork.header_slot_number_required() and self.slot_number is None: + updated_values["slot_number"] = 0 + return self.copy(**updated_values) def __hash__(self) -> int: diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index 1ad001cb1c6..96fcd6599a3 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -2225,6 +2225,36 @@ class Opcodes(Opcode, Enum): Source: [EIP-7516](https://eips.ethereum.org/EIPS/eip-7516) """ + SLOTNUM = Opcode(0x4B, popped_stack_items=0, pushed_stack_items=1) + """ + SLOTNUM() = slotNumber + ---- + + Description + ---- + Returns the current slot number as provided by the consensus layer. + The slot number is passed from the consensus layer to the execution + layer through the engine API. + + Inputs + ---- + - None + + Outputs + ---- + - slotNumber: current slot number (uint64) + + Fork + ---- + Amsterdam + + Gas + ---- + 2 + + Source: [EIP-7843](https://eips.ethereum.org/EIPS/eip-7843) + """ + POP = Opcode(0x50, popped_stack_items=1) """ POP() diff --git a/src/ethereum/forks/amsterdam/__init__.py b/src/ethereum/forks/amsterdam/__init__.py index e6f3e9476a0..bb33435cd9e 100644 --- a/src/ethereum/forks/amsterdam/__init__.py +++ b/src/ethereum/forks/amsterdam/__init__.py @@ -1,14 +1,17 @@ """ -The Amsterdam fork ([EIP-7773]) includes block-level access lists. +The Amsterdam fork ([EIP-7773]) includes block-level access lists and the +partial header hash that the consensus layer (Gloas) cross-checks against. ### Changes - [EIP-7928: Block-Level Access Lists][EIP-7928] +- [EIP-8237: Partial Header Hash][EIP-8237] ### Releases [EIP-7773]: https://eips.ethereum.org/EIPS/eip-7773 [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +[EIP-8237]: https://eips.ethereum.org/EIPS/eip-8237 """ from ethereum.fork_criteria import ForkCriteria, Unscheduled diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index f67d52adfc2..f130e8f8ff4 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -257,6 +257,24 @@ class Header: [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 [cbalh]: ref:ethereum.forks.amsterdam.block_access_lists.hash_block_access_list """ # noqa: E501 + slot_number: U64 + """ + The slot number of this block as provided by the consensus layer. + Introduced in [EIP-7843]. + + [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843 + """ + + partial_header_hash: Hash32 + """ + [SHA2-256] commitment cross-checked against the consensus layer. + Last RLP field of the header so the block hash covers it. Introduced + in [EIP-8237]. See [`compute_partial_header_hash`][cphh]. + + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + [EIP-8237]: https://eips.ethereum.org/EIPS/eip-8237 + [cphh]: ref:ethereum.forks.amsterdam.partial_header_hash.compute_partial_header_hash + """ # noqa: E501 @slotted_freezable diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 0ff0bf663ed..59833c58196 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -60,6 +60,7 @@ TransactionTypeContractCreationError, ) from .fork_types import Authorization, VersionedHash +from .partial_header_hash import compute_partial_header_hash from .requests import ( CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, @@ -312,6 +313,7 @@ def execute_block( excess_blob_gas=block.header.excess_blob_gas, parent_beacon_block_root=block.header.parent_beacon_block_root, block_access_list_builder=BlockAccessListBuilder(), + slot_number=block.header.slot_number, ) block_output = apply_body( @@ -353,6 +355,19 @@ def execute_block( if computed_block_access_list_hash != block.header.block_access_list_hash: raise InvalidBlock("Invalid block access list hash") + expected_partial_header_hash = compute_partial_header_hash( + parent_hash=block.header.parent_hash, + prev_randao=block.header.prev_randao, + gas_limit=U64(block.header.gas_limit), + timestamp=U64(block.header.timestamp), + withdrawals=block.withdrawals, + parent_beacon_block_root=block.header.parent_beacon_block_root, + slot_number=block.header.slot_number, + execution_requests=tuple(block_output.requests), + ) + if expected_partial_header_hash != block.header.partial_header_hash: + raise InvalidBlock("Invalid partial header hash") + return block_diff diff --git a/src/ethereum/forks/amsterdam/partial_header_hash.py b/src/ethereum/forks/amsterdam/partial_header_hash.py new file mode 100644 index 00000000000..b5d072ea90c --- /dev/null +++ b/src/ethereum/forks/amsterdam/partial_header_hash.py @@ -0,0 +1,66 @@ +""" +[EIP-8237] introduces a [SHA2-256] commitment over a fixed subset of fields +that the consensus layer also observes. The same value is embedded in the +header (covered by the block hash) and in the `ExecutionPayload` delivered +through the Engine API; the state transition function rejects the block +unless the recomputed value equals `header.partial_header_hash`. Because +the block hash RLP-covers that field, a faithful payload-to-block +conversion makes the EIP's `payload == expected == header` chain reduce to +the single header check performed here. + +Byte layout, in order: `parent_hash || prev_randao || gas_limit || +timestamp || withdrawals || parent_beacon_block_root || slot_number || +execution_requests`. Integers are little-endian `uint64`; each +`Withdrawal` contributes `index || validator_index || address || amount` +(44 bytes); lists are raw concatenation; `execution_requests` follow the +[EIP-7685] type-prefixed layout. Empty CL slots produce no +execution-layer block at all (the EL has no notion of an "empty block"); +the chain is linked through `parent_hash` and `slot_number` may jump by +more than one between adjacent blocks. + +The choice of SHA2-256 over a pinned byte layout (rather than an SSZ +hash-tree-root) is deferred pending [EIP-8025] / `ProofEngine`. + +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +[EIP-8025]: https://eips.ethereum.org/EIPS/eip-8025 +[EIP-8237]: https://eips.ethereum.org/EIPS/eip-8237 +[SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 +""" + +from hashlib import sha256 +from typing import Tuple + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64 + +from ethereum.crypto.hash import Hash32 + +from .blocks import Withdrawal + + +def compute_partial_header_hash( + parent_hash: Hash32, + prev_randao: Bytes32, + gas_limit: U64, + timestamp: U64, + withdrawals: Tuple[Withdrawal, ...], + parent_beacon_block_root: Hash32, + slot_number: U64, + execution_requests: Tuple[Bytes, ...], +) -> Hash32: + """Compute the [EIP-8237] partial header hash over the pinned layout.""" + digest = sha256() + digest.update(bytes(parent_hash)) + digest.update(bytes(prev_randao)) + digest.update(gas_limit.to_le_bytes8()) + digest.update(timestamp.to_le_bytes8()) + for w in withdrawals: + digest.update(w.index.to_le_bytes8()) + digest.update(w.validator_index.to_le_bytes8()) + digest.update(bytes(w.address)) + digest.update(U64(w.amount).to_le_bytes8()) + digest.update(bytes(parent_beacon_block_root)) + digest.update(slot_number.to_le_bytes8()) + for request in execution_requests: + digest.update(bytes(request)) + return Hash32(digest.digest()) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 44135d5e72a..b268b36aa1a 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -50,6 +50,7 @@ class BlockEnvironment: excess_blob_gas: U64 parent_beacon_block_root: Hash32 block_access_list_builder: BlockAccessListBuilder + slot_number: U64 @dataclass diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index f831e7f4ce9..fc427587318 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -165,6 +165,7 @@ class GasCosts: OPCODE_CHAINID = BASE OPCODE_BASEFEE = BASE OPCODE_BLOBBASEFEE = BASE + OPCODE_SLOTNUM = BASE OPCODE_BLOBHASH = Uint(3) OPCODE_PUSH = VERY_LOW OPCODE_PUSH0 = BASE diff --git a/src/ethereum/forks/amsterdam/vm/instructions/__init__.py b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py index 0da72c8ea5c..d858b5053f0 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py @@ -99,6 +99,7 @@ class Ops(enum.Enum): BASEFEE = 0x48 BLOBHASH = 0x49 BLOBBASEFEE = 0x4A + SLOTNUM = 0x4B # Control Flow Ops STOP = 0x00 @@ -251,6 +252,7 @@ class Ops(enum.Enum): Ops.PREVRANDAO: block_instructions.prev_randao, Ops.GASLIMIT: block_instructions.gas_limit, Ops.CHAINID: block_instructions.chain_id, + Ops.SLOTNUM: block_instructions.slot_number, Ops.MLOAD: memory_instructions.mload, Ops.MSTORE: memory_instructions.mstore, Ops.MSTORE8: memory_instructions.mstore8, diff --git a/src/ethereum/forks/amsterdam/vm/instructions/block.py b/src/ethereum/forks/amsterdam/vm/instructions/block.py index 8c93840b384..24c524673eb 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/block.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/block.py @@ -259,3 +259,36 @@ def chain_id(evm: Evm) -> None: # PROGRAM COUNTER evm.pc += Uint(1) + + +def slot_number(evm: Evm) -> None: + """ + Push the current slot number onto the stack. + + The slot number is provided by the consensus layer and passed to the + execution layer through the engine API. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SLOTNUM) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.slot_number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index aa15805521b..55eed6aa4d9 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -265,6 +265,9 @@ def add_genesis_block( if has_field(hardfork.Header, "block_access_list_hash"): fields["block_access_list_hash"] = keccak256(rlp.encode([])) + if has_field(hardfork.Header, "slot_number"): + fields["slot_number"] = U64(0) + genesis_header = hardfork.Header(**fields) block_fields = { diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py index 8caf5679c8e..be996227233 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -201,4 +201,8 @@ def json_to_header(self, raw: Any) -> Any: bal_hash = hex_to_bytes32(raw.get("blockAccessListHash")) parameters.append(bal_hash) + if "slotNumber" in raw: + slot_number = hex_to_u64(raw.get("slotNumber")) + parameters.append(slot_number) + return self.fork.Header(*parameters) 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 c88eec7d9bd..cc9896a7375 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -285,6 +285,15 @@ def has_withdrawal(self) -> bool: """Check if the fork has a `Withdrawal` class.""" return hasattr(self._module("blocks"), "Withdrawal") + @property + def has_slot_number(self) -> bool: + """Check if the fork supports the SLOTNUM opcode (EIP-7843).""" + try: + block_env = self._module("vm").BlockEnvironment + return "slot_number" in block_env.__dataclass_fields__ + except (ModuleNotFoundError, AttributeError): + return False + @property def decode_transaction(self) -> Any: """decode_transaction function 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 23781ad7b94..611b8cdea0a 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -414,6 +414,8 @@ def block_environment(self) -> Any: kw_arguments["block_access_list_builder"] = ( BlockAccessListBuilder() ) + if self.fork.has_slot_number: + kw_arguments["slot_number"] = self.env.slot_number return block_environment(**kw_arguments) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index f76c0caaf57..edf3763d573 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -53,6 +53,7 @@ class Env: parent_excess_blob_gas: Optional[U64] parent_blob_gas_used: Optional[U64] excess_blob_gas: Optional[U64] + slot_number: Optional[U64] requests: Any def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): @@ -86,6 +87,8 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None): ) self.read_excess_blob_gas(data, t8n) + self.read_slot_number(data, t8n) + 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 @@ -147,6 +150,8 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: if t8n.fork.has_hash_block_access_list: arguments["block_access_list_hash"] = Hash32(b"\0" * 32) + if t8n.fork.has_slot_number: + arguments["slot_number"] = U64(0) parent_header = t8n.fork.Header(**arguments) @@ -222,6 +227,16 @@ def read_randao(self, data: Any, t8n: "T8N") -> None: left_pad_zero_bytes(hex_to_bytes(current_random), 32) ) + def read_slot_number(self, data: Any, t8n: "T8N") -> None: + """ + Read the slot number from the data. + The slot number is provided by the consensus layer. + """ + self.slot_number = None + if t8n.fork.has_slot_number: + if "slotNumber" in data: + self.slot_number = parse_hex_or_int(data["slotNumber"], U64) + def read_withdrawals(self, data: Any, t8n: "T8N") -> None: """ Read the withdrawals from the data. diff --git a/tests/amsterdam/eip7843_slotnum/__init__.py b/tests/amsterdam/eip7843_slotnum/__init__.py new file mode 100644 index 00000000000..540001d5d95 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/__init__.py @@ -0,0 +1 @@ +"""Tests for [EIP-7843: SLOTNUM](https://eips.ethereum.org/EIPS/eip-7843).""" diff --git a/tests/amsterdam/eip7843_slotnum/spec.py b/tests/amsterdam/eip7843_slotnum/spec.py new file mode 100644 index 00000000000..a96172631f4 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/spec.py @@ -0,0 +1,22 @@ +"""Reference spec for [EIP-7843: SLOTNUM](https://eips.ethereum.org/EIPS/eip-7843).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_7843 = ReferenceSpec( + git_path="EIPS/eip-7843.md", + version="6bc5d6b7acbc016a79fa573f98975093b5c2ca52", +) + + +@dataclass(frozen=True) +class Spec: + """Constants and parameters from EIP-7843.""" diff --git a/tests/amsterdam/eip7843_slotnum/test_slotnum.py b/tests/amsterdam/eip7843_slotnum/test_slotnum.py new file mode 100644 index 00000000000..77f6f84a8b3 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/test_slotnum.py @@ -0,0 +1,112 @@ +"""Tests for EIP-7843 (SLOTNUM).""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Environment, + Fork, + Op, + StateTestFiller, + Transaction, +) + +from .spec import ref_spec_7843 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7843.git_path +REFERENCE_SPEC_VERSION = ref_spec_7843.version + +pytestmark = pytest.mark.valid_from("EIP7843") + + +@pytest.mark.parametrize( + "slot_number", + [ + pytest.param(0, id="slot_zero"), + pytest.param(1, id="slot_one"), + pytest.param(0x1000, id="slot_4096"), + pytest.param(2**32, id="slot_large"), + pytest.param(2**64 - 1, id="slot_max_u64"), + ], +) +def test_slotnum_value( + state_test: StateTestFiller, + pre: Alloc, + slot_number: int, +) -> None: + """ + Test that SLOTNUM opcode returns the correct slot number. + + The slot number is provided by the consensus layer and should be + accessible via the SLOTNUM opcode (0x4B). + """ + # Store SLOTNUM result at storage key 0 + code = Op.SSTORE(0, Op.SLOTNUM) + code_address = pre.deploy_contract(code) + + tx = Transaction( + sender=pre.fund_eoa(), + gas_limit=100_000, + to=code_address, + ) + + post = { + code_address: Account( + storage={0: slot_number}, + ), + } + + state_test( + env=Environment(slot_number=slot_number), + pre=pre, + tx=tx, + post=post, + ) + + +@pytest.mark.parametrize( + "gas_delta,call_succeeds", + [ + pytest.param(0, True, id="enough_gas"), + pytest.param(-1, False, id="out_of_gas"), + ], +) +def test_slotnum_gas_cost( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + gas_delta: int, + call_succeeds: bool, +) -> None: + """ + Test that SLOTNUM opcode costs exactly 2 gas (G_BASE). + """ + slotnum_gas = Op.SLOTNUM.gas_cost(fork) + call_gas = slotnum_gas + gas_delta + + # Callee just executes SLOTNUM + callee_code = Op.SLOTNUM + Op.STOP + callee_address = pre.deterministic_deploy_contract(deploy_code=callee_code) + + # Caller calls the callee with limited gas and stores result + caller_code = Op.SSTORE(0, Op.CALL(gas=call_gas, address=callee_address)) + caller_address = pre.deploy_contract(caller_code) + + tx = Transaction( + sender=pre.fund_eoa(), + gas_limit=100_000, + to=caller_address, + ) + + post = { + caller_address: Account( + storage={0: 1 if call_succeeds else 0}, + ), + } + + state_test( + env=Environment(slot_number=12345), + pre=pre, + tx=tx, + post=post, + ) diff --git a/vulture_whitelist.py b/vulture_whitelist.py index 5a44d33da7c..e91581236c6 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -171,3 +171,4 @@ _configure_client_manager # autouse fixture test_suite_name # hive test suite name fixture genesis_header # genesis header fixture + diff --git a/whitelist.txt b/whitelist.txt index 0c73495052e..fbad9e66265 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1109,6 +1109,7 @@ simlimit sload slot1 slot2 +SLOTNUM slt smod socketserver