Packages
signet
1.4.0
1.6.1
1.6.0
1.5.0
1.4.2
1.4.1
1.4.0
1.3.12
1.3.11
1.3.10
1.3.9
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0-echo8
1.0.0-echo7
1.0.0-echo6
1.0.0-echo5
1.0.0-echo4
1.0.0-echo3
1.0.0-echo2
1.0.0-echo1
1.0.0-delta8
1.0.0-delta7
1.0.0-delta6
1.0.0-delta5
1.0.0-delta4
1.0.0-delta3
1.0.0-delta2
1.0.0-delta1
1.0.0-charlie9
1.0.0-charlie8
1.0.0-charlie7
1.0.0-charlie6
1.0.0-charlie5
1.0.0-charlie4
1.0.0-charlie3
1.0.0-charlie2
1.0.0-charlie1
1.0.0-beta9
1.0.0-beta8
1.0.0-beta7
1.0.0-beta6
1.0.0-beta5
1.0.0-beta4
1.0.0-beta3
1.0.0-beta2
1.0.0-beta1
1.0.0-alpha9
1.0.0-alpha8
1.0.0-alpha7
1.0.0-alpha6
1.0.0-alpha5
1.0.0-alpha4
1.0.0-alpha3
1.0.0-alpha2
1.0.0-alpha10
1.0.0-alpha1
0.2.0-alpha6
0.2.0-alpha5
0.2.0-alpha4
0.2.0-alpha1
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.1.0-rc7
0.1.0-rc6
0.1.0-rc5
0.1.0-rc4
0.1.0-rc3
0.1.0-rc2
0.1.0-rc1
0.1.0-rc0
Lightweight Ethereum and Solana RPC client for Elixir
Current section
Files
Jump to
Current section
Files
lib/signet/vm.ex
defmodule Signet.VM do @moduledoc ~S""" An Ethereum VM in Signet, that can only execute pure functions. """ use Signet.Hex import Bitwise require Logger alias Signet.Assembly @type signed :: integer() @type unsigned :: non_neg_integer() @type opcode :: Signet.Assembly.opcode() @type code :: [opcode()] @type word :: <<_::256>> @type address :: <<_::160>> @type ffis :: %{address() => code()} @type context_result :: {:ok, Context.t()} | {:error, vm_error()} @type exec_opts :: [ callvalue: integer(), ffis: ffis() ] @type vm_error :: :pc_out_of_bounds | :value_overflow | :stack_underflow | :signed_integer_out_of_bounds | :out_of_memory | :invalid_operation | {:unknown_ffi, address()} | {:invalid_push, integer(), binary()} | {:impure, opcode()} | {:not_implemented, opcode()} @word_one <<1::256>> @word_zero <<0::256>> @two_pow_256 2 ** 256 @max_uint256 @two_pow_256 - 1 @gas_amount 4_000_000 defmodule FFIs do def log_ffi(args) do case Signet.Contract.IConsole.decode_call(args) do {:ok, f, values} -> IO.puts( "console.#{f}: #{inspect(values, limit: :infinity, printable_limit: :infinity)}" ) _ -> nil end {:return, <<>>} end end @builtin_ffis %{ ~h[0x000000000000000000636F6e736F6c652e6c6f67] => &FFIs.log_ffi/1 } defmodule Input do defstruct [:calldata, :value] @type t :: %__MODULE__{ calldata: binary(), value: Signet.VM.unsigned() } end defmodule Context do defstruct [ :code, :code_encoded, :op_map, :pc, :halted, :stack, :memory, :tstorage, :reverted, # TODO: Should return data be a stack :return_data, :ffis ] @type op_map :: %{integer() => Signet.VM.opcode()} @type t :: %__MODULE__{ code: Signet.VM.code(), code_encoded: binary(), op_map: op_map(), pc: integer(), halted: binary(), stack: [binary()], memory: binary(), tstorage: %{binary() => binary()}, reverted: boolean(), return_data: binary(), ffis: Signet.VM.ffis() } @spec init_from(Signet.VM.code(), Signet.VM.ffis()) :: t() def init_from(code, ffis) do code_encoded = Signet.Assembly.assemble(code) %__MODULE__{ code: code, code_encoded: code_encoded, op_map: build_op_map(code), pc: 0, halted: false, stack: [], memory: <<>>, tstorage: %{}, reverted: false, return_data: <<>>, ffis: ffis } end @spec fetch_ffi(t(), Signet.VM.address()) :: {:ok, Signet.VM.code()} | {:error, Signet.VM.vm_error()} def fetch_ffi(context, address) do with :error <- Map.fetch(context.ffis, address) do {:error, {:unknown_ffi, address}} end end @spec build_op_map(Signet.VM.code()) :: op_map() defp build_op_map(code) do Enum.reduce(code, {0, %{}}, fn operation, {pc, op_map} -> new_pc = pc + Assembly.opcode_size(operation) {new_pc, Map.put(op_map, pc, operation)} end) |> elem(1) end defp show_hex(i, padding \\ nil) do hex = Integer.to_string(i, 16) if padding == nil do hex else String.pad_leading(hex, padding, "0") end end def show_stack(stack) do hex_length = String.length(show_hex(Enum.count(stack) * 32)) Enum.with_index(Enum.reverse(stack), fn el, i -> "\t#{show_hex(i * 32, hex_length)} #{to_hex(el)}" end) |> Enum.join("\n") end def show(context) do [ "pc=#{context.pc}", "stack:", show_stack(context.stack) ] |> Enum.join("\n") end end defmodule ExecutionResult do defstruct [:stack, :reverted, :return_data] @type t :: %__MODULE__{ stack: [binary()], reverted: boolean(), return_data: binary() } @spec from_context(Signet.VM.Context.t()) :: t() def from_context(context) do %__MODULE__{ stack: context.stack, reverted: context.reverted, return_data: context.return_data } end end @spec get_operation(Context.t()) :: {:ok, opcode()} | {:error, vm_error()} defp get_operation(context) do with :error <- Map.fetch(context.op_map, context.pc) do {:error, :pc_out_of_bounds} end end @spec pad_to_word(binary()) :: {:ok, <<_::256>>} | {:error, vm_error()} def pad_to_word(bin) when is_binary(bin) do if byte_size(bin) > 32 do {:error, :value_overflow} else padded_bin = :binary.copy(<<0>>, 32 - byte_size(bin)) <> bin {:ok, padded_bin} end end @spec pop(Context.t()) :: {:ok, Context.t(), word()} | {:error, vm_error()} def pop(context) do case context.stack do [x | rest] -> {:ok, %{context | stack: rest}, x} [] -> {:error, :stack_underflow} end end @spec peek(Context.t(), integer()) :: {:ok, word()} | {:error, vm_error()} def peek(context, n) do case Enum.at(context.stack, n) do nil -> {:error, :stack_underflow} x -> {:ok, x} end end @spec pop_unsigned(Context.t()) :: {:ok, Context.t(), unsigned()} | {:error, vm_error()} def pop_unsigned(context) do case context.stack do [x_enc | rest] -> with {:ok, x} <- word_to_uint(x_enc) do {:ok, %{context | stack: rest}, x} end [] -> {:error, :stack_underflow} end end @spec pop2(Context.t()) :: {:ok, Context.t(), word(), word()} | {:error, vm_error()} def pop2(context) do case context.stack do [x, y | rest] -> {:ok, %{context | stack: rest}, x, y} [] -> {:error, :stack_underflow} end end @spec pop2_unsigned(Context.t()) :: {:ok, Context.t(), unsigned(), unsigned()} | {:error, vm_error()} def pop2_unsigned(context) do case context.stack do [x_enc, y_enc | rest] -> with {:ok, x} <- word_to_uint(x_enc), {:ok, y} <- word_to_uint(y_enc) do {:ok, %{context | stack: rest}, x, y} end [] -> {:error, :stack_underflow} end end @spec pop3_unsigned(Context.t()) :: {:ok, Context.t(), unsigned(), unsigned(), unsigned()} | {:error, vm_error()} def pop3_unsigned(context) do case context.stack do [x_enc, y_enc, z_enc | rest] -> with {:ok, x} <- word_to_uint(x_enc), {:ok, y} <- word_to_uint(y_enc), {:ok, z} <- word_to_uint(z_enc) do {:ok, %{context | stack: rest}, x, y, z} end [] -> {:error, :stack_underflow} end end @spec pop2_unsigned_word(Context.t()) :: {:ok, Context.t(), unsigned(), word()} | {:error, vm_error()} def pop2_unsigned_word(context) do case context.stack do [x_enc, y_enc | rest] -> with {:ok, x} <- word_to_uint(x_enc) do {:ok, %{context | stack: rest}, x, y_enc} end [] -> {:error, :stack_underflow} end end @spec pop3(Context.t()) :: {:ok, Context.t(), word(), word(), word()} | {:error, vm_error()} def pop3(context) do case context.stack do [x, y, z | rest] -> {:ok, %{context | stack: rest}, x, y, z} [] -> {:error, :stack_underflow} end end @spec push_word(Context.t(), word()) :: {:ok, Context.t()} | {:error, vm_error()} def push_word(context, v) when is_binary(v) and bit_size(v) == 256 do if Enum.count(context.stack) == 1024 do {:error, :stack_overflow} else {:ok, %{context | stack: [v | context.stack]}} end end @spec word_to_uint(binary()) :: {:ok, unsigned()} | {:error, vm_error()} def word_to_uint(v) when is_binary(v) do # TODO: Check for overflow? {:ok, :binary.decode_unsigned(v)} end @spec uint_to_word(unsigned()) :: {:ok, binary()} | {:error, vm_error()} def uint_to_word(v) when is_integer(v) do enc = :binary.encode_unsigned(v) pad_to_word(enc) end @spec word_to_sint(binary()) :: {:ok, signed()} | {:error, vm_error()} def word_to_sint(<<value::signed-size(256)>>) do {:ok, value} end def word_to_sint(_) do {:error, :signed_integer_out_of_bounds} end @spec sint_to_word(signed()) :: {:ok, binary()} | {:error, atom()} def sint_to_word(v) when is_integer(v) do min_value = -2 ** 255 max_value = 2 ** 255 - 1 if v >= min_value and v <= max_value do {:ok, <<v::signed-size(256)>>} else {:error, :signed_integer_out_of_bounds} end end @spec pop2_and_push(Context.t(), (word(), word() -> {:ok, word()})) :: context_result() defp pop2_and_push(context, fun) do with {:ok, context, a, b} <- pop2(context), {:ok, v_enc} <- fun.(a, b) do push_word(context, v_enc) end end @spec unsigned_op1(Context.t(), (unsigned() -> unsigned())) :: context_result() defp unsigned_op1(context, fun) do with {:ok, context, a} <- pop(context), {:ok, a_int} <- word_to_uint(a), v <- fun.(a_int), {:ok, v_enc} <- uint_to_word(v) do push_word(context, v_enc) end end @spec unsigned_op2(Context.t(), (unsigned(), unsigned() -> unsigned())) :: context_result() defp unsigned_op2(context, fun) do with {:ok, context, a, b} <- pop2(context), {:ok, a_int} <- word_to_uint(a), {:ok, b_int} <- word_to_uint(b), v <- fun.(a_int, b_int), {:ok, v_enc} <- uint_to_word(v) do push_word(context, v_enc) end end @spec unsigned_op3(Context.t(), (unsigned(), unsigned(), unsigned() -> unsigned())) :: context_result() defp unsigned_op3(context, fun) do with {:ok, context, a, b, c} <- pop3(context), {:ok, a_int} <- word_to_uint(a), {:ok, b_int} <- word_to_uint(b), {:ok, c_int} <- word_to_uint(c), v <- fun.(a_int, b_int, c_int), {:ok, v_enc} <- uint_to_word(v) do push_word(context, v_enc) end end @spec signed_op2(Context.t(), (signed(), signed() -> signed())) :: context_result() defp signed_op2(context, fun) do with {:ok, context, a, b} <- pop2(context), {:ok, a_int} <- word_to_sint(a), {:ok, b_int} <- word_to_sint(b), v <- fun.(a_int, b_int), {:ok, v_enc} <- sint_to_word(v) do push_word(context, v_enc) end end @spec unsigned_signed_op2(Context.t(), (unsigned(), signed() -> signed())) :: context_result() defp unsigned_signed_op2(context, fun) do with {:ok, context, a, b} <- pop2(context), {:ok, a_int} <- word_to_uint(a), {:ok, b_int} <- word_to_sint(b), v <- fun.(a_int, b_int), {:ok, v_enc} <- sint_to_word(v) do push_word(context, v_enc) end end @spec push_n(Context.t(), integer(), binary()) :: context_result() defp push_n(context, n, v) do if byte_size(v) > n do {:error, {:invalid_push, n, v}} else with {:ok, word_padded} <- pad_to_word(v) do push_word(context, word_padded) end end end @spec inc_pc(context_result(), opcode()) :: context_result() def inc_pc(context_result, operation) do with {:ok, context} <- context_result do # Note: we can increment even when there's a jump, since either # we'll increment over the jump _or_ the jumpdest {:ok, %{context | pc: context.pc + Assembly.opcode_size(operation)}} end end @spec cap_to_range(integer(), integer(), integer()) :: integer() def cap_to_range(x, min, max) do cond do x > max -> max x < min -> min true -> x end end defmodule Memory do # 10MB @max_memory 10_000_000 @spec expand_memory(binary(), Signet.VM.unsigned()) :: {:ok, binary()} | {:error, Signet.VM.vm_error()} defp expand_memory(memory, total_size) do memory_size = byte_size(memory) cond do total_size > @max_memory -> {:error, :out_of_memory} memory_size >= total_size -> {:ok, memory} true -> padding = total_size - memory_size {:ok, memory <> :binary.copy(<<0x0>>, padding)} end end @spec read_memory(binary(), Signet.VM.unsigned(), Signet.VM.unsigned()) :: {:ok, binary(), binary()} | {:error, Signet.VM.vm_error()} def read_memory(memory, index, count) do with {:ok, memory_expanded} <- expand_memory(memory, index + count) do <<_::binary-size(index), res::binary-size(count), _::binary>> = memory_expanded {:ok, memory_expanded, res} end end @spec write_memory(Signet.VM.Context.t(), Signet.VM.unsigned(), binary()) :: {:ok, Signet.VM.Context.t()} | {:error, Signet.VM.vm_error()} def write_memory(context, offset, value) do value_size = byte_size(value) with {:ok, memory_expanded} <- expand_memory(context.memory, offset + value_size) do <<start::binary-size(offset), _::binary-size(value_size), final::binary>> = memory_expanded memory_final = <<start::binary, value::binary, final::binary>> {:ok, %{context | memory: memory_final}} end end end defmodule Operations do def sign_extend(b, x) do with {:ok, b_int} <- Signet.VM.word_to_uint(b) do if b_int >= 31 do # No sign extend beyond 32nd bit {:ok, x} else val_len = b_int + 1 <<_::binary-size(32 - val_len), low_word::binary-size(val_len)>> = x if Bitwise.band(Bitwise.bsr(:binary.decode_unsigned(low_word), 8 * val_len - 1), 1) == 1 do # Fill in top bits {:ok, :binary.copy(<<0xFF>>, 32 - val_len) <> low_word} else # Positive {:ok, x} end end end end def get_byte(i, x) do with {:ok, i} <- Signet.VM.word_to_uint(i) do unless i < 32 do {:ok, <<0::256>>} else <<_::binary-size(i), word::binary-size(1), _::binary-size(31 - i)>> = x Signet.VM.pad_to_word(word) end end end end # Calls defp static_call(context) do with {:ok, context, _gas, address, args_offset, args_size, ret_offset, ret_size} <- pop_call_args(context), {:ok, memory_expanded, args} <- Memory.read_memory(context.memory, args_offset, args_size), context <- %{context | memory: memory_expanded}, {:ok, ffi} <- Context.fetch_ffi(context, address) do case ffi.(args) do {:return, return_data} -> return_data_to_copy = if byte_size(return_data) >= ret_size do # Take left N bytes <<v::binary-size(ret_size), _::binary>> = return_data v else # Pad right with zeros return_data <> :binary.copy(<<0x0>>, ret_size - byte_size(return_data)) end with {:ok, context} <- context |> Map.put(:return_data, return_data) |> Memory.write_memory( ret_offset, return_data_to_copy ) do push_word(context, @word_one) end {:revert, revert} -> context |> Map.merge(%{return_data: revert, halted: true, reverted: true}) |> push_word(@word_zero) end end end defp pop_call_args(context) do with {:ok, context, gas} <- pop_unsigned(context), {:ok, context, address_word} <- pop(context), {:ok, context, args_offset} <- pop_unsigned(context), {:ok, context, args_size} <- pop_unsigned(context), {:ok, context, ret_offset} <- pop_unsigned(context), {:ok, context, ret_size} <- pop_unsigned(context) do {:ok, context, gas, word_to_address(address_word), args_offset, args_size, ret_offset, ret_size} end end defp word_to_address(word) do <<_preface::binary-size(12), address::binary-size(20)>> = word address end @spec run_single_op(Context.t(), Input.t(), Keyword.t()) :: context_result() def run_single_op(context, input, opts) do if opts[:verbose] do Logger.debug(Context.show(context)) end with {:ok, operation} <- get_operation(context) do if opts[:verbose] do Logger.debug("Operation: #{Signet.Assembly.show_opcode(operation)}") end case operation do :stop -> {:ok, %{context | return_data: <<>>, halted: true}} :add -> unsigned_op2(context, &rem(&1 + &2, @two_pow_256)) :sub -> unsigned_op2(context, &rem(@two_pow_256 + &1 - &2, @two_pow_256)) :mul -> unsigned_op2(context, &rem(&1 * &2, @two_pow_256)) :div -> unsigned_op2(context, &if(&2 == 0, do: 0, else: Integer.floor_div(&1, &2))) :sdiv -> signed_op2(context, &if(&2 == 0, do: 0, else: Integer.floor_div(&1, &2))) :mod -> unsigned_op2(context, &if(&2 == 0, do: 0, else: rem(&1, &2))) :smod -> signed_op2(context, &if(&2 == 0, do: 0, else: rem(&1, &2))) :addmod -> unsigned_op3(context, &if(&3 == 0, do: 0, else: rem(&1 + &2, &3))) :mulmod -> unsigned_op3(context, &if(&3 == 0, do: 0, else: rem(&1 * &2, &3))) :exp -> unsigned_op2(context, &rem(&1 ** &2, @two_pow_256)) :signextend -> pop2_and_push(context, &Operations.sign_extend/2) :lt -> unsigned_op2(context, &if(&1 < &2, do: 1, else: 0)) :gt -> unsigned_op2(context, &if(&1 > &2, do: 1, else: 0)) :slt -> signed_op2(context, &if(&1 < &2, do: 1, else: 0)) :sgt -> signed_op2(context, &if(&1 > &2, do: 1, else: 0)) :eq -> unsigned_op2(context, &if(&1 == &2, do: 1, else: 0)) :iszero -> unsigned_op1(context, &if(&1 == 0, do: 1, else: 0)) :and -> unsigned_op2(context, &Bitwise.band(&1, &2)) :or -> unsigned_op2(context, &Bitwise.bor(&1, &2)) :xor -> unsigned_op2(context, &Bitwise.bxor(&1, &2)) :not -> unsigned_op1(context, &Bitwise.bxor(&1, @max_uint256)) :byte -> pop2_and_push(context, &Operations.get_byte/2) :shl -> unsigned_op2(context, &rem(Bitwise.bsl(&2, cap_to_range(&1, 0, 255)), @two_pow_256)) :shr -> unsigned_op2(context, &Bitwise.bsr(&2, cap_to_range(&1, 0, 255))) :sar -> unsigned_signed_op2(context, &(&2 >>> cap_to_range(&1, 0, 255))) :sha3 -> with {:ok, context, offset, size} <- pop2_unsigned(context), {:ok, memory_expanded, data} <- Memory.read_memory(context.memory, offset, size) do push_word(%{context | memory: memory_expanded}, Signet.Hash.keccak(data)) end :callvalue -> with {:ok, value} <- uint_to_word(input.value) do push_word(context, value) end :calldataload -> with {:ok, context, i} <- pop_unsigned(context), {:ok, _, res} <- Memory.read_memory(input.calldata, i, 32) do push_word(context, res) end :calldatasize -> with {:ok, calldata_size} <- uint_to_word(byte_size(input.calldata)) do push_word(context, calldata_size) end :calldatacopy -> with {:ok, context, dest_offset, offset, size} <- pop3_unsigned(context), {:ok, _, calldata} <- Memory.read_memory(input.calldata, offset, size) do Memory.write_memory(context, dest_offset, calldata) end :codesize -> with {:ok, codesize} <- uint_to_word(byte_size(context.code_encoded)) do push_word(context, codesize) end :codecopy -> with {:ok, context, dest_offset, offset, size} <- pop3_unsigned(context), {:ok, _, code} <- Memory.read_memory(context.code_encoded, offset, size) do Memory.write_memory(context, dest_offset, code) end :pop -> with {:ok, context, _} <- pop(context) do {:ok, context} end :mload -> with {:ok, context, i} <- pop_unsigned(context), {:ok, memory_expanded, res} <- Memory.read_memory(context.memory, i, 32) do push_word(%{context | memory: memory_expanded}, res) end :mstore -> with {:ok, context, offset, value} <- pop2_unsigned_word(context) do Memory.write_memory(context, offset, value) end :mstore8 -> with {:ok, context, offset, value} <- pop2_unsigned_word(context) do <<_::binary-size(31), byte::binary>> = value Memory.write_memory(context, offset, byte) end :jump -> with {:ok, context, jump_dest} <- pop_unsigned(context) do case Map.get(context.op_map, jump_dest) do :jumpdest -> {:ok, %{context | pc: jump_dest}} _ -> {:error, :invalid_jump_dest} end end :jumpi -> with {:ok, context, jump_dest, b} <- pop2_unsigned(context) do if b == 0 do {:ok, context} else case Map.get(context.op_map, jump_dest) do :jumpdest -> {:ok, %{context | pc: jump_dest}} _ -> {:error, :invalid_jump_dest} end end end :pc -> with {:ok, pc} <- uint_to_word(context.pc) do push_word(context, pc) end :msize -> with {:ok, memory_sz} <- uint_to_word(byte_size(context.memory)) do push_word(context, memory_sz) end :gas -> with {:ok, gas_amount} <- uint_to_word(@gas_amount) do push_word(context, gas_amount) end :jumpdest -> {:ok, context} :tload -> with {:ok, context, res} <- pop_unsigned(context) do push_word(context, Map.get(context.tstorage, res, <<0::256>>)) end :tstore -> with {:ok, context, key, value} <- pop2_unsigned_word(context) do {:ok, %{context | tstorage: Map.put(context.tstorage, key, value)}} end :mcopy -> with {:ok, context, dest_offset, offset, size} <- pop3_unsigned(context), {:ok, memory_expanded, value} <- Memory.read_memory(context.memory, offset, size) do Memory.write_memory(%{context | memory: memory_expanded}, dest_offset, value) end {:push, n, v} -> push_n(context, n, v) {:dup, n} -> with {:ok, val} <- peek(context, n - 1) do push_word(context, val) end {:swap, n} -> with {:ok, high} <- peek(context, n), {:ok, low} <- peek(context, 0) do stack = context.stack |> List.replace_at(n, low) |> List.replace_at(0, high) {:ok, %{context | stack: stack}} end :return -> with {:ok, context, offset, size} <- pop2_unsigned(context) do with {:ok, memory_expanded, return_data} <- Memory.read_memory(context.memory, offset, size) do {:ok, %{context | memory: memory_expanded, return_data: return_data, halted: true}} end end :revert -> with {:ok, context, offset, size} <- pop2_unsigned(context) do with {:ok, memory_expanded, return_data} <- Memory.read_memory(context.memory, offset, size) do {:ok, %{ context | memory: memory_expanded, return_data: return_data, halted: true, reverted: true }} end end {:invalid, _} -> {:error, :invalid_operation} :staticcall -> static_call(context) :returndatasize -> with {:ok, return_data_size} <- uint_to_word(byte_size(context.return_data)) do push_word(context, return_data_size) end :returndatacopy -> with {:ok, context, dest_offset, offset, size} <- pop3_unsigned(context), {:ok, _, calldata} <- Memory.read_memory(context.return_data, offset, size) do Memory.write_memory(context, dest_offset, calldata) end op when op in [ :address, :balance, :origin, :caller, :gasprice, :extcodesize, :extcodecopy, :extcodehash, :blockhash, :coinbase, :timestamp, :number, :prevrandao, :gaslimit, :chainid, :selfbalance, :basefee, :blobhash, :blobbasefee, :sload, :sstore, :log, :create, :call, :callcode, :delegatecall, :create2, :selfdestruct ] -> {:error, {:impure, operation}} _ -> {:error, {:not_implemented, operation}} end |> inc_pc(operation) end end @spec run_code(Context.t(), Input.t(), Keyword.t()) :: {:ok, ExecutionResult.t()} | {:error, vm_error()} defp run_code(context, input, opts \\ []) do case run_single_op(context, input, opts) do {:ok, context = %Context{halted: true}} -> {:ok, ExecutionResult.from_context(context)} {:ok, context} -> run_code(context, input) {:error, error} -> {:error, error} end end @doc ~S""" Executes the Ethereum Virtual Machine (EVM) with the given `code` and `input`. **Parameters** - `code`: The bytecode to be executed, either as a `binary` or decoded. - `calldata`: The call data for the execution. - `opts`: Execution options (see below) **Options** - `:callvalue`: value passed as callvalue for the execution. - `:ffis`: A mapping of address to functions to run as natively implemented ffis Returns the result of the execution. """ @spec exec(code() | binary(), binary(), exec_opts()) :: {:ok, ExecutionResult.t()} | {:error, vm_error()} def exec(code, calldata, opts \\ []) def exec(code, calldata, opts) when is_binary(code) do exec(Assembly.disassemble(code), calldata, opts) end def exec(code, calldata, opts) when is_list(code) do run_code( Context.init_from(code, Map.merge(@builtin_ffis, Keyword.get(opts, :ffis, %{}))), %Input{ calldata: calldata, value: Keyword.get(opts, :callvalue, 0) }, opts ) end defmodule VmError do defexception message: "VmError" end @doc ~S""" Runs the given EVM, returning the `RETURN` data or the `REVERT` data. Raises on any other exceptional state. **Parameters** - `code`: The bytecode to be executed, either as a `binary` or decoded. - `calldata`: The call data for the execution. - `opts`: Execution options (see below) **Options** - `:callvalue`: value passed as callvalue for the execution. - `:ffis`: A mapping of address to functions to run as natively implemented ffis """ @spec exec_call(code() | binary(), binary(), exec_opts()) :: {:ok, binary()} | {:revert, binary()} def exec_call(code, calldata, opts \\ []) do case exec(code, calldata, opts) do {:ok, %ExecutionResult{reverted: reverted, return_data: return_data}} -> if reverted do {:revert, return_data} else {:ok, return_data} end {:error, error} -> raise VmError, "VmError: #{inspect(error)}" end endend