feat: implement fetch_first_trace for Geth (#10087)

* feat: implement fetch_first_trace for Geth

* chore: add missing doc & spec
pull/10111/head
Kirill Fedoseev 6 months ago committed by GitHub
parent 77f313a89b
commit 2151248e10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 214
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs
  2. 94
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  3. 38
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  4. 14
      apps/explorer/lib/explorer/chain.ex
  5. 91
      apps/explorer/test/explorer/chain_test.exs

@ -664,21 +664,69 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
hex_reason =
"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000164e6f20637265646974206f662074686174207479706500000000000000000000"
# fail to trace_replayTransaction
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, :econnrefused}
end
)
# fallback to eth_call
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, %{code: -32015, message: "VM execution error.", data: hex_reason}}
fn
[%{method: "debug_traceTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"gasUsed" => "0x5208",
"input" => "0x01",
"output" => hex_reason,
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"type" => "CALL",
"value" => "0x86b3"
}
}
]}
[%{method: "trace_replayTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => hex_reason
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
}
}
]}
%{method: "eth_call"}, _options ->
{:error,
%{
code: 3,
data: hex_reason,
message: "execution reverted"
}}
end
)
@ -720,91 +768,65 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x0"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => "0x"
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269",
"vmTrace" => nil
fn
[%{method: "debug_traceTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"error" => "Reverted",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"gasUsed" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"type" => "CALL",
"value" => "0x86b3"
}
}
}
]}
end
)
params = %{
"module" => "transaction",
"action" => "gettxinfo",
"txhash" => "#{transaction.hash}"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"]["revertReason"] == "0x"
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a txhash with empty revert reason from DB if eth_call doesn't return an error", %{conn: conn} do
block = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
transaction =
:transaction
|> insert(
error: "Reverted",
status: :error,
block_hash: block.hash,
block_number: block.number,
cumulative_gas_used: 884_322,
gas_used: 106_025,
index: 0,
hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269"
)
insert(:address)
# fail to trace_replayTransaction
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, :econnrefused}
end
)
]}
[%{method: "trace_replayTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => "0x"
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
}
}
]}
# fallback to eth_call
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:ok, :ok}
%{method: "eth_call"}, _options ->
{:error,
%{
code: 3,
message: "execution reverted"
}}
end
)
@ -819,7 +841,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
|> get("/api", params)
|> json_response(200)
assert response["result"]["revertReason"] == ""
assert response["result"]["revertReason"] in ["", "0x"]
assert response["status"] == "1"
assert response["message"] == "OK"
end

@ -291,35 +291,81 @@ defmodule BlockScoutWeb.TransactionViewTest do
describe "transaction_revert_reason/2" do
test "handles transactions with gas_price set to nil" do
transaction = insert(:transaction, gas_price: nil, error: "execution reverted")
transaction =
:transaction
|> insert(error: "execution reverted")
|> with_block()
|> Map.put(:gas_price, nil)
# fail to trace_replayTransaction
EthereumJSONRPC.Mox
|> expect(
hex_reason =
"0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002b556e69737761705632526f757465723a20494e53554646494349454e545f4f55545055545f414d4f554e54000000000000000000000000000000000000000000"
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, :econnrefused}
fn
[%{method: "debug_traceTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"gasUsed" => "0x5208",
"input" => "0x01",
"output" => hex_reason,
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"type" => "CALL",
"value" => "0x86b3"
}
}
]}
[%{method: "trace_replayTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => hex_reason
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
}
}
]}
%{method: "eth_call"}, _options ->
{:error,
%{
code: 3,
data: hex_reason,
message: "execution reverted"
}}
end
)
# fallback to eth_call
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn %{
id: 0,
method: "eth_call",
params: [
%{gasPrice: "0x0"},
"latest"
]
},
_options ->
{:error,
%{
data:
"0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002b556e69737761705632526f757465723a20494e53554646494349454e545f4f55545055545f414d4f554e54000000000000000000000000000000000000000000"
}}
end)
revert_reason = TransactionView.transaction_revert_reason(transaction, nil)
assert revert_reason ==

@ -50,6 +50,7 @@ defmodule EthereumJSONRPC.Geth do
parsed_timeout ->
json_rpc_named_arguments
|> Keyword.update(:transport_options, [http_options: []], &Keyword.put_new(&1, :http_options, []))
|> put_in([:transport_options, :http_options, :timeout], parsed_timeout)
|> put_in([:transport_options, :http_options, :recv_timeout], parsed_timeout)
end
@ -59,7 +60,26 @@ defmodule EthereumJSONRPC.Geth do
Fetches the first trace from the trace URL.
"""
@impl EthereumJSONRPC.Variant
def fetch_first_trace(_transactions_params, _json_rpc_named_arguments), do: :ignore
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params)
json_rpc_named_arguments_corrected_timeout = correct_timeouts(json_rpc_named_arguments)
with {:ok, responses} <-
id_to_params
|> debug_trace_transaction_requests(true)
|> json_rpc(json_rpc_named_arguments_corrected_timeout),
{:ok, [first_trace]} <-
debug_trace_transaction_responses_to_internal_transactions_params(
responses,
id_to_params,
json_rpc_named_arguments_corrected_timeout
) do
%{block_hash: block_hash} = transactions_params |> Enum.at(0)
{:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
end
end
@doc """
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Geth trace URL.
@ -142,9 +162,9 @@ defmodule EthereumJSONRPC.Geth do
PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments)
end
def debug_trace_transaction_requests(id_to_params) when is_map(id_to_params) do
def debug_trace_transaction_requests(id_to_params, only_first_trace \\ false) 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})
debug_trace_transaction_request(%{id: id, hash_data: hash_data}, only_first_trace)
end)
end
@ -156,13 +176,13 @@ defmodule EthereumJSONRPC.Geth do
@external_resource @tracer_path
@tracer File.read!(@tracer_path)
defp debug_trace_transaction_request(%{id: id, hash_data: hash_data}) do
defp debug_trace_transaction_request(%{id: id, hash_data: hash_data}, only_first_trace) do
debug_trace_timeout = Application.get_env(:ethereum_jsonrpc, __MODULE__)[:debug_trace_timeout]
request(%{
id: id,
method: "debug_traceTransaction",
params: [hash_data, %{timeout: debug_trace_timeout} |> Map.merge(tracer_params())]
params: [hash_data, %{timeout: debug_trace_timeout} |> Map.merge(tracer_params(only_first_trace))]
})
end
@ -179,7 +199,7 @@ defmodule EthereumJSONRPC.Geth do
})
end
defp tracer_params do
defp tracer_params(only_first_trace \\ false) do
cond do
tracer_type() == "js" ->
%{"tracer" => @tracer}
@ -193,7 +213,11 @@ defmodule EthereumJSONRPC.Geth do
}
true ->
%{"tracer" => "callTracer"}
if only_first_trace do
%{"tracer" => "callTracer", "tracerConfig" => %{"onlyTopCall" => true}}
else
%{"tracer" => "callTracer"}
end
end
end

@ -118,6 +118,7 @@ defmodule Explorer.Chain do
@revert_msg_prefix_4 "Reverted "
# Geth-like node
@revert_msg_prefix_5 "execution reverted: "
@revert_msg_prefix_6_empty "execution reverted"
@limit_showing_transactions 10_000
@default_page_size 50
@ -3029,14 +3030,22 @@ defmodule Explorer.Chain do
end
end
@doc """
Parses the revert reason from an error returned by JSON RPC node during eth_call.
Returns the formatted revert reason as a hex or utf8 string.
Returns `nil` if the revert reason cannot be parsed or error format is unknown.
"""
@spec parse_revert_reason_from_error(any()) :: String.t() | nil
def parse_revert_reason_from_error(%{data: data}), do: format_revert_data(data)
def parse_revert_reason_from_error(%{message: message}), do: format_revert_reason_message(message)
def parse_revert_reason_from_error(_), do: nil
defp format_revert_data(revert_data) do
case revert_data do
"revert" ->
"0x"
""
"0x" <> _ ->
revert_data
@ -3063,6 +3072,9 @@ defmodule Explorer.Chain do
@revert_msg_prefix_5 <> rest ->
rest
@revert_msg_prefix_6_empty ->
""
_ ->
nil
end

@ -4242,39 +4242,66 @@ defmodule Explorer.ChainTest do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => hex_reason
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
fn
[%{method: "debug_traceTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"gasUsed" => "0x5208",
"input" => "0x01",
"output" => hex_reason,
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"type" => "CALL",
"value" => "0x86b3"
}
}
}
]}
]}
[%{method: "trace_replayTransaction"}], _options ->
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => hex_reason
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
}
}
]}
%{method: "eth_call"}, _options ->
{:error,
%{
code: 3,
data: hex_reason,
message: "execution reverted"
}}
end
)

Loading…
Cancel
Save