Currently in Geth variant raw traces are converted into internal_transactions using a custom JS tracer in [1]. cpp-ethereum nodes don't support custom JS tracers and return raw traces regardless of provided parameters. This PR adds an Elixir implementation of the tracer, which is utilized in case raw traces are detected in the response from the node. It was cross-checked for correctness with Geth builtin `callTracer`[2]. In the process some bugs were discovered in the our JS tracer, which will be fixed with a separate PR. Though, unlike JS tracer, we don't have access to EVM internal state during tracing, so additional requests to node are required: transactions and receipts are fetched before tracing, created contract codes and pre-selfdestruct balances are fetched after. [1] priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js [2] https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/internal/tracers/call_tracer.jspull/1857/head
parent
584ad9952f
commit
1aeea2b9ca
@ -0,0 +1,296 @@ |
||||
defmodule EthereumJSONRPC.Geth.Tracer do |
||||
@moduledoc """ |
||||
Elixir implementation of a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`) |
||||
for variants that don't support specifying tracer in [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction) calls. |
||||
""" |
||||
|
||||
import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] |
||||
|
||||
def replay(%{"structLogs" => logs} = result, receipt, tx) when is_list(logs) do |
||||
%{"from" => from, "to" => to, "contractAddress" => contract_address, "gasUsed" => gas_used} = receipt |
||||
%{"value" => value, "input" => input, "gas" => gas} = tx |
||||
|
||||
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" |
||||
} |
||||
end |
||||
|> Map.merge(%{ |
||||
"from" => from, |
||||
"traceAddress" => [], |
||||
"value" => value, |
||||
"gas" => 0, |
||||
"gasUsed" => quantity_to_integer(gas_used) - quantity_to_integer(gas) |
||||
}) |
||||
|
||||
ctx = %{ |
||||
depth: 1, |
||||
stack: [top], |
||||
trace_address: [0], |
||||
calls: [[]] |
||||
} |
||||
|
||||
logs |
||||
|> Enum.reduce(ctx, &step/2) |
||||
|> finalize() |
||||
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 |
||||
|> Enum.reverse() |
||||
|> Enum.map(fn |
||||
subcalls when is_list(subcalls) -> subcalls |
||||
subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} |
||||
end) |
||||
|
||||
%{ |
||||
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: [%{"type" => "create", "gas" => 0, "gasUsed" => 0} = call | stack]} = ctx |
||||
) do |
||||
step(log, %{ctx | stack: [%{call | "gas" => log_gas, "gasUsed" => log_gas} | stack]}) |
||||
end |
||||
|
||||
defp step(%{"gas" => log_gas} = log, %{stack: [%{"type" => "create", "gas" => 0} = call | stack]} = ctx) do |
||||
step(log, %{ctx | stack: [%{call | "gas" => log_gas} | stack]}) |
||||
end |
||||
|
||||
defp step(%{"gas" => log_gas} = log, %{stack: [%{"gas" => 0, "gasUsed" => gas_used} = call | stack]} = ctx) do |
||||
step(log, %{ctx | stack: [%{call | "gas" => log_gas, "gasUsed" => gas_used + log_gas} | stack]}) |
||||
end |
||||
|
||||
defp step( |
||||
%{"depth" => log_depth, "gas" => log_gas} = 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 | "gasUsed" => call["gasUsed"] - log_gas}) |
||||
|
||||
subsubcalls = |
||||
subsubcalls |
||||
|> Enum.reverse() |
||||
|> Enum.map(fn |
||||
subcalls when is_list(subcalls) -> subcalls |
||||
subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} |
||||
end) |
||||
|
||||
step(log, %{ |
||||
ctx |
||||
| depth: stack_depth - 1, |
||||
stack: stack, |
||||
trace_address: [trace_index + 1 | trace_address], |
||||
calls: [[subsubcalls, call | subcalls] | calls] |
||||
}) |
||||
end |
||||
|
||||
defp step(%{"op" => "CREATE"} = log, ctx), do: create_op(log, ctx) |
||||
defp step(%{"op" => "SELFDESTRUCT"} = log, ctx), do: self_destruct_op(log, ctx) |
||||
defp step(%{"op" => "CALL"} = log, ctx), do: call_op(log, "call", ctx) |
||||
defp step(%{"op" => "CALLCODE"} = log, ctx), do: call_op(log, "callcode", ctx) |
||||
defp step(%{"op" => "DELEGATECALL"} = log, ctx), do: call_op(log, "delegatecall", ctx) |
||||
defp step(%{"op" => "STATICCALL"} = log, ctx), do: call_op(log, "staticcall", ctx) |
||||
defp step(%{"op" => "REVERT"}, ctx), do: revert_op(ctx) |
||||
defp step(_, ctx), do: ctx |
||||
|
||||
defp process_return(%{"stack" => log_stack}, %{"type" => "create"} = call) do |
||||
[ret | _] = Enum.reverse(log_stack) |
||||
|
||||
case quantity_to_integer(ret) do |
||||
0 -> Map.put(call, "error", call["error"] || "internal failure") |
||||
_ -> %{call | "createdContractAddressHash" => "0x" <> String.slice(ret, 24, 40)} |
||||
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 create_op( |
||||
%{"stack" => log_stack, "memory" => log_memory, "gas" => log_gas, "gasCost" => log_gas_cost}, |
||||
%{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx |
||||
) do |
||||
[value, input_offset, input_length | _] = Enum.reverse(log_stack) |
||||
|
||||
init = |
||||
log_memory |
||||
|> IO.iodata_to_binary() |
||||
|> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) |
||||
|
||||
call = %{ |
||||
"type" => "create", |
||||
"from" => nil, |
||||
"traceAddress" => Enum.reverse(trace_address), |
||||
"init" => "0x" <> init, |
||||
"gas" => 0, |
||||
"gasUsed" => log_gas - log_gas_cost, |
||||
"value" => "0x" <> value, |
||||
"createdContractAddressHash" => nil, |
||||
"createdContractCode" => "0x" |
||||
} |
||||
|
||||
%{ |
||||
ctx |
||||
| depth: stack_depth + 1, |
||||
stack: [call | stack], |
||||
trace_address: [0 | trace_address], |
||||
calls: [[] | calls] |
||||
} |
||||
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" => "0x" <> String.slice(to, 24, 40), |
||||
"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" => log_stack, "memory" => log_memory, "gas" => log_gas, "gasCost" => log_gas_cost}, |
||||
call_type, |
||||
%{ |
||||
depth: stack_depth, |
||||
stack: [%{"value" => parent_value} = parent | stack], |
||||
trace_address: trace_address, |
||||
calls: calls |
||||
} = ctx |
||||
) do |
||||
[_, to | log_stack] = Enum.reverse(log_stack) |
||||
|
||||
{value, [input_offset, input_length, output_offset, output_length | _]} = |
||||
case call_type do |
||||
"delegatecall" -> |
||||
{parent_value, log_stack} |
||||
|
||||
"staticcall" -> |
||||
{"0x0", log_stack} |
||||
|
||||
_ -> |
||||
[value | rest] = log_stack |
||||
{"0x" <> value, rest} |
||||
end |
||||
|
||||
input = |
||||
log_memory |
||||
|> IO.iodata_to_binary() |
||||
|> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) |
||||
|
||||
call = %{ |
||||
"type" => "call", |
||||
"callType" => call_type, |
||||
"from" => nil, |
||||
"to" => "0x" <> String.slice(to, 24, 40), |
||||
"traceAddress" => Enum.reverse(trace_address), |
||||
"input" => "0x" <> input, |
||||
"output" => "0x", |
||||
"outputOffset" => quantity_to_integer("0x" <> output_offset) * 2, |
||||
"outputLength" => quantity_to_integer("0x" <> output_length) * 2, |
||||
"gas" => 0, |
||||
"gasUsed" => log_gas - log_gas_cost, |
||||
"value" => value |
||||
} |
||||
|
||||
%{ |
||||
ctx |
||||
| depth: stack_depth + 1, |
||||
stack: [call, parent | stack], |
||||
trace_address: [0 | trace_address], |
||||
calls: [[] | calls] |
||||
} |
||||
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]}) 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 | Enum.reverse(calls)] |
||||
|> List.flatten() |
||||
|> Enum.map(fn |
||||
%{"gas" => gas, "gasUsed" => gas_used} = call when gas_used < 0 -> |
||||
%{call | "gas" => integer_to_quantity(gas - gas_used), "gasUsed" => "0x0"} |
||||
|
||||
%{"gas" => gas, "gasUsed" => gas_used} = call -> |
||||
%{call | "gas" => integer_to_quantity(gas), "gasUsed" => integer_to_quantity(gas_used)} |
||||
end) |
||||
end |
||||
end |
Loading…
Reference in new issue