From af4d6acd193af3a1339bcbac028e6d4e68b404eb Mon Sep 17 00:00:00 2001 From: mw2000 Date: Thu, 30 Apr 2026 22:42:40 -0700 Subject: [PATCH 1/3] refactor(machine_state): extract Substate struct (phase 1 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the seven transaction-scoped accumulator fields out of MachineState into a dedicated Substate struct: touched_addresses, accessed_addresses, accessed_storage_keys, created_addresses, original_storage, transient_storage, logs. These all share the same lifecycle — accumulate across successful child frames, revert together on failure — so grouping them simplifies Journal.merge_child_result and clarifies what is and isn't substate per the Yellow Paper. No behavioural change: every field keeps its name and semantics; only the access path moves from state.X to state.substate.X. Co-Authored-By: Claude Opus 4.7 --- lib/eevm.ex | 4 +- lib/eevm/gas/access.ex | 21 ++++--- lib/eevm/handler/post_execution.ex | 2 +- lib/eevm/interpreter.ex | 2 +- lib/eevm/interpreter/instructions/logging.ex | 5 +- .../stack_memory_storage/storage_ops.ex | 26 +++++--- .../interpreter/instructions/system/calls.ex | 16 ++--- .../instructions/system/creation.ex | 19 +++--- .../instructions/system/termination.ex | 4 +- lib/eevm/interpreter/journal.ex | 26 +++----- lib/eevm/interpreter/machine_state.ex | 40 ++++++------ .../interpreter/machine_state/substate.ex | 63 +++++++++++++++++++ test/interpreter/journal_test.exs | 55 +++++++++------- .../machine_state/eip_3651_test.exs | 14 ++--- test/support/blockchain_test_runner.ex | 2 +- test/support/state_test_runner.ex | 2 +- 16 files changed, 190 insertions(+), 111 deletions(-) create mode 100644 lib/eevm/interpreter/machine_state/substate.ex diff --git a/lib/eevm.ex b/lib/eevm.ex index f8be713..3b7df50 100644 --- a/lib/eevm.ex +++ b/lib/eevm.ex @@ -84,12 +84,12 @@ defmodule EEVM do @doc "Returns the list of logs emitted during execution." @spec logs(MachineState.t()) :: [map()] - def logs(%MachineState{} = state), do: state.logs + def logs(%MachineState{} = state), do: state.substate.logs @doc "Computes the 256-byte logs bloom filter for the executed transaction." @spec logs_bloom(MachineState.t()) :: Bloom.t() def logs_bloom(%MachineState{} = state) do - Bloom.from_logs(state.logs) + Bloom.from_logs(state.substate.logs) end @doc """ diff --git a/lib/eevm/gas/access.ex b/lib/eevm/gas/access.ex index bcc0963..63c6196 100644 --- a/lib/eevm/gas/access.ex +++ b/lib/eevm/gas/access.ex @@ -37,11 +37,13 @@ defmodule EEVM.Gas.Access do {non_neg_integer(), MachineState.t()} def address_access_cost(state, address) do if HardforkConfig.enabled?(state.config.hardfork, :eip_2929) do - if MapSet.member?(state.accessed_addresses, address) do + sub = state.substate + + if MapSet.member?(sub.accessed_addresses, address) do {@warm_storage_read_cost, state} else - new_state = %{state | accessed_addresses: MapSet.put(state.accessed_addresses, address)} - {@cold_account_access_cost, new_state} + new_sub = %{sub | accessed_addresses: MapSet.put(sub.accessed_addresses, address)} + {@cold_account_access_cost, %{state | substate: new_sub}} end else # Pre-EIP-2929 (pre-Berlin): flat warm cost — the static cost table already @@ -55,12 +57,13 @@ defmodule EEVM.Gas.Access do def storage_access_cost(state, address, slot) do if HardforkConfig.enabled?(state.config.hardfork, :eip_2929) do key = {address, slot} + sub = state.substate - if MapSet.member?(state.accessed_storage_keys, key) do + if MapSet.member?(sub.accessed_storage_keys, key) do {@warm_storage_read_cost, state} else - new_state = %{state | accessed_storage_keys: MapSet.put(state.accessed_storage_keys, key)} - {@cold_sload_cost, new_state} + new_sub = %{sub | accessed_storage_keys: MapSet.put(sub.accessed_storage_keys, key)} + {@cold_sload_cost, %{state | substate: new_sub}} end else # Pre-EIP-2929: flat warm cost (pre-Berlin SLOAD was 800, but our static costs @@ -70,10 +73,12 @@ defmodule EEVM.Gas.Access do end @spec warm_address?(MachineState.t(), non_neg_integer()) :: boolean() - def warm_address?(state, address), do: MapSet.member?(state.accessed_addresses, address) + def warm_address?(state, address), + do: MapSet.member?(state.substate.accessed_addresses, address) @spec mark_address_warm(MachineState.t(), non_neg_integer()) :: MachineState.t() def mark_address_warm(state, address) do - %{state | accessed_addresses: MapSet.put(state.accessed_addresses, address)} + sub = state.substate + %{state | substate: %{sub | accessed_addresses: MapSet.put(sub.accessed_addresses, address)}} end end diff --git a/lib/eevm/handler/post_execution.ex b/lib/eevm/handler/post_execution.ex index 5b83b1e..7d4fcb0 100644 --- a/lib/eevm/handler/post_execution.ex +++ b/lib/eevm/handler/post_execution.ex @@ -47,7 +47,7 @@ defmodule EEVM.Handler.PostExecution do status = result_status(final_state.status) - logs = if status == :success, do: final_state.logs, else: [] + logs = if status == :success, do: final_state.substate.logs, else: [] logs_bloom = Bloom.from_logs(logs) receipt_status = if status == :success, do: 1, else: 0 diff --git a/lib/eevm/interpreter.ex b/lib/eevm/interpreter.ex index 0747d15..c9dafd1 100644 --- a/lib/eevm/interpreter.ex +++ b/lib/eevm/interpreter.ex @@ -165,7 +165,7 @@ defmodule EEVM.Interpreter do defp cleanup_touched_empty_accounts(%MachineState{status: :stopped} = state) do cleaned_db = - Enum.reduce(state.touched_addresses, state.db, fn address, db_acc -> + Enum.reduce(state.substate.touched_addresses, state.db, fn address, db_acc -> if Database.account_empty?(db_acc, address) do Database.delete_account(db_acc, address) else diff --git a/lib/eevm/interpreter/instructions/logging.ex b/lib/eevm/interpreter/instructions/logging.ex index 40fe381..2bda30a 100644 --- a/lib/eevm/interpreter/instructions/logging.ex +++ b/lib/eevm/interpreter/instructions/logging.ex @@ -13,7 +13,7 @@ defmodule EEVM.Interpreter.Instructions.Logging do ## Elixir Learning Notes - We use pattern matching on the topic count (0–4) to dispatch LOG variants. - - Logs accumulate in `state.logs` as a list of maps, preserving insertion order. + - Logs accumulate in `state.substate.logs` as a list of maps, preserving insertion order. - `binary_part/3` extracts a slice from a binary — used to read log data from memory bytes. """ @@ -52,7 +52,8 @@ defmodule EEVM.Interpreter.Instructions.Logging do topics: topics } - s5 = %{s4 | memory: new_memory, logs: s4.logs ++ [log_entry]} + new_substate = %{s4.substate | logs: s4.substate.logs ++ [log_entry]} + s5 = %{s4 | memory: new_memory, substate: new_substate} {:ok, MachineState.advance_pc(s5)} {:error, :out_of_gas, halted_state} -> diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex index fb20331..4b7227d 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex @@ -60,17 +60,20 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do state_after_stack = %{state | stack: s2} contract_address = state_after_stack.contract.address access_key = {contract_address, key} - is_warm = MapSet.member?(state_after_stack.accessed_storage_keys, access_key) + is_warm = MapSet.member?(state_after_stack.substate.accessed_storage_keys, access_key) state_after_access = if is_warm do state_after_stack else - %{ - state_after_stack - | accessed_storage_keys: - MapSet.put(state_after_stack.accessed_storage_keys, access_key) + sub = state_after_stack.substate + + new_sub = %{ + sub + | accessed_storage_keys: MapSet.put(sub.accessed_storage_keys, access_key) } + + %{state_after_stack | substate: new_sub} end cold_cost = if is_warm, do: 0, else: @cold_sload_cost @@ -99,7 +102,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do def execute(0x5C, %{config: %{hardfork: hardfork}} = state) do if HardforkConfig.enabled?(hardfork, :eip_1153) do with {:ok, key, s1} <- Stack.pop(state.stack), - value = Map.get(state.transient_storage, key, 0), + value = Map.get(state.substate.transient_storage, key, 0), {:ok, s2} <- Stack.push(s1, value) do {:ok, %{state | stack: s2} |> MachineState.advance_pc()} else @@ -117,8 +120,9 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do if HardforkConfig.enabled?(hardfork, :eip_1153) do with {:ok, key, s1} <- Stack.pop(state.stack), {:ok, value, s2} <- Stack.pop(s1) do - new_transient = Map.put(state.transient_storage, key, value) - {:ok, %{state | stack: s2, transient_storage: new_transient} |> MachineState.advance_pc()} + sub = state.substate + new_sub = %{sub | transient_storage: Map.put(sub.transient_storage, key, value)} + {:ok, %{state | stack: s2, substate: new_sub} |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -130,13 +134,15 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do def execute(_opcode, state), do: {:ok, MachineState.halt(state, :invalid)} defp get_original_value(state, key) do - case Map.fetch(state.original_storage, key) do + case Map.fetch(state.substate.original_storage, key) do {:ok, value} -> {value, state} :error -> value = Database.storage_load(state.db, state.contract.address, key) - {value, %{state | original_storage: Map.put(state.original_storage, key, value)}} + sub = state.substate + new_sub = %{sub | original_storage: Map.put(sub.original_storage, key, value)} + {value, %{state | substate: new_sub}} end end diff --git a/lib/eevm/interpreter/instructions/system/calls.ex b/lib/eevm/interpreter/instructions/system/calls.ex index 7468f4b..b0801f0 100644 --- a/lib/eevm/interpreter/instructions/system/calls.ex +++ b/lib/eevm/interpreter/instructions/system/calls.ex @@ -245,10 +245,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do block: state_after_touch.block, contract: child_contract, config: state_after_touch.config, - touched_addresses: state_after_touch.touched_addresses, - accessed_addresses: state_after_touch.accessed_addresses, - accessed_storage_keys: state_after_touch.accessed_storage_keys, - created_addresses: state_after_touch.created_addresses, + touched_addresses: state_after_touch.substate.touched_addresses, + accessed_addresses: state_after_touch.substate.accessed_addresses, + accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, + created_addresses: state_after_touch.substate.created_addresses, is_static: state_after_touch.is_static, depth: state_after_touch.depth + 1, tracer: state_after_touch.tracer @@ -365,10 +365,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do block: state_after_touch.block, contract: child_contract, config: state_after_touch.config, - touched_addresses: state_after_touch.touched_addresses, - accessed_addresses: state_after_touch.accessed_addresses, - accessed_storage_keys: state_after_touch.accessed_storage_keys, - created_addresses: state_after_touch.created_addresses, + touched_addresses: state_after_touch.substate.touched_addresses, + accessed_addresses: state_after_touch.substate.accessed_addresses, + accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, + created_addresses: state_after_touch.substate.created_addresses, is_static: state_after_touch.is_static, depth: state_after_touch.depth + 1, tracer: state_after_touch.tracer diff --git a/lib/eevm/interpreter/instructions/system/creation.ex b/lib/eevm/interpreter/instructions/system/creation.ex index 4a8340e..9c37c9f 100644 --- a/lib/eevm/interpreter/instructions/system/creation.ex +++ b/lib/eevm/interpreter/instructions/system/creation.ex @@ -123,10 +123,10 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do block: state_after_touch.block, contract: child_contract, config: state_after_touch.config, - touched_addresses: state_after_touch.touched_addresses, - accessed_addresses: state_after_touch.accessed_addresses, - accessed_storage_keys: state_after_touch.accessed_storage_keys, - created_addresses: state_after_touch.created_addresses, + touched_addresses: state_after_touch.substate.touched_addresses, + accessed_addresses: state_after_touch.substate.accessed_addresses, + accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, + created_addresses: state_after_touch.substate.created_addresses, is_static: state_after_touch.is_static, depth: state_after_touch.depth + 1, tracer: state_after_touch.tracer @@ -170,15 +170,18 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do merged = Journal.merge_child_result(state_after_initcode_cost, child_result) + merged_substate = %{ + merged.substate + | created_addresses: + MapSet.put(merged.substate.created_addresses, new_address) + } + {:ok, merged |> Map.put(:stack, stack_after_create) |> Map.put(:memory, memory_after_read) |> Map.put(:db, db_after_deploy) - |> Map.put( - :created_addresses, - MapSet.put(merged.created_addresses, new_address) - ) + |> Map.put(:substate, merged_substate) |> Map.put(:gas, child_result.gas - deposit_cost) |> Map.put(:return_data, child_result.return_data) |> MachineState.advance_pc()} diff --git a/lib/eevm/interpreter/instructions/system/termination.ex b/lib/eevm/interpreter/instructions/system/termination.ex index 6020595..9d8050b 100644 --- a/lib/eevm/interpreter/instructions/system/termination.ex +++ b/lib/eevm/interpreter/instructions/system/termination.ex @@ -73,7 +73,9 @@ defmodule EEVM.Interpreter.Instructions.System.Termination do # the contract was created in the same transaction. Pre-Cancun, every # SELFDESTRUCT unconditionally deletes the account and transfers the balance. eip_6780_active? = HardforkConfig.enabled?(state_after_touch.config.hardfork, :eip_6780) - created_this_tx = MapSet.member?(state_after_touch.created_addresses, contract_address) + + created_this_tx = + MapSet.member?(state_after_touch.substate.created_addresses, contract_address) full_delete? = not eip_6780_active? or created_this_tx diff --git a/lib/eevm/interpreter/journal.ex b/lib/eevm/interpreter/journal.ex index 9231356..9fd7601 100644 --- a/lib/eevm/interpreter/journal.ex +++ b/lib/eevm/interpreter/journal.ex @@ -10,15 +10,15 @@ defmodule EEVM.Interpreter.Journal do halt, those mutations must be discarded — the parent must observe the world state it had before the child was spawned. - Concretely, six fields of `EEVM.Interpreter.MachineState` participate in - this revert behaviour: + Concretely, the `db` field and the entire `substate` struct on + `EEVM.Interpreter.MachineState` participate in this revert behaviour: - `db` — the unified world database (accounts + storage) - - `logs` — emitted log entries (parent's logs come first, then child's) - - `accessed_addresses` — EIP-2929 access list - - `accessed_storage_keys` — EIP-2929 access list - - `created_addresses` — EIP-6780 set of contracts created in this tx - - `touched_addresses` — EIP-161 cleanup set + - `substate.logs` — emitted log entries (parent's first, then child's) + - `substate.accessed_addresses` — EIP-2929 access list + - `substate.accessed_storage_keys` — EIP-2929 access list + - `substate.created_addresses` — EIP-6780 set of contracts created in this tx + - `substate.touched_addresses` — EIP-161 cleanup set The `tracer` field is *not* revertible — trace events accumulate monotonically and are always preserved across child boundaries regardless @@ -60,16 +60,8 @@ defmodule EEVM.Interpreter.Journal do """ @spec merge_child_result(MachineState.t(), MachineState.t()) :: MachineState.t() def merge_child_result(%MachineState{} = parent, %MachineState{status: :stopped} = child) do - %{ - parent - | db: child.db, - logs: parent.logs ++ child.logs, - accessed_addresses: child.accessed_addresses, - accessed_storage_keys: child.accessed_storage_keys, - created_addresses: child.created_addresses, - touched_addresses: child.touched_addresses, - tracer: child.tracer - } + merged_substate = %{child.substate | logs: parent.substate.logs ++ child.substate.logs} + %{parent | db: child.db, substate: merged_substate, tracer: child.tracer} end def merge_child_result(%MachineState{} = parent, %MachineState{} = child) do diff --git a/lib/eevm/interpreter/machine_state.ex b/lib/eevm/interpreter/machine_state.ex index a5e087f..d1da64f 100644 --- a/lib/eevm/interpreter/machine_state.ex +++ b/lib/eevm/interpreter/machine_state.ex @@ -30,6 +30,7 @@ defmodule EEVM.Interpreter.MachineState do alias EEVM.Database alias EEVM.Database.InMemory, as: InMemoryDB alias EEVM.Interpreter.{CallFrame, Memory, Stack} + alias EEVM.Interpreter.MachineState.Substate alias EEVM.Precompiles alias EEVM.Tracer @@ -40,16 +41,11 @@ defmodule EEVM.Interpreter.MachineState do stack: Stack.t(), memory: Memory.t(), db: Database.t(), - original_storage: %{non_neg_integer() => non_neg_integer()}, - transient_storage: %{non_neg_integer() => non_neg_integer()}, + substate: Substate.t(), tx: Transaction.t(), block: Block.t(), contract: Contract.t(), config: Config.t(), - touched_addresses: MapSet.t(non_neg_integer()), - accessed_addresses: MapSet.t(non_neg_integer()), - accessed_storage_keys: MapSet.t({non_neg_integer(), non_neg_integer()}), - created_addresses: MapSet.t(non_neg_integer()), call_stack: [CallFrame.t()], frame_return_offset: non_neg_integer(), frame_return_size: non_neg_integer(), @@ -59,7 +55,6 @@ defmodule EEVM.Interpreter.MachineState do refund: non_neg_integer(), status: status(), return_data: binary(), - logs: [%{address: non_neg_integer(), data: binary(), topics: [non_neg_integer()]}], code: binary(), tracer: Tracer.t() | nil } @@ -69,16 +64,11 @@ defmodule EEVM.Interpreter.MachineState do stack: nil, memory: nil, db: nil, - original_storage: %{}, - transient_storage: %{}, + substate: nil, tx: nil, block: nil, contract: nil, config: nil, - touched_addresses: nil, - accessed_addresses: nil, - accessed_storage_keys: nil, - created_addresses: nil, call_stack: [], frame_return_offset: 0, frame_return_size: 0, @@ -88,7 +78,6 @@ defmodule EEVM.Interpreter.MachineState do refund: 0, status: :running, return_data: <<>>, - logs: [], code: <<>>, tracer: nil @@ -129,22 +118,28 @@ defmodule EEVM.Interpreter.MachineState do block = Keyword.get(opts, :block, Block.new()) config = Keyword.get(opts, :config, Config.new(Keyword.get(opts, :hardfork, :cancun))) + substate = + Substate.new( + touched_addresses: Keyword.get(opts, :touched_addresses, MapSet.new()), + accessed_addresses: + Keyword.get(opts, :accessed_addresses, pre_warm_addresses(contract, tx, block, config)), + accessed_storage_keys: Keyword.get(opts, :accessed_storage_keys, MapSet.new()), + created_addresses: Keyword.get(opts, :created_addresses, MapSet.new()), + original_storage: Keyword.get(opts, :original_storage, %{}), + transient_storage: Keyword.get(opts, :transient_storage, %{}), + logs: Keyword.get(opts, :logs, []) + ) + %__MODULE__{ code: code, stack: Stack.new(), memory: Memory.new(), db: init_db(opts, contract), - original_storage: Keyword.get(opts, :original_storage, %{}), - transient_storage: Keyword.get(opts, :transient_storage, %{}), + substate: substate, tx: tx, block: block, contract: contract, config: config, - touched_addresses: Keyword.get(opts, :touched_addresses, MapSet.new()), - accessed_addresses: - Keyword.get(opts, :accessed_addresses, pre_warm_addresses(contract, tx, block, config)), - accessed_storage_keys: Keyword.get(opts, :accessed_storage_keys, MapSet.new()), - created_addresses: Keyword.get(opts, :created_addresses, MapSet.new()), call_stack: Keyword.get(opts, :call_stack, []), frame_return_offset: Keyword.get(opts, :frame_return_offset, 0), frame_return_size: Keyword.get(opts, :frame_return_size, 0), @@ -341,7 +336,8 @@ defmodule EEVM.Interpreter.MachineState do @doc "Marks an address as touched for EIP-161 post-transaction cleanup." @spec touch_address(t(), non_neg_integer()) :: t() def touch_address(state, address) do - %{state | touched_addresses: MapSet.put(state.touched_addresses, address)} + sub = state.substate + %{state | substate: %{sub | touched_addresses: MapSet.put(sub.touched_addresses, address)}} end defp write_return_data(memory, _offset, 0, return_data), do: {memory, return_data} diff --git a/lib/eevm/interpreter/machine_state/substate.ex b/lib/eevm/interpreter/machine_state/substate.ex new file mode 100644 index 0000000..db8cbdf --- /dev/null +++ b/lib/eevm/interpreter/machine_state/substate.ex @@ -0,0 +1,63 @@ +defmodule EEVM.Interpreter.MachineState.Substate do + @moduledoc """ + Transaction-scoped accumulators that survive nested call frames. + + ## EVM Concepts + + The Yellow Paper calls this "substate" — fields that accumulate across + successful call frames within a single transaction and are discarded + on revert. Concretely: + + - `touched_addresses` — EIP-161 cleanup set; empty accounts touched during + the transaction are deleted on commit. + - `accessed_addresses` / `accessed_storage_keys` — EIP-2929 access lists + powering cold/warm gas pricing. + - `created_addresses` — EIP-6780 set of contracts created in this tx; + only these can be fully deleted by `SELFDESTRUCT` post-Cancun. + - `original_storage` — per-tx snapshot of the first-observed value of + each storage slot, used by EIP-2200 `SSTORE` accounting. + - `transient_storage` — EIP-1153 per-tx key/value store cleared at tx end. + - `logs` — emitted log entries, parent-then-child on success. + + All seven fields revert together when a child frame fails (see + `EEVM.Interpreter.Journal`). Stack, memory, pc, and gas are *not* + substate — they belong to the per-frame execution context. + """ + + @type log_entry :: %{ + address: non_neg_integer(), + data: binary(), + topics: [non_neg_integer()] + } + + @type t :: %__MODULE__{ + touched_addresses: MapSet.t(non_neg_integer()), + accessed_addresses: MapSet.t(non_neg_integer()), + accessed_storage_keys: MapSet.t({non_neg_integer(), non_neg_integer()}), + created_addresses: MapSet.t(non_neg_integer()), + original_storage: %{non_neg_integer() => non_neg_integer()}, + transient_storage: %{non_neg_integer() => non_neg_integer()}, + logs: [log_entry()] + } + + defstruct touched_addresses: nil, + accessed_addresses: nil, + accessed_storage_keys: nil, + created_addresses: nil, + original_storage: %{}, + transient_storage: %{}, + logs: [] + + @spec new(keyword()) :: t() + def new(opts \\ []) do + %__MODULE__{ + touched_addresses: Keyword.get(opts, :touched_addresses, MapSet.new()), + accessed_addresses: Keyword.get(opts, :accessed_addresses, MapSet.new()), + accessed_storage_keys: Keyword.get(opts, :accessed_storage_keys, MapSet.new()), + created_addresses: Keyword.get(opts, :created_addresses, MapSet.new()), + original_storage: Keyword.get(opts, :original_storage, %{}), + transient_storage: Keyword.get(opts, :transient_storage, %{}), + logs: Keyword.get(opts, :logs, []) + } + end +end diff --git a/test/interpreter/journal_test.exs b/test/interpreter/journal_test.exs index 7fd033c..4e00bc1 100644 --- a/test/interpreter/journal_test.exs +++ b/test/interpreter/journal_test.exs @@ -11,24 +11,24 @@ defmodule EEVM.Interpreter.JournalTest do child_db = InMemory.new() |> seed_account(0xCAFE, 100) - child = %{ - parent - | status: :stopped, - db: child_db, - accessed_addresses: MapSet.new([0xAA]), + child_substate = %{ + parent.substate + | accessed_addresses: MapSet.new([0xAA]), accessed_storage_keys: MapSet.new([{0xAA, 0x01}]), created_addresses: MapSet.new([0xCAFE]), touched_addresses: MapSet.new([0xCAFE]) } + child = %{parent | status: :stopped, db: child_db, substate: child_substate} + merged = Journal.merge_child_result(parent, child) assert merged.db == child_db assert merged.db != parent_db - assert merged.accessed_addresses == MapSet.new([0xAA]) - assert merged.accessed_storage_keys == MapSet.new([{0xAA, 0x01}]) - assert merged.created_addresses == MapSet.new([0xCAFE]) - assert merged.touched_addresses == MapSet.new([0xCAFE]) + assert merged.substate.accessed_addresses == MapSet.new([0xAA]) + assert merged.substate.accessed_storage_keys == MapSet.new([{0xAA, 0x01}]) + assert merged.substate.created_addresses == MapSet.new([0xCAFE]) + assert merged.substate.touched_addresses == MapSet.new([0xCAFE]) end test "appends child's logs after parent's, preserving emission order" do @@ -36,12 +36,18 @@ defmodule EEVM.Interpreter.JournalTest do child_log_1 = %{address: 0xBB, topics: [2], data: <<>>} child_log_2 = %{address: 0xCC, topics: [3], data: <<>>} - parent = %{parent_state() | logs: [parent_log]} - child = %{parent | status: :stopped, logs: [child_log_1, child_log_2]} + base = parent_state() + parent = %{base | substate: %{base.substate | logs: [parent_log]}} + + child = %{ + parent + | status: :stopped, + substate: %{parent.substate | logs: [child_log_1, child_log_2]} + } merged = Journal.merge_child_result(parent, child) - assert merged.logs == [parent_log, child_log_1, child_log_2] + assert merged.substate.logs == [parent_log, child_log_1, child_log_2] end end @@ -52,25 +58,30 @@ defmodule EEVM.Interpreter.JournalTest do child_db = InMemory.new() |> seed_account(0xCAFE, 100) - child = %{ - parent - | status: unquote(Macro.escape(status)), - db: child_db, - logs: [%{address: 0xBB, topics: [], data: <<>>}], + child_substate = %{ + parent.substate + | logs: [%{address: 0xBB, topics: [], data: <<>>}], accessed_addresses: MapSet.new([0xAA]), accessed_storage_keys: MapSet.new([{0xAA, 0x01}]), created_addresses: MapSet.new([0xCAFE]), touched_addresses: MapSet.new([0xCAFE]) } + child = %{ + parent + | status: unquote(Macro.escape(status)), + db: child_db, + substate: child_substate + } + merged = Journal.merge_child_result(parent, child) assert merged.db == parent.db - assert merged.logs == parent.logs - assert merged.accessed_addresses == parent.accessed_addresses - assert merged.accessed_storage_keys == parent.accessed_storage_keys - assert merged.created_addresses == parent.created_addresses - assert merged.touched_addresses == parent.touched_addresses + assert merged.substate.logs == parent.substate.logs + assert merged.substate.accessed_addresses == parent.substate.accessed_addresses + assert merged.substate.accessed_storage_keys == parent.substate.accessed_storage_keys + assert merged.substate.created_addresses == parent.substate.created_addresses + assert merged.substate.touched_addresses == parent.substate.touched_addresses end end end diff --git a/test/interpreter/machine_state/eip_3651_test.exs b/test/interpreter/machine_state/eip_3651_test.exs index 3ecabe1..b0d3e88 100644 --- a/test/interpreter/machine_state/eip_3651_test.exs +++ b/test/interpreter/machine_state/eip_3651_test.exs @@ -12,7 +12,7 @@ defmodule EEVM.Interpreter.MachineState.EIP3651Test do state = MachineState.new(<<0x00>>, block: block) - assert MapSet.member?(state.accessed_addresses, block.coinbase) + assert MapSet.member?(state.substate.accessed_addresses, block.coinbase) end test "new machine state pre-warms zero coinbase by default" do @@ -20,7 +20,7 @@ defmodule EEVM.Interpreter.MachineState.EIP3651Test do state = MachineState.new(<<0x00>>, block: block) - assert MapSet.member?(state.accessed_addresses, block.coinbase) + assert MapSet.member?(state.substate.accessed_addresses, block.coinbase) end test "existing pre-warmed addresses remain present" do @@ -29,10 +29,10 @@ defmodule EEVM.Interpreter.MachineState.EIP3651Test do state = MachineState.new(<<0x00>>, contract: contract, tx: tx) - assert MapSet.member?(state.accessed_addresses, contract.address) - assert MapSet.member?(state.accessed_addresses, contract.caller) - assert MapSet.member?(state.accessed_addresses, tx.origin) - assert MapSet.member?(state.accessed_addresses, 0x01) + assert MapSet.member?(state.substate.accessed_addresses, contract.address) + assert MapSet.member?(state.substate.accessed_addresses, contract.caller) + assert MapSet.member?(state.substate.accessed_addresses, tx.origin) + assert MapSet.member?(state.substate.accessed_addresses, 0x01) end test "BALANCE access to COINBASE is charged at warm rate (100 gas)" do @@ -52,7 +52,7 @@ defmodule EEVM.Interpreter.MachineState.EIP3651Test do state = MachineState.new(<<0x00>>, config: config) - assert MapSet.member?(state.accessed_addresses, 0x100) + assert MapSet.member?(state.substate.accessed_addresses, 0x100) end end end diff --git a/test/support/blockchain_test_runner.ex b/test/support/blockchain_test_runner.ex index 8c13162..109e126 100644 --- a/test/support/blockchain_test_runner.ex +++ b/test/support/blockchain_test_runner.ex @@ -248,7 +248,7 @@ defmodule EEVM.TestSupport.BlockchainTestRunner do %{ status: if(failed, do: 0, else: 1), gas_used: gas_used, - logs: if(failed, do: [], else: machine.logs), + logs: if(failed, do: [], else: machine.substate.logs), db: finalized_db }} end diff --git a/test/support/state_test_runner.ex b/test/support/state_test_runner.ex index 5909419..3891cbd 100644 --- a/test/support/state_test_runner.ex +++ b/test/support/state_test_runner.ex @@ -37,7 +37,7 @@ defmodule EEVM.TestSupport.StateTestRunner do {:ok, %{ state_root: StateRoot.compute_state_root(finalized_db), - logs_hash: logs_hash(if(failed, do: [], else: machine.logs)), + logs_hash: logs_hash(if(failed, do: [], else: machine.substate.logs)), output: if(failed, do: <<>>, else: machine.return_data), status: machine.status, gas_used: tx.gas_limit - machine.gas, From 09a11103f5eb7923a98300e72a3d3012b38d6918 Mon Sep 17 00:00:00 2001 From: mw2000 Date: Thu, 30 Apr 2026 22:45:57 -0700 Subject: [PATCH 2/3] refactor(machine_state): extract Env struct (phase 2 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group the three immutable per-call inputs — tx, block, config — under a single `env` field. They share a lifecycle (set on frame construction, never mutated, inherited unchanged by children), so collapsing them makes the distinction between "what evolves" (frame, substate) and "what the call observes" (env) explicit at every access site. Public API unchanged: `MachineState.new/2` still takes `tx:`, `block:`, `config:` as flat keyword opts. Co-Authored-By: Claude Opus 4.7 --- lib/eevm/gas/access.ex | 4 +- lib/eevm/handler/execution.ex | 2 +- lib/eevm/interpreter.ex | 8 ++-- .../interpreter/instructions/control_flow.ex | 2 +- .../instructions/environment/simple.ex | 24 ++++++------ .../stack_memory_storage/memory_ops.ex | 2 +- .../stack_memory_storage/storage_ops.ex | 4 +- .../interpreter/instructions/system/calls.ex | 22 ++++++----- .../instructions/system/creation.ex | 14 +++---- .../instructions/system/termination.ex | 3 +- lib/eevm/interpreter/machine_state.ex | 14 ++----- lib/eevm/interpreter/machine_state/env.ex | 39 +++++++++++++++++++ test/hardfork_config_test.exs | 2 +- 13 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 lib/eevm/interpreter/machine_state/env.ex diff --git a/lib/eevm/gas/access.ex b/lib/eevm/gas/access.ex index 63c6196..d795413 100644 --- a/lib/eevm/gas/access.ex +++ b/lib/eevm/gas/access.ex @@ -36,7 +36,7 @@ defmodule EEVM.Gas.Access do @spec address_access_cost(MachineState.t(), non_neg_integer()) :: {non_neg_integer(), MachineState.t()} def address_access_cost(state, address) do - if HardforkConfig.enabled?(state.config.hardfork, :eip_2929) do + if HardforkConfig.enabled?(state.env.config.hardfork, :eip_2929) do sub = state.substate if MapSet.member?(sub.accessed_addresses, address) do @@ -55,7 +55,7 @@ defmodule EEVM.Gas.Access do @spec storage_access_cost(MachineState.t(), non_neg_integer(), non_neg_integer()) :: {non_neg_integer(), MachineState.t()} def storage_access_cost(state, address, slot) do - if HardforkConfig.enabled?(state.config.hardfork, :eip_2929) do + if HardforkConfig.enabled?(state.env.config.hardfork, :eip_2929) do key = {address, slot} sub = state.substate diff --git a/lib/eevm/handler/execution.ex b/lib/eevm/handler/execution.ex index 6321c11..60b0f08 100644 --- a/lib/eevm/handler/execution.ex +++ b/lib/eevm/handler/execution.ex @@ -101,7 +101,7 @@ defmodule EEVM.Handler.Execution do {:error, %{state | gas: 0, status: :reverted}} size > 0 and :binary.first(runtime_code) == 0xEF and - HardforkConfig.enabled?(state.config.hardfork, :eip_3541) -> + HardforkConfig.enabled?(state.env.config.hardfork, :eip_3541) -> {:error, %{state | gas: 0, status: :reverted}} true -> diff --git a/lib/eevm/interpreter.ex b/lib/eevm/interpreter.ex index c9dafd1..780b27a 100644 --- a/lib/eevm/interpreter.ex +++ b/lib/eevm/interpreter.ex @@ -144,18 +144,18 @@ defmodule EEVM.Interpreter do # EIP-3529 (London+): cap refunds at 1/5 of gas used. # Pre-London: cap was 1/2 of gas used. refund_cap_divisor = - if HardforkConfig.enabled?(state.config.hardfork, :eip_3529), do: 5, else: 2 + if HardforkConfig.enabled?(state.env.config.hardfork, :eip_3529), do: 5, else: 2 effective_refund = min(state.refund, div(gas_used, refund_cap_divisor)) refunded_gas = state.gas + effective_refund calldata_floor_gas = - IntrinsicGas.calldata_floor_gas_cost(state.tx, state.config.hardfork) + IntrinsicGas.calldata_floor_gas_cost(state.env.tx, state.env.config.hardfork) gas_after_floor = - if calldata_floor_gas > 0 and state.tx.gas_limit > 0 do - min(refunded_gas, max(state.tx.gas_limit - calldata_floor_gas, 0)) + if calldata_floor_gas > 0 and state.env.tx.gas_limit > 0 do + min(refunded_gas, max(state.env.tx.gas_limit - calldata_floor_gas, 0)) else refunded_gas end diff --git a/lib/eevm/interpreter/instructions/control_flow.ex b/lib/eevm/interpreter/instructions/control_flow.ex index bf9a9a4..cbe1466 100644 --- a/lib/eevm/interpreter/instructions/control_flow.ex +++ b/lib/eevm/interpreter/instructions/control_flow.ex @@ -93,7 +93,7 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do # PUSH0 (EIP-3855, Shanghai+): pushes the constant 0 onto the stack without # consuming any inline bytecode bytes. Saves 1 byte and 2 gas vs PUSH1 0x00. # Pre-Shanghai, 0x5F is an undefined opcode and halts with :invalid. - def execute(0x5F, %{config: %{hardfork: hardfork}} = state) do + def execute(0x5F, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_3855) do Helpers.push_value(state, 0) else diff --git a/lib/eevm/interpreter/instructions/environment/simple.ex b/lib/eevm/interpreter/instructions/environment/simple.ex index 775f3a6..d7d0eec 100644 --- a/lib/eevm/interpreter/instructions/environment/simple.ex +++ b/lib/eevm/interpreter/instructions/environment/simple.ex @@ -17,26 +17,26 @@ defmodule EEVM.Interpreter.Instructions.Environment.Simple do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x30, state), do: Helpers.push_value(state, state.contract.address) - def execute(0x32, state), do: Helpers.push_value(state, state.tx.origin) + def execute(0x32, state), do: Helpers.push_value(state, state.env.tx.origin) def execute(0x33, state), do: Helpers.push_value(state, state.contract.caller) def execute(0x34, state), do: Helpers.push_value(state, state.contract.callvalue) def execute(0x36, state), do: Helpers.push_value(state, byte_size(state.contract.calldata)) def execute(0x38, state), do: Helpers.push_value(state, byte_size(state.code)) - def execute(0x3A, state), do: Helpers.push_value(state, state.tx.gasprice) + def execute(0x3A, state), do: Helpers.push_value(state, state.env.tx.gasprice) def execute(0x3D, state), do: Helpers.push_value(state, byte_size(state.return_data)) - def execute(0x41, state), do: Helpers.push_value(state, state.block.coinbase) - def execute(0x42, state), do: Helpers.push_value(state, state.block.timestamp) - def execute(0x43, state), do: Helpers.push_value(state, state.block.number) - def execute(0x44, state), do: Helpers.push_value(state, state.block.prevrandao) - def execute(0x45, state), do: Helpers.push_value(state, state.block.gaslimit) - def execute(0x46, state), do: Helpers.push_value(state, state.block.chain_id) - def execute(0x48, state), do: Helpers.push_value(state, state.block.basefee) - def execute(0x4A, state), do: Helpers.push_value(state, state.block.blob_base_fee) + def execute(0x41, state), do: Helpers.push_value(state, state.env.block.coinbase) + def execute(0x42, state), do: Helpers.push_value(state, state.env.block.timestamp) + def execute(0x43, state), do: Helpers.push_value(state, state.env.block.number) + def execute(0x44, state), do: Helpers.push_value(state, state.env.block.prevrandao) + def execute(0x45, state), do: Helpers.push_value(state, state.env.block.gaslimit) + def execute(0x46, state), do: Helpers.push_value(state, state.env.block.chain_id) + def execute(0x48, state), do: Helpers.push_value(state, state.env.block.basefee) + def execute(0x4A, state), do: Helpers.push_value(state, state.env.block.blob_base_fee) def execute(0x5A, state), do: Helpers.push_value(state, state.gas) def execute(0x40, state) do with {:ok, block_num, s1} <- Stack.pop(state.stack), - hash = Block.hash(state.block, block_num), + hash = Block.hash(state.env.block, block_num), {:ok, s2} <- Stack.push(s1, hash) do {:ok, %{state | stack: s2} |> MachineState.advance_pc()} else @@ -46,7 +46,7 @@ defmodule EEVM.Interpreter.Instructions.Environment.Simple do def execute(0x49, state) do with {:ok, index, s1} <- Stack.pop(state.stack) do - value = Enum.at(state.tx.blob_hashes, index, 0) + value = Enum.at(state.env.tx.blob_hashes, index, 0) case Stack.push(s1, value) do {:ok, s2} -> {:ok, %{state | stack: s2} |> MachineState.advance_pc()} diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex index f3c7cc3..1fb9c66 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex @@ -69,7 +69,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do # MCOPY (EIP-5656, Cancun+): copies a region of memory to another location. # Pre-Cancun, 0x5E is undefined → :invalid. - def execute(0x5E, %{config: %{hardfork: hardfork}} = state) do + def execute(0x5E, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_5656) do with {:ok, dst, s1} <- Stack.pop(state.stack), {:ok, src, s2} <- Stack.pop(s1), diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex index 4b7227d..79ac7a8 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex @@ -99,7 +99,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do # TLOAD (EIP-1153, Cancun+): reads from transient storage — a key-value map # cleared at the end of each transaction. Pre-Cancun, 0x5C is undefined → :invalid. - def execute(0x5C, %{config: %{hardfork: hardfork}} = state) do + def execute(0x5C, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_1153) do with {:ok, key, s1} <- Stack.pop(state.stack), value = Map.get(state.substate.transient_storage, key, 0), @@ -116,7 +116,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do # TSTORE (EIP-1153, Cancun+): writes to transient storage. The static-context # guard in the executor rejects TSTORE before this clause is reached in static mode. # Pre-Cancun, 0x5D is undefined → :invalid. - def execute(0x5D, %{config: %{hardfork: hardfork}} = state) do + def execute(0x5D, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_1153) do with {:ok, key, s1} <- Stack.pop(state.stack), {:ok, value, s2} <- Stack.pop(s1) do diff --git a/lib/eevm/interpreter/instructions/system/calls.ex b/lib/eevm/interpreter/instructions/system/calls.ex index b0801f0..31c9d68 100644 --- a/lib/eevm/interpreter/instructions/system/calls.ex +++ b/lib/eevm/interpreter/instructions/system/calls.ex @@ -194,10 +194,11 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do {:ok, state_after_forward} -> target_code = Database.get_code(db_after_transfer, address) - if Precompiles.precompile?(address, state_after_forward.config) and target_code == <<>> do + if Precompiles.precompile?(address, state_after_forward.env.config) and + target_code == <<>> do child_gas = forwarded_gas + Dynamic.call_stipend(value) - case Precompiles.execute(address, calldata, child_gas, state_after_forward.config) do + case Precompiles.execute(address, calldata, child_gas, state_after_forward.env.config) do {:ok, output, gas_used} -> remaining_gas = child_gas - gas_used memory_result = write_return_data(memory_after_read, ret_offset, ret_size, output) @@ -241,10 +242,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do MachineState.new(target_code, gas: child_gas, db: db_after_transfer, - tx: state_after_touch.tx, - block: state_after_touch.block, + tx: state_after_touch.env.tx, + block: state_after_touch.env.block, contract: child_contract, - config: state_after_touch.config, + config: state_after_touch.env.config, touched_addresses: state_after_touch.substate.touched_addresses, accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, @@ -318,8 +319,9 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do target_code = Database.get_code(state_after_forward.db, address) - if Precompiles.precompile?(address, state_after_forward.config) and target_code == <<>> do - case Precompiles.execute(address, calldata, child_gas, state_after_forward.config) do + if Precompiles.precompile?(address, state_after_forward.env.config) and + target_code == <<>> do + case Precompiles.execute(address, calldata, child_gas, state_after_forward.env.config) do {:ok, output, gas_used} -> remaining_gas = child_gas - gas_used memory_result = write_return_data(memory_after_read, ret_offset, ret_size, output) @@ -361,10 +363,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do MachineState.new(target_code, gas: child_gas, db: state_after_touch.db, - tx: state_after_touch.tx, - block: state_after_touch.block, + tx: state_after_touch.env.tx, + block: state_after_touch.env.block, contract: child_contract, - config: state_after_touch.config, + config: state_after_touch.env.config, touched_addresses: state_after_touch.substate.touched_addresses, accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, diff --git a/lib/eevm/interpreter/instructions/system/creation.ex b/lib/eevm/interpreter/instructions/system/creation.ex index 9c37c9f..c3b17f5 100644 --- a/lib/eevm/interpreter/instructions/system/creation.ex +++ b/lib/eevm/interpreter/instructions/system/creation.ex @@ -79,12 +79,12 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do db_after_nonce = Database.increment_nonce(state_after_cost.db, creator) - if HardforkConfig.enabled?(state_after_cost.config.hardfork, :eip_3860) and + if HardforkConfig.enabled?(state_after_cost.env.config.hardfork, :eip_3860) and size > @max_initcode_size do create_failed(%{state_after_cost | db: db_after_nonce}, stack) else initcode_cost = - if HardforkConfig.enabled?(state_after_cost.config.hardfork, :eip_3860), + if HardforkConfig.enabled?(state_after_cost.env.config.hardfork, :eip_3860), do: @initcode_word_cost * div(size + 31, 32), else: 0 @@ -119,10 +119,10 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do MachineState.new(init_code, gas: state_after_initcode_cost.gas, db: db_after_transfer, - tx: state_after_touch.tx, - block: state_after_touch.block, + tx: state_after_touch.env.tx, + block: state_after_touch.env.block, contract: child_contract, - config: state_after_touch.config, + config: state_after_touch.env.config, touched_addresses: state_after_touch.substate.touched_addresses, accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, @@ -144,7 +144,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do if deployment_success do runtime_code = child_result.return_data - if HardforkConfig.enabled?(child_result.config.hardfork, :eip_170) and + if HardforkConfig.enabled?(child_result.env.config.hardfork, :eip_170) and byte_size(runtime_code) > @max_code_size do create_failed(post_child_fail_state, stack) else @@ -153,7 +153,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do # This check runs after EIP-170 size validation and after init code # execution -- so init code gas is already consumed but no code is # deposited. Empty runtime code is explicitly allowed. - if HardforkConfig.enabled?(child_result.config.hardfork, :eip_3541) and + if HardforkConfig.enabled?(child_result.env.config.hardfork, :eip_3541) and reject_eip_3541_runtime_code?(runtime_code) do create_failed(post_child_fail_state, stack) else diff --git a/lib/eevm/interpreter/instructions/system/termination.ex b/lib/eevm/interpreter/instructions/system/termination.ex index 9d8050b..cb239d7 100644 --- a/lib/eevm/interpreter/instructions/system/termination.ex +++ b/lib/eevm/interpreter/instructions/system/termination.ex @@ -72,7 +72,8 @@ defmodule EEVM.Interpreter.Instructions.System.Termination do # EIP-6780 (Cancun+): SELFDESTRUCT only fully deletes the account when # the contract was created in the same transaction. Pre-Cancun, every # SELFDESTRUCT unconditionally deletes the account and transfers the balance. - eip_6780_active? = HardforkConfig.enabled?(state_after_touch.config.hardfork, :eip_6780) + eip_6780_active? = + HardforkConfig.enabled?(state_after_touch.env.config.hardfork, :eip_6780) created_this_tx = MapSet.member?(state_after_touch.substate.created_addresses, contract_address) diff --git a/lib/eevm/interpreter/machine_state.ex b/lib/eevm/interpreter/machine_state.ex index d1da64f..c6a74d1 100644 --- a/lib/eevm/interpreter/machine_state.ex +++ b/lib/eevm/interpreter/machine_state.ex @@ -30,7 +30,7 @@ defmodule EEVM.Interpreter.MachineState do alias EEVM.Database alias EEVM.Database.InMemory, as: InMemoryDB alias EEVM.Interpreter.{CallFrame, Memory, Stack} - alias EEVM.Interpreter.MachineState.Substate + alias EEVM.Interpreter.MachineState.{Env, Substate} alias EEVM.Precompiles alias EEVM.Tracer @@ -41,11 +41,9 @@ defmodule EEVM.Interpreter.MachineState do stack: Stack.t(), memory: Memory.t(), db: Database.t(), + env: Env.t(), substate: Substate.t(), - tx: Transaction.t(), - block: Block.t(), contract: Contract.t(), - config: Config.t(), call_stack: [CallFrame.t()], frame_return_offset: non_neg_integer(), frame_return_size: non_neg_integer(), @@ -64,11 +62,9 @@ defmodule EEVM.Interpreter.MachineState do stack: nil, memory: nil, db: nil, + env: nil, substate: nil, - tx: nil, - block: nil, contract: nil, - config: nil, call_stack: [], frame_return_offset: 0, frame_return_size: 0, @@ -135,11 +131,9 @@ defmodule EEVM.Interpreter.MachineState do stack: Stack.new(), memory: Memory.new(), db: init_db(opts, contract), + env: Env.new(tx: tx, block: block, config: config), substate: substate, - tx: tx, - block: block, contract: contract, - config: config, call_stack: Keyword.get(opts, :call_stack, []), frame_return_offset: Keyword.get(opts, :frame_return_offset, 0), frame_return_size: Keyword.get(opts, :frame_return_size, 0), diff --git a/lib/eevm/interpreter/machine_state/env.ex b/lib/eevm/interpreter/machine_state/env.ex new file mode 100644 index 0000000..adc5909 --- /dev/null +++ b/lib/eevm/interpreter/machine_state/env.ex @@ -0,0 +1,39 @@ +defmodule EEVM.Interpreter.MachineState.Env do + @moduledoc """ + Per-call read-only execution environment. + + Bundles the three fields that are set when a frame is constructed and + never mutate during its lifetime: + + - `tx` — the originating transaction context (origin, gasprice, blob hashes…) + - `block` — the block context (number, timestamp, coinbase, basefee…) + - `config` — the active hardfork config (which EIPs are enabled, registered + precompiles) + + Child frames inherit the parent's env unchanged. Treating them as a single + immutable record makes intent explicit at call sites — `state.env` is + what you pass through, `state.frame` and `state.substate` are what you + evolve. + """ + + alias EEVM.Config + alias EEVM.Context.{Block, Transaction} + + @type t :: %__MODULE__{ + tx: Transaction.t(), + block: Block.t(), + config: Config.t() + } + + @enforce_keys [:tx, :block, :config] + defstruct [:tx, :block, :config] + + @spec new(keyword()) :: t() + def new(opts \\ []) do + %__MODULE__{ + tx: Keyword.get(opts, :tx, Transaction.new()), + block: Keyword.get(opts, :block, Block.new()), + config: Keyword.get(opts, :config, Config.new()) + } + end +end diff --git a/test/hardfork_config_test.exs b/test/hardfork_config_test.exs index 0a1d11d..fe1cec8 100644 --- a/test/hardfork_config_test.exs +++ b/test/hardfork_config_test.exs @@ -392,7 +392,7 @@ defmodule EEVM.HardforkConfigTest do test "config.hardfork is set to Cancun by default" do result = EEVM.execute(<<0x00>>) - assert result.config.hardfork.spec_id == :cancun + assert result.env.config.hardfork.spec_id == :cancun end end end From 9dbbf9c907ed3a685d0ae1738c5d90a858d02eec Mon Sep 17 00:00:00 2001 From: mw2000 Date: Thu, 30 Apr 2026 23:44:54 -0700 Subject: [PATCH 3/3] refactor(machine_state): extract Frame struct (phase 3 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the per-call execution frame out of MachineState into its own struct at lib/eevm/interpreter/machine_state/frame.ex, completing the three-way split of the envelope into frame / env / substate. Frame holds the per-call mutable execution context — pc, stack, memory, gas, refund, code, return_data, contract, depth, is_static, plus the parent-memory return write-back location. The active frame lives at state.frame; suspended parent frames use the same struct in state.call_stack. CallFrame is removed and its single usage replaced. All opcode modules, handlers, and tests now read these fields through state.frame.X and write via MachineState.update_frame/2 (which threads the closure over the active frame). pop_frame now also carries the child's refund forward into the restored parent frame, preserving the old envelope-level refund accumulation semantic. Co-Authored-By: Claude Opus 4.7 --- lib/eevm.ex | 2 +- lib/eevm/handler/execution.ex | 19 +- lib/eevm/handler/post_execution.ex | 6 +- lib/eevm/interpreter.ex | 34 +-- lib/eevm/interpreter/call_frame.ex | 73 ------ .../interpreter/instructions/arithmetic.ex | 55 +++-- lib/eevm/interpreter/instructions/bitwise.ex | 20 +- .../interpreter/instructions/comparison.ex | 4 +- .../interpreter/instructions/control_flow.ex | 41 ++-- lib/eevm/interpreter/instructions/crypto.ex | 11 +- .../instructions/environment/data.ex | 78 ++++--- .../instructions/environment/external.ex | 69 ++++-- .../instructions/environment/simple.ex | 31 ++- lib/eevm/interpreter/instructions/helpers.ex | 18 +- lib/eevm/interpreter/instructions/logging.ex | 20 +- .../stack_memory_storage/memory_ops.ex | 80 +++++-- .../stack_memory_storage/stack_ops.ex | 7 +- .../stack_memory_storage/storage_ops.ex | 44 ++-- .../interpreter/instructions/system/calls.ex | 197 ++++++++++------ .../instructions/system/creation.ex | 50 ++-- .../instructions/system/termination.ex | 41 ++-- lib/eevm/interpreter/machine_state.ex | 213 +++++++----------- lib/eevm/interpreter/machine_state/frame.ex | 53 +++++ test/gas_test.exs | 28 +-- test/hardfork_config_test.exs | 8 +- test/interpreter/call_frame_test.exs | 29 ++- .../instructions/access_list_test.exs | 14 +- test/interpreter/instructions/blob_test.exs | 4 +- .../instructions/control_flow_test.exs | 4 +- test/interpreter/instructions/crypto_test.exs | 4 +- .../instructions/environment_test.exs | 14 +- .../instructions/gas_refund_test.exs | 41 ++-- .../interpreter/instructions/logging_test.exs | 2 +- .../instructions/precompile_test.exs | 4 +- .../instructions/selfdestruct_test.exs | 8 +- .../instructions/sstore_gas_test.exs | 36 +-- .../instructions/stack_memory_test.exs | 2 +- .../instructions/system/eip_3541_test.exs | 2 +- test/interpreter/instructions/system_test.exs | 20 +- .../instructions/transient_storage_test.exs | 4 +- test/interpreter/journal_test.exs | 19 +- .../machine_state/eip_3651_test.exs | 4 +- test/storage_test.exs | 4 +- test/support/blockchain_test_runner.ex | 4 +- test/support/state_test_runner.ex | 6 +- test/system_contracts/beacon_roots_test.exs | 4 +- test/system_contracts/block_hashes_test.exs | 4 +- 47 files changed, 808 insertions(+), 627 deletions(-) delete mode 100644 lib/eevm/interpreter/call_frame.ex create mode 100644 lib/eevm/interpreter/machine_state/frame.ex diff --git a/lib/eevm.ex b/lib/eevm.ex index 3b7df50..90ec200 100644 --- a/lib/eevm.ex +++ b/lib/eevm.ex @@ -79,7 +79,7 @@ defmodule EEVM do """ @spec stack_values(EEVM.Interpreter.MachineState.t()) :: [non_neg_integer()] def stack_values(state) do - Stack.to_list(state.stack) + Stack.to_list(state.frame.stack) end @doc "Returns the list of logs emitted during execution." diff --git a/lib/eevm/handler/execution.ex b/lib/eevm/handler/execution.ex index 60b0f08..d6d1e6b 100644 --- a/lib/eevm/handler/execution.ex +++ b/lib/eevm/handler/execution.ex @@ -89,20 +89,20 @@ defmodule EEVM.Handler.Execution do end defp deploy_runtime_code(%MachineState{status: :stopped} = state, new_address) do - runtime_code = state.return_data + runtime_code = state.frame.return_data size = byte_size(runtime_code) deposit_cost = size * @code_deposit_gas_per_byte cond do size > @max_code_size -> - {:error, %{state | gas: 0, status: :reverted}} + {:error, zero_gas_revert(state)} - state.gas < deposit_cost -> - {:error, %{state | gas: 0, status: :reverted}} + state.frame.gas < deposit_cost -> + {:error, zero_gas_revert(state)} size > 0 and :binary.first(runtime_code) == 0xEF and HardforkConfig.enabled?(state.env.config.hardfork, :eip_3541) -> - {:error, %{state | gas: 0, status: :reverted}} + {:error, zero_gas_revert(state)} true -> updated_db = @@ -110,12 +110,19 @@ defmodule EEVM.Handler.Execution do |> Database.put_code(new_address, runtime_code) |> Database.set_nonce(new_address, 1) - {:ok, %{state | db: updated_db, gas: state.gas - deposit_cost}} + deducted = MachineState.update_frame(state, &%{&1 | gas: &1.gas - deposit_cost}) + {:ok, %{deducted | db: updated_db}} end end defp deploy_runtime_code(state, _new_address), do: {:error, state} + defp zero_gas_revert(state) do + state + |> MachineState.update_frame(&%{&1 | gas: 0}) + |> Map.put(:status, :reverted) + end + defp transfer_value(%Database{} = db, _from, _to, 0), do: db defp transfer_value(%Database{} = db, from, to, value) do diff --git a/lib/eevm/handler/post_execution.ex b/lib/eevm/handler/post_execution.ex index 7d4fcb0..a6ec8c9 100644 --- a/lib/eevm/handler/post_execution.ex +++ b/lib/eevm/handler/post_execution.ex @@ -39,7 +39,7 @@ defmodule EEVM.Handler.PostExecution do %Database{} = db_nonce_bumped, %Block{} = block ) do - gas_used = tx.gas_limit - final_state.gas + gas_used = tx.gas_limit - final_state.frame.gas effective_price = PreExecution.effective_gas_price(tx, block) settled_db = settle_state(final_state, db_nonce_bumped, tx, gas_used, effective_price) @@ -62,13 +62,13 @@ defmodule EEVM.Handler.PostExecution do %TransactionResult{ status: status, gas_used: gas_used, - gas_refunded: max(final_state.gas, 0), + gas_refunded: max(final_state.frame.gas, 0), sender: Validation.decode_address(sender_bytes), logs: logs, logs_bloom: logs_bloom, receipt: receipt, post_state_db: coinbase_credited_db, - return_data: if(status == :success, do: final_state.return_data, else: <<>>), + return_data: if(status == :success, do: final_state.frame.return_data, else: <<>>), contract_address: if(status == :success, do: contract_address, else: nil) }} end diff --git a/lib/eevm/interpreter.ex b/lib/eevm/interpreter.ex index 780b27a..720cb07 100644 --- a/lib/eevm/interpreter.ex +++ b/lib/eevm/interpreter.ex @@ -96,7 +96,7 @@ defmodule EEVM.Interpreter do MachineState.halt(state, :stopped) opcode -> - static_cost = if opcode == 0xFE, do: state.gas, else: Static.static_cost(opcode) + static_cost = if opcode == 0xFE, do: state.frame.gas, else: Static.static_cost(opcode) traced_state = trace_opcode(state, opcode, static_cost) case MachineState.consume_gas(traced_state, static_cost) do @@ -121,7 +121,9 @@ defmodule EEVM.Interpreter do end def run_loop(%MachineState{status: :reverted, call_stack: [parent | _]} = state) do - state_with_restored_refund = %{state | refund: parent.refund} + state_with_restored_refund = + MachineState.update_frame(state, &%{&1 | refund: parent.refund}) + {:ok, resumed_state} = MachineState.pop_frame(state_with_restored_refund) run_loop(resumed_state) end @@ -136,19 +138,19 @@ defmodule EEVM.Interpreter do defp apply_refund(%MachineState{status: status} = state, _initial_gas) when status in [:reverted, :out_of_gas, :invalid] do - %{state | refund: 0} + MachineState.update_frame(state, &%{&1 | refund: 0}) end defp apply_refund(%MachineState{} = state, initial_gas) do - gas_used = initial_gas - state.gas + gas_used = initial_gas - state.frame.gas # EIP-3529 (London+): cap refunds at 1/5 of gas used. # Pre-London: cap was 1/2 of gas used. refund_cap_divisor = if HardforkConfig.enabled?(state.env.config.hardfork, :eip_3529), do: 5, else: 2 - effective_refund = min(state.refund, div(gas_used, refund_cap_divisor)) + effective_refund = min(state.frame.refund, div(gas_used, refund_cap_divisor)) - refunded_gas = state.gas + effective_refund + refunded_gas = state.frame.gas + effective_refund calldata_floor_gas = IntrinsicGas.calldata_floor_gas_cost(state.env.tx, state.env.config.hardfork) @@ -160,7 +162,7 @@ defmodule EEVM.Interpreter do refunded_gas end - %{state | gas: gas_after_floor, refund: 0} + MachineState.update_frame(state, &%{&1 | gas: gas_after_floor, refund: 0}) end defp cleanup_touched_empty_accounts(%MachineState{status: :stopped} = state) do @@ -188,7 +190,7 @@ defmodule EEVM.Interpreter do defp execute_opcode(opcode, state) do case Registry.info(opcode) do - {:ok, %{state_mutating: true}} when state.is_static -> + {:ok, %{state_mutating: true}} when state.frame.is_static -> {:ok, MachineState.halt(state, :reverted)} {:ok, %{module: module}} -> @@ -204,18 +206,18 @@ defmodule EEVM.Interpreter do defp trace_opcode(%MachineState{tracer: nil} = state, _opcode, _static_cost), do: state - defp trace_opcode(%MachineState{tracer: tracer} = state, opcode, static_cost) do + defp trace_opcode(%MachineState{tracer: tracer, frame: frame} = state, opcode, static_cost) do step = %TraceStep{ - pc: state.pc, + pc: frame.pc, op: Tracer.op_name(opcode), op_byte: opcode, - gas_remaining: state.gas, + gas_remaining: frame.gas, gas_cost: static_cost, - stack: Stack.to_list(state.stack), - memory_size: Memory.size(state.memory), - depth: state.depth, - refund: state.refund, - return_data: state.return_data, + stack: Stack.to_list(frame.stack), + memory_size: Memory.size(frame.memory), + depth: frame.depth, + refund: frame.refund, + return_data: frame.return_data, error: nil } diff --git a/lib/eevm/interpreter/call_frame.ex b/lib/eevm/interpreter/call_frame.ex deleted file mode 100644 index 9047848..0000000 --- a/lib/eevm/interpreter/call_frame.ex +++ /dev/null @@ -1,73 +0,0 @@ -defmodule EEVM.Interpreter.CallFrame do - @moduledoc """ - Execution frame snapshot used for nested EVM calls. - - ## EVM Concepts - - Each CALL-like opcode creates a new execution context while suspending the - parent context. This struct captures the parent frame so execution can return - correctly after the child frame halts. - - A frame stores: - - - code and program counter - - stack and memory - - available gas for that frame - - contract context (`msg.sender`, `msg.value`, `address`) - - return write-back metadata (`return_offset`, `return_size`) - - static-call mode and depth - - ## Elixir Learning Notes - - - This struct is a plain value object; frame transitions are handled in - `EEVM.Interpreter.MachineState` and `EEVM.Interpreter`. - - `from_state/2` acts as a focused constructor that copies only the fields - needed to suspend and restore execution. - """ - - alias EEVM.Context.Contract - alias EEVM.Interpreter.{Memory, Stack} - - @type t :: %__MODULE__{ - code: binary(), - pc: non_neg_integer(), - stack: Stack.t(), - memory: Memory.t(), - gas: non_neg_integer(), - refund: non_neg_integer(), - contract: Contract.t(), - return_offset: non_neg_integer(), - return_size: non_neg_integer(), - is_static: boolean(), - depth: non_neg_integer() - } - - defstruct code: <<>>, - pc: 0, - stack: nil, - memory: nil, - gas: 0, - refund: 0, - contract: nil, - return_offset: 0, - return_size: 0, - is_static: false, - depth: 0 - - @spec from_state(map(), keyword()) :: t() - def from_state(state, opts \\ []) do - %__MODULE__{ - code: state.code, - pc: state.pc, - stack: state.stack, - memory: state.memory, - gas: state.gas, - refund: state.refund, - contract: state.contract, - return_offset: Keyword.get(opts, :return_offset, 0), - return_size: Keyword.get(opts, :return_size, 0), - is_static: Keyword.get(opts, :is_static, false), - depth: Keyword.get(opts, :depth, 0) - } - end -end diff --git a/lib/eevm/interpreter/instructions/arithmetic.ex b/lib/eevm/interpreter/instructions/arithmetic.ex index c6f764b..c66b5ce 100644 --- a/lib/eevm/interpreter/instructions/arithmetic.ex +++ b/lib/eevm/interpreter/instructions/arithmetic.ex @@ -62,44 +62,44 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x01, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), result = band(a + b, @max_uint256), {:ok, s3} <- Stack.push(s2, result) do - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x02, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), result = band(a * b, @max_uint256), {:ok, s3} <- Stack.push(s2, result) do - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x03, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), result = band(a - b, @max_uint256), {:ok, s3} <- Stack.push(s2, result) do - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x04, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if b == 0, do: 0, else: div(a, b) {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -109,7 +109,7 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do # the EVM spec for SDIV. Special case: -2^255 / -1 would overflow to 2^255 # (outside the signed 256-bit range), so the EVM defines the result as -2^255. def execute(0x05, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if b == 0 do @@ -121,18 +121,18 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do end {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x06, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if b == 0, do: 0, else: rem(a, b) {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -141,7 +141,7 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do # SMOD: signed modulo. The result takes the sign of the dividend (a), # matching the behavior of Elixir's `rem/2` for negative numbers. def execute(0x07, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if b == 0 do @@ -153,44 +153,51 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do end {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x08, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), {:ok, n, s3} <- Stack.pop(s2) do result = if n == 0, do: 0, else: rem(a + b, n) {:ok, s4} = Stack.push(s3, result) - {:ok, %{state | stack: s4} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s4}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x09, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), {:ok, n, s3} <- Stack.pop(s2) do result = if n == 0, do: 0, else: rem(a * b, n) {:ok, s4} = Stack.push(s3, result) - {:ok, %{state | stack: s4} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s4}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x0A, state) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s2}, Dynamic.exp_dynamic_cost(b)) do + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s2}), + Dynamic.exp_dynamic_cost(b) + ) do result = Helpers.mod_pow(a, b, @max_uint256 + 1) - {:ok, s3} = Stack.push(state_after_gas.stack, result) - {:ok, %{state_after_gas | stack: s3} |> MachineState.advance_pc()} + {:ok, s3} = Stack.push(state_after_gas.frame.stack, result) + + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s3}) + |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -203,7 +210,7 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do # all higher bits are set to 1 (bnot mask); if 0 (positive), higher bits # are cleared. def execute(0x0B, state) do - with {:ok, b, s1} <- Stack.pop(state.stack), + with {:ok, b, s1} <- Stack.pop(state.frame.stack), {:ok, x, s2} <- Stack.pop(s1) do result = if b < 31 do @@ -220,7 +227,7 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do end {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end diff --git a/lib/eevm/interpreter/instructions/bitwise.ex b/lib/eevm/interpreter/instructions/bitwise.ex index 1626507..da2b15c 100644 --- a/lib/eevm/interpreter/instructions/bitwise.ex +++ b/lib/eevm/interpreter/instructions/bitwise.ex @@ -58,10 +58,10 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do def execute(0x18, state), do: Helpers.bitwise_op(state, &bxor/2) def execute(0x19, state) do - with {:ok, a, s1} <- Stack.pop(state.stack) do + with {:ok, a, s1} <- Stack.pop(state.frame.stack) do result = band(Bitwise.bnot(a), @max_uint256) {:ok, s2} = Stack.push(s1, result) - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -71,7 +71,7 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do # `(31 - i) * 8` computes the right-shift amount so the target byte lands in # the lowest 8 bits, then we mask with 0xFF to isolate it. def execute(0x1A, state) do - with {:ok, i, s1} <- Stack.pop(state.stack), + with {:ok, i, s1} <- Stack.pop(state.frame.stack), {:ok, x, s2} <- Stack.pop(s1) do result = if i < 32 do @@ -82,7 +82,7 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do end {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -91,11 +91,11 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do # SHL: logical left shift. Shifting by >= 256 must return 0 because no original # bits remain. We mask with @max_uint256 after shifting to stay within 256 bits. def execute(0x1B, state) do - with {:ok, shift, s1} <- Stack.pop(state.stack), + with {:ok, shift, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1) do result = if shift >= 256, do: 0, else: band(value <<< shift, @max_uint256) {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -104,11 +104,11 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do # SHR: logical right shift. Shifting by >= 256 returns 0. # Elixir's `>>>` on a non-negative integer is a logical right shift. def execute(0x1C, state) do - with {:ok, shift, s1} <- Stack.pop(state.stack), + with {:ok, shift, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1) do result = if shift >= 256, do: 0, else: value >>> shift {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -118,7 +118,7 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do # fills vacated high bits with 1s for negative values. If shift >= 256 and # the value is negative, the result is all 1s (@max_uint256, representing -1). def execute(0x1D, state) do - with {:ok, shift, s1} <- Stack.pop(state.stack), + with {:ok, shift, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1) do signed = Helpers.to_signed(value) @@ -130,7 +130,7 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do end {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end diff --git a/lib/eevm/interpreter/instructions/comparison.ex b/lib/eevm/interpreter/instructions/comparison.ex index d9a093b..26809a0 100644 --- a/lib/eevm/interpreter/instructions/comparison.ex +++ b/lib/eevm/interpreter/instructions/comparison.ex @@ -50,10 +50,10 @@ defmodule EEVM.Interpreter.Instructions.Comparison do def execute(0x14, state), do: Helpers.comparison_op(state, &Kernel.==/2) def execute(0x15, state) do - with {:ok, a, s1} <- Stack.pop(state.stack) do + with {:ok, a, s1} <- Stack.pop(state.frame.stack) do result = if a == 0, do: 1, else: 0 {:ok, s2} = Stack.push(s1, result) - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end diff --git a/lib/eevm/interpreter/instructions/control_flow.ex b/lib/eevm/interpreter/instructions/control_flow.ex index cbe1466..b3db6da 100644 --- a/lib/eevm/interpreter/instructions/control_flow.ex +++ b/lib/eevm/interpreter/instructions/control_flow.ex @@ -57,9 +57,9 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do # JUMPDEST byte in the bytecode. Any other destination is an error. def execute(0x56, state) do - with {:ok, dest, s1} <- Stack.pop(state.stack) do - if Helpers.valid_jumpdest?(state.code, dest) do - {:ok, %{state | stack: s1, pc: dest}} + with {:ok, dest, s1} <- Stack.pop(state.frame.stack) do + if Helpers.valid_jumpdest?(state.frame.code, dest) do + {:ok, MachineState.update_frame(state, &%{&1 | stack: s1, pc: dest})} else {:error, :invalid_jump_destination, state} end @@ -72,23 +72,23 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do # If condition is non-zero, validates and jumps. If zero, falls through. def execute(0x57, state) do - with {:ok, dest, s1} <- Stack.pop(state.stack), + with {:ok, dest, s1} <- Stack.pop(state.frame.stack), {:ok, condition, s2} <- Stack.pop(s1) do if condition != 0 do - if Helpers.valid_jumpdest?(state.code, dest) do - {:ok, %{state | stack: s2, pc: dest}} + if Helpers.valid_jumpdest?(state.frame.code, dest) do + {:ok, MachineState.update_frame(state, &%{&1 | stack: s2, pc: dest})} else {:error, :invalid_jump_destination, state} end else - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} end else {:error, reason} -> {:error, reason, state} end end - def execute(0x58, state), do: Helpers.push_value(state, state.pc) + def execute(0x58, state), do: Helpers.push_value(state, state.frame.pc) def execute(0x5B, state), do: {:ok, MachineState.advance_pc(state)} # PUSH0 (EIP-3855, Shanghai+): pushes the constant 0 onto the stack without # consuming any inline bytecode bytes. Saves 1 byte and 2 gas vs PUSH1 0x00. @@ -108,16 +108,19 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do def execute(op, state) when op >= 0x60 and op <= 0x7F do n = Registry.push_bytes(op) - bytes = MachineState.read_code(state, state.pc + 1, n) + bytes = MachineState.read_code(state, state.frame.pc + 1, n) value = bytes |> :binary.bin_to_list() |> Enum.reduce(0, fn byte, acc -> acc * 256 + byte end) - case Stack.push(state.stack, value) do + case Stack.push(state.frame.stack, value) do {:ok, new_stack} -> - {:ok, %{state | stack: new_stack} |> MachineState.advance_pc(1 + n)} + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: new_stack}) + |> MachineState.advance_pc(1 + n)} {:error, reason} -> {:error, reason, state} @@ -131,9 +134,12 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do def execute(op, state) when op >= 0x80 and op <= 0x8F do depth = op - 0x80 - with {:ok, value} <- Stack.peek(state.stack, depth), - {:ok, new_stack} <- Stack.push(state.stack, value) do - {:ok, %{state | stack: new_stack} |> MachineState.advance_pc()} + with {:ok, value} <- Stack.peek(state.frame.stack, depth), + {:ok, new_stack} <- Stack.push(state.frame.stack, value) do + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: new_stack}) + |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -145,9 +151,12 @@ defmodule EEVM.Interpreter.Instructions.ControlFlow do def execute(op, state) when op >= 0x90 and op <= 0x9F do depth = op - 0x90 + 1 - case Stack.swap(state.stack, depth) do + case Stack.swap(state.frame.stack, depth) do {:ok, new_stack} -> - {:ok, %{state | stack: new_stack} |> MachineState.advance_pc()} + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: new_stack}) + |> MachineState.advance_pc()} {:error, reason} -> {:error, reason, state} diff --git a/lib/eevm/interpreter/instructions/crypto.ex b/lib/eevm/interpreter/instructions/crypto.ex index fd7b434..ec7217c 100644 --- a/lib/eevm/interpreter/instructions/crypto.ex +++ b/lib/eevm/interpreter/instructions/crypto.ex @@ -42,7 +42,7 @@ defmodule EEVM.Interpreter.Instructions.Crypto do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x20, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, length, s2} <- Stack.pop(s1) do dynamic_cost = Dynamic.keccak256_dynamic_cost(length) @@ -51,7 +51,7 @@ defmodule EEVM.Interpreter.Instructions.Crypto do # no memory and therefore never triggers expansion. mem_cost = if length > 0 do - GasMemory.memory_expansion_cost(Memory.size(state.memory), offset, length) + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), offset, length) else 0 end @@ -62,9 +62,9 @@ defmodule EEVM.Interpreter.Instructions.Crypto do # For length == 0 we hash an empty binary without touching memory. {data, updated_memory} = if length > 0 do - Memory.read_bytes(state_after_gas.memory, offset, length) + Memory.read_bytes(state_after_gas.frame.memory, offset, length) else - {<<>>, state_after_gas.memory} + {<<>>, state_after_gas.frame.memory} end # ExKeccak.hash_256 returns a raw 32-byte binary. The pattern @@ -74,7 +74,8 @@ defmodule EEVM.Interpreter.Instructions.Crypto do {:ok, new_stack} = Stack.push(s2, hash_int) {:ok, - %{state_after_gas | stack: new_stack, memory: updated_memory} + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: new_stack, memory: updated_memory}) |> MachineState.advance_pc()} {:error, :out_of_gas, halted} -> diff --git a/lib/eevm/interpreter/instructions/environment/data.ex b/lib/eevm/interpreter/instructions/environment/data.ex index d573476..60770b1 100644 --- a/lib/eevm/interpreter/instructions/environment/data.ex +++ b/lib/eevm/interpreter/instructions/environment/data.ex @@ -17,28 +17,31 @@ defmodule EEVM.Interpreter.Instructions.Environment.Data do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x35, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), - value = Contract.calldata_load(state.contract, offset), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), + value = Contract.calldata_load(state.frame.contract, offset), {:ok, s2} <- Stack.push(s1, value) do - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x37, state) do - with {:ok, dest_offset, s1} <- Stack.pop(state.stack), + with {:ok, dest_offset, s1} <- Stack.pop(state.frame.stack), {:ok, data_offset, s2} <- Stack.pop(s1), {:ok, length, s3} <- Stack.pop(s2) do if length == 0 do - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else expansion_cost = - GasMemory.memory_expansion_cost(Memory.size(state.memory), dest_offset, length) + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), dest_offset, length) - case MachineState.consume_gas(%{state | stack: s3}, expansion_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s3}), + expansion_cost + ) do {:ok, state_after_gas} -> - calldata = state_after_gas.contract.calldata + calldata = state_after_gas.frame.contract.calldata cd_size = byte_size(calldata) bytes = @@ -54,11 +57,14 @@ defmodule EEVM.Interpreter.Instructions.Environment.Data do bytes |> :binary.bin_to_list() |> Enum.with_index() - |> Enum.reduce(state_after_gas.memory, fn {byte, i}, mem -> + |> Enum.reduce(state_after_gas.frame.memory, fn {byte, i}, mem -> Memory.store_byte(mem, dest_offset + i, byte) end) - {:ok, %{state_after_gas | memory: new_memory} |> MachineState.advance_pc()} + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> MachineState.advance_pc()} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -70,17 +76,20 @@ defmodule EEVM.Interpreter.Instructions.Environment.Data do end def execute(0x39, state) do - with {:ok, dest_offset, s1} <- Stack.pop(state.stack), + with {:ok, dest_offset, s1} <- Stack.pop(state.frame.stack), {:ok, code_offset, s2} <- Stack.pop(s1), {:ok, length, s3} <- Stack.pop(s2) do if length == 0 do - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else dynamic_cost = Dynamic.copy_cost(length) + - GasMemory.memory_expansion_cost(Memory.size(state.memory), dest_offset, length) + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), dest_offset, length) - case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s3}), + dynamic_cost + ) do {:ok, state_after_gas} -> bytes = MachineState.read_code(state_after_gas, code_offset, length) @@ -88,11 +97,14 @@ defmodule EEVM.Interpreter.Instructions.Environment.Data do bytes |> :binary.bin_to_list() |> Enum.with_index() - |> Enum.reduce(state_after_gas.memory, fn {byte, i}, mem -> + |> Enum.reduce(state_after_gas.frame.memory, fn {byte, i}, mem -> Memory.store_byte(mem, dest_offset + i, byte) end) - {:ok, %{state_after_gas | memory: new_memory} |> MachineState.advance_pc()} + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> MachineState.advance_pc()} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -104,34 +116,48 @@ defmodule EEVM.Interpreter.Instructions.Environment.Data do end def execute(0x3E, state) do - with {:ok, dest_offset, s1} <- Stack.pop(state.stack), + with {:ok, dest_offset, s1} <- Stack.pop(state.frame.stack), {:ok, data_offset, s2} <- Stack.pop(s1), {:ok, length, s3} <- Stack.pop(s2) do cond do length == 0 -> - {:ok, MachineState.advance_pc(%{state | stack: s3})} + {:ok, + state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} - data_offset + length > byte_size(state.return_data) -> - {:ok, MachineState.halt(%{state | stack: s3}, :reverted)} + data_offset + length > byte_size(state.frame.return_data) -> + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: s3}) + |> MachineState.halt(:reverted)} true -> dynamic_cost = Dynamic.copy_cost(length) + - GasMemory.memory_expansion_cost(Memory.size(state.memory), dest_offset, length) - - case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do + GasMemory.memory_expansion_cost( + Memory.size(state.frame.memory), + dest_offset, + length + ) + + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s3}), + dynamic_cost + ) do {:ok, s4} -> - bytes = binary_part(s4.return_data, data_offset, length) + bytes = binary_part(s4.frame.return_data, data_offset, length) new_memory = bytes |> :binary.bin_to_list() |> Enum.with_index() - |> Enum.reduce(s4.memory, fn {byte, i}, mem -> + |> Enum.reduce(s4.frame.memory, fn {byte, i}, mem -> Memory.store_byte(mem, dest_offset + i, byte) end) - {:ok, MachineState.advance_pc(%{s4 | memory: new_memory})} + {:ok, + s4 + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> MachineState.advance_pc()} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} diff --git a/lib/eevm/interpreter/instructions/environment/external.ex b/lib/eevm/interpreter/instructions/environment/external.ex index 2b94c7f..bf165d8 100644 --- a/lib/eevm/interpreter/instructions/environment/external.ex +++ b/lib/eevm/interpreter/instructions/environment/external.ex @@ -21,16 +21,23 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x31, state) do - with {:ok, addr, s1} <- Stack.pop(state.stack) do - {access_cost, state_after_access} = Access.address_access_cost(%{state | stack: s1}, addr) + with {:ok, addr, s1} <- Stack.pop(state.frame.stack) do + {access_cost, state_after_access} = + Access.address_access_cost(MachineState.update_frame(state, &%{&1 | stack: s1}), addr) case MachineState.consume_gas(state_after_access, access_cost) do {:ok, state_after_gas} -> balance = lookup_balance(state_after_gas, addr) - case Stack.push(state_after_gas.stack, balance) do - {:ok, s2} -> {:ok, %{state_after_gas | stack: s2} |> MachineState.advance_pc()} - {:error, reason} -> {:error, reason, state_after_gas} + case Stack.push(state_after_gas.frame.stack, balance) do + {:ok, s2} -> + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2}) + |> MachineState.advance_pc()} + + {:error, reason} -> + {:error, reason, state_after_gas} end {:error, :out_of_gas, halted_state} -> @@ -42,16 +49,23 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do end def execute(0x3B, state) do - with {:ok, addr, s1} <- Stack.pop(state.stack) do - {access_cost, state_after_access} = Access.address_access_cost(%{state | stack: s1}, addr) + with {:ok, addr, s1} <- Stack.pop(state.frame.stack) do + {access_cost, state_after_access} = + Access.address_access_cost(MachineState.update_frame(state, &%{&1 | stack: s1}), addr) case MachineState.consume_gas(state_after_access, access_cost) do {:ok, state_after_gas} -> size = byte_size(Database.get_code(state_after_gas.db, addr)) - case Stack.push(state_after_gas.stack, size) do - {:ok, s2} -> {:ok, %{state_after_gas | stack: s2} |> MachineState.advance_pc()} - {:error, reason} -> {:error, reason, state_after_gas} + case Stack.push(state_after_gas.frame.stack, size) do + {:ok, s2} -> + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2}) + |> MachineState.advance_pc()} + + {:error, reason} -> + {:error, reason, state_after_gas} end {:error, :out_of_gas, halted_state} -> @@ -63,11 +77,12 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do end def execute(0x3C, state) do - with {:ok, addr, s1} <- Stack.pop(state.stack), + with {:ok, addr, s1} <- Stack.pop(state.frame.stack), {:ok, dest_offset, s2} <- Stack.pop(s1), {:ok, code_offset, s3} <- Stack.pop(s2), {:ok, length, s4} <- Stack.pop(s3) do - {access_cost, state_after_access} = Access.address_access_cost(%{state | stack: s4}, addr) + {access_cost, state_after_access} = + Access.address_access_cost(MachineState.update_frame(state, &%{&1 | stack: s4}), addr) case MachineState.consume_gas(state_after_access, access_cost) do {:ok, state_after_access_gas} -> @@ -77,7 +92,7 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do dynamic_cost = Dynamic.copy_cost(length) + GasMemory.memory_expansion_cost( - Memory.size(state_after_access_gas.memory), + Memory.size(state_after_access_gas.frame.memory), dest_offset, length ) @@ -90,11 +105,14 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do bytes |> :binary.bin_to_list() |> Enum.with_index() - |> Enum.reduce(state_after_gas.memory, fn {byte, i}, mem -> + |> Enum.reduce(state_after_gas.frame.memory, fn {byte, i}, mem -> Memory.store_byte(mem, dest_offset + i, byte) end) - {:ok, %{state_after_gas | memory: new_memory} |> MachineState.advance_pc()} + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> MachineState.advance_pc()} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -110,8 +128,9 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do end def execute(0x3F, state) do - with {:ok, addr, s1} <- Stack.pop(state.stack) do - {access_cost, state_after_access} = Access.address_access_cost(%{state | stack: s1}, addr) + with {:ok, addr, s1} <- Stack.pop(state.frame.stack) do + {access_cost, state_after_access} = + Access.address_access_cost(MachineState.update_frame(state, &%{&1 | stack: s1}), addr) case MachineState.consume_gas(state_after_access, access_cost) do {:ok, state_after_gas} -> @@ -125,9 +144,15 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do 0 end - case Stack.push(state_after_gas.stack, hash_value) do - {:ok, s2} -> {:ok, %{state_after_gas | stack: s2} |> MachineState.advance_pc()} - {:error, reason} -> {:error, reason, state_after_gas} + case Stack.push(state_after_gas.frame.stack, hash_value) do + {:ok, s2} -> + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2}) + |> MachineState.advance_pc()} + + {:error, reason} -> + {:error, reason, state_after_gas} end {:error, :out_of_gas, halted_state} -> @@ -139,7 +164,7 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do end def execute(0x47, state) do - balance = lookup_balance(state, state.contract.address) + balance = lookup_balance(state, state.frame.contract.address) Helpers.push_value(state, balance) end @@ -149,7 +174,7 @@ defmodule EEVM.Interpreter.Instructions.Environment.External do if Database.account_exists?(state.db, address) do Database.get_balance(state.db, address) else - Contract.balance(state.contract, address) + Contract.balance(state.frame.contract, address) end end diff --git a/lib/eevm/interpreter/instructions/environment/simple.ex b/lib/eevm/interpreter/instructions/environment/simple.ex index d7d0eec..eb6f8e9 100644 --- a/lib/eevm/interpreter/instructions/environment/simple.ex +++ b/lib/eevm/interpreter/instructions/environment/simple.ex @@ -16,14 +16,17 @@ defmodule EEVM.Interpreter.Instructions.Environment.Simple do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} - def execute(0x30, state), do: Helpers.push_value(state, state.contract.address) + def execute(0x30, state), do: Helpers.push_value(state, state.frame.contract.address) def execute(0x32, state), do: Helpers.push_value(state, state.env.tx.origin) - def execute(0x33, state), do: Helpers.push_value(state, state.contract.caller) - def execute(0x34, state), do: Helpers.push_value(state, state.contract.callvalue) - def execute(0x36, state), do: Helpers.push_value(state, byte_size(state.contract.calldata)) - def execute(0x38, state), do: Helpers.push_value(state, byte_size(state.code)) + def execute(0x33, state), do: Helpers.push_value(state, state.frame.contract.caller) + def execute(0x34, state), do: Helpers.push_value(state, state.frame.contract.callvalue) + + def execute(0x36, state), + do: Helpers.push_value(state, byte_size(state.frame.contract.calldata)) + + def execute(0x38, state), do: Helpers.push_value(state, byte_size(state.frame.code)) def execute(0x3A, state), do: Helpers.push_value(state, state.env.tx.gasprice) - def execute(0x3D, state), do: Helpers.push_value(state, byte_size(state.return_data)) + def execute(0x3D, state), do: Helpers.push_value(state, byte_size(state.frame.return_data)) def execute(0x41, state), do: Helpers.push_value(state, state.env.block.coinbase) def execute(0x42, state), do: Helpers.push_value(state, state.env.block.timestamp) def execute(0x43, state), do: Helpers.push_value(state, state.env.block.number) @@ -32,25 +35,29 @@ defmodule EEVM.Interpreter.Instructions.Environment.Simple do def execute(0x46, state), do: Helpers.push_value(state, state.env.block.chain_id) def execute(0x48, state), do: Helpers.push_value(state, state.env.block.basefee) def execute(0x4A, state), do: Helpers.push_value(state, state.env.block.blob_base_fee) - def execute(0x5A, state), do: Helpers.push_value(state, state.gas) + def execute(0x5A, state), do: Helpers.push_value(state, state.frame.gas) def execute(0x40, state) do - with {:ok, block_num, s1} <- Stack.pop(state.stack), + with {:ok, block_num, s1} <- Stack.pop(state.frame.stack), hash = Block.hash(state.env.block, block_num), {:ok, s2} <- Stack.push(s1, hash) do - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end end def execute(0x49, state) do - with {:ok, index, s1} <- Stack.pop(state.stack) do + with {:ok, index, s1} <- Stack.pop(state.frame.stack) do value = Enum.at(state.env.tx.blob_hashes, index, 0) case Stack.push(s1, value) do - {:ok, s2} -> {:ok, %{state | stack: s2} |> MachineState.advance_pc()} - {:error, reason} -> {:error, reason, state} + {:ok, s2} -> + {:ok, + state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} + + {:error, reason} -> + {:error, reason, state} end else {:error, reason} -> {:error, reason, state} diff --git a/lib/eevm/interpreter/instructions/helpers.ex b/lib/eevm/interpreter/instructions/helpers.ex index bf968bb..3b9233a 100644 --- a/lib/eevm/interpreter/instructions/helpers.ex +++ b/lib/eevm/interpreter/instructions/helpers.ex @@ -46,11 +46,11 @@ defmodule EEVM.Interpreter.Instructions.Helpers do @spec comparison_op(MachineState.t(), (non_neg_integer(), non_neg_integer() -> boolean())) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def comparison_op(state, fun) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if fun.(a, b), do: 1, else: 0 {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -67,11 +67,11 @@ defmodule EEVM.Interpreter.Instructions.Helpers do (integer(), integer() -> boolean()) ) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def signed_comparison_op(state, fun) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = if fun.(to_signed(a), to_signed(b)), do: 1, else: 0 {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -86,11 +86,11 @@ defmodule EEVM.Interpreter.Instructions.Helpers do @spec bitwise_op(MachineState.t(), (non_neg_integer(), non_neg_integer() -> non_neg_integer())) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def bitwise_op(state, fun) do - with {:ok, a, s1} <- Stack.pop(state.stack), + with {:ok, a, s1} <- Stack.pop(state.frame.stack), {:ok, b, s2} <- Stack.pop(s1) do result = fun.(a, b) {:ok, s3} = Stack.push(s2, result) - {:ok, %{state | stack: s3} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s3}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -149,8 +149,10 @@ defmodule EEVM.Interpreter.Instructions.Helpers do @spec push_value(MachineState.t(), non_neg_integer()) :: {:ok, MachineState.t()} def push_value(state, value) do - {:ok, new_stack} = Stack.push(state.stack, value) - {:ok, %{state | stack: new_stack} |> MachineState.advance_pc()} + {:ok, new_stack} = Stack.push(state.frame.stack, value) + + {:ok, + state |> MachineState.update_frame(&%{&1 | stack: new_stack}) |> MachineState.advance_pc()} end @doc """ diff --git a/lib/eevm/interpreter/instructions/logging.ex b/lib/eevm/interpreter/instructions/logging.ex index 2bda30a..832417f 100644 --- a/lib/eevm/interpreter/instructions/logging.ex +++ b/lib/eevm/interpreter/instructions/logging.ex @@ -35,25 +35,33 @@ defmodule EEVM.Interpreter.Instructions.Logging do def execute(_opcode, state), do: {:ok, MachineState.halt(state, :invalid)} defp execute_log(state, topic_count) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, size, s2} <- Stack.pop(s1), {:ok, topics, s3} <- pop_topics(s2, topic_count, []) do dynamic_cost = Dynamic.log_cost(topic_count, size) + - GasMemory.memory_expansion_cost(Memory.size(state.memory), offset, size) + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), offset, size) - case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s3}), + dynamic_cost + ) do {:ok, s4} -> - {data, new_memory} = Memory.read_bytes(s4.memory, offset, size) + {data, new_memory} = Memory.read_bytes(s4.frame.memory, offset, size) log_entry = %{ - address: s4.contract.address, + address: s4.frame.contract.address, data: data, topics: topics } new_substate = %{s4.substate | logs: s4.substate.logs ++ [log_entry]} - s5 = %{s4 | memory: new_memory, substate: new_substate} + + s5 = + s4 + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> Map.put(:substate, new_substate) + {:ok, MachineState.advance_pc(s5)} {:error, :out_of_gas, halted_state} -> diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex index 1fb9c66..acfe56b 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/memory_ops.ex @@ -21,13 +21,21 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x51, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), - expansion_cost = GasMemory.memory_expansion_cost_word(Memory.size(state.memory), offset), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), + expansion_cost = + GasMemory.memory_expansion_cost_word(Memory.size(state.frame.memory), offset), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s1}, expansion_cost) do - {value, new_memory} = Memory.load(state_after_gas.memory, offset) - {:ok, s2} = Stack.push(state_after_gas.stack, value) - {:ok, %{state_after_gas | stack: s2, memory: new_memory} |> MachineState.advance_pc()} + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s1}), + expansion_cost + ) do + {value, new_memory} = Memory.load(state_after_gas.frame.memory, offset) + {:ok, s2} = Stack.push(state_after_gas.frame.stack, value) + + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2, memory: new_memory}) + |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -35,13 +43,21 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do end def execute(0x52, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1), - expansion_cost = GasMemory.memory_expansion_cost_word(Memory.size(state.memory), offset), + expansion_cost = + GasMemory.memory_expansion_cost_word(Memory.size(state.frame.memory), offset), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s2}, expansion_cost) do - new_memory = Memory.store(state_after_gas.memory, offset, value) - {:ok, %{state_after_gas | stack: s2, memory: new_memory} |> MachineState.advance_pc()} + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s2}), + expansion_cost + ) do + new_memory = Memory.store(state_after_gas.frame.memory, offset, value) + + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2, memory: new_memory}) + |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -49,13 +65,21 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do end def execute(0x53, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1), - expansion_cost = GasMemory.memory_expansion_cost_byte(Memory.size(state.memory), offset), + expansion_cost = + GasMemory.memory_expansion_cost_byte(Memory.size(state.frame.memory), offset), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s2}, expansion_cost) do - new_memory = Memory.store_byte(state_after_gas.memory, offset, value) - {:ok, %{state_after_gas | stack: s2, memory: new_memory} |> MachineState.advance_pc()} + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s2}), + expansion_cost + ) do + new_memory = Memory.store_byte(state_after_gas.frame.memory, offset, value) + + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2, memory: new_memory}) + |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -63,7 +87,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do end def execute(0x59, state) do - size = Memory.size(state.memory) + size = Memory.size(state.frame.memory) Helpers.push_value(state, size) end @@ -71,23 +95,33 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.MemoryOps do # Pre-Cancun, 0x5E is undefined → :invalid. def execute(0x5E, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_5656) do - with {:ok, dst, s1} <- Stack.pop(state.stack), + with {:ok, dst, s1} <- Stack.pop(state.frame.stack), {:ok, src, s2} <- Stack.pop(s1), {:ok, length, s3} <- Stack.pop(s2) do if length == 0 do - {:ok, MachineState.advance_pc(%{state | stack: s3})} + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: s3}) + |> MachineState.advance_pc()} else max_offset = max(src + length, dst + length) expansion_cost = - GasMemory.memory_expansion_cost(Memory.size(state.memory), 0, max_offset) + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), 0, max_offset) dynamic_cost = Dynamic.copy_cost(length) + expansion_cost - case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s3}), + dynamic_cost + ) do {:ok, s4} -> - new_memory = Memory.copy(s4.memory, dst, src, length) - {:ok, MachineState.advance_pc(%{s4 | memory: new_memory})} + new_memory = Memory.copy(s4.frame.memory, dst, src, length) + + {:ok, + s4 + |> MachineState.update_frame(&%{&1 | memory: new_memory}) + |> MachineState.advance_pc()} {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/stack_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/stack_ops.ex index 80c83f1..4ecfc94 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/stack_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/stack_ops.ex @@ -11,9 +11,12 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StackOps do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x50, state) do - case Stack.pop(state.stack) do + case Stack.pop(state.frame.stack) do {:ok, _value, new_stack} -> - {:ok, %{state | stack: new_stack} |> MachineState.advance_pc()} + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: new_stack}) + |> MachineState.advance_pc()} {:error, reason} -> {:error, reason, state} diff --git a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex index 79ac7a8..2a1a850 100644 --- a/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex +++ b/lib/eevm/interpreter/instructions/stack_memory_storage/storage_ops.ex @@ -31,19 +31,29 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0x54, state) do - with {:ok, key, s1} <- Stack.pop(state.stack) do - contract_address = state.contract.address + with {:ok, key, s1} <- Stack.pop(state.frame.stack) do + contract_address = state.frame.contract.address {access_cost, state_after_access} = - Access.storage_access_cost(%{state | stack: s1}, contract_address, key) + Access.storage_access_cost( + MachineState.update_frame(state, &%{&1 | stack: s1}), + contract_address, + key + ) case MachineState.consume_gas(state_after_access, access_cost) do {:ok, state_after_gas} -> value = Database.storage_load(state_after_gas.db, contract_address, key) - case Stack.push(state_after_gas.stack, value) do - {:ok, s2} -> {:ok, %{state_after_gas | stack: s2} |> MachineState.advance_pc()} - {:error, reason} -> {:error, reason, state_after_gas} + case Stack.push(state_after_gas.frame.stack, value) do + {:ok, s2} -> + {:ok, + state_after_gas + |> MachineState.update_frame(&%{&1 | stack: s2}) + |> MachineState.advance_pc()} + + {:error, reason} -> + {:error, reason, state_after_gas} end {:error, :out_of_gas, halted} -> @@ -55,10 +65,10 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do end def execute(0x55, state) do - with {:ok, key, s1} <- Stack.pop(state.stack), + with {:ok, key, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1) do - state_after_stack = %{state | stack: s2} - contract_address = state_after_stack.contract.address + state_after_stack = MachineState.update_frame(state, &%{&1 | stack: s2}) + contract_address = state_after_stack.frame.contract.address access_key = {contract_address, key} is_warm = MapSet.member?(state_after_stack.substate.accessed_storage_keys, access_key) @@ -101,10 +111,10 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do # cleared at the end of each transaction. Pre-Cancun, 0x5C is undefined → :invalid. def execute(0x5C, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_1153) do - with {:ok, key, s1} <- Stack.pop(state.stack), + with {:ok, key, s1} <- Stack.pop(state.frame.stack), value = Map.get(state.substate.transient_storage, key, 0), {:ok, s2} <- Stack.push(s1, value) do - {:ok, %{state | stack: s2} |> MachineState.advance_pc()} + {:ok, state |> MachineState.update_frame(&%{&1 | stack: s2}) |> MachineState.advance_pc()} else {:error, reason} -> {:error, reason, state} end @@ -118,11 +128,17 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do # Pre-Cancun, 0x5D is undefined → :invalid. def execute(0x5D, %{env: %{config: %{hardfork: hardfork}}} = state) do if HardforkConfig.enabled?(hardfork, :eip_1153) do - with {:ok, key, s1} <- Stack.pop(state.stack), + with {:ok, key, s1} <- Stack.pop(state.frame.stack), {:ok, value, s2} <- Stack.pop(s1) do sub = state.substate new_sub = %{sub | transient_storage: Map.put(sub.transient_storage, key, value)} - {:ok, %{state | stack: s2, substate: new_sub} |> MachineState.advance_pc()} + + state_after = + state + |> MachineState.update_frame(&%{&1 | stack: s2}) + |> Map.put(:substate, new_sub) + + {:ok, MachineState.advance_pc(state_after)} else {:error, reason} -> {:error, reason, state} end @@ -139,7 +155,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryStorage.StorageOps do {value, state} :error -> - value = Database.storage_load(state.db, state.contract.address, key) + value = Database.storage_load(state.db, state.frame.contract.address, key) sub = state.substate new_sub = %{sub | original_storage: Map.put(sub.original_storage, key, value)} {value, %{state | substate: new_sub}} diff --git a/lib/eevm/interpreter/instructions/system/calls.ex b/lib/eevm/interpreter/instructions/system/calls.ex index 31c9d68..ea86e67 100644 --- a/lib/eevm/interpreter/instructions/system/calls.ex +++ b/lib/eevm/interpreter/instructions/system/calls.ex @@ -28,7 +28,7 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0xF1, state) do - with {:ok, gas_requested, s1} <- Stack.pop(state.stack), + with {:ok, gas_requested, s1} <- Stack.pop(state.frame.stack), {:ok, address, s2} <- Stack.pop(s1), {:ok, value, s3} <- Stack.pop(s2), {:ok, args_offset, s4} <- Stack.pop(s3), @@ -36,10 +36,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do {:ok, ret_offset, s6} <- Stack.pop(s5), {:ok, ret_size, s7} <- Stack.pop(s6) do cond do - state.depth >= 1024 -> + state.frame.depth >= 1024 -> call_failed(state, s7) - state.is_static and value > 0 -> + state.frame.is_static and value > 0 -> call_failed(state, s7) true -> @@ -61,7 +61,7 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do end def execute(0xF2, state) do - with {:ok, gas_requested, s1} <- Stack.pop(state.stack), + with {:ok, gas_requested, s1} <- Stack.pop(state.frame.stack), {:ok, address, s2} <- Stack.pop(s1), {:ok, value, s3} <- Stack.pop(s2), {:ok, args_offset, s4} <- Stack.pop(s3), @@ -69,10 +69,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do {:ok, ret_offset, s6} <- Stack.pop(s5), {:ok, ret_size, s7} <- Stack.pop(s6) do cond do - state.depth >= 1024 -> + state.frame.depth >= 1024 -> call_failed(state, s7) - state.is_static and value > 0 -> + state.frame.is_static and value > 0 -> call_failed(state, s7) true -> @@ -86,7 +86,7 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do args_size, ret_offset, ret_size, - state.contract.address, + state.frame.contract.address, true, true ) @@ -97,13 +97,13 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do end def execute(0xF4, state) do - with {:ok, gas_requested, s1} <- Stack.pop(state.stack), + with {:ok, gas_requested, s1} <- Stack.pop(state.frame.stack), {:ok, address, s2} <- Stack.pop(s1), {:ok, args_offset, s3} <- Stack.pop(s2), {:ok, args_size, s4} <- Stack.pop(s3), {:ok, ret_offset, s5} <- Stack.pop(s4), {:ok, ret_size, s6} <- Stack.pop(s5) do - if state.depth >= 1024 do + if state.frame.depth >= 1024 do call_failed(state, s6) else execute_delegatecall( @@ -111,12 +111,12 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do s6, gas_requested, address, - state.contract.callvalue, + state.frame.contract.callvalue, args_offset, args_size, ret_offset, ret_size, - state.contract.caller, + state.frame.contract.caller, false, false ) @@ -127,17 +127,17 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do end def execute(0xFA, state) do - with {:ok, gas_requested, s1} <- Stack.pop(state.stack), + with {:ok, gas_requested, s1} <- Stack.pop(state.frame.stack), {:ok, address, s2} <- Stack.pop(s1), {:ok, args_offset, s3} <- Stack.pop(s2), {:ok, args_size, s4} <- Stack.pop(s3), {:ok, ret_offset, s5} <- Stack.pop(s4), {:ok, ret_size, s6} <- Stack.pop(s5) do - if state.depth >= 1024 do + if state.frame.depth >= 1024 do call_failed(state, s6) else execute_call( - %{state | is_static: true}, + MachineState.update_frame(state, &%{&1 | is_static: true}), s6, gas_requested, address, @@ -172,23 +172,33 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do call_cost = Dynamic.call_value_cost(value) + Dynamic.call_new_account_cost(account_exists, value) + - call_memory_expansion_cost(state.memory, args_offset, args_size, ret_offset, ret_size) + call_memory_expansion_cost( + state.frame.memory, + args_offset, + args_size, + ret_offset, + ret_size + ) - with {:ok, state_after_cost} <- MachineState.consume_gas(%{state | stack: stack}, call_cost), + with {:ok, state_after_cost} <- + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: stack}), + call_cost + ), {:ok, db_after_transfer} <- Database.transfer( state_after_cost.db, - state_after_cost.contract.address, + state_after_cost.frame.contract.address, address, value ) do {calldata, memory_after_read} = - Memory.read_bytes(state_after_cost.memory, args_offset, args_size) + Memory.read_bytes(state_after_cost.frame.memory, args_offset, args_size) - forwarded_gas = Dynamic.call_forwarded_gas(state_after_cost.gas, gas_requested) + forwarded_gas = Dynamic.call_forwarded_gas(state_after_cost.frame.gas, gas_requested) case MachineState.consume_gas( - %{state_after_cost | memory: memory_after_read}, + MachineState.update_frame(state_after_cost, &%{&1 | memory: memory_after_read}), forwarded_gas ) do {:ok, state_after_forward} -> @@ -202,26 +212,36 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do {:ok, output, gas_used} -> remaining_gas = child_gas - gas_used memory_result = write_return_data(memory_after_read, ret_offset, ret_size, output) - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, 1) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, 1) state_after_touch = MachineState.touch_address(state_after_forward, address) {:ok, state_after_touch - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, output) - |> Map.put(:gas, state_after_touch.gas + remaining_gas) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: output, + gas: &1.gas + remaining_gas + } + ) |> MachineState.advance_pc()} {:error, _reason} -> memory_result = write_return_data(memory_after_read, ret_offset, ret_size, <<>>) - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, 0) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, 0) {:ok, state_after_forward - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, <<>>) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: <<>> + } + ) |> MachineState.advance_pc()} end else @@ -230,10 +250,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do child_contract = Contract.new( address: address, - caller: state_after_forward.contract.address, + caller: state_after_forward.frame.contract.address, callvalue: value, calldata: calldata, - balances: state_after_forward.contract.balances + balances: state_after_forward.frame.contract.balances ) child_gas = forwarded_gas + Dynamic.call_stipend(value) @@ -250,8 +270,8 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, created_addresses: state_after_touch.substate.created_addresses, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -259,17 +279,27 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do merged = Journal.merge_child_result(state_after_forward, child_result) memory_result = - write_return_data(memory_after_read, ret_offset, ret_size, child_result.return_data) + write_return_data( + memory_after_read, + ret_offset, + ret_size, + child_result.frame.return_data + ) result_flag = if child_result.status == :stopped, do: 1, else: 0 - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, result_flag) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, result_flag) {:ok, merged - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, child_result.return_data) - |> Map.put(:gas, state_after_forward.gas + child_result.gas) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: child_result.frame.return_data, + gas: state_after_forward.frame.gas + child_result.frame.gas + } + ) |> MachineState.advance_pc()} end @@ -278,7 +308,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do end else {:error, :insufficient_balance} -> - call_failed(%{state | stack: stack, gas: state.gas - call_cost}, stack) + call_failed( + MachineState.update_frame(state, &%{&1 | stack: stack, gas: &1.gas - call_cost}), + stack + ) {:error, :out_of_gas, halted_state} -> {:error, :out_of_gas, halted_state} @@ -301,16 +334,26 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do ) do call_cost = if(charge_value_cost?, do: Dynamic.call_value_cost(callvalue), else: 0) + - call_memory_expansion_cost(state.memory, args_offset, args_size, ret_offset, ret_size) + call_memory_expansion_cost( + state.frame.memory, + args_offset, + args_size, + ret_offset, + ret_size + ) - with {:ok, state_after_cost} <- MachineState.consume_gas(%{state | stack: stack}, call_cost) do + with {:ok, state_after_cost} <- + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: stack}), + call_cost + ) do {calldata, memory_after_read} = - Memory.read_bytes(state_after_cost.memory, args_offset, args_size) + Memory.read_bytes(state_after_cost.frame.memory, args_offset, args_size) - forwarded_gas = Dynamic.call_forwarded_gas(state_after_cost.gas, gas_requested) + forwarded_gas = Dynamic.call_forwarded_gas(state_after_cost.frame.gas, gas_requested) case MachineState.consume_gas( - %{state_after_cost | memory: memory_after_read}, + MachineState.update_frame(state_after_cost, &%{&1 | memory: memory_after_read}), forwarded_gas ) do {:ok, state_after_forward} -> @@ -325,26 +368,36 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do {:ok, output, gas_used} -> remaining_gas = child_gas - gas_used memory_result = write_return_data(memory_after_read, ret_offset, ret_size, output) - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, 1) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, 1) state_after_touch = MachineState.touch_address(state_after_forward, address) {:ok, state_after_touch - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, output) - |> Map.put(:gas, state_after_touch.gas + remaining_gas) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: output, + gas: &1.gas + remaining_gas + } + ) |> MachineState.advance_pc()} {:error, _reason} -> memory_result = write_return_data(memory_after_read, ret_offset, ret_size, <<>>) - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, 0) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, 0) {:ok, state_after_forward - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, <<>>) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: <<>> + } + ) |> MachineState.advance_pc()} end else @@ -352,11 +405,11 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do child_contract = Contract.new( - address: state_after_forward.contract.address, + address: state_after_forward.frame.contract.address, caller: caller, callvalue: callvalue, calldata: calldata, - balances: state_after_forward.contract.balances + balances: state_after_forward.frame.contract.balances ) child_state = @@ -371,8 +424,8 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, created_addresses: state_after_touch.substate.created_addresses, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -380,17 +433,27 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do merged = Journal.merge_child_result(state_after_forward, child_result) memory_result = - write_return_data(memory_after_read, ret_offset, ret_size, child_result.return_data) + write_return_data( + memory_after_read, + ret_offset, + ret_size, + child_result.frame.return_data + ) result_flag = if child_result.status == :stopped, do: 1, else: 0 - {:ok, stack_after_call} = Stack.push(state_after_forward.stack, result_flag) + {:ok, stack_after_call} = Stack.push(state_after_forward.frame.stack, result_flag) {:ok, merged - |> Map.put(:stack, stack_after_call) - |> Map.put(:memory, memory_result) - |> Map.put(:return_data, child_result.return_data) - |> Map.put(:gas, state_after_forward.gas + child_result.gas) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_call, + memory: memory_result, + return_data: child_result.frame.return_data, + gas: state_after_forward.frame.gas + child_result.frame.gas + } + ) |> MachineState.advance_pc()} end @@ -435,6 +498,10 @@ defmodule EEVM.Interpreter.Instructions.System.Calls do defp call_failed(state, stack) do {:ok, stack_after_call} = Stack.push(stack, 0) - {:ok, %{state | stack: stack_after_call} |> MachineState.advance_pc()} + + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: stack_after_call}) + |> MachineState.advance_pc()} end end diff --git a/lib/eevm/interpreter/instructions/system/creation.ex b/lib/eevm/interpreter/instructions/system/creation.ex index c3b17f5..9880422 100644 --- a/lib/eevm/interpreter/instructions/system/creation.ex +++ b/lib/eevm/interpreter/instructions/system/creation.ex @@ -31,7 +31,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do @spec execute(non_neg_integer(), MachineState.t()) :: {:ok, MachineState.t()} | {:error, atom(), MachineState.t()} def execute(0xF0, state) do - with {:ok, value, s1} <- Stack.pop(state.stack), + with {:ok, value, s1} <- Stack.pop(state.frame.stack), {:ok, offset, s2} <- Stack.pop(s1), {:ok, size, s3} <- Stack.pop(s2) do execute_create(state, s3, value, offset, size, nil) @@ -41,7 +41,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do end def execute(0xF5, state) do - with {:ok, value, s1} <- Stack.pop(state.stack), + with {:ok, value, s1} <- Stack.pop(state.frame.stack), {:ok, offset, s2} <- Stack.pop(s1), {:ok, size, s3} <- Stack.pop(s2), {:ok, salt, s4} <- Stack.pop(s3) do @@ -55,10 +55,10 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do defp execute_create(state, stack, value, offset, size, salt) do cond do - state.depth >= 1024 -> + state.frame.depth >= 1024 -> create_failed(state, stack) - state.is_static -> + state.frame.is_static -> create_failed(state, stack) true -> @@ -68,12 +68,15 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do defp execute_create_inner(state, stack, value, offset, size, salt) do extra_cost = - GasMemory.memory_expansion_cost(Memory.size(state.memory), offset, size) + + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), offset, size) + if(salt == nil, do: 0, else: Dynamic.create2_hash_cost(size)) - case MachineState.consume_gas(%{state | stack: stack}, extra_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: stack}), + extra_cost + ) do {:ok, state_after_cost} -> - creator = state_after_cost.contract.address + creator = state_after_cost.frame.contract.address nonce = Database.get_nonce(state_after_cost.db, creator) db_after_nonce = @@ -91,7 +94,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do case MachineState.consume_gas(state_after_cost, initcode_cost) do {:ok, state_after_initcode_cost} -> {init_code, memory_after_read} = - Memory.read_bytes(state_after_initcode_cost.memory, offset, size) + Memory.read_bytes(state_after_initcode_cost.frame.memory, offset, size) new_address = if salt == nil, @@ -112,12 +115,12 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do caller: creator, callvalue: value, calldata: <<>>, - balances: state_after_initcode_cost.contract.balances + balances: state_after_initcode_cost.frame.contract.balances ) child_state = MachineState.new(init_code, - gas: state_after_initcode_cost.gas, + gas: state_after_initcode_cost.frame.gas, db: db_after_transfer, tx: state_after_touch.env.tx, block: state_after_touch.env.block, @@ -127,8 +130,8 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do accessed_addresses: state_after_touch.substate.accessed_addresses, accessed_storage_keys: state_after_touch.substate.accessed_storage_keys, created_addresses: state_after_touch.substate.created_addresses, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -142,7 +145,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do } if deployment_success do - runtime_code = child_result.return_data + runtime_code = child_result.frame.return_data if HardforkConfig.enabled?(child_result.env.config.hardfork, :eip_170) and byte_size(runtime_code) > @max_code_size do @@ -159,7 +162,7 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do else deposit_cost = Dynamic.code_deposit_cost(byte_size(runtime_code)) - if child_result.gas >= deposit_cost do + if child_result.frame.gas >= deposit_cost do db_after_deploy = child_result.db |> Database.put_code(new_address, runtime_code) @@ -178,12 +181,17 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do {:ok, merged - |> Map.put(:stack, stack_after_create) - |> Map.put(:memory, memory_after_read) + |> MachineState.update_frame( + &%{ + &1 + | stack: stack_after_create, + memory: memory_after_read, + gas: child_result.frame.gas - deposit_cost, + return_data: child_result.frame.return_data + } + ) |> Map.put(:db, db_after_deploy) |> Map.put(:substate, merged_substate) - |> Map.put(:gas, child_result.gas - deposit_cost) - |> Map.put(:return_data, child_result.return_data) |> MachineState.advance_pc()} else create_failed(post_child_fail_state, stack) @@ -295,6 +303,10 @@ defmodule EEVM.Interpreter.Instructions.System.Creation do defp create_failed(state, stack) do {:ok, stack_after_create} = Stack.push(stack, 0) - {:ok, %{state | stack: stack_after_create} |> MachineState.advance_pc()} + + {:ok, + state + |> MachineState.update_frame(&%{&1 | stack: stack_after_create}) + |> MachineState.advance_pc()} end end diff --git a/lib/eevm/interpreter/instructions/system/termination.ex b/lib/eevm/interpreter/instructions/system/termination.ex index cb239d7..3f46012 100644 --- a/lib/eevm/interpreter/instructions/system/termination.ex +++ b/lib/eevm/interpreter/instructions/system/termination.ex @@ -22,16 +22,22 @@ defmodule EEVM.Interpreter.Instructions.System.Termination do def execute(0x00, state), do: {:ok, MachineState.halt(state, :stopped)} def execute(0xF3, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, length, s2} <- Stack.pop(s1), expansion_cost = - GasMemory.memory_expansion_cost(Memory.size(state.memory), offset, length), + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), offset, length), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s2}, expansion_cost) do - {return_data, new_memory} = Memory.read_bytes(state_after_gas.memory, offset, length) + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s2}), + expansion_cost + ) do + {return_data, new_memory} = Memory.read_bytes(state_after_gas.frame.memory, offset, length) {:ok, - %{state_after_gas | stack: s2, memory: new_memory, return_data: return_data} + state_after_gas + |> MachineState.update_frame( + &%{&1 | stack: s2, memory: new_memory, return_data: return_data} + ) |> MachineState.halt(:stopped)} else {:error, reason} -> {:error, reason, state} @@ -40,16 +46,22 @@ defmodule EEVM.Interpreter.Instructions.System.Termination do end def execute(0xFD, state) do - with {:ok, offset, s1} <- Stack.pop(state.stack), + with {:ok, offset, s1} <- Stack.pop(state.frame.stack), {:ok, length, s2} <- Stack.pop(s1), expansion_cost = - GasMemory.memory_expansion_cost(Memory.size(state.memory), offset, length), + GasMemory.memory_expansion_cost(Memory.size(state.frame.memory), offset, length), {:ok, state_after_gas} <- - MachineState.consume_gas(%{state | stack: s2}, expansion_cost) do - {return_data, new_memory} = Memory.read_bytes(state_after_gas.memory, offset, length) + MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s2}), + expansion_cost + ) do + {return_data, new_memory} = Memory.read_bytes(state_after_gas.frame.memory, offset, length) {:ok, - %{state_after_gas | stack: s2, memory: new_memory, return_data: return_data} + state_after_gas + |> MachineState.update_frame( + &%{&1 | stack: s2, memory: new_memory, return_data: return_data} + ) |> MachineState.halt(:reverted)} else {:error, reason} -> {:error, reason, state} @@ -58,14 +70,17 @@ defmodule EEVM.Interpreter.Instructions.System.Termination do end def execute(0xFF, state) do - with {:ok, beneficiary, s1} <- Stack.pop(state.stack) do - contract_address = state.contract.address + with {:ok, beneficiary, s1} <- Stack.pop(state.frame.stack) do + contract_address = state.frame.contract.address balance = Database.get_balance(state.db, contract_address) beneficiary_exists = Database.account_exists?(state.db, beneficiary) dynamic_cost = if not beneficiary_exists and balance > 0, do: 25_000, else: 0 - case MachineState.consume_gas(%{state | stack: s1}, dynamic_cost) do + case MachineState.consume_gas( + MachineState.update_frame(state, &%{&1 | stack: s1}), + dynamic_cost + ) do {:ok, state_after_gas} -> state_after_touch = MachineState.touch_address(state_after_gas, beneficiary) diff --git a/lib/eevm/interpreter/machine_state.ex b/lib/eevm/interpreter/machine_state.ex index c6a74d1..7f79089 100644 --- a/lib/eevm/interpreter/machine_state.ex +++ b/lib/eevm/interpreter/machine_state.ex @@ -1,80 +1,55 @@ defmodule EEVM.Interpreter.MachineState do @moduledoc """ - The EVM machine state — holds all mutable state during execution. - - ## EVM Concepts - - The machine state consists of: - - **pc** (program counter): points to the current instruction - - **stack**: the operand stack (max 1024 elements) - - **memory**: byte-addressable linear memory - - **db**: unified external state backend (accounts + contract storage) - - **call_stack**: suspended parent frames during nested execution - - **frame return metadata**: parent memory write-back offset and size - - **is_static/depth**: execution mode and current call depth - - **gas**: remaining gas for execution - - **status**: whether the machine is running, stopped, or reverted - - ## Elixir Learning Notes - - - Structs in Elixir are just maps with a `__struct__` key. They give you - compile-time guarantees about which fields exist. - - `@enforce_keys` makes certain fields required when creating a struct. - - We use atoms like `:running`, `:stopped`, `:reverted` for status — - atoms are constants whose name IS their value (like symbols in Ruby). - - The `alias` keyword lets us reference modules by their short name. + 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. """ alias EEVM.Config alias EEVM.Context.{Block, Contract, Transaction} alias EEVM.Database alias EEVM.Database.InMemory, as: InMemoryDB - alias EEVM.Interpreter.{CallFrame, Memory, Stack} - alias EEVM.Interpreter.MachineState.{Env, Substate} + alias EEVM.Interpreter.MachineState.{Env, Frame, Substate} + alias EEVM.Interpreter.{Memory, Stack} alias EEVM.Precompiles alias EEVM.Tracer @type status :: :running | :stopped | :reverted | :invalid | :out_of_gas | {:error, atom()} @type t :: %__MODULE__{ - pc: non_neg_integer(), - stack: Stack.t(), - memory: Memory.t(), - db: Database.t(), + frame: Frame.t(), env: Env.t(), substate: Substate.t(), - contract: Contract.t(), - call_stack: [CallFrame.t()], - frame_return_offset: non_neg_integer(), - frame_return_size: non_neg_integer(), - is_static: boolean(), - depth: non_neg_integer(), - gas: non_neg_integer(), - refund: non_neg_integer(), + db: Database.t(), + call_stack: [Frame.t()], status: status(), - return_data: binary(), - code: binary(), tracer: Tracer.t() | nil } - @enforce_keys [:code] - defstruct pc: 0, - stack: nil, - memory: nil, - db: nil, + @enforce_keys [:frame] + defstruct frame: nil, env: nil, substate: nil, - contract: nil, + db: nil, call_stack: [], - frame_return_offset: 0, - frame_return_size: 0, - is_static: false, - depth: 0, - gas: 1_000_000, - refund: 0, status: :running, - return_data: <<>>, - code: <<>>, tracer: nil @doc """ @@ -104,7 +79,7 @@ defmodule EEVM.Interpreter.MachineState do ## Example iex> state = EEVM.Interpreter.MachineState.new(<<0x60, 0x01, 0x60, 0x02, 0x01>>) - iex> state.pc + iex> state.frame.pc 0 """ @spec new(binary(), keyword()) :: t() @@ -126,22 +101,26 @@ defmodule EEVM.Interpreter.MachineState do logs: Keyword.get(opts, :logs, []) ) - %__MODULE__{ + frame = %Frame{ code: code, stack: Stack.new(), memory: Memory.new(), - db: init_db(opts, contract), - env: Env.new(tx: tx, block: block, config: config), - substate: substate, contract: contract, - call_stack: Keyword.get(opts, :call_stack, []), - frame_return_offset: Keyword.get(opts, :frame_return_offset, 0), - frame_return_size: Keyword.get(opts, :frame_return_size, 0), + return_offset: Keyword.get(opts, :frame_return_offset, 0), + return_size: Keyword.get(opts, :frame_return_size, 0), is_static: Keyword.get(opts, :is_static, false), depth: Keyword.get(opts, :depth, 0), return_data: Keyword.get(opts, :return_data, <<>>), gas: Keyword.get(opts, :gas, 1_000_000), - refund: Keyword.get(opts, :refund, 0), + refund: Keyword.get(opts, :refund, 0) + } + + %__MODULE__{ + frame: frame, + env: Env.new(tx: tx, block: block, config: config), + substate: substate, + db: init_db(opts, contract), + call_stack: Keyword.get(opts, :call_stack, []), tracer: Keyword.get(opts, :tracer) } end @@ -184,7 +163,7 @@ defmodule EEVM.Interpreter.MachineState do @doc "Returns the opcode byte at the current program counter, or nil if past end." @spec current_opcode(t()) :: non_neg_integer() | nil - def current_opcode(%__MODULE__{pc: pc, code: code}) when pc < byte_size(code) do + def current_opcode(%__MODULE__{frame: %Frame{pc: pc, code: code}}) when pc < byte_size(code) do :binary.at(code, pc) end @@ -196,7 +175,7 @@ defmodule EEVM.Interpreter.MachineState do Used by PUSH instructions to read their immediate data. """ @spec read_code(t(), non_neg_integer(), non_neg_integer()) :: binary() - def read_code(%__MODULE__{code: code}, offset, n) do + def read_code(%__MODULE__{frame: %Frame{code: code}}, offset, n) do code_size = byte_size(code) if offset >= code_size do @@ -214,95 +193,62 @@ defmodule EEVM.Interpreter.MachineState do @doc "Advances the program counter by `n` positions." @spec advance_pc(t(), non_neg_integer()) :: t() def advance_pc(state, n \\ 1) do - %{state | pc: state.pc + n} + update_frame(state, &%{&1 | pc: &1.pc + n}) end @spec current_depth(t()) :: non_neg_integer() - def current_depth(%__MODULE__{depth: depth}), do: depth + def current_depth(%__MODULE__{frame: %Frame{depth: depth}}), do: depth - @spec push_frame(t(), CallFrame.t()) :: {:ok, t()} | {:error, :max_call_depth, t()} - def push_frame(%__MODULE__{depth: depth} = state, _new_frame) when depth >= 1024 do - {:error, :max_call_depth, state} + @doc """ + 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. + """ + @spec update_frame(t(), (Frame.t() -> Frame.t())) :: t() + def update_frame(%__MODULE__{frame: frame} = state, fun) do + %{state | frame: fun.(frame)} end - def push_frame(%__MODULE__{} = state, %CallFrame{} = new_frame) do - parent_frame = - CallFrame.from_state(state, - return_offset: state.frame_return_offset, - return_size: state.frame_return_size, - is_static: state.is_static, - depth: state.depth - ) + @spec push_frame(t(), Frame.t()) :: {:ok, t()} | {:error, :max_call_depth, t()} + def push_frame(%__MODULE__{frame: %Frame{depth: depth}} = state, _new_frame) + when depth >= 1024 do + {:error, :max_call_depth, state} + end - {:ok, - %{ - state - | call_stack: [parent_frame | state.call_stack], - code: new_frame.code, - pc: new_frame.pc, - stack: new_frame.stack, - memory: new_frame.memory, - gas: new_frame.gas, - contract: new_frame.contract, - frame_return_offset: new_frame.return_offset, - frame_return_size: new_frame.return_size, - is_static: new_frame.is_static, - depth: new_frame.depth, - status: :running, - return_data: <<>> - }} + def push_frame(%__MODULE__{frame: parent} = state, %Frame{} = new_frame) do + {:ok, %{state | call_stack: [parent | state.call_stack], frame: new_frame, status: :running}} end @spec pop_frame(t()) :: {:ok, t()} | {:error, :empty_call_stack, t()} def pop_frame(%__MODULE__{call_stack: []} = state), do: {:error, :empty_call_stack, state} - def pop_frame(%__MODULE__{call_stack: [parent | rest]} = state) do - child_return_data = state.return_data + def pop_frame(%__MODULE__{call_stack: [parent | rest], frame: child} = state) do + child_return_data = child.return_data {parent_memory, _} = - write_return_data( - parent.memory, - state.frame_return_offset, - state.frame_return_size, - child_return_data - ) + write_return_data(parent.memory, child.return_offset, child.return_size, child_return_data) + + restored_frame = %{ + parent + | memory: parent_memory, + gas: parent.gas + child.gas, + refund: child.refund, + return_data: child_return_data + } - restored_state = - %{ - state - | call_stack: rest, - code: parent.code, - pc: parent.pc, - stack: parent.stack, - memory: parent_memory, - gas: parent.gas + state.gas, - contract: parent.contract, - frame_return_offset: parent.return_offset, - frame_return_size: parent.return_size, - is_static: parent.is_static, - depth: parent.depth, - status: :running, - return_data: child_return_data - } - - {:ok, restored_state} + {:ok, %{state | call_stack: rest, frame: restored_frame, status: :running}} end @doc """ - Deducts gas from the machine state. + 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. - - ## Elixir Learning Note - - This uses a guard clause (`when cost <= gas`) to branch at the function - head level — no `if/else` needed. The first clause matches when we have - enough gas, the second is the fallback. """ @spec consume_gas(t(), non_neg_integer()) :: {:ok, t()} | {:error, :out_of_gas, t()} - def consume_gas(%__MODULE__{gas: gas} = state, cost) when cost <= gas do - {:ok, %{state | gas: gas - cost}} + def consume_gas(%__MODULE__{frame: %Frame{gas: gas}} = state, cost) when cost <= gas do + {:ok, update_frame(state, &%{&1 | gas: &1.gas - cost})} end def consume_gas(state, _cost) do @@ -311,15 +257,16 @@ defmodule EEVM.Interpreter.MachineState do @doc "Returns the gas remaining." @spec gas_remaining(t()) :: non_neg_integer() - def gas_remaining(%__MODULE__{gas: gas}), do: gas + def gas_remaining(%__MODULE__{frame: %Frame{gas: gas}}), do: gas @doc "Adds `amount` to the accumulated gas refund." @spec add_refund(t(), non_neg_integer()) :: t() - def add_refund(state, amount), do: %{state | refund: state.refund + amount} + def add_refund(state, amount), do: update_frame(state, &%{&1 | refund: &1.refund + amount}) @doc "Subtracts `amount` from refund, flooring at 0." @spec sub_refund(t(), non_neg_integer()) :: t() - def sub_refund(state, amount), do: %{state | refund: max(state.refund - amount, 0)} + def sub_refund(state, amount), + do: update_frame(state, &%{&1 | refund: max(&1.refund - amount, 0)}) @doc "Halts execution with the given status." @spec halt(t(), status()) :: t() diff --git a/lib/eevm/interpreter/machine_state/frame.ex b/lib/eevm/interpreter/machine_state/frame.ex new file mode 100644 index 0000000..c60e858 --- /dev/null +++ b/lib/eevm/interpreter/machine_state/frame.ex @@ -0,0 +1,53 @@ +defmodule EEVM.Interpreter.MachineState.Frame do + @moduledoc """ + Per-call execution frame. + + A frame is everything that gets a fresh value when a CALL/CREATE-like + opcode spawns a child, and that gets restored when the child halts: + + - `pc`, `stack`, `memory`, `gas`, `refund` — execution registers + - `code` — the currently-executing bytecode + - `return_data` — buffer holding the most recent child frame's output + - `contract` — the per-call message context (`msg.sender`, `msg.value`, + `address`, `calldata`, `balances`) + - `depth` — the EVM call depth (max 1024) + - `is_static` — STATICCALL flag; state-mutating opcodes revert under it + - `return_offset` / `return_size` — where in the *parent's* memory the + child should write its return data + + The active frame lives at `MachineState.frame`; suspended parent frames + live in `MachineState.call_stack` as the same struct. `push_frame` and + `pop_frame` move frames between the two. + """ + + alias EEVM.Context.Contract + alias EEVM.Interpreter.{Memory, Stack} + + @type t :: %__MODULE__{ + pc: non_neg_integer(), + stack: Stack.t(), + memory: Memory.t(), + gas: non_neg_integer(), + refund: non_neg_integer(), + code: binary(), + return_data: binary(), + contract: Contract.t(), + depth: non_neg_integer(), + is_static: boolean(), + return_offset: non_neg_integer(), + return_size: non_neg_integer() + } + + defstruct pc: 0, + stack: nil, + memory: nil, + gas: 0, + refund: 0, + code: <<>>, + return_data: <<>>, + contract: nil, + depth: 0, + is_static: false, + return_offset: 0, + return_size: 0 +end diff --git a/test/gas_test.exs b/test/gas_test.exs index 33ddb82..f548820 100644 --- a/test/gas_test.exs +++ b/test/gas_test.exs @@ -10,7 +10,7 @@ defmodule EEVM.GasTest do code = <<0x60, 2, 0x60, 3, 0x01, 0x00>> result = EEVM.execute(code, gas: 1_000_000) assert result.status == :stopped - assert result.gas == 1_000_000 - 9 + assert result.frame.gas == 1_000_000 - 9 end test "out of gas halts execution" do @@ -25,7 +25,7 @@ defmodule EEVM.GasTest do code = <<0x60, 1, 0x00>> result = EEVM.execute(code, gas: 3) assert result.status == :stopped - assert result.gas == 0 + assert result.frame.gas == 0 end test "one gas short causes out_of_gas" do @@ -39,28 +39,28 @@ defmodule EEVM.GasTest do # PUSH1 2, PUSH1 3, MUL, STOP → 3+3+5+0 = 11 code = <<0x60, 2, 0x60, 3, 0x02, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 11 + assert result.frame.gas == 1000 - 11 end test "DIV costs 5 gas" do # PUSH1 2, PUSH1 10, DIV, STOP → 3+3+5+0 = 11 code = <<0x60, 2, 0x60, 10, 0x04, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 11 + assert result.frame.gas == 1000 - 11 end test "comparison opcodes cost 3 gas" do # PUSH1 1, PUSH1 2, LT, STOP → 3+3+3+0 = 9 code = <<0x60, 1, 0x60, 2, 0x10, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 9 + assert result.frame.gas == 1000 - 9 end test "INVALID consumes all remaining gas" do code = <<0xFE>> result = EEVM.execute(code, gas: 5000) assert result.status == :invalid - assert result.gas == 0 + assert result.frame.gas == 0 end test "EXP dynamic gas charges per byte of exponent" do @@ -71,7 +71,7 @@ defmodule EEVM.GasTest do code = <<0x60, 0xFF, 0x60, 2, 0x0A, 0x00>> result = EEVM.execute(code, gas: 1000) assert result.status == :stopped - assert result.gas == 1000 - 66 + assert result.frame.gas == 1000 - 66 end test "EXP with zero exponent has no dynamic gas" do @@ -80,7 +80,7 @@ defmodule EEVM.GasTest do code = <<0x60, 0, 0x60, 2, 0x0A, 0x00>> result = EEVM.execute(code, gas: 1000) assert result.status == :stopped - assert result.gas == 1000 - 16 + assert result.frame.gas == 1000 - 16 end test "EXP with 2-byte exponent charges 100 dynamic gas" do @@ -89,7 +89,7 @@ defmodule EEVM.GasTest do code = <<0x61, 0x01, 0x00, 0x60, 2, 0x0A, 0x00>> result = EEVM.execute(code, gas: 1000) assert result.status == :stopped - assert result.gas == 1000 - 116 + assert result.frame.gas == 1000 - 116 end test "memory expansion gas for MSTORE" do @@ -100,7 +100,7 @@ defmodule EEVM.GasTest do result = EEVM.execute(code, gas: 1000) assert result.status == :stopped expected_gas = 3 + 3 + 3 + Memory.memory_expansion_cost_word(0, 0) + 0 - assert result.gas == 1000 - expected_gas + assert result.frame.gas == 1000 - expected_gas end test "memory expansion gas grows with offset" do @@ -112,7 +112,7 @@ defmodule EEVM.GasTest do # Memory expanded from 0 to cover offset 1024+32 = 1056 → 33 words mem_cost = Memory.memory_expansion_cost_word(0, 1024) expected_gas = 3 + 3 + 3 + mem_cost + 0 - assert result.gas == 100_000 - expected_gas + assert result.frame.gas == 100_000 - expected_gas end test "second memory access to same region costs no expansion" do @@ -126,14 +126,14 @@ defmodule EEVM.GasTest do mem_cost1 = Memory.memory_expansion_cost_word(0, 0) mem_cost2 = Memory.memory_expansion_cost_word(32, 0) expected = 3 + 3 + 3 + mem_cost1 + 3 + 3 + mem_cost2 + 0 - assert result.gas == 100_000 - expected + assert result.frame.gas == 100_000 - expected end test "POP costs 2 gas" do # PUSH1 1, POP, STOP → 3+2+0 = 5 code = <<0x60, 1, 0x50, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 5 + assert result.frame.gas == 1000 - 5 end test "JUMP costs 8 gas" do @@ -141,7 +141,7 @@ defmodule EEVM.GasTest do code = <<0x60, 3, 0x56, 0x5B, 0x00>> result = EEVM.execute(code, gas: 1000) # PUSH1=3, JUMP=8, JUMPDEST=1, STOP=0 → 12 - assert result.gas == 1000 - 12 + assert result.frame.gas == 1000 - 12 end test "CREATE and CREATE2 static costs are 32000" do diff --git a/test/hardfork_config_test.exs b/test/hardfork_config_test.exs index fe1cec8..2417439 100644 --- a/test/hardfork_config_test.exs +++ b/test/hardfork_config_test.exs @@ -331,12 +331,12 @@ defmodule EEVM.HardforkConfigTest do # On Berlin, first SLOAD hits cold access (2100 extra on top of static 0) berlin_result = EEVM.execute(code, hardfork: :berlin, gas: 100_000) # PUSH1 costs 3; static SLOAD is 0; cold access adds 2100 - berlin_gas_used = 100_000 - berlin_result.gas + berlin_gas_used = 100_000 - berlin_result.frame.gas assert berlin_gas_used == 3 + 2100 # Pre-Berlin: no cold/warm distinction — storage_access_cost returns {0, state} istanbul_result = EEVM.execute(code, hardfork: :istanbul, gas: 100_000) - istanbul_gas_used = 100_000 - istanbul_result.gas + istanbul_gas_used = 100_000 - istanbul_result.frame.gas # PUSH1(3) + SLOAD(0 extra, no cold/warm penalty) = 3 assert istanbul_gas_used == 3 end @@ -360,7 +360,7 @@ defmodule EEVM.HardforkConfigTest do result = EEVM.execute(code, hardfork: :london, gas: 200_000) # Refund was accrued but then capped at 1/5 of gas used - assert result.refund == 0 + assert result.frame.refund == 0 end test "pre-London: refund capped at 1/2 of gas used" do @@ -374,7 +374,7 @@ defmodule EEVM.HardforkConfigTest do result = EEVM.execute(code, hardfork: :berlin, gas: 200_000) # Refund was applied (1/2 cap) and then zeroed out - assert result.refund == 0 + assert result.frame.refund == 0 end end diff --git a/test/interpreter/call_frame_test.exs b/test/interpreter/call_frame_test.exs index 30b1674..5a23f33 100644 --- a/test/interpreter/call_frame_test.exs +++ b/test/interpreter/call_frame_test.exs @@ -3,12 +3,13 @@ defmodule EEVM.CallFrameTest do alias EEVM.Context.Contract alias EEVM.Interpreter - alias EEVM.Interpreter.{CallFrame, MachineState, Memory, Stack} + alias EEVM.Interpreter.{MachineState, Memory, Stack} + alias EEVM.Interpreter.MachineState.Frame test "push_frame stores parent frame and switches execution context" do parent_state = MachineState.new(<<0x00>>, contract: Contract.new(address: 1), gas: 1000) - child_frame = %CallFrame{ + child_frame = %Frame{ code: <<0x60, 1, 0x00>>, pc: 0, stack: Stack.new(), @@ -23,8 +24,8 @@ defmodule EEVM.CallFrameTest do {:ok, state_after_push} = MachineState.push_frame(parent_state, child_frame) - assert state_after_push.code == child_frame.code - assert state_after_push.contract.address == 2 + assert state_after_push.frame.code == child_frame.code + assert state_after_push.frame.contract.address == 2 assert length(state_after_push.call_stack) == 1 assert MachineState.current_depth(state_after_push) == 1 end @@ -32,7 +33,7 @@ defmodule EEVM.CallFrameTest do test "push_frame enforces max call depth of 1024" do state = MachineState.new(<<0x00>>, depth: 1024) - frame = %CallFrame{ + frame = %Frame{ code: <<0x00>>, stack: Stack.new(), memory: Memory.new(), @@ -45,7 +46,7 @@ defmodule EEVM.CallFrameTest do test "pop_frame restores parent and writes return data into parent memory" do parent_state = MachineState.new(<<0x00>>, contract: Contract.new(address: 1), gas: 700) - child_frame = %CallFrame{ + child_frame = %Frame{ code: <<0x00>>, stack: Stack.new(), memory: Memory.new(), @@ -57,21 +58,25 @@ defmodule EEVM.CallFrameTest do } {:ok, state_after_push} = MachineState.push_frame(parent_state, child_frame) - child_finished = %{state_after_push | status: :stopped, return_data: <<0xAA, 0xBB>>} + + child_finished = + MachineState.update_frame(%{state_after_push | status: :stopped}, fn frame -> + %{frame | return_data: <<0xAA, 0xBB>>} + end) {:ok, resumed_state} = MachineState.pop_frame(child_finished) - {bytes, _} = Memory.read_bytes(resumed_state.memory, 4, 4) + {bytes, _} = Memory.read_bytes(resumed_state.frame.memory, 4, 4) - assert resumed_state.contract.address == 1 + assert resumed_state.frame.contract.address == 1 assert resumed_state.status == :running - assert resumed_state.gas == 950 + assert resumed_state.frame.gas == 950 assert bytes == <<0xAA, 0xBB, 0x00, 0x00>> end test "run_loop pops halted child frame and resumes parent" do parent_state = MachineState.new(<<0x00>>, contract: Contract.new(address: 10), gas: 900) - child_frame = %CallFrame{ + child_frame = %Frame{ code: <<0x00>>, stack: Stack.new(), memory: Memory.new(), @@ -88,7 +93,7 @@ defmodule EEVM.CallFrameTest do result = Interpreter.run_loop(child_halted) assert result.status == :stopped - assert result.contract.address == 10 + assert result.frame.contract.address == 10 assert result.call_stack == [] end end diff --git a/test/interpreter/instructions/access_list_test.exs b/test/interpreter/instructions/access_list_test.exs index c536592..1b34c50 100644 --- a/test/interpreter/instructions/access_list_test.exs +++ b/test/interpreter/instructions/access_list_test.exs @@ -8,13 +8,13 @@ defmodule EEVM.Interpreter.Instructions.AccessListTest do test "first SLOAD is cold (2100 gas)" do code = <<0x60, 0x00, 0x54, 0x00>> result = EEVM.execute(code, gas: 10_000) - assert result.gas == 10_000 - 3 - 2100 + assert result.frame.gas == 10_000 - 3 - 2100 end test "second SLOAD to same slot is warm (100 gas)" do code = <<0x60, 0x00, 0x54, 0x50, 0x60, 0x00, 0x54, 0x00>> result = EEVM.execute(code, gas: 10_000) - assert result.gas == 10_000 - 3 - 2100 - 2 - 3 - 100 + assert result.frame.gas == 10_000 - 3 - 2100 - 2 - 3 - 100 end test "SLOAD on different slots are both cold" do @@ -26,21 +26,21 @@ defmodule EEVM.Interpreter.Instructions.AccessListTest do ]) result = EEVM.execute(code, gas: 10_000) - assert result.gas == 10_000 - (3 + 2100 + 2) - (3 + 2100 + 2) + assert result.frame.gas == 10_000 - (3 + 2100 + 2) - (3 + 2100 + 2) end test "BALANCE first access is cold (2600 gas)" do world_state = WorldState.new(%{0x42 => %{balance: 100}}) code = <<0x60, 0x42, 0x31, 0x50, 0x00>> result = EEVM.execute(code, world_state: world_state, gas: 10_000) - assert result.gas == 10_000 - 3 - 2600 - 2 + assert result.frame.gas == 10_000 - 3 - 2600 - 2 end test "BALANCE second access is warm (100 gas)" do world_state = WorldState.new(%{0x42 => %{balance: 100}}) code = <<0x60, 0x42, 0x31, 0x50, 0x60, 0x42, 0x31, 0x50, 0x00>> result = EEVM.execute(code, world_state: world_state, gas: 10_000) - assert result.gas == 10_000 - (3 + 2600 + 2) - (3 + 100 + 2) + assert result.frame.gas == 10_000 - (3 + 2600 + 2) - (3 + 100 + 2) end test "pre-warmed addresses are warm from start" do @@ -48,14 +48,14 @@ defmodule EEVM.Interpreter.Instructions.AccessListTest do world_state = WorldState.new(%{0xAA => %{balance: 50}}) code = <<0x60, 0xAA, 0x31, 0x50, 0x00>> result = EEVM.execute(code, world_state: world_state, contract: contract, gas: 10_000) - assert result.gas == 10_000 - 3 - 100 - 2 + assert result.frame.gas == 10_000 - 3 - 100 - 2 end test "precompile addresses are pre-warmed" do world_state = WorldState.new(%{0x01 => %{balance: 0}}) code = <<0x60, 0x01, 0x31, 0x50, 0x00>> result = EEVM.execute(code, world_state: world_state, gas: 10_000) - assert result.gas == 10_000 - 3 - 100 - 2 + assert result.frame.gas == 10_000 - 3 - 100 - 2 end end end diff --git a/test/interpreter/instructions/blob_test.exs b/test/interpreter/instructions/blob_test.exs index 225b192..31ae48f 100644 --- a/test/interpreter/instructions/blob_test.exs +++ b/test/interpreter/instructions/blob_test.exs @@ -36,7 +36,7 @@ defmodule EEVM.Interpreter.Instructions.BlobTest do code = <<0x60, 0x00, 0x49, 0x00>> result = EEVM.execute(code, gas: 10_000) expected_cost = Static.static_cost(0x60) + Static.static_cost(0x49) - assert result.gas == 10_000 - expected_cost + assert result.frame.gas == 10_000 - expected_cost end end @@ -58,7 +58,7 @@ defmodule EEVM.Interpreter.Instructions.BlobTest do code = <<0x4A, 0x00>> result = EEVM.execute(code, gas: 10_000) expected_cost = Static.static_cost(0x4A) - assert result.gas == 10_000 - expected_cost + assert result.frame.gas == 10_000 - expected_cost end end end diff --git a/test/interpreter/instructions/control_flow_test.exs b/test/interpreter/instructions/control_flow_test.exs index e52a5ad..fc4ffbf 100644 --- a/test/interpreter/instructions/control_flow_test.exs +++ b/test/interpreter/instructions/control_flow_test.exs @@ -75,7 +75,7 @@ defmodule EEVM.Interpreter.Instructions.ControlFlowTest do test "PUSH0 costs 2 gas (base)" do code = <<0x5F, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 2 + assert result.frame.gas == 1000 - 2 end test "PUSH0 + PUSH0 + ADD = 0" do @@ -91,7 +91,7 @@ defmodule EEVM.Interpreter.Instructions.ControlFlowTest do push1_code = <<0x60, 0, 0x00>> r0 = EEVM.execute(push0_code, gas: 1000) r1 = EEVM.execute(push1_code, gas: 1000) - assert r0.gas > r1.gas + assert r0.frame.gas > r1.frame.gas end test "disassembles as PUSH0" do diff --git a/test/interpreter/instructions/crypto_test.exs b/test/interpreter/instructions/crypto_test.exs index 8381684..f80692e 100644 --- a/test/interpreter/instructions/crypto_test.exs +++ b/test/interpreter/instructions/crypto_test.exs @@ -33,7 +33,7 @@ defmodule EEVM.Interpreter.Instructions.CryptoTest do code = <<0x60, 32, 0x60, 0, 0x20, 0x00>> result = EEVM.execute(code, gas: 10_000) # Total: 3 + 3 + 30 + 6 + 3 + 0 = 45 - assert result.gas == 10_000 - 45 + assert result.frame.gas == 10_000 - 45 end test "dynamic gas scales with data size" do @@ -44,7 +44,7 @@ defmodule EEVM.Interpreter.Instructions.CryptoTest do r1 = EEVM.execute(code1, gas: 100_000) r2 = EEVM.execute(code2, gas: 100_000) # r2 should use 6 more gas (1 extra word) + some memory expansion - assert r1.gas > r2.gas + assert r1.frame.gas > r2.frame.gas end test "out of gas on large hash" do diff --git a/test/interpreter/instructions/environment_test.exs b/test/interpreter/instructions/environment_test.exs index 981ba33..32ce90a 100644 --- a/test/interpreter/instructions/environment_test.exs +++ b/test/interpreter/instructions/environment_test.exs @@ -213,27 +213,27 @@ defmodule EEVM.Interpreter.Instructions.EnvironmentTest do result = EEVM.execute(code, gas: 1000) # GAS pushes remaining gas (1000-2=998), then STOP costs 0 assert EEVM.stack_values(result) == [998] - assert result.gas == 998 + assert result.frame.gas == 998 end test "env opcodes have correct gas costs" do # ADDRESS costs 2 (base) code = <<0x30, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 2 + assert result.frame.gas == 1000 - 2 end test "BALANCE costs 2600 gas" do code = <<0x60, 0x0B, 0x31, 0x00>> result = EEVM.execute(code, gas: 10_000) # PUSH1=3 + BALANCE=2600 + STOP=0 = 2603 - assert result.gas == 10_000 - 2603 + assert result.frame.gas == 10_000 - 2603 end test "SELFBALANCE costs 5 gas" do code = <<0x47, 0x00>> result = EEVM.execute(code, gas: 1000) - assert result.gas == 1000 - 5 + assert result.frame.gas == 1000 - 5 end end @@ -278,7 +278,7 @@ defmodule EEVM.Interpreter.Instructions.EnvironmentTest do Memory.memory_expansion_cost(0, 0, 4) + Static.static_cost(0x51) - assert result.gas == initial_gas - expected_spent + assert result.frame.gas == initial_gas - expected_spent end end @@ -336,7 +336,7 @@ defmodule EEVM.Interpreter.Instructions.EnvironmentTest do Memory.memory_expansion_cost(0, 0, 4) + Static.static_cost(0x51) - assert result.gas == initial_gas - expected_spent + assert result.frame.gas == initial_gas - expected_spent end test "partial copy with destination offset" do @@ -402,7 +402,7 @@ defmodule EEVM.Interpreter.Instructions.EnvironmentTest do Memory.memory_expansion_cost(0, 0, 4) + Static.static_cost(0x51) - assert result.gas == initial_gas - expected_spent + assert result.frame.gas == initial_gas - expected_spent end test "EXTCODEHASH returns 0 for non-existent account" do diff --git a/test/interpreter/instructions/gas_refund_test.exs b/test/interpreter/instructions/gas_refund_test.exs index 9c3edca..21b2f10 100644 --- a/test/interpreter/instructions/gas_refund_test.exs +++ b/test/interpreter/instructions/gas_refund_test.exs @@ -5,28 +5,29 @@ defmodule EEVM.Interpreter.Instructions.GasRefundTest do alias EEVM.Context.Transaction alias EEVM.Gas.Static alias EEVM.Interpreter - alias EEVM.Interpreter.{CallFrame, MachineState, Memory, Stack} + alias EEVM.Interpreter.{MachineState, Memory, Stack} + alias EEVM.Interpreter.MachineState.Frame alias EEVM.Transaction.IntrinsicGas test "refund counter defaults to 0" do result = EEVM.execute(<<0x00>>) assert result.status == :stopped - assert result.refund == 0 + assert result.frame.refund == 0 end test "add_refund increments refund counter" do state = MachineState.new(<<0x00>>) updated = MachineState.add_refund(state, 15) - assert updated.refund == 15 + assert updated.frame.refund == 15 end test "sub_refund decrements with floor at 0" do state = MachineState.new(<<0x00>>, refund: 10) - assert MachineState.sub_refund(state, 3).refund == 7 - assert MachineState.sub_refund(state, 100).refund == 0 + assert MachineState.sub_refund(state, 3).frame.refund == 7 + assert MachineState.sub_refund(state, 100).frame.refund == 0 end test "refund is capped at gas_used/5" do @@ -41,8 +42,8 @@ defmodule EEVM.Interpreter.Instructions.GasRefundTest do expected_gas = initial_gas - gas_used + effective_refund assert result.status == :stopped - assert result.gas == expected_gas - assert result.refund == 0 + assert result.frame.gas == expected_gas + assert result.frame.refund == 0 end test "Prague floors final charged gas after refunds to calldata floor cost" do @@ -66,25 +67,25 @@ defmodule EEVM.Interpreter.Instructions.GasRefundTest do ) assert prague_result.status == :stopped - assert prague_result.refund == 0 - assert tx.gas_limit - prague_result.gas == 21_010 + assert prague_result.frame.refund == 0 + assert tx.gas_limit - prague_result.frame.gas == 21_010 assert cancun_result.status == :stopped - assert tx.gas_limit - cancun_result.gas == 21_008 + assert tx.gas_limit - cancun_result.frame.gas == 21_008 end test "top-level revert resets refund to 0" do result = EEVM.execute(<<0x60, 0x00, 0x60, 0x00, 0xFD>>, refund: 25) assert result.status == :reverted - assert result.refund == 0 + assert result.frame.refund == 0 end test "nested call success preserves accumulated refund" do parent_state = MachineState.new(<<0x00>>, contract: Contract.new(address: 1), gas: 1_000, refund: 7) - child_frame = %CallFrame{ + child_frame = %Frame{ code: <<0x00>>, pc: 0, stack: Stack.new(), @@ -94,24 +95,25 @@ defmodule EEVM.Interpreter.Instructions.GasRefundTest do return_offset: 0, return_size: 0, is_static: false, - depth: 1 + depth: 1, + refund: 12 } {:ok, state_after_push} = MachineState.push_frame(parent_state, child_frame) - child_halted = %{state_after_push | status: :stopped, refund: 12} + child_halted = %{state_after_push | status: :stopped} result = Interpreter.run_loop(child_halted) assert result.status == :stopped assert result.call_stack == [] - assert result.refund == 12 + assert result.frame.refund == 12 end test "nested call revert restores parent refund snapshot" do parent_state = MachineState.new(<<0x00>>, contract: Contract.new(address: 1), gas: 1_000, refund: 7) - child_frame = %CallFrame{ + child_frame = %Frame{ code: <<0xFD>>, pc: 0, stack: Stack.new(), @@ -121,16 +123,17 @@ defmodule EEVM.Interpreter.Instructions.GasRefundTest do return_offset: 0, return_size: 0, is_static: false, - depth: 1 + depth: 1, + refund: 12 } {:ok, state_after_push} = MachineState.push_frame(parent_state, child_frame) - child_reverted = %{state_after_push | status: :reverted, refund: 12} + child_reverted = %{state_after_push | status: :reverted} result = Interpreter.run_loop(child_reverted) assert result.status == :stopped assert result.call_stack == [] - assert result.refund == 7 + assert result.frame.refund == 7 end end diff --git a/test/interpreter/instructions/logging_test.exs b/test/interpreter/instructions/logging_test.exs index 4d83cbc..535c60d 100644 --- a/test/interpreter/instructions/logging_test.exs +++ b/test/interpreter/instructions/logging_test.exs @@ -129,7 +129,7 @@ defmodule EEVM.Interpreter.Instructions.LoggingTest do code = <<0x60, 0x40, 0x60, 0x00, 0xA0, 0x00>> result = EEVM.execute(code) - assert result.memory.size == 64 + assert result.frame.memory.size == 64 assert [%{data: data}] = EEVM.logs(result) assert byte_size(data) == 64 end diff --git a/test/interpreter/instructions/precompile_test.exs b/test/interpreter/instructions/precompile_test.exs index 191a5ca..8dd5f4f 100644 --- a/test/interpreter/instructions/precompile_test.exs +++ b/test/interpreter/instructions/precompile_test.exs @@ -50,7 +50,7 @@ defmodule EEVM.Interpreter.Instructions.PrecompileTest do result = EEVM.execute(code, world_state: world_state, gas: 1_000_000) assert result.status == :stopped assert EEVM.stack_values(result) == [1] - assert result.return_data == <<0x2A>> + assert result.frame.return_data == <<0x2A>> end test "precompile detection for addresses 0x01 through 0x0A on Cancun" do @@ -97,7 +97,7 @@ defmodule EEVM.Interpreter.Instructions.PrecompileTest do result = EEVM.execute(code, config: config, gas: 1_000_000) assert result.status == :stopped assert EEVM.stack_values(result) == [1] - assert result.return_data == <<0xAA>> + assert result.frame.return_data == <<0xAA>> end test "custom precompile detection is config-dependent" do diff --git a/test/interpreter/instructions/selfdestruct_test.exs b/test/interpreter/instructions/selfdestruct_test.exs index 2f5bee1..8085855 100644 --- a/test/interpreter/instructions/selfdestruct_test.exs +++ b/test/interpreter/instructions/selfdestruct_test.exs @@ -66,7 +66,7 @@ defmodule EEVM.Interpreter.Instructions.SelfdestructTest do assert result.status == :stopped expected_gas = 1_000_000 - Static.static_cost(0x60) - 5000 - assert result.gas == expected_gas + assert result.frame.gas == expected_gas end test "charges extra 25000 gas for new beneficiary with value" do @@ -78,7 +78,7 @@ defmodule EEVM.Interpreter.Instructions.SelfdestructTest do assert result.status == :stopped expected_gas = 1_000_000 - Static.static_cost(0x60) - 5000 - 25_000 - assert result.gas == expected_gas + assert result.frame.gas == expected_gas assert Database.get_balance(result.db, 0xCC) == 100 end @@ -95,7 +95,7 @@ defmodule EEVM.Interpreter.Instructions.SelfdestructTest do result = EEVM.execute(code, world_state: world_state, contract: contract, gas: 1_000_000) expected_gas = 1_000_000 - Static.static_cost(0x60) - 5000 - assert result.gas == expected_gas + assert result.frame.gas == expected_gas end test "no extra gas for new beneficiary with zero value" do @@ -106,7 +106,7 @@ defmodule EEVM.Interpreter.Instructions.SelfdestructTest do result = EEVM.execute(code, world_state: world_state, contract: contract, gas: 1_000_000) expected_gas = 1_000_000 - Static.static_cost(0x60) - 5000 - assert result.gas == expected_gas + assert result.frame.gas == expected_gas end test "contract code/nonce/storage preserved (post-Cancun, not created this tx)" do diff --git a/test/interpreter/instructions/sstore_gas_test.exs b/test/interpreter/instructions/sstore_gas_test.exs index d2c391d..d8ef6d1 100644 --- a/test/interpreter/instructions/sstore_gas_test.exs +++ b/test/interpreter/instructions/sstore_gas_test.exs @@ -14,8 +14,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do accessed_storage_keys: MapSet.new([{0, 0}]) ) - assert result.gas == 10_000 - 3 - 3 - 100 - assert result.refund == 0 + assert result.frame.gas == 10_000 - 3 - 3 - 100 + assert result.frame.refund == 0 end test "fresh write (0 -> nonzero) on clean slot costs 20_000" do @@ -27,8 +27,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do accessed_storage_keys: MapSet.new([{0, 0}]) ) - assert result.gas == 30_000 - 3 - 3 - 20_000 - assert result.refund == 0 + assert result.frame.gas == 30_000 - 3 - 3 - 20_000 + assert result.frame.refund == 0 end test "clean update (nonzero -> different nonzero) costs 2_900" do @@ -41,8 +41,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do accessed_storage_keys: MapSet.new([{0, 0}]) ) - assert result.gas == 10_000 - 3 - 3 - 2_900 - assert result.refund == 0 + assert result.frame.gas == 10_000 - 3 - 3 - 2_900 + assert result.frame.refund == 0 end test "clean clear (nonzero -> 0) costs 2_900 and adds 4_800 refund" do @@ -59,8 +59,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do # gas_used = 3 + 3 + 2900 = 2906, refund = 4800, effective = min(4800, 2906/5) = 581 gas_used = 3 + 3 + 2_900 effective_refund = min(4_800, div(gas_used, 5)) - assert result.gas == initial_gas - gas_used + effective_refund - assert result.refund == 0 + assert result.frame.gas == initial_gas - gas_used + effective_refund + assert result.frame.refund == 0 end test "dirty slot rewrite costs 100" do @@ -74,8 +74,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do storage: Storage.new(%{0 => 5}) ) - assert result.gas == 20_000 - (3 + 3 + 2_100 + 2_900) - (3 + 3 + 100) - assert result.refund == 0 + assert result.frame.gas == 20_000 - (3 + 3 + 2_100 + 2_900) - (3 + 3 + 100) + assert result.frame.refund == 0 end test "dirty slot reset to original nonzero adds 2_800 refund" do @@ -93,8 +93,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do # gas_used = 3 + 3 + 100 = 106, refund = 2800, effective = min(2800, 106/5) = 21 gas_used = 3 + 3 + 100 effective_refund = min(2_800, div(gas_used, 5)) - assert result.gas == initial_gas - gas_used + effective_refund - assert result.refund == 0 + assert result.frame.gas == initial_gas - gas_used + effective_refund + assert result.frame.refund == 0 end test "dirty slot reset to original zero adds 19_900 refund" do @@ -112,8 +112,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do # gas_used = 3 + 3 + 100 = 106, refund = 19900, effective = min(19900, 106/5) = 21 gas_used = 3 + 3 + 100 effective_refund = min(19_900, div(gas_used, 5)) - assert result.gas == initial_gas - gas_used + effective_refund - assert result.refund == 0 + assert result.frame.gas == initial_gas - gas_used + effective_refund + assert result.frame.refund == 0 end test "cold slot access adds 2_100 surcharge" do @@ -121,8 +121,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do result = EEVM.execute(code, gas: 10_000, storage: Storage.new(%{0 => 7})) - assert result.gas == 10_000 - 3 - 3 - 2_100 - 100 - assert result.refund == 0 + assert result.frame.gas == 10_000 - 3 - 3 - 2_100 - 100 + assert result.frame.refund == 0 end test "warm second access has no extra surcharge" do @@ -132,8 +132,8 @@ defmodule EEVM.Interpreter.Instructions.SstoreGasTest do result = EEVM.execute(code, gas: 10_000, storage: Storage.new(%{0 => 7})) - assert result.gas == 10_000 - (3 + 3 + 2_100 + 100) - (3 + 3 + 100) - assert result.refund == 0 + assert result.frame.gas == 10_000 - (3 + 3 + 2_100 + 100) - (3 + 3 + 100) + assert result.frame.refund == 0 end end end diff --git a/test/interpreter/instructions/stack_memory_test.exs b/test/interpreter/instructions/stack_memory_test.exs index e48bd37..17589ac 100644 --- a/test/interpreter/instructions/stack_memory_test.exs +++ b/test/interpreter/instructions/stack_memory_test.exs @@ -97,7 +97,7 @@ defmodule EEVM.Interpreter.Instructions.StackMemoryTest do Memory.memory_expansion_cost(0, 0, 97) assert result.status == :stopped - assert result.gas == 100_000 - expected + assert result.frame.gas == 100_000 - expected end test "memory expansion covers both src and dst ranges" do diff --git a/test/interpreter/instructions/system/eip_3541_test.exs b/test/interpreter/instructions/system/eip_3541_test.exs index 14396e9..a1b0552 100644 --- a/test/interpreter/instructions/system/eip_3541_test.exs +++ b/test/interpreter/instructions/system/eip_3541_test.exs @@ -53,7 +53,7 @@ defmodule EEVM.Interpreter.Instructions.System.EIP3541Test do assert EEVM.stack_values(result) == [0] # Gas was consumed by init code execution (rejection does not refund it) - assert result.gas < initial_gas + assert result.frame.gas < initial_gas # Nonce was incremented (sender nonce advances before deployment attempt) assert Database.get_nonce(result.db, 0) == 1 diff --git a/test/interpreter/instructions/system_test.exs b/test/interpreter/instructions/system_test.exs index 6941674..6c352dc 100644 --- a/test/interpreter/instructions/system_test.exs +++ b/test/interpreter/instructions/system_test.exs @@ -13,7 +13,7 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do code = <<0x60, 0xAB, 0x60, 0, 0x53, 0x60, 1, 0x60, 0, 0xF3>> result = EEVM.execute(code) assert result.status == :stopped - assert result.return_data == <<0xAB>> + assert result.frame.return_data == <<0xAB>> end test "REVERT halts with :reverted" do @@ -106,8 +106,8 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert result.status == :stopped assert EEVM.stack_values(result) == [1] - assert result.return_data == <<0x2A>> - {mem, _} = Memory.read_bytes(result.memory, 0, 1) + assert result.frame.return_data == <<0x2A>> + {mem, _} = Memory.read_bytes(result.frame.memory, 0, 1) assert mem == <<0x2A>> end @@ -159,7 +159,7 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert EEVM.stack_values(result) == [0] assert Database.get_balance(result.db, 0) == 8 assert Database.get_balance(result.db, 1) == 1 - assert result.return_data == <<>> + assert result.frame.return_data == <<>> end end @@ -180,8 +180,8 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert result.status == :stopped assert EEVM.stack_values(result) == [1] - assert result.return_data == <<0x2A>> - {mem, _} = Memory.read_bytes(result.memory, 0, 1) + assert result.frame.return_data == <<0x2A>> + {mem, _} = Memory.read_bytes(result.frame.memory, 0, 1) assert mem == <<0x2A>> end @@ -244,8 +244,8 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert result.status == :stopped assert EEVM.stack_values(result) == [1] - assert result.return_data == <<0x00>> - {mem, _} = Memory.read_bytes(result.memory, 0, 1) + assert result.frame.return_data == <<0x00>> + {mem, _} = Memory.read_bytes(result.frame.memory, 0, 1) assert mem == <<0x00>> assert Database.storage_load(result.db, 0, 0) == 0 end @@ -342,7 +342,7 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert result.status == :stopped assert EEVM.stack_values(result) == [0] - assert result.return_data == <<>> + assert result.frame.return_data == <<>> end end @@ -390,7 +390,7 @@ defmodule EEVM.Interpreter.Instructions.SystemTest do assert result.status == :stopped assert EEVM.stack_values(result) == [0] - assert result.return_data == <<>> + assert result.frame.return_data == <<>> end end end diff --git a/test/interpreter/instructions/transient_storage_test.exs b/test/interpreter/instructions/transient_storage_test.exs index f9ddf5a..41fdddd 100644 --- a/test/interpreter/instructions/transient_storage_test.exs +++ b/test/interpreter/instructions/transient_storage_test.exs @@ -72,14 +72,14 @@ defmodule EEVM.Interpreter.Instructions.TransientStorageTest do code = <<0x60, 0x00, 0x5C, 0x00>> result = EEVM.execute(code, gas: 10_000) expected_cost = Static.static_cost(0x60) + Static.static_cost(0x5C) - assert result.gas == 10_000 - expected_cost + assert result.frame.gas == 10_000 - expected_cost end test "TSTORE costs 100 gas (warm access)" do code = <<0x60, 0xFF, 0x60, 0x01, 0x5D, 0x00>> result = EEVM.execute(code, gas: 10_000) expected_cost = Static.static_cost(0x60) * 2 + Static.static_cost(0x5D) - assert result.gas == 10_000 - expected_cost + assert result.frame.gas == 10_000 - expected_cost end test "transient storage does not affect persistent storage" do diff --git a/test/interpreter/journal_test.exs b/test/interpreter/journal_test.exs index 4e00bc1..de68bcb 100644 --- a/test/interpreter/journal_test.exs +++ b/test/interpreter/journal_test.exs @@ -105,16 +105,21 @@ defmodule EEVM.Interpreter.JournalTest do describe "merge_child_result/2 — non-revertible fields" do test "parent's stack, memory, pc, gas, refund, contract are not touched on success" do parent = parent_state() - child = %{parent | status: :stopped, pc: 999, gas: 999_999, refund: 555} + + child = %{ + parent + | status: :stopped, + frame: %{parent.frame | pc: 999, gas: 999_999, refund: 555} + } merged = Journal.merge_child_result(parent, child) - assert merged.pc == parent.pc - assert merged.gas == parent.gas - assert merged.refund == parent.refund - assert merged.stack == parent.stack - assert merged.memory == parent.memory - assert merged.contract == parent.contract + assert merged.frame.pc == parent.frame.pc + assert merged.frame.gas == parent.frame.gas + assert merged.frame.refund == parent.frame.refund + assert merged.frame.stack == parent.frame.stack + assert merged.frame.memory == parent.frame.memory + assert merged.frame.contract == parent.frame.contract end end diff --git a/test/interpreter/machine_state/eip_3651_test.exs b/test/interpreter/machine_state/eip_3651_test.exs index b0d3e88..ee55bd2 100644 --- a/test/interpreter/machine_state/eip_3651_test.exs +++ b/test/interpreter/machine_state/eip_3651_test.exs @@ -43,8 +43,8 @@ defmodule EEVM.Interpreter.MachineState.EIP3651Test do result = EEVM.execute(code, gas: initial_gas, block: block, world_state: world_state) - assert result.gas == initial_gas - 2 - 100 - refute result.gas == initial_gas - 2 - 2600 + assert result.frame.gas == initial_gas - 2 - 100 + refute result.frame.gas == initial_gas - 2 - 2600 end test "custom precompile addresses are pre-warmed" do diff --git a/test/storage_test.exs b/test/storage_test.exs index 6d374c6..a3c7322 100644 --- a/test/storage_test.exs +++ b/test/storage_test.exs @@ -56,7 +56,7 @@ defmodule EEVM.StorageTest do # PUSH1 1, PUSH1 0, SSTORE, STOP code = <<0x60, 1, 0x60, 0, 0x55, 0x00>> result = EEVM.execute(code, gas: 100_000) - assert result.gas == 100_000 - 22_106 + assert result.frame.gas == 100_000 - 22_106 end test "SLOAD cold gas cost is 2100" do @@ -66,7 +66,7 @@ defmodule EEVM.StorageTest do assert result.status == :out_of_gas result = EEVM.execute(code, gas: 3_000) - assert result.gas == 3_000 - 2103 + assert result.frame.gas == 3_000 - 2103 end test "SSTORE out of gas" do diff --git a/test/support/blockchain_test_runner.ex b/test/support/blockchain_test_runner.ex index 109e126..3a26cfd 100644 --- a/test/support/blockchain_test_runner.ex +++ b/test/support/blockchain_test_runner.ex @@ -242,7 +242,7 @@ defmodule EEVM.TestSupport.BlockchainTestRunner do with {:ok, machine} <- run_interpreter(tx, db_after_nonce, block_ctx, config), {:ok, finalized_db} <- finalize_fees(db_after_nonce, machine, tx, block_ctx) do failed = failed_status?(machine.status) - gas_used = tx.gas_limit - machine.gas + gas_used = tx.gas_limit - machine.frame.gas {:ok, %{ @@ -268,7 +268,7 @@ defmodule EEVM.TestSupport.BlockchainTestRunner do defp finalize_fees(db_after_nonce, machine, tx, %Block{} = block_ctx) do failed = failed_status?(machine.status) settled = if failed, do: db_after_nonce, else: machine.db - gas_used = tx.gas_limit - machine.gas + gas_used = tx.gas_limit - machine.frame.gas apply_fees(settled, tx, block_ctx, gas_used) end diff --git a/test/support/state_test_runner.ex b/test/support/state_test_runner.ex index 3891cbd..5b152c4 100644 --- a/test/support/state_test_runner.ex +++ b/test/support/state_test_runner.ex @@ -38,9 +38,9 @@ defmodule EEVM.TestSupport.StateTestRunner do %{ state_root: StateRoot.compute_state_root(finalized_db), logs_hash: logs_hash(if(failed, do: [], else: machine.substate.logs)), - output: if(failed, do: <<>>, else: machine.return_data), + output: if(failed, do: <<>>, else: machine.frame.return_data), status: machine.status, - gas_used: tx.gas_limit - machine.gas, + gas_used: tx.gas_limit - machine.frame.gas, db: finalized_db }} end @@ -83,7 +83,7 @@ defmodule EEVM.TestSupport.StateTestRunner do defp finalize_fees(db_after_nonce, machine, tx, block) do failed = failed_status?(machine.status) settled_db = settle_state(db_after_nonce, machine, failed) - gas_used = tx.gas_limit - machine.gas + gas_used = tx.gas_limit - machine.frame.gas apply_fees(settled_db, tx, block, gas_used) end diff --git a/test/system_contracts/beacon_roots_test.exs b/test/system_contracts/beacon_roots_test.exs index 5587d02..04fac88 100644 --- a/test/system_contracts/beacon_roots_test.exs +++ b/test/system_contracts/beacon_roots_test.exs @@ -108,8 +108,8 @@ defmodule EEVM.SystemContracts.BeaconRootsTest do |> EEVM.Interpreter.run_loop() assert final_state.status == :stopped - assert byte_size(final_state.return_data) == 32 - <> = final_state.return_data + assert byte_size(final_state.frame.return_data) == 32 + <> = final_state.frame.return_data assert returned_root == root end diff --git a/test/system_contracts/block_hashes_test.exs b/test/system_contracts/block_hashes_test.exs index 9f576a4..f390cee 100644 --- a/test/system_contracts/block_hashes_test.exs +++ b/test/system_contracts/block_hashes_test.exs @@ -90,8 +90,8 @@ defmodule EEVM.SystemContracts.BlockHashesTest do final_state = call_contract(db, block_number, target_block) assert final_state.status == :stopped - assert byte_size(final_state.return_data) == 32 - <> = final_state.return_data + assert byte_size(final_state.frame.return_data) == 32 + <> = final_state.frame.return_data assert returned_hash == hash end