diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b590f8e884a..716d727d4d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- [#9072](https://github.com/blockscout/blockscout/pull/9072) - Add tracing by block logic for geth - [#6721](https://github.com/blockscout/blockscout/pull/6721) - Implement fetching internal transactions from callTracer - [#5561](https://github.com/blockscout/blockscout/pull/5561), [#6523](https://github.com/blockscout/blockscout/pull/6523) - Improve working with contracts implementations - [#6401](https://github.com/blockscout/blockscout/pull/6401) - Add Sol2Uml contract visualization diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index 599dbb32e262..8f9da7346782 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -46,12 +46,65 @@ defmodule EthereumJSONRPC.Geth do def fetch_first_trace(_transactions_params, _json_rpc_named_arguments), do: :ignore @doc """ - Internal transaction fetching for entire blocks is not currently supported for Geth. - - To signal to the caller that fetching is not supported, `:ignore` is returned. + Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Geth trace URL. """ @impl EthereumJSONRPC.Variant - def fetch_block_internal_transactions(_block_range, _json_rpc_named_arguments), do: :ignore + def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) do + id_to_params = id_to_params(block_numbers) + + with {:ok, blocks_responses} <- + id_to_params + |> debug_trace_block_by_number_requests() + |> json_rpc(json_rpc_named_arguments), + :ok <- check_errors_exist(blocks_responses, id_to_params) do + transactions_params = to_transactions_params(blocks_responses, id_to_params) + + {transactions_id_to_params, transactions_responses} = + Enum.reduce(transactions_params, {%{}, []}, fn {params, calls}, {id_to_params_acc, calls_acc} -> + {Map.put(id_to_params_acc, params[:id], params), [calls | calls_acc]} + end) + + debug_trace_transaction_responses_to_internal_transactions_params( + transactions_responses, + transactions_id_to_params, + json_rpc_named_arguments + ) + end + end + + defp check_errors_exist(blocks_responses, id_to_params) do + blocks_responses + |> EthereumJSONRPC.sanitize_responses(id_to_params) + |> Enum.reduce([], fn + %{result: _result}, acc -> acc + %{error: error}, acc -> [error | acc] + end) + |> case do + [] -> :ok + errors -> {:error, errors} + end + end + + defp to_transactions_params(blocks_responses, id_to_params) do + Enum.reduce(blocks_responses, [], fn %{id: id, result: tx_result}, blocks_acc -> + extract_transactions_params(Map.fetch!(id_to_params, id), tx_result) ++ blocks_acc + end) + end + + defp extract_transactions_params(block_number, tx_result) do + tx_result + |> Enum.reduce({[], 0}, fn %{"txHash" => tx_hash, "result" => calls_result}, {tx_acc, counter} -> + { + [ + {%{block_number: block_number, hash_data: tx_hash, transaction_index: counter, id: counter}, + %{id: counter, result: calls_result}} + | tx_acc + ], + counter + 1 + } + end) + |> elem(0) + end @doc """ Fetches the pending transactions from the Geth node. @@ -73,6 +126,10 @@ defmodule EthereumJSONRPC.Geth do end) end + defp debug_trace_block_by_number_requests(id_to_params) do + Enum.map(id_to_params, &debug_trace_block_by_number_request/1) + end + @tracer_path "priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js" @external_resource @tracer_path @tracer File.read!(@tracer_path) @@ -80,22 +137,47 @@ defmodule EthereumJSONRPC.Geth do defp debug_trace_transaction_request(%{id: id, hash_data: hash_data}) do timeout = Application.get_env(:ethereum_jsonrpc, :internal_transaction_timeout) - tracer = - case Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] do - "js" -> @tracer - "call_tracer" -> "callTracer" - end - request(%{ id: id, method: "debug_traceTransaction", params: [ hash_data, - %{tracer: tracer, disableStack: true, disableMemory: true, disableStorage: true, timeout: timeout} + %{timeout: timeout} |> Map.merge(tracer_params()) ] }) end + defp debug_trace_block_by_number_request({id, block_number}) do + request(%{ + id: id, + method: "debug_traceBlockByNumber", + params: [integer_to_quantity(block_number), tracer_params()] + }) + end + + defp tracer_params do + cond do + tracer_type() == "js" -> + %{"tracer" => @tracer} + + tracer_type() in ~w(opcode polygon_edge) -> + %{ + "enableMemory" => true, + "disableStack" => false, + "disableStorage" => true, + "enableReturnData" => false + } + + true -> + %{ + "tracer" => "callTracer", + "disableStack" => true, + "disableMemory" => true, + "disableStorage" => true + } + end + end + defp debug_trace_transaction_responses_to_internal_transactions_params( [%{result: %{"structLogs" => _}} | _] = responses, id_to_params, @@ -328,4 +410,8 @@ defmodule EthereumJSONRPC.Geth do defp finalize_internal_transactions_params({:error, acc_reasons}) do {:error, Enum.reverse(acc_reasons)} end + + defp tracer_type do + Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] + end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs index 214ed1bd6a1b..cf139b276353 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs @@ -437,8 +437,172 @@ defmodule EthereumJSONRPC.GethTest do end describe "fetch_block_internal_transactions/1" do - test "is not supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do - EthereumJSONRPC.Geth.fetch_block_internal_transactions([], json_rpc_named_arguments) + setup do + EthereumJSONRPC.Case.Geth.Mox.setup() + end + + test "is supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_287_375 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^block_quantity, %{"tracer" => "callTracer"}]}], + _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash + } + ] + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + assert {:ok, + [ + %{ + block_number: 3_287_375, + call_type: "call", + from_address_hash: "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + gas: 1_000_000, + gas_used: 46841, + index: 0, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x4200000000000000000000000000000000000015", + trace_address: [], + transaction_hash: ^transaction_hash, + transaction_index: 0, + type: "call", + value: 0 + }, + %{ + block_number: 3_287_375, + call_type: "delegatecall", + from_address_hash: "0x4200000000000000000000000000000000000015", + gas: 956_988, + gas_used: 18984, + index: 1, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + trace_address: [0], + transaction_hash: ^transaction_hash, + transaction_index: 0, + type: "call", + value: 0 + } + ]} = Geth.fetch_block_internal_transactions([block_number], json_rpc_named_arguments) + end + + test "result is the same as fetch_internal_transactions/2", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_287_375 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + + expect(EthereumJSONRPC.Mox, :json_rpc, 2, fn + [%{id: id, params: [^block_quantity, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash + } + ] + } + ]} + + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + } + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + assert Geth.fetch_block_internal_transactions([block_number], json_rpc_named_arguments) == + Geth.fetch_internal_transactions( + [%{block_number: block_number, transaction_index: 0, hash_data: transaction_hash}], + json_rpc_named_arguments + ) end end diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index ee9bcfbdce9d..5707c10fe017 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -89,24 +89,7 @@ defmodule Indexer.Fetcher.InternalTransaction do json_rpc_named_arguments |> Keyword.fetch!(:variant) - |> case do - EthereumJSONRPC.Nethermind -> - EthereumJSONRPC.fetch_block_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) - - EthereumJSONRPC.Erigon -> - EthereumJSONRPC.fetch_block_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) - - EthereumJSONRPC.Besu -> - EthereumJSONRPC.fetch_block_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) - - _jsonrpc_variant -> - try do - fetch_block_internal_transactions_by_transactions(filtered_unique_numbers, json_rpc_named_arguments) - rescue - error -> - {:error, error} - end - end + |> fetch_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) |> case do {:ok, internal_transactions_params} -> import_internal_transaction(internal_transactions_params, filtered_unique_numbers) @@ -126,6 +109,33 @@ defmodule Indexer.Fetcher.InternalTransaction do end end + defp fetch_internal_transactions(variant, block_numbers, json_rpc_named_arguments) do + if variant in block_traceable_variants() do + EthereumJSONRPC.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) + else + try do + fetch_block_internal_transactions_by_transactions(block_numbers, json_rpc_named_arguments) + rescue + error -> + {:error, error, __STACKTRACE__} + end + end + end + + @default_block_traceable_variants [ + EthereumJSONRPC.Nethermind, + EthereumJSONRPC.Erigon, + EthereumJSONRPC.Besu, + EthereumJSONRPC.RSK + ] + defp block_traceable_variants do + if Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth)[:block_traceable?] do + [EthereumJSONRPC.Geth | @default_block_traceable_variants] + else + @default_block_traceable_variants + end + end + def import_first_trace(internal_transactions_params) do imports = Chain.import(%{ diff --git a/config/runtime.exs b/config/runtime.exs index e78184ff4195..8da9c07b39d4 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -194,6 +194,7 @@ debug_trace_transaction_timeout = System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_T config :ethereum_jsonrpc, :internal_transaction_timeout, debug_trace_transaction_timeout config :ethereum_jsonrpc, EthereumJSONRPC.Geth, + block_traceable?: ConfigHelper.parse_bool_env_var("ETHEREUM_JSONRPC_GETH_TRACE_BY_BLOCK"), tracer: System.get_env("INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE", "js") config :ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction, diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 7f496a61b154..f8acffc27e94 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -10,6 +10,7 @@ LOGO_FOOTER=/images/blockscout_logo.svg # ETHEREUM_JSONRPC_WS_URL= ETHEREUM_JSONRPC_TRANSPORT=http ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES=false +# ETHEREUM_JSONRPC_GETH_TRACE_BY_BLOCK= IPC_PATH= NETWORK_PATH=/ API_PATH=/