Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/testing/src/execution_testing/cli/evm_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
27 changes: 24 additions & 3 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
72 changes: 72 additions & 0 deletions packages/testing/src/execution_testing/vm/opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 31 additions & 1 deletion packages/testing/src/execution_testing/vm/tests/test_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions src/ethereum/forks/monad_next/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/ethereum/forks/monad_next/vm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum/forks/monad_next/vm/gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/ethereum/forks/monad_next/vm/instructions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -202,6 +203,9 @@ class Ops(enum.Enum):
LOG3 = 0xA3
LOG4 = 0xA4

# Extension Prefix
EXTENSION = 0xAE

# System Operations
CREATE = 0xF0
CALL = 0xF1
Expand Down Expand Up @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions src/ethereum/forks/monad_next/vm/instructions/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions src/ethereum/forks/monad_next/vm/instructions/extension.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/monad_next/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MONAD_NEXT fork tests."""
Loading
Loading