From 50ec4af8a7679f722bc8aa6c8bc5bd3a505073e7 Mon Sep 17 00:00:00 2001 From: felix Date: Wed, 21 Jan 2026 18:50:15 +0100 Subject: [PATCH 1/8] feat(src): Initial EIP-7843 (SLOTNUM) Implementation (#2007) * feat(amsterdam): Implement EIP-7843 SLOTNUM opcode * mario feedback --- .../consume/simulators/single_test_client.py | 3 + .../execution_testing/fixtures/blockchain.py | 8 +- .../src/execution_testing/forks/base_fork.py | 6 + .../forks/forks/eips/amsterdam/eip_7843.py | 49 ++++++++ .../execution_testing/forks/forks/forks.py | 5 + .../src/execution_testing/specs/blockchain.py | 12 +- .../src/execution_testing/specs/state.py | 1 + .../specs/static_state/environment.py | 3 + .../test_types/block_types.py | 4 + .../src/execution_testing/vm/opcodes.py | 30 +++++ src/ethereum/forks/amsterdam/blocks.py | 7 ++ src/ethereum/forks/amsterdam/fork.py | 1 + src/ethereum/forks/amsterdam/vm/__init__.py | 1 + .../amsterdam/vm/instructions/__init__.py | 2 + .../forks/amsterdam/vm/instructions/block.py | 33 ++++++ .../evm_tools/loaders/fork_loader.py | 9 ++ .../evm_tools/t8n/__init__.py | 2 + src/ethereum_spec_tools/evm_tools/t8n/env.py | 15 +++ tests/amsterdam/eip7843_slotnum/__init__.py | 1 + tests/amsterdam/eip7843_slotnum/spec.py | 22 ++++ .../amsterdam/eip7843_slotnum/test_slotnum.py | 112 ++++++++++++++++++ whitelist.txt | 1 + 22 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7843.py create mode 100644 tests/amsterdam/eip7843_slotnum/__init__.py create mode 100644 tests/amsterdam/eip7843_slotnum/spec.py create mode 100644 tests/amsterdam/eip7843_slotnum/test_slotnum.py 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/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..d16716380a1 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 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..f0c20549e87 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7843.py @@ -0,0 +1,49 @@ +""" +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 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.GAS_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..ba8b9c05c93 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.""" diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 3313e8795f3..f7a87dcbfff 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -165,6 +165,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 +352,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. @@ -688,6 +691,10 @@ def generate_block_data( 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: @@ -776,8 +783,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..b113f90262d 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 + ---- + TBD + + 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..d642bc65459 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -257,6 +257,13 @@ 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 + """ @slotted_freezable 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/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..11419e7105e 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, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.slot_number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) 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/spec.py b/tests/amsterdam/eip7843_slotnum/spec.py new file mode 100644 index 00000000000..3896ea8ae1b --- /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="8140e7f3a1c93249e9e9ee5ab5281396341306ec", +) + + +@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..1e18cf536aa --- /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("Amsterdam") + + +@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 From e2e5eaeb1c1e3e49b1abc649ff59f65ae406cd3e Mon Sep 17 00:00:00 2001 From: felipe Date: Wed, 21 Jan 2026 14:07:08 -0700 Subject: [PATCH 2/8] fix(tests): minor fixes and updates for eip7843 (#2057) * fix(tests): minor updates to eip-7843 following #2007 * fix: account for slotnum in genesis creation * fix: append slot_number in fixture_loader.py --- packages/testing/src/execution_testing/vm/opcodes.py | 2 +- src/ethereum/genesis.py | 3 +++ src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py | 4 ++++ tests/amsterdam/eip7843_slotnum/spec.py | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index b113f90262d..96fcd6599a3 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -2246,7 +2246,7 @@ class Opcodes(Opcode, Enum): Fork ---- - TBD + Amsterdam Gas ---- 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/tests/amsterdam/eip7843_slotnum/spec.py b/tests/amsterdam/eip7843_slotnum/spec.py index 3896ea8ae1b..a96172631f4 100644 --- a/tests/amsterdam/eip7843_slotnum/spec.py +++ b/tests/amsterdam/eip7843_slotnum/spec.py @@ -13,7 +13,7 @@ class ReferenceSpec: ref_spec_7843 = ReferenceSpec( git_path="EIPS/eip-7843.md", - version="8140e7f3a1c93249e9e9ee5ab5281396341306ec", + version="6bc5d6b7acbc016a79fa573f98975093b5c2ca52", ) From 7e20de82a615ff2274674ffdcb068c9ebef43e4a Mon Sep 17 00:00:00 2001 From: felix Date: Thu, 29 Jan 2026 19:01:02 +0000 Subject: [PATCH 3/8] execution-api 7843 pr 731 implemented (#2101) --- .../plugins/execute/rpc/chain_builder_eth_rpc.py | 3 +++ packages/testing/src/execution_testing/forks/base_fork.py | 8 ++++++++ .../forks/forks/eips/amsterdam/eip_7843.py | 5 +++++ .../testing/src/execution_testing/forks/forks/forks.py | 7 +++++++ packages/testing/src/execution_testing/rpc/rpc_types.py | 1 + 5 files changed, 24 insertions(+) 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/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index d16716380a1..03059aff283 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -887,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 index f0c20549e87..20024f5f8d4 100644 --- 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 @@ -31,6 +31,11 @@ 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, diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ba8b9c05c93..41dc660309e 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -970,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): From 35c33513b8d2d444ab16705ee3fd4e02ddc63cbe Mon Sep 17 00:00:00 2001 From: felix Date: Fri, 30 Jan 2026 12:37:23 +0000 Subject: [PATCH 4/8] src(fix): framework bug fix for slotnum (#2108) --- .../src/execution_testing/specs/blockchain.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index f7a87dcbfff..b66db851f67 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, @@ -675,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"", + slot_number=slot_number_value, fork=fork, ) From cf7e5e93c917203f4e8baffa6df4716d16414789 Mon Sep 17 00:00:00 2001 From: marioevz Date: Wed, 8 Apr 2026 16:59:45 -0600 Subject: [PATCH 5/8] refactor(tests): Condition EIP-7843 tests to EIP inclusion --- tests/amsterdam/eip7843_slotnum/test_slotnum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/amsterdam/eip7843_slotnum/test_slotnum.py b/tests/amsterdam/eip7843_slotnum/test_slotnum.py index 1e18cf536aa..77f6f84a8b3 100644 --- a/tests/amsterdam/eip7843_slotnum/test_slotnum.py +++ b/tests/amsterdam/eip7843_slotnum/test_slotnum.py @@ -16,7 +16,7 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7843.git_path REFERENCE_SPEC_VERSION = ref_spec_7843.version -pytestmark = pytest.mark.valid_from("Amsterdam") +pytestmark = pytest.mark.valid_from("EIP7843") @pytest.mark.parametrize( From 409d33fecf7d4305db665bfe3767e6791f7c8619 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Mon, 20 Apr 2026 23:07:25 +0200 Subject: [PATCH 6/8] fix(specs): align EIP-7843 SLOTNUM gas references with renamed constants Replace stale GAS_BASE reference with GasCosts.OPCODE_SLOTNUM (= BASE) to match the opcode gas naming convention used across block instructions on forks/amsterdam. Also update the test-fork mixin to use gas_costs.BASE instead of the retired gas_costs.GAS_BASE. --- .../execution_testing/forks/forks/eips/amsterdam/eip_7843.py | 2 +- src/ethereum/forks/amsterdam/vm/gas.py | 1 + src/ethereum/forks/amsterdam/vm/instructions/block.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) 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 index 20024f5f8d4..d701bef1cc5 100644 --- 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 @@ -45,7 +45,7 @@ def opcode_gas_map( base_map = super(EIP7843, cls).opcode_gas_map() return { **base_map, - Opcodes.SLOTNUM: gas_costs.GAS_BASE, + Opcodes.SLOTNUM: gas_costs.BASE, } @classmethod 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/block.py b/src/ethereum/forks/amsterdam/vm/instructions/block.py index 11419e7105e..24c524673eb 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/block.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/block.py @@ -285,7 +285,7 @@ def slot_number(evm: Evm) -> None: pass # GAS - charge_gas(evm, GAS_BASE) + charge_gas(evm, GasCosts.OPCODE_SLOTNUM) # OPERATION push(evm.stack, U256(evm.message.block_env.slot_number)) From 525ecde53be88412783776c8a7929efba1188162 Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 14 May 2026 10:25:34 -0600 Subject: [PATCH 7/8] Remove stray header_bal_hash_required checks --- .../src/execution_testing/specs/blockchain.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index b66db851f67..f0b1530f57c 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -696,17 +696,13 @@ def generate_block_data( ), 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, ) - # 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: @@ -795,7 +791,7 @@ def generate_block_data( bal = block.expected_block_access_list.modify_if_invalid_test( t8n_bal ) - if bal != t8n_bal and fork.header_bal_hash_required(): + if bal != t8n_bal: # If the BAL was modified and the fork requires it, update the # header hash header.block_access_list_hash = Hash(bal.rlp.keccak256()) From 08a7dac93212180ed5d06bb59e6a8d6857aedf53 Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 14 May 2026 10:38:57 -0600 Subject: [PATCH 8/8] Remove unneeded eip7843 spec.py --- src/ethereum/forks/amsterdam/blocks.py | 3 ++- tests/amsterdam/eip7843_slotnum/spec.py | 22 ------------------- .../amsterdam/eip7843_slotnum/test_slotnum.py | 16 +++++++++++++- 3 files changed, 17 insertions(+), 24 deletions(-) delete mode 100644 tests/amsterdam/eip7843_slotnum/spec.py diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index d642bc65459..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 """ @@ -257,6 +257,7 @@ 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. diff --git a/tests/amsterdam/eip7843_slotnum/spec.py b/tests/amsterdam/eip7843_slotnum/spec.py deleted file mode 100644 index a96172631f4..00000000000 --- a/tests/amsterdam/eip7843_slotnum/spec.py +++ /dev/null @@ -1,22 +0,0 @@ -"""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 index 77f6f84a8b3..65358ebb31c 100644 --- a/tests/amsterdam/eip7843_slotnum/test_slotnum.py +++ b/tests/amsterdam/eip7843_slotnum/test_slotnum.py @@ -1,5 +1,7 @@ """Tests for EIP-7843 (SLOTNUM).""" +from dataclasses import dataclass + import pytest from execution_testing import ( Account, @@ -11,7 +13,19 @@ Transaction, ) -from .spec import ref_spec_7843 + +@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