Skip to content
Merged
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
110 changes: 23 additions & 87 deletions lib/eevm/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,17 @@ defmodule EEVM.Interpreter do
runs in constant stack space even for long-running contracts.
- The three `run_loop/1` clauses use pattern matching on `status` to cleanly
separate running, halted, and out-of-gas states without any conditionals.
- `execute_opcode/2` is a private dispatch table. Specific opcodes come first,
then ranges. Elixir matches clauses top-to-bottom, so narrower patterns
must precede broader ones.
- Opcode dispatch is data-driven: `EEVM.Interpreter.Instructions.Registry`
maps each opcode to its implementing module, and `execute_opcode/2` is a
single registry lookup rather than a hand-maintained case table.
- Separation of concerns: the executor only orchestrates; opcode modules own
their semantics.
"""

alias EEVM.Database
alias EEVM.Gas.Static
alias EEVM.HardforkConfig

alias EEVM.Interpreter.Instructions.{
Arithmetic,
Bitwise,
Comparison,
ControlFlow,
Crypto,
Environment.Data,
Environment.External,
Environment.Simple,
Logging,
StackMemoryStorage.MemoryOps,
StackMemoryStorage.StackOps,
StackMemoryStorage.StorageOps,
System.Calls,
System.Creation,
System.Termination
}

alias EEVM.Interpreter.Instructions.Registry
alias EEVM.Interpreter.{MachineState, Memory, Stack}
alias EEVM.Tracer
alias EEVM.Tracer.TraceStep
Expand Down Expand Up @@ -196,72 +178,26 @@ defmodule EEVM.Interpreter do

defp cleanup_touched_empty_accounts(state), do: state

# Dispatch table for execute_opcode/2.
# Dispatch an opcode byte to its implementing module via the Registry.
#
# Routes opcode bytes to the module that implements them. Specific opcodes
# are listed first; ranges follow. This ordering matters — Elixir matches
# clauses top-to-bottom, so e.g. 0x50 must appear before the 0x51..0x55
# range to ensure it is caught by its dedicated StackMemoryStorage clause.
# The fallback clause treats unknown opcodes as INVALID (halt, no gas refund).

defp execute_opcode(0x55, %{is_static: true} = state),
do: {:ok, MachineState.halt(state, :reverted)}

defp execute_opcode(0x5D, %{is_static: true} = state),
do: {:ok, MachineState.halt(state, :reverted)}

defp execute_opcode(op, %{is_static: true} = state) when op in 0xA0..0xA4,
do: {:ok, MachineState.halt(state, :reverted)}

defp execute_opcode(op, %{is_static: true} = state) when op in [0xF0, 0xF5],
do: {:ok, MachineState.halt(state, :reverted)}

defp execute_opcode(0xFF, %{is_static: true} = state),
do: {:ok, MachineState.halt(state, :reverted)}

defp execute_opcode(0x00, state), do: Termination.execute(0x00, state)
defp execute_opcode(op, state) when op in 0x01..0x0B, do: Arithmetic.execute(op, state)
defp execute_opcode(op, state) when op in 0x10..0x15, do: Comparison.execute(op, state)
defp execute_opcode(op, state) when op in 0x16..0x1D, do: Bitwise.execute(op, state)
defp execute_opcode(0x20, state), do: Crypto.execute(0x20, state)

defp execute_opcode(op, state) when op in [0x30, 0x32, 0x33, 0x34, 0x36, 0x38, 0x3A, 0x3D],
do: Simple.execute(op, state)

defp execute_opcode(op, state) when op in [0x35, 0x37, 0x39, 0x3E], do: Data.execute(op, state)

defp execute_opcode(op, state) when op in [0x31, 0x3B, 0x3C, 0x3F],
do: External.execute(op, state)

defp execute_opcode(op, state)
when op in [0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x48, 0x49, 0x4A],
do: Simple.execute(op, state)

defp execute_opcode(0x47, state), do: External.execute(0x47, state)
defp execute_opcode(0x50, state), do: StackOps.execute(0x50, state)

defp execute_opcode(op, state) when op in [0x51, 0x52, 0x53, 0x59, 0x5E],
do: MemoryOps.execute(op, state)

defp execute_opcode(op, state) when op in [0x54, 0x55, 0x5C, 0x5D],
do: StorageOps.execute(op, state)

defp execute_opcode(0x5A, state), do: Simple.execute(0x5A, state)
defp execute_opcode(op, state) when op in 0x56..0x5B, do: ControlFlow.execute(op, state)
defp execute_opcode(0x5F, state), do: ControlFlow.execute(0x5F, state)
defp execute_opcode(op, state) when op in 0x60..0x9F, do: ControlFlow.execute(op, state)
defp execute_opcode(op, state) when op in 0xA0..0xA4, do: Logging.execute(op, state)

defp execute_opcode(op, state) when op in [0xF0, 0xF5],
do: Creation.execute(op, state)

defp execute_opcode(op, state) when op in [0xF1, 0xF2, 0xF4, 0xFA],
do: Calls.execute(op, state)

defp execute_opcode(op, state) when op in [0xF3, 0xFD, 0xFE, 0xFF],
do: Termination.execute(op, state)

defp execute_opcode(_op, state), do: {:ok, MachineState.halt(state, :invalid)}
# The Registry maps each opcode to a `:module` (the `Instructions.*` module
# whose `execute/2` handles it) and a `:state_mutating` flag. Inside a
# STATICCALL frame (`state.is_static`), state-mutating opcodes (SSTORE,
# TSTORE, LOG0..4, CREATE, CREATE2, SELFDESTRUCT) halt with `:reverted`
# before reaching the module. Unknown opcodes halt as `:invalid`.

defp execute_opcode(opcode, state) do
case Registry.info(opcode) do
{:ok, %{state_mutating: true}} when state.is_static ->
{:ok, MachineState.halt(state, :reverted)}

{:ok, %{module: module}} ->
module.execute(opcode, state)

{:error, :unknown_opcode} ->
{:ok, MachineState.halt(state, :invalid)}
end
end

# Trace bookkeeping is a no-op when no tracer is attached — one pattern match,
# one return. All helpers below keep this fast path.
Expand Down
215 changes: 122 additions & 93 deletions lib/eevm/interpreter/instructions/registry.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
defmodule EEVM.Interpreter.Instructions.Registry do
@moduledoc """
Opcode metadata registry — maps opcode bytes to names and stack I/O signatures.
Opcode metadata registry — single source of truth for every opcode's name,
stack I/O signature, implementing module, and static-context flag.

This module is the single source of truth for opcode metadata. Given an opcode byte
(0x00–0xFF), it returns the human-readable name, the number of stack inputs consumed,
and the number of stack outputs produced. This information is used by the disassembler
and can be used to validate stack depth statically.
Given an opcode byte (0x00–0xFF) `info/1` returns:

- `:name` — the human-readable mnemonic (e.g. `"ADD"`)
- `:inputs` / `:outputs` — stack arity, used by the disassembler and stack-depth checks
- `:module` — the `EEVM.Interpreter.Instructions.*` module that implements
the opcode; `EEVM.Interpreter` dispatches on this field
- `:state_mutating` (when present) — opcode mutates persistent state and must
halt with `:reverted` if executed inside a STATICCALL frame

PUSH1–PUSH32, DUP1–DUP16, and SWAP1–SWAP16 are generated dynamically from their
opcode ranges rather than stored in the static map.
"""

alias EEVM.Interpreter.Instructions.{
Arithmetic,
Bitwise,
Comparison,
ControlFlow,
Crypto,
Environment.Data,
Environment.External,
Environment.Simple,
Logging,
StackMemoryStorage.MemoryOps,
StackMemoryStorage.StackOps,
StackMemoryStorage.StorageOps,
System.Calls,
System.Creation,
System.Termination
}

@stop 0x00
@add 0x01
@mul 0x02
Expand Down Expand Up @@ -111,107 +134,113 @@ defmodule EEVM.Interpreter.Instructions.Registry do
@invalid 0xFE

@opcodes %{
@stop => %{name: "STOP", inputs: 0, outputs: 0},
@add => %{name: "ADD", inputs: 2, outputs: 1},
@mul => %{name: "MUL", inputs: 2, outputs: 1},
@sub => %{name: "SUB", inputs: 2, outputs: 1},
@div_ => %{name: "DIV", inputs: 2, outputs: 1},
@sdiv => %{name: "SDIV", inputs: 2, outputs: 1},
@mod => %{name: "MOD", inputs: 2, outputs: 1},
@smod => %{name: "SMOD", inputs: 2, outputs: 1},
@addmod => %{name: "ADDMOD", inputs: 3, outputs: 1},
@mulmod => %{name: "MULMOD", inputs: 3, outputs: 1},
@exp => %{name: "EXP", inputs: 2, outputs: 1},
@signextend => %{name: "SIGNEXTEND", inputs: 2, outputs: 1},
@keccak256 => %{name: "KECCAK256", inputs: 2, outputs: 1},
@lt => %{name: "LT", inputs: 2, outputs: 1},
@gt => %{name: "GT", inputs: 2, outputs: 1},
@slt => %{name: "SLT", inputs: 2, outputs: 1},
@sgt => %{name: "SGT", inputs: 2, outputs: 1},
@eq => %{name: "EQ", inputs: 2, outputs: 1},
@iszero => %{name: "ISZERO", inputs: 1, outputs: 1},
@and_ => %{name: "AND", inputs: 2, outputs: 1},
@or_ => %{name: "OR", inputs: 2, outputs: 1},
@xor_ => %{name: "XOR", inputs: 2, outputs: 1},
@not_ => %{name: "NOT", inputs: 1, outputs: 1},
@byte_ => %{name: "BYTE", inputs: 2, outputs: 1},
@shl => %{name: "SHL", inputs: 2, outputs: 1},
@shr => %{name: "SHR", inputs: 2, outputs: 1},
@sar => %{name: "SAR", inputs: 2, outputs: 1},
@address => %{name: "ADDRESS", inputs: 0, outputs: 1},
@balance => %{name: "BALANCE", inputs: 1, outputs: 1},
@origin => %{name: "ORIGIN", inputs: 0, outputs: 1},
@caller => %{name: "CALLER", inputs: 0, outputs: 1},
@callvalue => %{name: "CALLVALUE", inputs: 0, outputs: 1},
@calldataload => %{name: "CALLDATALOAD", inputs: 1, outputs: 1},
@calldatasize => %{name: "CALLDATASIZE", inputs: 0, outputs: 1},
@calldatacopy => %{name: "CALLDATACOPY", inputs: 3, outputs: 0},
@codecopy => %{name: "CODECOPY", inputs: 3, outputs: 0},
@extcodecopy => %{name: "EXTCODECOPY", inputs: 4, outputs: 0},
@returndatacopy => %{name: "RETURNDATACOPY", inputs: 3, outputs: 0},
@codesize => %{name: "CODESIZE", inputs: 0, outputs: 1},
@extcodesize => %{name: "EXTCODESIZE", inputs: 1, outputs: 1},
@gasprice => %{name: "GASPRICE", inputs: 0, outputs: 1},
@returndatasize => %{name: "RETURNDATASIZE", inputs: 0, outputs: 1},
@extcodehash => %{name: "EXTCODEHASH", inputs: 1, outputs: 1},
@blockhash => %{name: "BLOCKHASH", inputs: 1, outputs: 1},
@coinbase => %{name: "COINBASE", inputs: 0, outputs: 1},
@timestamp => %{name: "TIMESTAMP", inputs: 0, outputs: 1},
@number => %{name: "NUMBER", inputs: 0, outputs: 1},
@prevrandao => %{name: "PREVRANDAO", inputs: 0, outputs: 1},
@gaslimit => %{name: "GASLIMIT", inputs: 0, outputs: 1},
@chainid => %{name: "CHAINID", inputs: 0, outputs: 1},
@selfbalance => %{name: "SELFBALANCE", inputs: 0, outputs: 1},
@basefee => %{name: "BASEFEE", inputs: 0, outputs: 1},
@blobhash => %{name: "BLOBHASH", inputs: 1, outputs: 1},
@blobbasefee => %{name: "BLOBBASEFEE", inputs: 0, outputs: 1},
@gas_ => %{name: "GAS", inputs: 0, outputs: 1},
@push0 => %{name: "PUSH0", inputs: 0, outputs: 1},
@pop => %{name: "POP", inputs: 1, outputs: 0},
@mload => %{name: "MLOAD", inputs: 1, outputs: 1},
@mstore => %{name: "MSTORE", inputs: 2, outputs: 0},
@mstore8 => %{name: "MSTORE8", inputs: 2, outputs: 0},
@sload => %{name: "SLOAD", inputs: 1, outputs: 1},
@sstore => %{name: "SSTORE", inputs: 2, outputs: 0},
@tload => %{name: "TLOAD", inputs: 1, outputs: 1},
@tstore => %{name: "TSTORE", inputs: 2, outputs: 0},
@msize => %{name: "MSIZE", inputs: 0, outputs: 1},
@mcopy => %{name: "MCOPY", inputs: 3, outputs: 0},
@jump => %{name: "JUMP", inputs: 1, outputs: 0},
@jumpi => %{name: "JUMPI", inputs: 2, outputs: 0},
@pc => %{name: "PC", inputs: 0, outputs: 1},
@jumpdest => %{name: "JUMPDEST", inputs: 0, outputs: 0},
@log0 => %{name: "LOG0", inputs: 2, outputs: 0},
@log1 => %{name: "LOG1", inputs: 3, outputs: 0},
@log2 => %{name: "LOG2", inputs: 4, outputs: 0},
@log3 => %{name: "LOG3", inputs: 5, outputs: 0},
@log4 => %{name: "LOG4", inputs: 6, outputs: 0},
@create => %{name: "CREATE", inputs: 3, outputs: 1},
@call => %{name: "CALL", inputs: 7, outputs: 1},
@callcode => %{name: "CALLCODE", inputs: 7, outputs: 1},
@delegatecall => %{name: "DELEGATECALL", inputs: 6, outputs: 1},
@create2 => %{name: "CREATE2", inputs: 4, outputs: 1},
@staticcall => %{name: "STATICCALL", inputs: 6, outputs: 1},
@return_ => %{name: "RETURN", inputs: 2, outputs: 0},
@revert => %{name: "REVERT", inputs: 2, outputs: 0},
@invalid => %{name: "INVALID", inputs: 0, outputs: 0},
@selfdestruct => %{name: "SELFDESTRUCT", inputs: 1, outputs: 0}
@stop => %{name: "STOP", inputs: 0, outputs: 0, module: Termination},
@add => %{name: "ADD", inputs: 2, outputs: 1, module: Arithmetic},
@mul => %{name: "MUL", inputs: 2, outputs: 1, module: Arithmetic},
@sub => %{name: "SUB", inputs: 2, outputs: 1, module: Arithmetic},
@div_ => %{name: "DIV", inputs: 2, outputs: 1, module: Arithmetic},
@sdiv => %{name: "SDIV", inputs: 2, outputs: 1, module: Arithmetic},
@mod => %{name: "MOD", inputs: 2, outputs: 1, module: Arithmetic},
@smod => %{name: "SMOD", inputs: 2, outputs: 1, module: Arithmetic},
@addmod => %{name: "ADDMOD", inputs: 3, outputs: 1, module: Arithmetic},
@mulmod => %{name: "MULMOD", inputs: 3, outputs: 1, module: Arithmetic},
@exp => %{name: "EXP", inputs: 2, outputs: 1, module: Arithmetic},
@signextend => %{name: "SIGNEXTEND", inputs: 2, outputs: 1, module: Arithmetic},
@keccak256 => %{name: "KECCAK256", inputs: 2, outputs: 1, module: Crypto},
@lt => %{name: "LT", inputs: 2, outputs: 1, module: Comparison},
@gt => %{name: "GT", inputs: 2, outputs: 1, module: Comparison},
@slt => %{name: "SLT", inputs: 2, outputs: 1, module: Comparison},
@sgt => %{name: "SGT", inputs: 2, outputs: 1, module: Comparison},
@eq => %{name: "EQ", inputs: 2, outputs: 1, module: Comparison},
@iszero => %{name: "ISZERO", inputs: 1, outputs: 1, module: Comparison},
@and_ => %{name: "AND", inputs: 2, outputs: 1, module: Bitwise},
@or_ => %{name: "OR", inputs: 2, outputs: 1, module: Bitwise},
@xor_ => %{name: "XOR", inputs: 2, outputs: 1, module: Bitwise},
@not_ => %{name: "NOT", inputs: 1, outputs: 1, module: Bitwise},
@byte_ => %{name: "BYTE", inputs: 2, outputs: 1, module: Bitwise},
@shl => %{name: "SHL", inputs: 2, outputs: 1, module: Bitwise},
@shr => %{name: "SHR", inputs: 2, outputs: 1, module: Bitwise},
@sar => %{name: "SAR", inputs: 2, outputs: 1, module: Bitwise},
@address => %{name: "ADDRESS", inputs: 0, outputs: 1, module: Simple},
@balance => %{name: "BALANCE", inputs: 1, outputs: 1, module: External},
@origin => %{name: "ORIGIN", inputs: 0, outputs: 1, module: Simple},
@caller => %{name: "CALLER", inputs: 0, outputs: 1, module: Simple},
@callvalue => %{name: "CALLVALUE", inputs: 0, outputs: 1, module: Simple},
@calldataload => %{name: "CALLDATALOAD", inputs: 1, outputs: 1, module: Data},
@calldatasize => %{name: "CALLDATASIZE", inputs: 0, outputs: 1, module: Simple},
@calldatacopy => %{name: "CALLDATACOPY", inputs: 3, outputs: 0, module: Data},
@codecopy => %{name: "CODECOPY", inputs: 3, outputs: 0, module: Data},
@extcodecopy => %{name: "EXTCODECOPY", inputs: 4, outputs: 0, module: External},
@returndatacopy => %{name: "RETURNDATACOPY", inputs: 3, outputs: 0, module: Data},
@codesize => %{name: "CODESIZE", inputs: 0, outputs: 1, module: Simple},
@extcodesize => %{name: "EXTCODESIZE", inputs: 1, outputs: 1, module: External},
@gasprice => %{name: "GASPRICE", inputs: 0, outputs: 1, module: Simple},
@returndatasize => %{name: "RETURNDATASIZE", inputs: 0, outputs: 1, module: Simple},
@extcodehash => %{name: "EXTCODEHASH", inputs: 1, outputs: 1, module: External},
@blockhash => %{name: "BLOCKHASH", inputs: 1, outputs: 1, module: Simple},
@coinbase => %{name: "COINBASE", inputs: 0, outputs: 1, module: Simple},
@timestamp => %{name: "TIMESTAMP", inputs: 0, outputs: 1, module: Simple},
@number => %{name: "NUMBER", inputs: 0, outputs: 1, module: Simple},
@prevrandao => %{name: "PREVRANDAO", inputs: 0, outputs: 1, module: Simple},
@gaslimit => %{name: "GASLIMIT", inputs: 0, outputs: 1, module: Simple},
@chainid => %{name: "CHAINID", inputs: 0, outputs: 1, module: Simple},
@selfbalance => %{name: "SELFBALANCE", inputs: 0, outputs: 1, module: External},
@basefee => %{name: "BASEFEE", inputs: 0, outputs: 1, module: Simple},
@blobhash => %{name: "BLOBHASH", inputs: 1, outputs: 1, module: Simple},
@blobbasefee => %{name: "BLOBBASEFEE", inputs: 0, outputs: 1, module: Simple},
@gas_ => %{name: "GAS", inputs: 0, outputs: 1, module: Simple},
@push0 => %{name: "PUSH0", inputs: 0, outputs: 1, module: ControlFlow},
@pop => %{name: "POP", inputs: 1, outputs: 0, module: StackOps},
@mload => %{name: "MLOAD", inputs: 1, outputs: 1, module: MemoryOps},
@mstore => %{name: "MSTORE", inputs: 2, outputs: 0, module: MemoryOps},
@mstore8 => %{name: "MSTORE8", inputs: 2, outputs: 0, module: MemoryOps},
@sload => %{name: "SLOAD", inputs: 1, outputs: 1, module: StorageOps},
@sstore => %{name: "SSTORE", inputs: 2, outputs: 0, module: StorageOps, state_mutating: true},
@tload => %{name: "TLOAD", inputs: 1, outputs: 1, module: StorageOps},
@tstore => %{name: "TSTORE", inputs: 2, outputs: 0, module: StorageOps, state_mutating: true},
@msize => %{name: "MSIZE", inputs: 0, outputs: 1, module: MemoryOps},
@mcopy => %{name: "MCOPY", inputs: 3, outputs: 0, module: MemoryOps},
@jump => %{name: "JUMP", inputs: 1, outputs: 0, module: ControlFlow},
@jumpi => %{name: "JUMPI", inputs: 2, outputs: 0, module: ControlFlow},
@pc => %{name: "PC", inputs: 0, outputs: 1, module: ControlFlow},
@jumpdest => %{name: "JUMPDEST", inputs: 0, outputs: 0, module: ControlFlow},
@log0 => %{name: "LOG0", inputs: 2, outputs: 0, module: Logging, state_mutating: true},
@log1 => %{name: "LOG1", inputs: 3, outputs: 0, module: Logging, state_mutating: true},
@log2 => %{name: "LOG2", inputs: 4, outputs: 0, module: Logging, state_mutating: true},
@log3 => %{name: "LOG3", inputs: 5, outputs: 0, module: Logging, state_mutating: true},
@log4 => %{name: "LOG4", inputs: 6, outputs: 0, module: Logging, state_mutating: true},
@create => %{name: "CREATE", inputs: 3, outputs: 1, module: Creation, state_mutating: true},
@call => %{name: "CALL", inputs: 7, outputs: 1, module: Calls},
@callcode => %{name: "CALLCODE", inputs: 7, outputs: 1, module: Calls},
@delegatecall => %{name: "DELEGATECALL", inputs: 6, outputs: 1, module: Calls},
@create2 => %{name: "CREATE2", inputs: 4, outputs: 1, module: Creation, state_mutating: true},
@staticcall => %{name: "STATICCALL", inputs: 6, outputs: 1, module: Calls},
@return_ => %{name: "RETURN", inputs: 2, outputs: 0, module: Termination},
@revert => %{name: "REVERT", inputs: 2, outputs: 0, module: Termination},
@invalid => %{name: "INVALID", inputs: 0, outputs: 0, module: Termination},
@selfdestruct => %{
name: "SELFDESTRUCT",
inputs: 1,
outputs: 0,
module: Termination,
state_mutating: true
}
}

@spec info(non_neg_integer()) :: {:ok, map()} | {:error, :unknown_opcode}
def info(op) when op >= @push1 and op <= @push32 do
n = op - @push1 + 1
{:ok, %{name: "PUSH#{n}", inputs: 0, outputs: 1, push_bytes: n}}
{:ok, %{name: "PUSH#{n}", inputs: 0, outputs: 1, push_bytes: n, module: ControlFlow}}
end

def info(op) when op >= @dup1 and op <= @dup16 do
n = op - @dup1 + 1
{:ok, %{name: "DUP#{n}", inputs: n, outputs: n + 1, dup_depth: n - 1}}
{:ok, %{name: "DUP#{n}", inputs: n, outputs: n + 1, dup_depth: n - 1, module: ControlFlow}}
end

def info(op) when op >= @swap1 and op <= @swap16 do
n = op - @swap1 + 1
{:ok, %{name: "SWAP#{n}", inputs: n + 1, outputs: n + 1, swap_depth: n}}
{:ok, %{name: "SWAP#{n}", inputs: n + 1, outputs: n + 1, swap_depth: n, module: ControlFlow}}
end

def info(op) do
Expand Down
Loading