diff --git a/packages/testing/src/execution_testing/cli/evm_bytes.py b/packages/testing/src/execution_testing/cli/evm_bytes.py index 5827037bdb..ed54ce94a9 100644 --- a/packages/testing/src/execution_testing/cli/evm_bytes.py +++ b/packages/testing/src/execution_testing/cli/evm_bytes.py @@ -128,6 +128,14 @@ def process_evm_bytes( # noqa: D103 break else: raise ValueError(f"Unknown opcode: {opcode_byte}") + if opcode == Op.EXTENSION and evm_bytes_array: + # An extended opcode (MIP-7) is EXTENSION plus a selector byte. + extended_opcode = (opcode_byte << 8) | evm_bytes_array[0] + for op in Op: + if op.int() == extended_opcode: + opcode = op + evm_bytes_array.pop(0) + break opcode_with_operands: OpcodeWithOperands if opcode.data_portion_length > 0: opcode_with_operands = OpcodeWithOperands( diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 3ad3941b09..b2e1620ce3 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -1363,6 +1363,14 @@ def valid_opcodes(cls) -> List[Opcodes]: """Return list of Opcodes that are valid to work on this fork.""" pass + @classmethod + def extension_opcodes(cls) -> List[Opcodes]: + """ + Return the extended opcodes the fork defines behind the EXTENSION + prefix (MIP-7), each a two-byte opcode of its own. + """ + return [] + @classmethod @abstractmethod def create_opcodes(cls) -> List[Opcodes]: diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ec72ab7204..b9143fec38 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1837,10 +1837,31 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" +class MONAD_NEXT(eips.EIP8163, MONAD_TEN): # noqa: N801 + """ + MONAD_NEXT fork. - pass + Adopts EIP-8163 through MIP-7, which gives EXTENSION a selector byte + and so the extended opcodes, and defines the call stack introspection + opcodes of MIP-18. + """ + + @classmethod + def extension_opcodes(cls) -> List[Opcodes]: + """MIP-18 defines the call stack introspection opcodes.""" + return [Opcodes.CALLSTACKDEPTH, Opcodes.CALLERN] + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add gas costs for CALLSTACKDEPTH and CALLERN.""" + gas_costs = cls.gas_costs() + return { + **super().opcode_gas_map(), + Opcodes.CALLSTACKDEPTH: gas_costs.BASE, + Opcodes.CALLERN: gas_costs.BASE, + } class BPO1( diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index 101434ad74..889ee17c99 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -82,6 +82,18 @@ def _stack_argument_to_bytecode( return new_opcode +def _extended_opcode(selector: int, **kwargs: Any) -> "Opcode": + """ + Return the two-byte opcode that EXTENSION forms with `selector` + (MIP-7). + + The selector is never JUMPDEST nor a PUSH byte, so that jump + destination analysis stays unaware of extended opcodes. + """ + assert selector != 0x5B and not 0x60 <= selector <= 0x7F + return Opcode(bytes([0xAE, selector]), **kwargs) + + class Opcode(Bytecode, OpcodeBase): """ Represents a single Opcode instruction in the EVM, with extra @@ -5349,6 +5361,66 @@ class Opcodes(Opcode, Enum): Source: [EIP-8163](https://eips.ethereum.org/EIPS/eip-8163) """ + CALLSTACKDEPTH = _extended_opcode(0x00, pushed_stack_items=1) + """ + CALLSTACKDEPTH() = depth + ---- + + Description + ---- + Push the depth of the current call frame, 0 for the transaction's + top-level frame + + Inputs + ---- + None + + Outputs + ---- + - depth: number of frames above the current one + + Fork + ---- + MONAD_NEXT + + Gas + ---- + 2 + + Source: [MIP-18](https://github.com/monad-crypto/MIPs/blob/main/MIPs/MIP-18.md) + """ + + CALLERN = _extended_opcode( + 0x01, popped_stack_items=1, pushed_stack_items=1, kwargs=["n"] + ) + """ + CALLERN(n) = caller + ---- + + Description + ---- + Push the address CALLER returns n frames above the current one, or 0 + when the call stack holds fewer frames + + Inputs + ---- + - n: number of frames to look up, 0 for the current frame + + Outputs + ---- + - caller: 20-byte address zero-extended to 32 bytes, or 0 + + Fork + ---- + MONAD_NEXT + + Gas + ---- + 2 + + Source: [MIP-18](https://github.com/monad-crypto/MIPs/blob/main/MIPs/MIP-18.md) + """ + DUPN = Opcode( 0xE6, pushed_stack_items=1, diff --git a/packages/testing/src/execution_testing/vm/tests/test_vm.py b/packages/testing/src/execution_testing/vm/tests/test_vm.py index 5d40a90824..f2ea81f587 100644 --- a/packages/testing/src/execution_testing/vm/tests/test_vm.py +++ b/packages/testing/src/execution_testing/vm/tests/test_vm.py @@ -6,7 +6,7 @@ from execution_testing.forks.forks.forks import Prague from ..helpers import create_op -from ..opcodes import Bytecode +from ..opcodes import Bytecode, _extended_opcode from ..opcodes import Macros as Om from ..opcodes import Opcodes as Op @@ -714,3 +714,33 @@ def test_create_op_rejects_non_create_opcode() -> None: """Test that `create_op` raises for a non-create opcode.""" with pytest.raises(ValueError, match="Not a create opcode: CALL"): create_op(Op.CALL) + + +@pytest.mark.parametrize( + "bytecode,expected", + [ + pytest.param(_extended_opcode(0x12), b"\xae\x12", id="bare"), + pytest.param( + _extended_opcode(0x12, popped_stack_items=1)(5), + b"\x60\x05\xae\x12", + id="stack_argument", + ), + ], +) +def test_extended_opcode_encoding(bytecode: Bytecode, expected: bytes) -> None: + """The EXTENSION prefix and its selector form one two-byte opcode.""" + assert bytes(bytecode) == expected + + +def test_extended_opcode_identity() -> None: + """An extended opcode is keyed apart from the bare prefix.""" + assert _extended_opcode(0x12) != Op.EXTENSION + assert _extended_opcode(0x12) == _extended_opcode(0x12) + assert _extended_opcode(0x12) != _extended_opcode(0x13) + + +@pytest.mark.parametrize("selector", [0x5B, 0x60, 0x7F]) +def test_extended_opcode_excluded_selector(selector: int) -> None: + """Selectors that JUMPDEST analysis interprets are rejected.""" + with pytest.raises(AssertionError): + _extended_opcode(selector) diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_next/__init__.py index b6c71ab245..8f51c6c3e2 100644 --- a/src/ethereum/forks/monad_next/__init__.py +++ b/src/ethereum/forks/monad_next/__init__.py @@ -1,6 +1,6 @@ """ -MONAD_NEXT fork is a placeholder for upcoming Monad changes and is -currently identical to MONAD_TEN. +MONAD_NEXT fork introduces the extended opcodes of MIP-7 and the call +stack introspection opcodes of MIP-18 on top of MONAD_TEN. """ from ethereum.fork_criteria import ByTimestamp, ForkCriteria diff --git a/src/ethereum/forks/monad_next/vm/exceptions.py b/src/ethereum/forks/monad_next/vm/exceptions.py index 7c1473772c..03bfc09af0 100644 --- a/src/ethereum/forks/monad_next/vm/exceptions.py +++ b/src/ethereum/forks/monad_next/vm/exceptions.py @@ -79,6 +79,15 @@ def __init__(self, code: int) -> None: self.code = code +class InvalidExtension(ExceptionalHalt): + """ + Raised when `EXTENSION` is not followed by the selector of a defined + extended opcode. + """ + + pass + + class InvalidJumpDestError(ExceptionalHalt): """ Occurs when the destination of a jump operation doesn't meet any of the diff --git a/src/ethereum/forks/monad_next/vm/gas.py b/src/ethereum/forks/monad_next/vm/gas.py index 0186e2a22d..46f42de4d9 100644 --- a/src/ethereum/forks/monad_next/vm/gas.py +++ b/src/ethereum/forks/monad_next/vm/gas.py @@ -179,6 +179,8 @@ class GasCosts: OPCODE_ADDRESS: Final[Uint] = BASE OPCODE_ORIGIN: Final[Uint] = BASE OPCODE_CALLER: Final[Uint] = BASE + OPCODE_CALLSTACKDEPTH: Final[Uint] = BASE + OPCODE_CALLERN: Final[Uint] = BASE OPCODE_CALLVALUE: Final[Uint] = BASE OPCODE_CALLDATASIZE: Final[Uint] = BASE OPCODE_CODESIZE: Final[Uint] = BASE diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_next/vm/instructions/__init__.py index 0da72c8ea5..af4bebef53 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/__init__.py +++ b/src/ethereum/forks/monad_next/vm/instructions/__init__.py @@ -21,6 +21,7 @@ from . import comparison as comparison_instructions from . import control_flow as control_flow_instructions from . import environment as environment_instructions +from . import extension as extension_instructions from . import keccak as keccak_instructions from . import log as log_instructions from . import memory as memory_instructions @@ -202,6 +203,9 @@ class Ops(enum.Enum): LOG3 = 0xA3 LOG4 = 0xA4 + # Extension Prefix + EXTENSION = 0xAE + # System Operations CREATE = 0xF0 CALL = 0xF1 @@ -355,6 +359,7 @@ class Ops(enum.Enum): Ops.LOG2: log_instructions.log2, Ops.LOG3: log_instructions.log3, Ops.LOG4: log_instructions.log4, + Ops.EXTENSION: extension_instructions.extension, Ops.CREATE: system_instructions.create, Ops.RETURN: system_instructions.return_, Ops.CALL: system_instructions.call, diff --git a/src/ethereum/forks/monad_next/vm/instructions/environment.py b/src/ethereum/forks/monad_next/vm/instructions/environment.py index 622a4d782c..e43db32f3e 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/environment.py +++ b/src/ethereum/forks/monad_next/vm/instructions/environment.py @@ -133,6 +133,68 @@ def caller(evm: Evm) -> None: evm.pc += Uint(1) +def callstackdepth(evm: Evm) -> None: + """ + Push the depth of the current call frame onto the stack, `0` for the + transaction's top-level frame (MIP-18). + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLSTACKDEPTH) + + # OPERATION + push(evm.stack, U256(evm.message.depth)) + + # PROGRAM COUNTER + # EXTENSION prefix and selector + evm.pc += Uint(2) + + +def callern(evm: Evm) -> None: + """ + Push the address `CALLER` returns `n` frames above the current one + onto the stack, or `0` when the call stack holds fewer frames + (MIP-18). + + `n` is popped from the stack: `0` names the current frame and the + depth names the top-level frame, whose caller is the transaction + origin. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + n = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLERN) + + # OPERATION + if n > U256(evm.message.depth): + push(evm.stack, U256(0)) + else: + frame = evm + for _ in range(int(n)): + assert frame.message.parent_evm is not None + frame = frame.message.parent_evm + push(evm.stack, U256.from_be_bytes(frame.message.caller)) + + # PROGRAM COUNTER + # EXTENSION prefix and selector + evm.pc += Uint(2) + + def callvalue(evm: Evm) -> None: """ Push the value (in wei) sent with the call onto the stack. diff --git a/src/ethereum/forks/monad_next/vm/instructions/extension.py b/src/ethereum/forks/monad_next/vm/instructions/extension.py new file mode 100644 index 0000000000..c9633ef0b6 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/extension.py @@ -0,0 +1,74 @@ +""" +Ethereum Virtual Machine (EVM) Extension Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Dispatch of the extended opcodes behind the `EXTENSION` prefix, as +defined by MIP-7 (https://github.com/monad-crypto/MIPs/blob/main/MIPs/MIP-7.md). + +An extended opcode is the two-byte sequence of `EXTENSION` followed by a +selector byte. Selectors `0x5B` (`JUMPDEST`) and `0x60`-`0x7F` +(`PUSH1`-`PUSH32`) are never assigned, which keeps jump destination +analysis unaware of extended opcodes: it reads the selector as a plain +opcode and finds the same destinations as an analysis that knows +nothing of `EXTENSION`. +""" + +import enum +from typing import Callable, Dict + +from ethereum_types.numeric import Uint, ulen + +from .. import Evm +from ..exceptions import InvalidExtension +from . import environment as environment_instructions + + +class ExtensionOps(enum.Enum): + """ + Enum for the selectors of the extended opcodes. + + A selector is never `0x5B` nor in the range `0x60`-`0x7F`. + """ + + # Call Stack Introspection Ops + CALLSTACKDEPTH = 0x00 + CALLERN = 0x01 + + +extension_implementation: Dict[ExtensionOps, Callable] = { + ExtensionOps.CALLSTACKDEPTH: environment_instructions.callstackdepth, + ExtensionOps.CALLERN: environment_instructions.callern, +} + + +def extension(evm: Evm) -> None: + """ + Execute the extended opcode selected by the byte following + `EXTENSION`. + + `EXTENSION` with no selector byte, or with a selector no extended + opcode is defined for, behaves like `INVALID`. The extended opcode + charges its own gas and advances the program counter past both bytes + and any immediates of its own. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + selector_pc = evm.pc + Uint(1) + if selector_pc >= ulen(evm.code): + raise InvalidExtension + selector = evm.code[selector_pc] + for extension_op, implementation in extension_implementation.items(): + if extension_op.value == selector: + implementation(evm) + return + raise InvalidExtension diff --git a/tests/monad_next/__init__.py b/tests/monad_next/__init__.py new file mode 100644 index 0000000000..6f5caca8cb --- /dev/null +++ b/tests/monad_next/__init__.py @@ -0,0 +1 @@ +"""MONAD_NEXT fork tests.""" diff --git a/tests/monad_next/mip18_call_stack_introspection/__init__.py b/tests/monad_next/mip18_call_stack_introspection/__init__.py new file mode 100644 index 0000000000..2c857591d5 --- /dev/null +++ b/tests/monad_next/mip18_call_stack_introspection/__init__.py @@ -0,0 +1 @@ +"""Tests of the MIP-18 call stack introspection opcodes.""" diff --git a/tests/monad_next/mip18_call_stack_introspection/spec.py b/tests/monad_next/mip18_call_stack_introspection/spec.py new file mode 100644 index 0000000000..8f605f5e21 --- /dev/null +++ b/tests/monad_next/mip18_call_stack_introspection/spec.py @@ -0,0 +1,16 @@ +"""Defines the MIP-18 call stack introspection specification reference.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_18 = ReferenceSpec( + "MIPS/MIP-18.md", "628ffa40505cde048fba859919cc14e87b8dda54" +) diff --git a/tests/monad_next/mip18_call_stack_introspection/test_call_stack_introspection.py b/tests/monad_next/mip18_call_stack_introspection/test_call_stack_introspection.py new file mode 100644 index 0000000000..549a958248 --- /dev/null +++ b/tests/monad_next/mip18_call_stack_introspection/test_call_stack_introspection.py @@ -0,0 +1,180 @@ +""" +Tests of the MIP-18 call stack introspection opcodes. + +CALLSTACKDEPTH pushes the depth of the current frame and CALLERN(n) +pushes what CALLER returns n frames above it, so CALLERN(0) is CALLER +and CALLERN(CALLSTACKDEPTH) is ORIGIN. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Bytecode, + CodeGasMeasure, + Fork, + Op, + StateTestFiller, + Storage, + Transaction, +) + +from .spec import ref_spec_18 + +REFERENCE_SPEC_GIT_PATH = ref_spec_18.git_path +REFERENCE_SPEC_VERSION = ref_spec_18.version + +pytestmark = pytest.mark.valid_from("MONAD_NEXT") + +slot_gas_measured = 0 + + +def test_top_level_frame( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Introspect the call stack from a transaction's top-level frame. + + The depth is 0, CALLERN(0) is the origin and any deeper index is + out of range. + """ + sender = pre.fund_eoa() + storage = Storage() + code = ( + Op.SSTORE(storage.store_next(0), Op.CALLSTACKDEPTH) + + Op.SSTORE(storage.store_next(sender), Op.CALLERN(0)) + + Op.SSTORE(storage.store_next(0), Op.CALLERN(1)) + + Op.SSTORE(storage.store_next(0), Op.CALLERN(2**256 - 1)) + ) + contract_address = pre.deploy_contract(code, storage=storage.canary()) + + tx = Transaction(to=contract_address, sender=sender) + + state_test( + pre=pre, + post={contract_address: Account(storage=storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize_by_fork( + "top_call_opcode", lambda fork: fork.call_opcodes() +) +@pytest.mark.parametrize_by_fork( + "middle_call_opcode", lambda fork: fork.call_opcodes() +) +def test_call_chain( + state_test: StateTestFiller, + pre: Alloc, + top_call_opcode: Op, + middle_call_opcode: Op, +) -> None: + """ + Introspect the call stack from the leaf of a two-hop chain, each hop + made with its own call opcode, and relay the answers to the top-level + frame's storage. + """ + sender = pre.fund_eoa() + depth = 2 + queries = [Op.CALLSTACKDEPTH] + [Op.CALLERN(n) for n in range(depth + 2)] + sentinel = 0xBA5E + + leaf_address = pre.deploy_contract( + sum( + (Op.MSTORE(32 * i, query) for i, query in enumerate(queries)), + Bytecode(), + ) + + Op.RETURN(0, 32 * len(queries)) + ) + middle_address = pre.deploy_contract( + middle_call_opcode(gas=Op.GAS, address=leaf_address) + + Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE) + + Op.RETURN(0, Op.RETURNDATASIZE) + ) + top_address = pre.deploy_contract( + top_call_opcode(gas=Op.GAS, address=middle_address) + + Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE) + + sum( + (Op.SSTORE(i, Op.MLOAD(32 * i)) for i in range(len(queries))), + Bytecode(), + ), + storage=dict.fromkeys(range(len(queries)), sentinel), + ) + + # Replay the chain as (running account, CALLER) per frame. + frames: list[tuple[Address, Address]] = [(top_address, sender)] + for call_opcode, callee in ( + (top_call_opcode, middle_address), + (middle_call_opcode, leaf_address), + ): + address, caller = frames[-1] + if call_opcode == Op.DELEGATECALL: + frames.append((address, caller)) + elif call_opcode == Op.CALLCODE: + frames.append((address, address)) + else: + frames.append((callee, address)) + assert len(frames) == depth + 1 + answers: list[int | Address] = [ + depth, + *(frames[depth - n][1] if n <= depth else 0 for n in range(depth + 2)), + ] + + tx = Transaction(to=top_address, sender=sender) + + state_test( + pre=pre, + post={top_address: Account(storage=dict(enumerate(answers)))}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "code,setup", + [ + pytest.param(Op.CALLSTACKDEPTH, Bytecode(), id="CALLSTACKDEPTH"), + pytest.param(Op.CALLERN(0), Op.PUSH1(0), id="CALLERN"), + pytest.param( + Op.CALLERN(2**256 - 1), + Op.PUSH32(2**256 - 1), + id="CALLERN_out_of_range", + ), + ], +) +def test_gas_cost( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + code: Bytecode, + setup: Bytecode, +) -> None: + """ + Measure the gas of an extended opcode net of the `setup` of its + stack input: the EXTENSION prefix adds nothing and CALLERN costs the + same whatever its index. + """ + contract_address = pre.deploy_contract( + CodeGasMeasure( + code=code, + overhead_cost=setup.gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction(to=contract_address, sender=pre.fund_eoa()) + + state_test( + pre=pre, + post={ + contract_address: Account( + storage={ + slot_gas_measured: code.gas_cost(fork) + - setup.gas_cost(fork) + } + ) + }, + tx=tx, + ) diff --git a/tests/monad_next/mip7_extension_opcodes/__init__.py b/tests/monad_next/mip7_extension_opcodes/__init__.py new file mode 100644 index 0000000000..a858bee5c5 --- /dev/null +++ b/tests/monad_next/mip7_extension_opcodes/__init__.py @@ -0,0 +1 @@ +"""Tests of the EXTENSION opcode prefix and its selectors under MIP-7.""" diff --git a/tests/monad_next/mip7_extension_opcodes/spec.py b/tests/monad_next/mip7_extension_opcodes/spec.py new file mode 100644 index 0000000000..8811668e9b --- /dev/null +++ b/tests/monad_next/mip7_extension_opcodes/spec.py @@ -0,0 +1,16 @@ +"""Defines the MIP-7 extension opcodes specification reference.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_7 = ReferenceSpec( + "MIPS/MIP-7.md", "4504a0dc1637eb4b4b8cbab155cbf509a5f0c259" +) diff --git a/tests/monad_next/mip7_extension_opcodes/test_extension.py b/tests/monad_next/mip7_extension_opcodes/test_extension.py new file mode 100644 index 0000000000..2562311f06 --- /dev/null +++ b/tests/monad_next/mip7_extension_opcodes/test_extension.py @@ -0,0 +1,89 @@ +""" +Tests of the EXTENSION opcode prefix under MIP-7. + +MIP-7 gives EXTENSION a selector byte and leaves every selector that no +MIP assigns behaving like INVALID. The EIP-8163 suite, which the fork +is valid for, covers the selector-less EXTENSION, the selectors MIP-7 +excludes and the neutrality of the prefix to JUMPDEST analysis. +""" + +from typing import Generator + +import pytest +from _pytest.mark.structures import ParameterSet +from execution_testing import ( + Account, + Alloc, + CodeGasMeasure, + Fork, + Op, + StateTestFiller, + Transaction, +) + +from .spec import ref_spec_7 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7.git_path +REFERENCE_SPEC_VERSION = ref_spec_7.version + +pytestmark = pytest.mark.valid_from("MONAD_NEXT") + +slot_code_worked = 1 +slot_gas_measured = 2 +value_code_worked = 0x1234 +child_gas = 100_000 + + +def undefined_selectors(fork: Fork) -> Generator[ParameterSet, None, None]: + """Yield every selector byte the fork assigns no extended opcode to.""" + defined = {bytes(opcode)[1] for opcode in fork.extension_opcodes()} + for selector in range(256): + if selector not in defined: + yield pytest.param(selector, id=f"0x{selector:02x}") + + +@pytest.mark.parametrize_by_fork("selector", undefined_selectors) +@pytest.mark.parametrize("stack_item", [0, 1]) +def test_undefined_selector( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + selector: int, + stack_item: int, +) -> None: + """ + Execute EXTENSION with a selector no extended opcode is defined for. + + The frame halts like INVALID: its storage write is reverted and the + caller measures the whole forwarded gas as consumed. + """ + push = Op.PUSH0 if stack_item == 0 else Op.PUSH1(stack_item) + child_code = ( + Op.SSTORE(slot_code_worked, value_code_worked) + + push * 256 + + Op.EXTENSION + + bytes([selector]) + ) + child_address = pre.deploy_contract(code=child_code) + + call = Op.CALL(child_gas, child_address, 0, 0, 0, 0, 0, address_warm=False) + parent_address = pre.deploy_contract( + code=CodeGasMeasure( + code=call, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction(to=parent_address, sender=pre.fund_eoa()) + + state_test( + pre=pre, + post={ + child_address: Account(storage={}), + parent_address: Account( + storage={slot_gas_measured: call.gas_cost(fork) + child_gas} + ), + }, + tx=tx, + )