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 419b27a915b..03059aff283 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -436,6 +436,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 @@ -881,6 +887,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 454f3d8cabd..41dc660309e 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -925,6 +925,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.""" @@ -965,6 +970,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 3313e8795f3..f0b1530f57c 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() """ @@ -351,6 +353,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. @@ -672,19 +676,30 @@ 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"", + extra_data=( + block.extra_data if block.extra_data is not None else b"" + ), + slot_number=slot_number_value, fork=fork, ) @@ -777,7 +792,8 @@ def generate_block_data( t8n_bal ) if bal != t8n_bal: - # If the BAL was modified, update the header hash + # 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/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index 5a4e1b410f0..302533da837 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -114,7 +114,7 @@ class Header: [`keccak256`]: ref:ethereum.crypto.hash.keccak256 [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes [Trie]: ref:ethereum.merkle_patricia_trie.Trie - """ # noqa: E501 + """ transactions_root: Root """ @@ -258,6 +258,14 @@ class Header: [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 + """ + @slotted_freezable @dataclass diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 0af84822dc8..531c086a9f7 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -314,6 +314,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( diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index ff523e2c33c..11cb126ee4f 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -57,6 +57,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 2ca816dbbf0..3a93f850fbd 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -197,4 +197,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 7953f97db4f..f9ec92d6ded 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -302,6 +302,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 e17cde49005..c5fc7cde359 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -400,6 +400,8 @@ def block_environment(self) -> Any: kw_arguments["block_access_list_builder"] = ( self.fork.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/test_slotnum.py b/tests/amsterdam/eip7843_slotnum/test_slotnum.py new file mode 100644 index 00000000000..65358ebb31c --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/test_slotnum.py @@ -0,0 +1,126 @@ +"""Tests for EIP-7843 (SLOTNUM).""" + +from dataclasses import dataclass + +import pytest +from execution_testing import ( + Account, + Alloc, + Environment, + Fork, + Op, + StateTestFiller, + Transaction, +) + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_7843 = ReferenceSpec( + git_path="EIPS/eip-7843.md", + version="6bc5d6b7acbc016a79fa573f98975093b5c2ca52", +) + +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/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