diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index 150f197ffe..46ff56c64e 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -3,6 +3,10 @@ defmodule EthereumJSONRPC.Geth do Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth). """ + import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, request: 1] + + alias EthereumJSONRPC.Geth.Calls + @behaviour EthereumJSONRPC.Variant @doc """ @@ -19,8 +23,16 @@ defmodule EthereumJSONRPC.Geth do To signal to the caller that fetching is not supported, `:ignore` is returned. """ @impl EthereumJSONRPC.Variant - def fetch_internal_transactions(transaction_params, _json_rpc_named_arguments) when is_list(transaction_params), - do: :ignore + def fetch_internal_transactions(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do + id_to_params = id_to_params(transactions_params) + + with {:ok, responses} <- + id_to_params + |> debug_trace_transaction_requests() + |> json_rpc(json_rpc_named_arguments) do + debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + end + end @doc """ Pending transaction fetching is not supported currently for Geth. @@ -29,4 +41,110 @@ defmodule EthereumJSONRPC.Geth do """ @impl EthereumJSONRPC.Variant def fetch_pending_transactions(_json_rpc_named_arguments), do: :ignore + + defp debug_trace_transaction_requests(id_to_params) when is_map(id_to_params) do + Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} -> + debug_trace_transaction_request(%{id: id, hash_data: hash_data}) + end) + end + + @tracer_path "priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js" + @external_resource @tracer_path + @tracer File.read!(@tracer_path) + + defp debug_trace_transaction_request(%{id: id, hash_data: hash_data}) do + request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]}) + end + + defp debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + when is_list(responses) and is_map(id_to_params) do + responses + |> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params)) + |> reduce_internal_transactions_params() + end + + defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, result: calls}, id_to_params) + when is_map(id_to_params) do + %{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} = + Map.fetch!(id_to_params, id) + + internal_transaction_params = + calls + |> Stream.with_index() + |> Enum.map(fn {trace, index} -> + Map.merge(trace, %{ + "blockNumber" => block_number, + "index" => index, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash + }) + end) + |> Calls.to_internal_transactions_params() + + {:ok, internal_transaction_params} + end + + defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, error: error}, id_to_params) + when is_map(id_to_params) do + %{ + block_number: block_number, + hash_data: "0x" <> transaction_hash_digits = transaction_hash, + transaction_index: transaction_index + } = Map.fetch!(id_to_params, id) + + not_found_message = "transaction " <> transaction_hash_digits <> " not found" + + normalized_error = + case error do + %{code: -32_000, message: ^not_found_message} -> + %{message: :not_found} + + %{code: -32_000, message: "execution timeout"} -> + %{message: :timeout} + + _ -> + error + end + + annotated_error = + Map.put(normalized_error, :data, %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash + }) + + {:error, annotated_error} + end + + defp reduce_internal_transactions_params(internal_transactions_params) when is_list(internal_transactions_params) do + internal_transactions_params + |> Enum.reduce({:ok, []}, &internal_transactions_params_reducer/2) + |> finalize_internal_transactions_params() + end + + defp internal_transactions_params_reducer( + {:ok, internal_transactions_params}, + {:ok, acc_internal_transactions_params_list} + ), + do: {:ok, [internal_transactions_params, acc_internal_transactions_params_list]} + + defp internal_transactions_params_reducer({:ok, _}, {:error, _} = acc_error), do: acc_error + defp internal_transactions_params_reducer({:error, reason}, {:ok, _}), do: {:error, [reason]} + + defp internal_transactions_params_reducer({:error, reason}, {:error, acc_reasons}) when is_list(acc_reasons), + do: {:error, [reason | acc_reasons]} + + defp finalize_internal_transactions_params({:ok, acc_internal_transactions_params_list}) + when is_list(acc_internal_transactions_params_list) do + internal_transactions_params = + acc_internal_transactions_params_list + |> Enum.reverse() + |> List.flatten() + + {:ok, internal_transactions_params} + end + + defp finalize_internal_transactions_params({:error, acc_reasons}) do + {:error, Enum.reverse(acc_reasons)} + end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex new file mode 100644 index 0000000000..ba6e97b6c7 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex @@ -0,0 +1,487 @@ +defmodule EthereumJSONRPC.Geth.Call do + @moduledoc """ + A single call returned from [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction) + using a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`). + """ + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + @doc """ + A call can call another another contract: + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3287375, + ...> "transactionIndex" => 13, + ...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + ...> "index" => 0, + ...> "type" => "call", + ...> "callType" => "call", + ...> "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + ...> "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + ...> "gas" => "0x8600", + ...> "gasUsed" => "0x7d37", + ...> "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + ...> "output" => "0x", + ...> "value" => "0x174876e800" + ...> } + ...> ) + %{ + block_number: 3287375, + transaction_index: 13, + transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + index: 0, + type: "call", + call_type: "call", + from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + gas: 34304, + gas_used: 32055, + input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + output: "0x", + trace_address: [], + value: 100000000000 + } + + A call can run out of gas: + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3293221, + ...> "callType" => "call", + ...> "error" => "out of gas", + ...> "from" => "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef", + ...> "gas" => "0x4c9", + ...> "gasUsed" => "0x4c9", + ...> "index" => 0, + ...> "input" => "0xa83627de", + ...> "to" => "0xaae465ad04b12e90c32291e59b65ca781c57e361", + ...> "transactionHash" => "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073", + ...> "transactionIndex" => 16, + ...> "type" => "call", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3293221, + transaction_index: 16, + transaction_hash: "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073", + index: 0, + type: "call", + call_type: "call", + error: "out of gas", + from_address_hash: "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef", + to_address_hash: "0xaae465ad04b12e90c32291e59b65ca781c57e361", + gas: 1225, + input: "0xa83627de", + trace_address: [], + value: 0 + } + + A call can reach the stack limit (1024): + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3293621, + ...> "transactionIndex" => 7, + ...> "transactionHash" => "0xc4f4ba28bf8e6093b3f5932191a7a6af1dd17517c2b0e1be3b76dc445564a9ff", + ...> "index" => 64, + ...> "type" => "call", + ...> "callType" => "call", + ...> "from" => "0xaf7cf620c3df1b9ccbc640be903d5ea6cea7bc96", + ...> "to" => "0x80629758f88b3f30b7f1244e4588444d6276eef0", + ...> "input" => "0x49b46d5d", + ...> "error" => "stack limit reached 1024 (1024)", + ...> "gas" => "0x160ecc", + ...> "gasUsed" => "0x160ecc", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3293621, + transaction_index: 7, + transaction_hash: "0xc4f4ba28bf8e6093b3f5932191a7a6af1dd17517c2b0e1be3b76dc445564a9ff", + index: 64, + type: "call", + call_type: "call", + from_address_hash: "0xaf7cf620c3df1b9ccbc640be903d5ea6cea7bc96", + to_address_hash: "0x80629758f88b3f30b7f1244e4588444d6276eef0", + input: "0x49b46d5d", + error: "stack limit reached 1024 (1024)", + gas: 1445580, + trace_address: [], + value: 0 + } + + A contract creation: + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3292697, + ...> "transactionIndex" => 1, + ...> "transactionHash" => "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6", + ...> "index" => 0, + ...> "type" => "create", + ...> "from" => "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c", + ...> "createdContractAddressHash" => "0x08d24f568715041e72223cc023e806060de8a2a5", + ...> "gas" => "0x5e46ef", + ...> "gasUsed" => "0x168a8a", + ...> "init" => "0x", + ...> "createdContractCode" => "0x", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3292697, + transaction_index: 1, + transaction_hash: "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6", + index: 0, + type: "create", + from_address_hash: "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c", + created_contract_address_hash: "0x08d24f568715041e72223cc023e806060de8a2a5", + gas: 6178543, + gas_used: 1477258, + init: "0x", + created_contract_code: "0x", + trace_address: [], + value: 0 + } + + A contract creation can fail: + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3299287, + ...> "transactionIndex" => 14, + ...> "transactionHash" => "0x5c0c728190e593f2bbcbd9d7f851cbfbcaf041e41ce1b1eead97c301deb071fa", + ...> "index" => 0, + ...> "type" => "create", + ...> "from" => "0x0a49007c56c5f9eda04a2ae4229da03a30be892e", + ...> "gas" => "0x84068", + ...> "gasUsed" => "0x84068", + ...> "init" => "0xf49e4745", + ...> "error" => "stack underflow (0 <=> 6)", + ...> "value" => "0x12c94dd59ce493" + ...> } + ...> ) + %{ + block_number: 3299287, + transaction_index: 14, + transaction_hash: "0x5c0c728190e593f2bbcbd9d7f851cbfbcaf041e41ce1b1eead97c301deb071fa", + index: 0, + type: "create", + from_address_hash: "0x0a49007c56c5f9eda04a2ae4229da03a30be892e", + init: "0xf49e4745", + error: "stack underflow (0 <=> 6)", + gas: 540776, + trace_address: [], + value: 5287885714285715 + } + + A delegate call uses the current contract's state, but the called contract's code: + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3292842, + ...> "transactionIndex" => 21, + ...> "transactionHash" => "0x6cf0aa434f6500251ce8579d031c821b9fd4b687685b21c368f1c1106e9a49a9", + ...> "index" => 1, + ...> "type" => "call", + ...> "callType" => "delegatecall", + ...> "from" => "0x54a298ee9fccbf0ad8e55bc641d3086b81a48c41", + ...> "to" => "0x147e7f491ddabc0488edb47f8700633dbaad1fd1", + ...> "gas" => "0x40289", + ...> "gasUsed" => "0x17df", + ...> "input" => "0xeb9d50e46930b3227102b442f93b4aed3dead4ed76f850a76ee7f8b2cbe763428f2790530000000000000000000000000000000000000000000000000926708dfd7272e3", + ...> "output" => "0x", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3292842, + transaction_index: 21, + transaction_hash: "0x6cf0aa434f6500251ce8579d031c821b9fd4b687685b21c368f1c1106e9a49a9", + index: 1, + type: "call", + call_type: "delegatecall", + from_address_hash: "0x54a298ee9fccbf0ad8e55bc641d3086b81a48c41", + to_address_hash: "0x147e7f491ddabc0488edb47f8700633dbaad1fd1", + gas: 262793, + gas_used: 6111, + input: "0xeb9d50e46930b3227102b442f93b4aed3dead4ed76f850a76ee7f8b2cbe763428f2790530000000000000000000000000000000000000000000000000926708dfd7272e3", + output: "0x", + trace_address: [], + value: 0 + } + + A static call calls another contract, but no state can change. This includes no value transfer, so the value for the + call is always `0`. If the called contract does attempt a state change, the call will error. + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3293660, + ...> "transactionIndex" => 0, + ...> "transactionHash" => "0xb49ac6385dce60e2d88d8b4579f4e70a23cd40b45ecb29eb6c6069efc895325b", + ...> "index" => 1, + ...> "type" => "call", + ...> "callType" => "staticcall", + ...> "from" => "0xa4b3886db53bebdabbe17592a57886810b906200", + ...> "to" => "0x20f47d830b01c4f4af4b7663a8143d230fcdc0c8", + ...> "input" => "0x0f370699", + ...> "output" => "0x", + ...> "gas" => "0x478d26", + ...> "gasUsed" => "0x410", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3293660, + transaction_index: 0, + transaction_hash: "0xb49ac6385dce60e2d88d8b4579f4e70a23cd40b45ecb29eb6c6069efc895325b", + index: 1, + type: "call", + call_type: "staticcall", + from_address_hash: "0xa4b3886db53bebdabbe17592a57886810b906200", + to_address_hash: "0x20f47d830b01c4f4af4b7663a8143d230fcdc0c8", + gas: 4689190, + gas_used: 1040, + input: "0x0f370699", + output: "0x", + trace_address: [], + value: 0 + } + + A selfdestruct destroys the calling contract and sends any left over balance to the to address. + + iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params( + ...> %{ + ...> "blockNumber" => 3298074, + ...> "transactionIndex" => 9, + ...> "transactionHash" => "0xe098557c8fa82be6779f5c2b3f248e990e2dc67b6bd60a4fa4a9aa66f6c24c08", + ...> "index" => 32, + ...> "type" => "selfdestruct", + ...> "from" => "0x9317da7be8e05f36f329a95f004a44552effb968", + ...> "to" => "0xff77830c100623316736b45c4983df970423aaf4", + ...> "gas" => "0xb52c8", + ...> "gasUsed" => "0xaf6b5", + ...> "value" => "0x0" + ...> } + ...> ) + %{ + block_number: 3298074, + from_address_hash: "0x9317da7be8e05f36f329a95f004a44552effb968", + gas: 742088, + gas_used: 718517, + index: 32, + to_address_hash: "0xff77830c100623316736b45c4983df970423aaf4", + transaction_hash: "0xe098557c8fa82be6779f5c2b3f248e990e2dc67b6bd60a4fa4a9aa66f6c24c08", + transaction_index: 9, + type: "selfdestruct", + value: 0 + } + + """ + def to_internal_transaction_params(call) when is_map(call) do + call + |> to_elixir() + |> elixir_to_internal_transaction_params() + end + + defp to_elixir(call) when is_map(call) do + Enum.into(call, %{}, &entry_to_elixir/1) + end + + defp entry_to_elixir({key, value} = entry) + when key in ~w(callType createdContractAddressHash createdContractCode error from init input output to transactionHash type) and + is_binary(value), + do: entry + + defp entry_to_elixir({key, value} = entry) when key in ~w(blockNumber index transactionIndex) and is_integer(value), + do: entry + + defp entry_to_elixir({key, quantity}) when key in ~w(gas gasUsed value) and is_binary(quantity) do + {key, quantity_to_integer(quantity)} + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "call" = type, + "callType" => call_type, + "from" => from_address_hash, + "to" => to_address_hash, + "gas" => gas, + "gasUsed" => gas_used, + "input" => input, + "output" => output, + "value" => value + }) + when call_type in ~w(call callcode delegatecall) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: type, + call_type: call_type, + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, + gas: gas, + gas_used: gas_used, + input: input, + output: output, + trace_address: [], + value: value + } + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "call" = type, + "callType" => call_type, + "from" => from_address_hash, + "to" => to_address_hash, + "gas" => gas, + "input" => input, + "error" => error, + "value" => value + }) + when call_type in ~w(call callcode delegatecall) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: type, + call_type: call_type, + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, + gas: gas, + input: input, + error: error, + trace_address: [], + value: value + } + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "call" = type, + "callType" => "staticcall" = call_type, + "from" => from_address_hash, + "to" => to_address_hash, + "input" => input, + "output" => output, + "gas" => gas, + "gasUsed" => gas_used, + "value" => 0 = value + }) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: type, + call_type: call_type, + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, + gas: gas, + gas_used: gas_used, + input: input, + output: output, + trace_address: [], + value: value + } + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "create", + "from" => from_address_hash, + "createdContractAddressHash" => created_contract_address_hash, + "gas" => gas, + "gasUsed" => gas_used, + "init" => init, + "createdContractCode" => created_contract_code, + "value" => value + }) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: "create", + from_address_hash: from_address_hash, + gas: gas, + gas_used: gas_used, + created_contract_address_hash: created_contract_address_hash, + init: init, + created_contract_code: created_contract_code, + trace_address: [], + value: value + } + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "create" = type, + "from" => from_address_hash, + "error" => error, + "gas" => gas, + "init" => init, + "value" => value + }) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: type, + from_address_hash: from_address_hash, + gas: gas, + error: error, + init: init, + trace_address: [], + value: value + } + end + + defp elixir_to_internal_transaction_params(%{ + "blockNumber" => block_number, + "transactionIndex" => transaction_index, + "transactionHash" => transaction_hash, + "index" => index, + "type" => "selfdestruct" = type, + "from" => from_address_hash, + "to" => to_address_hash, + "gas" => gas, + "gasUsed" => gas_used, + "value" => value + }) do + %{ + block_number: block_number, + transaction_index: transaction_index, + transaction_hash: transaction_hash, + index: index, + type: type, + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, + gas: gas, + gas_used: gas_used, + value: value + } + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/calls.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/calls.ex new file mode 100644 index 0000000000..af8b9e786c --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/calls.ex @@ -0,0 +1,233 @@ +defmodule EthereumJSONRPC.Geth.Calls do + @moduledoc """ + Calls returned from [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction) + using a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`). + """ + + alias EthereumJSONRPC.Geth.Call + + @doc """ + Converts a sequence of calls to internal transaction params. + + A sequence of calls: + + iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params( + ...> [ + ...> %{ + ...> "blockNumber" => 3287375, + ...> "callType" => "call", + ...> "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + ...> "gas" => "0x8600", + ...> "gasUsed" => "0x7d37", + ...> "index" => 0, + ...> "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + ...> "output" => "0x", + ...> "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + ...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + ...> "transactionIndex" => 13, + ...> "type" => "call", + ...> "value" => "0x174876e800" + ...> }, + ...> %{ + ...> "blockNumber" => 3287375, + ...> "callType" => "call", + ...> "from" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + ...> "gas" => "0x25e4", + ...> "gasUsed" => "0x1ce8", + ...> "index" => 1, + ...> "input" => "0x", + ...> "output" => "0x", + ...> "to" => "0xf8d67a2d17b7936bda99585d921fd7276fc5cac7", + ...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + ...> "transactionIndex" => 13, + ...> "type" => "call", + ...> "value" => "0x174876e800" + ...> } + ...> ] + ...> ) + [ + %{ + block_number: 3287375, + call_type: "call", + from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + gas: 34304, + gas_used: 32055, + index: 0, + input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + output: "0x", + to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + trace_address: [], + transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + transaction_index: 13, + type: "call", + value: 100000000000 + }, + %{ + block_number: 3287375, + call_type: "call", + from_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + gas: 9700, + gas_used: 7400, + index: 1, + input: "0x", + output: "0x", + to_address_hash: "0xf8d67a2d17b7936bda99585d921fd7276fc5cac7", + trace_address: [], + transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c", + transaction_index: 13, + type: "call", + value: 100000000000 + } + ] + + A call can run out of gas: + + iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params( + ...> [ + ...> %{ + ...> "blockNumber" => 3293221, + ...> "callType" => "call", + ...> "error" => "out of gas", + ...> "from" => "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef", + ...> "gas" => "0x4c9", + ...> "gasUsed" => "0x4c9", + ...> "index" => 0, + ...> "input" => "0xa83627de", + ...> "to" => "0xaae465ad04b12e90c32291e59b65ca781c57e361", + ...> "transactionHash" => "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073", + ...> "transactionIndex" => 16, + ...> "type" => "call", + ...> "value" => "0x0" + ...> } + ...> ] + ...> ) + [ + %{ + block_number: 3293221, + transaction_index: 16, + transaction_hash: "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073", + index: 0, + type: "call", + call_type: "call", + error: "out of gas", + from_address_hash: "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef", + to_address_hash: "0xaae465ad04b12e90c32291e59b65ca781c57e361", + gas: 1225, + input: "0xa83627de", + trace_address: [], + value: 0 + } + ] + + A contract creation: + + iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params( + ...> [ + ...> %{ + ...> "blockNumber" => 3292697, + ...> "type" => "create", + ...> "transactionIndex" => 1, + ...> "transactionHash" => "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6", + ...> "index" => 0, + ...> "from" => "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c", + ...> "createdContractAddressHash" => "0x08d24f568715041e72223cc023e806060de8a2a5", + ...> "gas" => "0x5e46ef", + ...> "gasUsed" => "0x168a8a", + ...> "init" => "0x", + ...> "createdContractCode" => "0x", + ...> "value" => "0x0" + ...> } + ...> ] + ...> ) + [ + %{ + block_number: 3292697, + transaction_index: 1, + transaction_hash: "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6", + index: 0, + type: "create", + from_address_hash: "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c", + created_contract_address_hash: "0x08d24f568715041e72223cc023e806060de8a2a5", + gas: 6178543, + gas_used: 1477258, + init: "0x", + created_contract_code: "0x", + trace_address: [], + value: 0 + } + ] + + Contract creation can happen indirectly through a call: + + iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params( + ...> [ + ...> %{ + ...> "blockNumber" => 3293393, + ...> "transactionIndex" => 13, + ...> "transactionHash" => "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461", + ...> "index" => 0, + ...> "callType" => "call", + ...> "from" => "0x129f447137b03ee3d8bbad62ef5d89021d944324", + ...> "to" => "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518", + ...> "gas" => "0x18d2c2", + ...> "gasUsed" => "0x106e24", + ...> "input" => "0xe9696f54", + ...> "output" => "0x0000000000000000000000009b5a1dcfd53caa108ef83cf2ff0e17db27facf0f", + ...> "type" => "call", + ...> "value" => "0x0" + ...> }, + ...> %{ + ...> "blockNumber" => 3293393, + ...> "transactionIndex" => 13, + ...> "transactionHash" => "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461", + ...> "index" => 1, + ...> "type" => "create", + ...> "from" => "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518", + ...> "createdContractAddressHash" => "0x9b5a1dcfd53caa108ef83cf2ff0e17db27facf0f", + ...> "gas" => "0x18c869", + ...> "gasUsed" => "0xfe428", + ...> "init" => "0x6080604", + ...> "createdContractCode" => "0x608060", + ...> "value" => "0x0" + ...> } + ...> ] + ...> ) + [ + %{ + block_number: 3293393, + transaction_index: 13, + transaction_hash: "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461", + index: 0, + type: "call", + call_type: "call", + from_address_hash: "0x129f447137b03ee3d8bbad62ef5d89021d944324", + to_address_hash: "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518", + gas: 1626818, + gas_used: 1076772, + input: "0xe9696f54", + output: "0x0000000000000000000000009b5a1dcfd53caa108ef83cf2ff0e17db27facf0f", + trace_address: [], + value: 0 + }, + %{ + block_number: 3293393, + transaction_index: 13, + transaction_hash: "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461", + index: 1, + type: "create", + created_contract_address_hash: "0x9b5a1dcfd53caa108ef83cf2ff0e17db27facf0f", + from_address_hash: "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518", + gas: 1624169, + gas_used: 1041448, + init: "0x6080604", + created_contract_code: "0x608060", + trace_address: [], + value: 0 + } + ] + + """ + def to_internal_transactions_params(calls) when is_list(calls) do + Enum.map(calls, &Call.to_internal_transaction_params/1) + end +end diff --git a/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js b/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js new file mode 100644 index 0000000000..1835fd2b28 --- /dev/null +++ b/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js @@ -0,0 +1,418 @@ +// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction` +{ + // The call stack of the EVM execution. + callStack: [{}], + + // step is invoked for every opcode that the VM executes. + step(log, db) { + // Capture any errors immediately + const error = log.getError(); + + if (error !== undefined) { + this.fault(log, db); + } else { + this.success(log, db); + } + }, + + // fault is invoked when the actual execution of an opcode fails. + fault(log, db) { + // If the topmost call already reverted, don't handle the additional fault again + if (this.topCall().error === undefined) { + this.putError(log); + } + }, + + putError(log) { + if (this.callStack.length > 1) { + this.putErrorInTopCall(log); + } else { + this.putErrorInBottomCall(log); + } + }, + + putErrorInTopCall(log) { + // Pop off the just failed call + const call = this.callStack.pop(); + this.putErrorInCall(log, call); + this.pushChildCall(call); + }, + + putErrorInBottomCall(log) { + const call = this.bottomCall(); + this.putErrorInCall(log, call); + }, + + putErrorInCall(log, call) { + call.error = log.getError(); + + // Consume all available gas and clean any leftovers + if (call.gasBigInt !== undefined) { + call.gasUsedBigInt = call.gasBigInt; + } + + delete call.outputOffset; + delete call.outputLength; + }, + + topCall() { + return this.callStack[this.callStack.length - 1]; + }, + + bottomCall() { + return this.callStack[0]; + }, + + pushChildCall(childCall) { + const topCall = this.topCall(); + + if (topCall.calls === undefined) { + topCall.calls = []; + } + + topCall.calls.push(childCall); + }, + + success(log, db) { + const op = log.op.toString(); + + this.beforeOp(log, db); + + switch (op) { + case 'CREATE': + this.createOp(log); + break; + case 'SELFDESTRUCT': + this.selfDestructOp(log, db); + break; + case 'CALL': + case 'CALLCODE': + case 'DELEGATECALL': + case 'STATICCALL': + this.callOp(log, op); + break; + case 'REVERT': + this.revertOp(); + break; + } + }, + + beforeOp(log, db) { + /** + * Depths + * 0 - `ctx`. Never shows up in `log.getDepth()` + * 1 - first level of `log.getDepth()` + * + * callStack indexes + * + * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`) + * 1 - first callOp inside of `ctx` + */ + const logDepth = log.getDepth(); + const callStackDepth = this.callStack.length; + + if (logDepth < callStackDepth) { + // Pop off the last call and get the execution results + const call = this.callStack.pop(); + + call.gasUsedBigInt = call.gasBigInt.subtract(log.getGas()); + + const ret = log.stack.peek(0); + + if (!ret.equals(0)) { + if (call.type === 'create') { + call.createdContractAddressHash = toHex(toAddress(ret.toString(16))); + call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16)))); + } else { + call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen)); + } + } else if (call.error === undefined) { + call.error = 'internal failure'; + } + + delete call.outputOffset; + delete call.outputLength; + + this.pushChildCall(call); + } + }, + + createOp(log) { + const inputOffset = log.stack.peek(1).valueOf(); + const inputLength = log.stack.peek(2).valueOf(); + const inputEnd = inputOffset + inputLength; + const stackValue = log.stack.peek(0); + + const call = { + type: 'create', + from: toHex(log.contract.getAddress()), + init: toHex(log.memory.slice(inputOffset, inputEnd)), + gasBigInt: bigInt(log.getGas()), + valueBigInt: bigInt(stackValue.toString(10)) + }; + this.callStack.push(call); + }, + + selfDestructOp(log, db) { + const contractAddress = log.contract.getAddress(); + + this.pushChildCall({ + type: 'selfdestruct', + from: toHex(contractAddress), + to: toHex(toAddress(log.stack.peek(0).toString(16))), + gasBigInt: bigInt(log.getGas()), + valueBigInt: db.getBalance(contractAddress) + }); + }, + + callOp(log, op) { + const to = toAddress(log.stack.peek(1).toString(16)); + + // Skip any pre-compile invocations, those are just fancy opcodes + if (!isPrecompiled(to)) { + this.callCustomOp(log, op, to); + } + }, + + callCustomOp(log, op, to) { + const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1); + + const inputOffset = log.stack.peek(2 + stackOffset).valueOf(); + const inputLength = log.stack.peek(3 + stackOffset).valueOf(); + const inputEnd = inputOffset + inputLength; + + const call = { + type: 'call', + callType: op.toLowerCase(), + from: toHex(log.contract.getAddress()), + to: toHex(to), + gasBigInt: bigInt(log.getGas()), + input: toHex(log.memory.slice(inputOffset, inputEnd)), + outputOffset: log.stack.peek(4 + stackOffset).valueOf(), + outputLength: log.stack.peek(5 + stackOffset).valueOf() + }; + + switch (op) { + case 'CALL': + case 'CALLCODE': + call.valueBigInt = bigInt(log.stack.peek(2)); + break; + case 'DELEGATECALL': + // value inherited from scope during call sequencing + break; + case 'STATICCALL': + // by definition static calls transfer no value + call.valueBigInt = bigInt.zero; + break; + default: + throw "Unknown custom call op " + op; + } + + this.callStack.push(call); + }, + + revertOp() { + this.topCall().error = 'execution reverted'; + }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result(ctx, db) { + const result = this.ctxToResult(ctx, db); + const filtered = this.filterNotUndefined(result); + const callSequence = this.sequence(filtered, [], filtered.valueBigInt, filtered.gasUsedBigInt).callSequence; + return this.encodeCallSequence(callSequence); + }, + + ctxToResult(ctx, db) { + var result; + + switch (ctx.type) { + case 'CALL': + result = this.ctxToCall(ctx); + break; + case 'CREATE': + result = this.ctxToCreate(ctx, db); + break; + } + + return result; + }, + + ctxToCall(ctx) { + const result = { + type: 'call', + callType: 'call', + from: toHex(ctx.from), + to: toHex(ctx.to), + valueBigInt: bigInt(ctx.value.toString(10)), + gasBigInt: bigInt(ctx.gas), + gasUsedBigInt: bigInt(ctx.gasUsed), + input: toHex(ctx.input) + }; + + this.putBottomChildCalls(result); + this.putErrorOrOutput(result, ctx); + + return result; + }, + + putErrorOrOutput(result, ctx) { + const error = this.error(ctx); + + if (error !== undefined) { + result.error = error; + } else { + result.output = toHex(ctx.output); + } + }, + + ctxToCreate(ctx, db) { + const result = { + type: 'create', + from: toHex(ctx.from), + init: toHex(ctx.input), + valueBigInt: bigInt(ctx.value.toString(10)), + gasBigInt: bigInt(ctx.gas), + gasUsedBigInt: bigInt(ctx.gasUsed) + }; + + this.putBottomChildCalls(result); + this.putErrorOrCreatedContract(result, ctx, db); + + return result; + }, + + putBottomChildCalls(result) { + const bottomCall = this.bottomCall(); + const bottomChildCalls = bottomCall.calls; + + if (bottomChildCalls !== undefined) { + result.calls = bottomChildCalls; + } + }, + + putErrorOrCreatedContract(result, ctx, db) { + const error = this.error(ctx); + + if (error !== undefined) { + result.error = error + } else { + result.createdContractAddressHash = toHex(ctx.to); + result.createdContractCode = toHex(db.getCode(ctx.to)); + } + }, + + error(ctx) { + var error; + + const bottomCall = this.bottomCall(); + const bottomCallError = bottomCall.error; + + if (bottomCallError !== undefined) { + error = bottomCallError; + } else { + const ctxError = ctx.error; + + if (ctxError !== undefined) { + error = ctxError; + } + } + + return error; + }, + + filterNotUndefined(call) { + for (var key in call) { + if (call[key] === undefined) { + delete call[key]; + } + } + + if (call.calls !== undefined) { + for (var i = 0; i < call.calls.length; i++) { + call.calls[i] = this.filterNotUndefined(call.calls[i]); + } + } + + return call; + }, + + // sequence converts the finalized calls from a call tree to a call sequence + sequence(call, callSequence, availableValueBigInt, availableGasBigInt) { + const subcalls = call.calls; + delete call.calls; + + if (call.type === 'call' && call.callType === 'delegatecall') { + call.valueBigInt = availableValueBigInt; + } else if (call.type === 'selfdestruct') { + call.gasUsedBigInt = availableGasBigInt + } + + var newCallSequence = callSequence.concat([call]); + + if (subcalls !== undefined) { + var nestedAvailableValueBigInt = availableValueBigInt; + var nestedAvailableGasBigInt = availableGasBigInt; + + for (var i = 0; i < subcalls.length; i++) { + const nestedSequenced = this.sequence(subcalls[i], newCallSequence, nestedAvailableValueBigInt, availableGasBigInt); + newCallSequence = nestedSequenced.callSequence; + nestedAvailableValueBigInt = nestedSequenced.availableValueBigInt; + nestedAvailableGasBigInt = nestedSequenced.availableGasBigInt; + } + } + + const newAvailableValueBigInt = availableValueBigInt.subtract(call.valueBigInt); + + const newAvailableGasUsedBigInt = availableGasBigInt.subtract(call.gasUsedBigInt); + + return {callSequence: newCallSequence, availableValueBigInt: newAvailableValueBigInt, availableGasBigInt: newAvailableGasUsedBigInt}; + }, + + encodeCallSequence(calls) { + for (var i = 0; i < calls.length; i++) { + this.encodeCall(calls[i]); + } + + return calls; + }, + + encodeCall(call) { + this.putValue(call); + this.putGas(call); + this.putGasUsed(call); + + return call; + }, + + putValue(call) { + const valueBigInt = call.valueBigInt; + delete call.valueBigInt; + + call.value = '0x' + valueBigInt.toString(16); + }, + + putGas(call) { + const gasBigInt = call.gasBigInt; + delete call.gasBigInt; + + if (gasBigInt === undefined) { + throw "gasBigInt undefined in " + JSON.stringify(call); + } + + call.gas = '0x' + gasBigInt.toString(16); + }, + + putGasUsed(call) { + const gasUsedBigInt = call.gasUsedBigInt; + delete call.gasUsedBigInt; + + if (gasUsedBigInt === undefined) { + throw "gasUsedBigInt undefined in " + JSON.stringify(call); + } + + call.gasUsed = '0x' + gasUsedBigInt.toString(16); + } +} + diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs new file mode 100644 index 0000000000..69815f203f --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.Geth.CallTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Geth.Call +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/calls_tests.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/calls_tests.exs new file mode 100644 index 0000000000..6d64dabd62 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/calls_tests.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.Geth.CallsTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Geth.Calls +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs index 92ba931de1..4ae4c2ed62 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs @@ -1,18 +1,79 @@ defmodule EthereumJSONRPC.GethTest do use EthereumJSONRPC.Case, async: false + import Mox + alias EthereumJSONRPC.Geth @moduletag :no_parity describe "fetch_internal_transactions/2" do - test "is not supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do - Geth.fetch_internal_transactions( - [ - "0x2ec382949ba0b22443aa4cb38267b1fb5e68e188109ac11f7a82f67571a0adf3" - ], - json_rpc_named_arguments - ) + # Infura Mainnet does not support debug_traceTransaction, so this cannot be tested expect in Mox + setup do + EthereumJSONRPC.Case.Geth.Mox.setup() + end + + setup :verify_on_exit! + + # Data taken from Rinkeby + test "is supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_287_375 + transaction_index = 13 + transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + tracer = File.read!("priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js") + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^transaction_hash, %{tracer: ^tracer}]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "traceAddress" => [], + "type" => "call", + "callType" => "call", + "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + "gas" => "0x8600", + "gasUsed" => "0x7d37", + "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + "output" => "0x", + "value" => "0x174876e800" + } + ] + } + ]} + end) + + assert {:ok, + [ + %{ + block_number: ^block_number, + transaction_index: ^transaction_index, + transaction_hash: ^transaction_hash, + index: 0, + trace_address: [], + type: "call", + call_type: "call", + from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + gas: 34304, + gas_used: 32055, + input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + output: "0x", + value: 100_000_000_000 + } + ]} = + Geth.fetch_internal_transactions( + [ + %{ + block_number: block_number, + transaction_index: transaction_index, + hash_data: transaction_hash + } + ], + json_rpc_named_arguments + ) end end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 41a705a869..bb35eb8036 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -140,6 +140,7 @@ defmodule Indexer.Block.Fetcher do {:ok, {inserted, next}} else {step, {:error, reason}} -> {:error, {step, reason}} + {:error, :timeout} = error -> error {:error, changesets} = error when is_list(changesets) -> error {:error, step, failed_value, changes_so_far} -> {:error, {step, failed_value, changes_so_far}} end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 88682c149a..214f302830 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -10,6 +10,7 @@ defmodule Indexer.Block.Realtime.Fetcher do import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] import Indexer.Block.Fetcher, only: [async_import_tokens: 1, async_import_uncles: 1, fetch_and_import_range: 2] + alias Ecto.Changeset alias EthereumJSONRPC.Subscription alias Explorer.Chain alias Indexer.{AddressExtraction, Block, TokenBalances} @@ -146,7 +147,7 @@ defmodule Indexer.Block.Realtime.Fetcher do ] end) - {:error, changesets} when is_list(changesets) -> + {:error, [%Changeset{} | _] = changesets} -> params = %{ changesets: changesets, block_number_to_fetch: block_number_to_fetch,