diff --git a/lib/eevm/interpreter.ex b/lib/eevm/interpreter.ex index 987a94a..0747d15 100644 --- a/lib/eevm/interpreter.ex +++ b/lib/eevm/interpreter.ex @@ -29,9 +29,9 @@ 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. """ @@ -39,25 +39,7 @@ defmodule EEVM.Interpreter do 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 @@ -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. diff --git a/lib/eevm/interpreter/instructions/registry.ex b/lib/eevm/interpreter/instructions/registry.ex index fb0d15d..ffc97b0 100644 --- a/lib/eevm/interpreter/instructions/registry.ex +++ b/lib/eevm/interpreter/instructions/registry.ex @@ -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 @@ -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