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 4045f92221..00670b28fe 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 @@ -33,6 +33,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 23145e6238..02c4c53f36 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -210,6 +210,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) @@ -361,7 +365,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( @@ -378,6 +382,11 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: if fork.header_bal_hash_required(block_number=0, timestamp=0) else None ), + "slot_number": ( + 0 + if fork.header_slot_number_required(block_number=0, timestamp=0) + else None + ), "fork": fork, } return cls(**environment_values, **extras) @@ -418,6 +427,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 89fa18afc2..f36fd557ab 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -361,6 +361,14 @@ def header_bal_hash_required( """Return true if the header must contain block access list hash.""" pass + @classmethod + @abstractmethod + def header_slot_number_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> 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/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index c2deb27bae..72624785d2 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -976,6 +976,14 @@ def header_beacon_root_required( del block_number, timestamp return False + @classmethod + def header_slot_number_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """At genesis, header must not contain slot number (EIP-7843).""" + del block_number, timestamp + return False + @classmethod def engine_new_payload_blob_hashes( cls, *, block_number: int = 0, timestamp: int = 0 @@ -3338,9 +3346,7 @@ class Amsterdam(BPO2): def header_bal_hash_required( cls, *, block_number: int = 0, timestamp: int = 0 ) -> bool: - """ - From Amsterdam, header must contain block access list hash (EIP-7928). - """ + """BAL hash in header required from Amsterdam (EIP-7928).""" del block_number, timestamp return True @@ -3367,3 +3373,36 @@ def engine_execution_payload_block_access_list( """ del block_number, timestamp return True + + @classmethod + def header_slot_number_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """Slot number in header required from Amsterdam (EIP-7843).""" + del block_number, timestamp + return True + + @classmethod + def opcode_gas_map( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add SLOTNUM opcode gas cost for Amsterdam (EIP-7843).""" + gas_costs = cls.gas_costs( + block_number=block_number, timestamp=timestamp + ) + base_map = super(Amsterdam, cls).opcode_gas_map( + block_number=block_number, timestamp=timestamp + ) + return { + **base_map, + Opcodes.SLOTNUM: gas_costs.G_BASE, + } + + @classmethod + def valid_opcodes( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> List[Opcodes]: + """Add SLOTNUM opcode for Amsterdam (EIP-7843).""" + return [Opcodes.SLOTNUM] + super(Amsterdam, cls).valid_opcodes( + block_number=block_number, timestamp=timestamp + ) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index b5a6ddffe9..fdd5dd1d2c 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -154,6 +154,7 @@ class Header(CamelModel): parent_beacon_block_root: Removable | Hash | None = None requests_hash: Removable | Hash | None = None bal_hash: Removable | Hash | None = None + slot_number: Removable | HexNumber | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """ @@ -325,6 +326,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. @@ -651,6 +654,12 @@ def generate_block_data( fork=self.fork, ) + # Clear block_access_list_hash if the fork doesn't require it + if not self.fork.header_bal_hash_required( + block_number=int(env.number), timestamp=int(env.timestamp) + ): + header.block_access_list_hash = None + if block.header_verify is not None: # Verify the header after transition tool processing. try: @@ -747,8 +756,10 @@ 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 self.fork.header_bal_hash_required( + block_number=int(env.number), timestamp=int(env.timestamp) + ): + # 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 e4d5f9c61c..1e1869e33f 100644 --- a/packages/testing/src/execution_testing/specs/state.py +++ b/packages/testing/src/execution_testing/specs/state.py @@ -310,6 +310,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 32cd3b4e6d..89b42cbe45 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 2c73a558b9..26bcb4ea03 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) @@ -223,6 +224,14 @@ def set_fork_requirements(self, fork: Fork) -> "Environment": ): updated_values["parent_beacon_block_root"] = 0 + if ( + fork.header_slot_number_required( + block_number=number, timestamp=timestamp + ) + 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 31f7cf10e9..aaf52a9501 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 ef44549d28..df28972b47 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -253,6 +253,13 @@ class Header: [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 [cbalh]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_utils.compute_block_access_list_hash """ # 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 3e45c3e953..3bde867199 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -249,6 +249,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: excess_blob_gas=block.header.excess_blob_gas, parent_beacon_block_root=block.header.parent_beacon_block_root, state_changes=StateChanges(), + 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 3d69fbd706..d6596ee5d6 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 state_changes: StateChanges + 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 0da72c8ea5..d858b5053f 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 e563a2e96e..2ca11e7462 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 9a14efa54c..8204e12ea3 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -268,6 +268,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 4988ef25bc..258f812c8b 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -311,6 +311,8 @@ def block_environment(self) -> Any: if self.fork.has_block_access_list_hash: kw_arguments["state_changes"] = StateChanges() + 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 be719ba7af..c68cb4fd7f 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_block_access_list_hash: 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 0000000000..540001d5d9 --- /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 0000000000..3896ea8ae1 --- /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 0000000000..1e18cf536a --- /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 2cf28a9928..ac4aabc52b 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1099,6 +1099,7 @@ SignerType signextend simlimit sload +SLOTNUM slt smod socketserver