diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d5ab7714..c6234368b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current ### Features +- [#7513](https://github.com/blockscout/blockscout/pull/7513) - Add Polygon Edge support ### Fixes diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index e280ae5f9c..a12434ea95 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -8,7 +8,7 @@ defmodule EthereumJSONRPC.Geth do import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] alias EthereumJSONRPC.{FetchedBalance, FetchedCode, PendingTransaction, Utility.CommonHelper} - alias EthereumJSONRPC.Geth.{Calls, Tracer} + alias EthereumJSONRPC.Geth.{Calls, PolygonTracer, Tracer} @behaviour EthereumJSONRPC.Variant @@ -94,14 +94,25 @@ defmodule EthereumJSONRPC.Geth do tracer = case Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] do - "js" -> @tracer - "call_tracer" -> "callTracer" + "js" -> + %{"tracer" => @tracer} + + "call_tracer" -> + %{"tracer" => "callTracer"} + + _ -> + %{ + "enableMemory" => true, + "disableStack" => false, + "disableStorage" => true, + "enableReturnData" => false + } end request(%{ id: id, method: "debug_traceTransaction", - params: [hash_data, %{tracer: tracer, timeout: debug_trace_transaction_timeout}] + params: [hash_data, %{timeout: debug_trace_transaction_timeout} |> Map.merge(tracer)] }) end @@ -126,10 +137,15 @@ defmodule EthereumJSONRPC.Geth do receipts_map = Enum.into(receipts, %{}, fn %{id: id, result: receipt} -> {id, receipt} end) txs_map = Enum.into(txs, %{}, fn %{id: id, result: tx} -> {id, tx} end) + tracer = + if Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] == "polygon_edge", + do: PolygonTracer, + else: Tracer + responses |> Enum.map(fn %{id: id, result: %{"structLogs" => _} = result} -> debug_trace_transaction_response_to_internal_transactions_params( - %{id: id, result: Tracer.replay(result, Map.fetch!(receipts_map, id), Map.fetch!(txs_map, id))}, + %{id: id, result: tracer.replay(result, Map.fetch!(receipts_map, id), Map.fetch!(txs_map, id))}, id_to_params ) end) @@ -158,7 +174,7 @@ defmodule EthereumJSONRPC.Geth do {id, %{created_contract_address_hash: address, block_number: block_number}} -> FetchedCode.request(%{id: id, block_quantity: integer_to_quantity(block_number), address: address}) - {id, %{type: "selfdestruct", from: hash_data, block_number: block_number}} -> + {id, %{type: "selfdestruct", from_address_hash: hash_data, block_number: block_number}} -> FetchedBalance.request(%{id: id, block_quantity: integer_to_quantity(block_number), hash_data: hash_data}) _ -> @@ -244,7 +260,7 @@ defmodule EthereumJSONRPC.Geth do def prepare_calls(calls) do case Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] do "call_tracer" -> {calls, 0} |> parse_call_tracer_calls([], [], false) |> Enum.reverse() - "js" -> calls + _ -> calls end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/polygon_tracer.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/polygon_tracer.ex new file mode 100644 index 0000000000..ce08951bc7 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/polygon_tracer.ex @@ -0,0 +1,355 @@ +defmodule EthereumJSONRPC.Geth.PolygonTracer do + @moduledoc """ + Elixir implementation of a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`) + for Polygon edge nodes that don't support specifying tracer in [debug_traceTransaction](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracetransaction) calls. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] + + @burn_address "0x0000000000000000000000000000000000000000" + + def replay( + %{"structLogs" => logs, "gas" => top_call_gas, "returnValue" => return_value} = result, + %{"contractAddress" => contract_address}, + %{"from" => from, "to" => to, "value" => value, "input" => input} + ) + when is_list(logs) do + top = + to + |> if do + %{ + "type" => "call", + "callType" => "call", + "to" => to, + "input" => input, + "output" => Map.get(result, "return", "0x" <> Map.get(result, "returnValue", "")) + } + else + %{ + "type" => "create", + "init" => input, + "createdContractAddressHash" => contract_address, + "createdContractCode" => "0x" <> return_value + } + end + |> Map.merge(%{ + "from" => from, + "traceAddress" => [], + "value" => value, + "gas" => 0, + "gasUsed" => 0 + }) + + ctx = %{ + depth: 1, + stack: [top], + trace_address: [0], + calls: [[]], + descended: false + } + + logs + |> Enum.reduce(ctx, &step/2) + |> finalize(top_call_gas) + end + + defp step(%{"error" => _}, %{stack: [%{"error" => _} | _]} = ctx), do: ctx + + defp step( + %{"error" => _} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) do + call = process_return(log, Map.put(call, "error", "error")) + + subsubcalls = + subsubcalls + |> Stream.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + |> Enum.reverse() + + %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + } + end + + defp step( + %{"gas" => log_gas} = log, + %{ + stack: [%{"gas" => call_gas} = call | stack], + descended: true + } = ctx + ) do + gas = max(call_gas, log_gas) + call = %{call | "gas" => gas} + step(log, %{ctx | stack: [call | stack], descended: false}) + end + + defp step( + %{"depth" => log_depth} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) + when log_depth == stack_depth - 1 do + call = process_return(log, call) + + subsubcalls = + subsubcalls + |> Stream.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + |> Enum.reverse() + + step(log, %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + }) + end + + defp step(%{"gas" => log_gas, "gasCost" => log_gas_cost} = log, %{stack: [%{"gas" => call_gas} = call | stack]} = ctx) do + gas = max(call_gas, log_gas) + op(log, %{ctx | stack: [%{call | "gas" => gas, "gasUsed" => gas - log_gas - log_gas_cost} | stack]}) + end + + defp op(%{"op" => "CREATE"} = log, ctx), do: create_op(log, ctx) + defp op(%{"op" => "CREATE2"} = log, ctx), do: create_op(log, ctx, "create2") + defp op(%{"op" => "SELFDESTRUCT"} = log, ctx), do: self_destruct_op(log, ctx) + defp op(%{"op" => "CALL"} = log, ctx), do: call_op(log, "call", ctx) + defp op(%{"op" => "CALLCODE"} = log, ctx), do: call_op(log, "callcode", ctx) + defp op(%{"op" => "DELEGATECALL"} = log, ctx), do: call_op(log, "delegatecall", ctx) + defp op(%{"op" => "STATICCALL"} = log, ctx), do: call_op(log, "staticcall", ctx) + defp op(%{"op" => "REVERT"}, ctx), do: revert_op(ctx) + defp op(_, ctx), do: ctx + + defp process_return( + %{"stack" => log_stack}, + %{"type" => create} = call + ) + when create in ~w(create create2) do + [ret | _] = Enum.reverse(log_stack) + + ret + |> quantity_to_integer() + |> case do + 0 -> + Map.put(call, "error", call["error"] || "internal failure") + + _ -> + %{call | "createdContractAddressHash" => ret} + end + end + + defp process_return( + %{"stack" => log_stack, "memory" => log_memory}, + %{"outputOffset" => out_off, "outputLength" => out_len} = call + ) do + [ret | _] = Enum.reverse(log_stack) + + ret + |> quantity_to_integer() + |> case do + 0 -> + Map.put(call, "error", call["error"] || "internal failure") + + _ -> + output = + log_memory + |> IO.iodata_to_binary() + |> String.slice(out_off, out_len) + + %{call | "output" => "0x" <> output} + end + |> Map.drop(["outputOffset", "outputLength"]) + end + + defp process_return(_log, call) do + call + end + + defp create_op( + %{"stack" => log_stack, "memory" => log_memory}, + %{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx, + type \\ "create" + ) do + [value, input_length | _] = Enum.reverse(log_stack) + + init = + log_memory + |> IO.iodata_to_binary() + |> String.slice(0, quantity_to_integer(input_length) * 2) + + call = %{ + "type" => type, + "from" => nil, + "traceAddress" => Enum.reverse(trace_address), + "init" => "0x" <> init, + "gas" => 0, + "gasUsed" => 0, + "value" => value, + "createdContractAddressHash" => nil, + "createdContractCode" => "0x" + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call | stack], + trace_address: [0 | trace_address], + calls: [[] | calls], + descended: true + } + end + + defp self_destruct_op( + %{"stack" => log_stack, "gas" => log_gas, "gasCost" => log_gas_cost}, + %{trace_address: [trace_index | trace_address], calls: [subcalls | calls]} = ctx + ) do + [to | _] = Enum.reverse(log_stack) + + if quantity_to_integer(to) in 1..8 do + ctx + else + call = %{ + "type" => "selfdestruct", + "from" => nil, + "to" => to, + "traceAddress" => Enum.reverse([trace_index | trace_address]), + "gas" => log_gas, + "gasUsed" => log_gas_cost, + "value" => "0x0" + } + + %{ctx | trace_address: [trace_index + 1 | trace_address], calls: [[call | subcalls] | calls]} + end + end + + defp call_op( + %{"stack" => call_stack}, + call_type, + %{ + depth: stack_depth, + stack: stack, + trace_address: trace_address, + calls: calls + } = ctx + ) + when length(call_stack) < 3 do + call = %{ + "type" => "call", + "callType" => call_type, + "from" => nil, + "to" => @burn_address, + "traceAddress" => Enum.reverse(trace_address), + "input" => "0x", + "output" => "0x", + "outputOffset" => 0, + "outputLength" => 0, + "gas" => 0, + "gasUsed" => 0, + "value" => "0x0" + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call | stack], + trace_address: [0 | trace_address], + calls: [[] | calls], + descended: true + } + end + + defp call_op( + %{"stack" => log_stack, "memory" => log_memory}, + call_type, + %{ + depth: stack_depth, + stack: [%{"value" => parent_value} = parent | stack], + trace_address: trace_address, + calls: calls + } = ctx + ) do + log_stack = Enum.reverse(log_stack) + + {value, [input_length, output_length | _]} = + case call_type do + "delegatecall" -> + {parent_value, log_stack} + + "staticcall" -> + {"0x0", log_stack} + + _ -> + [value | rest] = log_stack + {value, rest} + end + + input = + log_memory + |> IO.iodata_to_binary() + |> String.slice(0, quantity_to_integer(input_length) * 2) + + call = %{ + "type" => "call", + "callType" => call_type, + "from" => nil, + "to" => @burn_address, + "traceAddress" => Enum.reverse(trace_address), + "input" => "0x" <> input, + "output" => "0x", + "outputOffset" => quantity_to_integer(input_length) * 2, + "outputLength" => quantity_to_integer(output_length) * 2, + "gas" => 0, + "gasUsed" => 0, + "value" => value + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call, parent | stack], + trace_address: [0 | trace_address], + calls: [[] | calls], + descended: true + } + end + + defp revert_op(%{stack: [last | stack]} = ctx) do + %{ctx | stack: [Map.put(last, "error", "execution reverted") | stack]} + end + + defp finalize(%{stack: [top], calls: [calls]}, top_call_gas) do + calls = + Enum.map(calls, fn + subcalls when is_list(subcalls) -> + subcalls + + subcall when is_map(subcall) -> + %{subcall | "from" => top["createdContractAddressHash"] || top["to"]} + end) + + [%{top | "gasUsed" => top_call_gas} | Enum.reverse(calls)] + |> List.flatten() + |> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call -> + %{call | "gas" => integer_to_quantity(gas), "gasUsed" => gas_used |> max(0) |> integer_to_quantity()} + end) + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex index 8ccf42730c..92aa18fc3d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex @@ -311,7 +311,7 @@ defmodule EthereumJSONRPC.Geth.Tracer do [%{top | "gasUsed" => top_call_gas} | Enum.reverse(calls)] |> List.flatten() |> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call -> - %{call | "gas" => integer_to_quantity(gas), "gasUsed" => gas_used |> max(0) |> integer_to_quantity()} + %{call | "gas" => integer_to_quantity(gas), "gasUsed" => integer_to_quantity(gas_used)} end) end end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 27d5c079e8..7f5758fbbe 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -558,6 +558,7 @@ defmodule Indexer.Block.Fetcher do hash: hash } = address_params ) do - {{hash, fetched_coin_balance_block_number}, Map.delete(address_params, :fetched_coin_balance_block_number)} + {{String.downcase(hash), fetched_coin_balance_block_number}, + Map.delete(address_params, :fetched_coin_balance_block_number)} end end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 3a1571a6e5..a179718883 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -485,7 +485,7 @@ defmodule Indexer.Block.Realtime.Fetcher do block_number _ -> - Map.fetch!(address_hash_to_block_number, address_hash) + Map.fetch!(address_hash_to_block_number, String.downcase(address_hash)) end %{hash_data: address_hash, block_quantity: integer_to_quantity(block_number)} diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index 1f249da9f1..fcb7843269 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -220,7 +220,7 @@ defmodule Indexer.Fetcher.InternalTransaction do address_hash_to_block_number = Enum.into(addresses_params, %{}, fn %{fetched_coin_balance_block_number: block_number, hash: hash} -> - {hash, block_number} + {String.downcase(hash), block_number} end) empty_block_numbers =