diff --git a/lib/eevm.ex b/lib/eevm.ex index f8be713..90ec200 100644 --- a/lib/eevm.ex +++ b/lib/eevm.ex @@ -79,17 +79,17 @@ 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." @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..d795413 100644 --- a/lib/eevm/gas/access.ex +++ b/lib/eevm/gas/access.ex @@ -36,12 +36,14 @@ 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 MapSet.member?(state.accessed_addresses, address) do + if HardforkConfig.enabled?(state.env.config.hardfork, :eip_2929) 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 @@ -53,14 +55,15 @@ 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 - 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/execution.ex b/lib/eevm/handler/execution.ex index 6321c11..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.config.hardfork, :eip_3541) -> - {:error, %{state | gas: 0, status: :reverted}} + HardforkConfig.enabled?(state.env.config.hardfork, :eip_3541) -> + {: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 5b83b1e..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) @@ -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 @@ -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 0747d15..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,36 +138,36 @@ 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.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)) + 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.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 - %{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 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 @@ -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 bf9a9a4..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,28 +72,28 @@ 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. # 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 @@ -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 775f3a6..eb6f8e9 100644 --- a/lib/eevm/interpreter/instructions/environment/simple.ex +++ b/lib/eevm/interpreter/instructions/environment/simple.ex @@ -16,41 +16,48 @@ 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(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(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(0x5A, state), do: Helpers.push_value(state, state.gas) + 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.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.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) + 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.frame.gas) def execute(0x40, state) do - with {:ok, block_num, s1} <- Stack.pop(state.stack), - hash = Block.hash(state.block, block_num), + 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 - value = Enum.at(state.tx.blob_hashes, index, 0) + 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 40fe381..832417f 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. """ @@ -35,24 +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 } - s5 = %{s4 | memory: new_memory, logs: s4.logs ++ [log_entry]} + new_substate = %{s4.substate | logs: s4.substate.logs ++ [log_entry]} + + 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 f3c7cc3..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,31 +87,41 @@ 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 # 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), + 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 fb20331..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,22 +65,25 @@ 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.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 @@ -96,12 +109,12 @@ 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.transient_storage, key, 0), + 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 @@ -113,12 +126,19 @@ 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), + with {:ok, key, s1} <- Stack.pop(state.frame.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)} + + 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 @@ -130,13 +150,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)}} + 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}} end end diff --git a/lib/eevm/interpreter/instructions/system/calls.ex b/lib/eevm/interpreter/instructions/system/calls.ex index 7468f4b..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,55 +172,76 @@ 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} -> 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) - {: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 @@ -229,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) @@ -241,16 +262,16 @@ 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, - 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, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + 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, + created_addresses: state_after_touch.substate.created_addresses, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -258,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 @@ -277,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} @@ -300,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} -> @@ -318,31 +362,42 @@ 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) - {: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 @@ -350,27 +405,27 @@ 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 = 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, - 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, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + 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, + created_addresses: state_after_touch.substate.created_addresses, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -378,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 @@ -433,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 4a8340e..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,30 +68,33 @@ 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 = 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 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,23 +115,23 @@ 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.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, - 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, - is_static: state_after_touch.is_static, - depth: state_after_touch.depth + 1, + 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, + created_addresses: state_after_touch.substate.created_addresses, + is_static: state_after_touch.frame.is_static, + depth: state_after_touch.frame.depth + 1, tracer: state_after_touch.tracer ) @@ -142,9 +145,9 @@ 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.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,13 +156,13 @@ 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 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) @@ -170,17 +173,25 @@ 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) + |> 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(:gas, child_result.gas - deposit_cost) - |> Map.put(:return_data, child_result.return_data) + |> Map.put(:db, db_after_deploy) + |> Map.put(:substate, merged_substate) |> MachineState.advance_pc()} else create_failed(post_child_fail_state, stack) @@ -292,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 6020595..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,22 +70,28 @@ 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) # 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) - created_this_tx = MapSet.member?(state_after_touch.created_addresses, contract_address) + 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) 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..7f79089 100644 --- a/lib/eevm/interpreter/machine_state.ex +++ b/lib/eevm/interpreter/machine_state.ex @@ -1,95 +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, 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(), + frame: Frame.t(), + env: Env.t(), + substate: Substate.t(), db: Database.t(), - original_storage: %{non_neg_integer() => non_neg_integer()}, - transient_storage: %{non_neg_integer() => non_neg_integer()}, - 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(), - is_static: boolean(), - depth: non_neg_integer(), - gas: non_neg_integer(), - refund: non_neg_integer(), + call_stack: [Frame.t()], status: status(), - return_data: binary(), - logs: [%{address: non_neg_integer(), data: binary(), topics: [non_neg_integer()]}], - code: binary(), tracer: Tracer.t() | nil } - @enforce_keys [:code] - defstruct pc: 0, - stack: nil, - memory: nil, + @enforce_keys [:frame] + defstruct frame: nil, + env: nil, + substate: nil, db: nil, - original_storage: %{}, - transient_storage: %{}, - 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, - is_static: false, - depth: 0, - gas: 1_000_000, - refund: 0, status: :running, - return_data: <<>>, - logs: [], - code: <<>>, tracer: nil @doc """ @@ -119,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() @@ -129,30 +89,38 @@ defmodule EEVM.Interpreter.MachineState do block = Keyword.get(opts, :block, Block.new()) config = Keyword.get(opts, :config, Config.new(Keyword.get(opts, :hardfork, :cancun))) - %__MODULE__{ + 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, []) + ) + + frame = %Frame{ 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, %{}), - 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), + 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 @@ -195,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 @@ -207,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 @@ -225,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 @@ -322,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() @@ -341,7 +277,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/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/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/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/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 0a1d11d..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 @@ -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 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 7fd033c..de68bcb 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 @@ -94,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 3ecabe1..ee55bd2 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 @@ -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 @@ -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/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 8c13162..3a26cfd 100644 --- a/test/support/blockchain_test_runner.ex +++ b/test/support/blockchain_test_runner.ex @@ -242,13 +242,13 @@ 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, %{ 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 @@ -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 5909419..5b152c4 100644 --- a/test/support/state_test_runner.ex +++ b/test/support/state_test_runner.ex @@ -37,10 +37,10 @@ defmodule EEVM.TestSupport.StateTestRunner do {:ok, %{ state_root: StateRoot.compute_state_root(finalized_db), - logs_hash: logs_hash(if(failed, do: [], else: machine.logs)), - output: if(failed, do: <<>>, else: machine.return_data), + logs_hash: logs_hash(if(failed, do: [], else: machine.substate.logs)), + 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