EEVM.Interpreter.MachineState (eevm v0.1.0)

Copy Markdown View Source

The EVM machine state — the envelope that holds everything that survives a single opcode step.

Anatomy

  • frame (Frame.t()) — the active per-call execution context: pc, stack, memory, gas, refund, code, return_data, contract, depth, is_static, parent return write-back location.
  • env (Env.t()) — read-only execution environment for this call: tx, block, hardfork config.
  • substate (Substate.t()) — transaction-scoped accumulators: touched / accessed / created / original / transient / logs.
  • db — unified world-state backend (accounts + storage).
  • call_stack — [Frame.t()] of suspended parent frames.
  • status — :running | :stopped | :reverted | :invalid | :out_of_gas or {:error, atom()}.

  • tracer — optional opcode-level trace recorder.

Frame, env, and substate isolate the three lifecycles cleanly: frame is per-call mutable, env is per-call immutable, substate is transaction-scoped.

Summary

Functions

Adds amount to the accumulated gas refund.

Advances the program counter by n positions.

Deducts gas from the active frame.

Returns the opcode byte at the current program counter, or nil if past end.

Returns the gas remaining.

Halts execution with the given status.

Creates a new machine state for executing the given bytecode.

Reads n bytes from the code starting at offset.

Subtracts amount from refund, flooring at 0.

Marks an address as touched for EIP-161 post-transaction cleanup.

Replaces the active frame using the given function.

Types

status()

@type status() ::
  :running | :stopped | :reverted | :invalid | :out_of_gas | {:error, atom()}

t()

@type t() :: %EEVM.Interpreter.MachineState{
  call_stack: [EEVM.Interpreter.MachineState.Frame.t()],
  db: EEVM.Database.t(),
  env: EEVM.Interpreter.MachineState.Env.t(),
  frame: EEVM.Interpreter.MachineState.Frame.t(),
  status: status(),
  substate: EEVM.Interpreter.MachineState.Substate.t(),
  tracer: EEVM.Tracer.t() | nil
}

Functions

add_refund(state, amount)

@spec add_refund(t(), non_neg_integer()) :: t()

Adds amount to the accumulated gas refund.

advance_pc(state, n \\ 1)

@spec advance_pc(t(), non_neg_integer()) :: t()

Advances the program counter by n positions.

consume_gas(state, cost)

@spec consume_gas(t(), non_neg_integer()) :: {:ok, t()} | {:error, :out_of_gas, t()}

Deducts gas from the active frame.

Returns {:ok, updated_state} if sufficient gas remains, or {:error, :out_of_gas, state} if the gas would go negative.

current_depth(machine_state)

@spec current_depth(t()) :: non_neg_integer()

current_opcode(arg1)

@spec current_opcode(t()) :: non_neg_integer() | nil

Returns the opcode byte at the current program counter, or nil if past end.

gas_remaining(machine_state)

@spec gas_remaining(t()) :: non_neg_integer()

Returns the gas remaining.

halt(state, status)

@spec halt(t(), status()) :: t()

Halts execution with the given status.

:stopped and :reverted keep the frame's remaining gas. Exceptional halts (:invalid, :out_of_gas, {:error, reason}) consume all of it and produce no return data.

new(code, opts \\ [])

@spec new(
  binary(),
  keyword()
) :: t()

Creates a new machine state for executing the given bytecode.

Includes EIP-2929/EIP-3651 access-list pre-warming so sender, recipient, precompiles, and block.coinbase start warm for address-access gas metering.

Parameters

  • code — the raw EVM bytecode as an Elixir binary
  • opts — optional keyword list:
    • :gas — initial gas (default: 1,000,000)
    • :db — unified external database (default: empty in-memory DB)
    • :storage — legacy initial storage (default: empty, backward-compat)
    • :tx — transaction context (default: empty)
    • :block — block context (default: empty)
    • :hardfork — hardfork spec id (e.g. :cancun, :berlin; default: :cancun)
    • :world_state — legacy external account state (default: empty, backward-compat)
    • :call_stack — internal frame stack (default: [])
    • :frame_return_offset — parent memory write-back offset (default: 0)
    • :frame_return_size — parent memory write-back size (default: 0)
    • :is_static — static context flag for this frame (default: false)
    • :depth — current call depth (default: 0)
    • :refund — gas refund counter (default: 0)
    • :tracer — optional EEVM.Tracer to record per-opcode trace (default: nil)

Example

iex> state = EEVM.Interpreter.MachineState.new(<<0x60, 0x01, 0x60, 0x02, 0x01>>)
iex> state.frame.pc
0

pop_frame(state)

@spec pop_frame(t()) :: {:ok, t()} | {:error, :empty_call_stack, t()}

push_frame(state, new_frame)

@spec push_frame(t(), EEVM.Interpreter.MachineState.Frame.t()) ::
  {:ok, t()} | {:error, :max_call_depth, t()}

read_code(machine_state, offset, n)

@spec read_code(t(), non_neg_integer(), non_neg_integer()) :: binary()

Reads n bytes from the code starting at offset.

Used by PUSH instructions to read their immediate data.

sub_refund(state, amount)

@spec sub_refund(t(), non_neg_integer()) :: t()

Subtracts amount from refund, flooring at 0.

touch_address(state, address)

@spec touch_address(t(), non_neg_integer()) :: t()

Marks an address as touched for EIP-161 post-transaction cleanup.

update_frame(state, fun)

Replaces the active frame using the given function.

Convenience for callers that want to update one or more per-frame fields without re-spelling the nested struct update.