Merge pull request #7243 from blockscout/mf-op-code-logger-tracer

Fix Elixir tracer to work with polygon edge
pull/7332/head
Victor Baranov 2 years ago committed by GitHub
commit 5be070a4ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  3. 77
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex
  4. 14
      apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js
  5. 53
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/tracer_test.exs
  6. 1
      apps/ethereum_jsonrpc/test/support/fixture/geth/trace/calltracer.json
  7. 1
      apps/ethereum_jsonrpc/test/support/fixture/geth/trace/struct_logger.json

@ -88,6 +88,7 @@
### Fixes ### Fixes
- [#7243](https://github.com/blockscout/blockscout/pull/7243) - Fix Elixir tracer to work with polygon edge
- [#7162](https://github.com/blockscout/blockscout/pull/7162) - Hide indexing alert, if internal transactions indexer disabled - [#7162](https://github.com/blockscout/blockscout/pull/7162) - Hide indexing alert, if internal transactions indexer disabled
- [#7096](https://github.com/blockscout/blockscout/pull/7096) - Hide indexing alert, if indexer disabled - [#7096](https://github.com/blockscout/blockscout/pull/7096) - Hide indexing alert, if indexer disabled
- [#7102](https://github.com/blockscout/blockscout/pull/7102) - Set infinity timeout timestamp_to_block_number query - [#7102](https://github.com/blockscout/blockscout/pull/7102) - Set infinity timeout timestamp_to_block_number query

@ -241,7 +241,7 @@ defmodule EthereumJSONRPC.Geth do
{:error, annotated_error} {:error, annotated_error}
end end
defp prepare_calls(calls) do def prepare_calls(calls) do
case Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] do case Application.get_env(:ethereum_jsonrpc, __MODULE__)[:tracer] do
"call_tracer" -> {calls, 0} |> parse_call_tracer_calls([], [], false) |> Enum.reverse() "call_tracer" -> {calls, 0} |> parse_call_tracer_calls([], [], false) |> Enum.reverse()
"js" -> calls "js" -> calls

@ -6,7 +6,8 @@ defmodule EthereumJSONRPC.Geth.Tracer do
import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1]
def replay(%{"structLogs" => logs} = result, receipt, tx) when is_list(logs) do def replay(%{"structLogs" => logs, "gas" => top_call_gas, "returnValue" => return_value} = result, receipt, tx)
when is_list(logs) do
%{"contractAddress" => contract_address} = receipt %{"contractAddress" => contract_address} = receipt
%{"from" => from, "to" => to, "value" => value, "input" => input} = tx %{"from" => from, "to" => to, "value" => value, "input" => input} = tx
@ -25,7 +26,7 @@ defmodule EthereumJSONRPC.Geth.Tracer do
"type" => "create", "type" => "create",
"init" => input, "init" => input,
"createdContractAddressHash" => contract_address, "createdContractAddressHash" => contract_address,
"createdContractCode" => "0x" "createdContractCode" => "0x" <> return_value
} }
end end
|> Map.merge(%{ |> Map.merge(%{
@ -40,12 +41,13 @@ defmodule EthereumJSONRPC.Geth.Tracer do
depth: 1, depth: 1,
stack: [top], stack: [top],
trace_address: [0], trace_address: [0],
calls: [[]] calls: [[]],
descended: false
} }
logs logs
|> Enum.reduce(ctx, &step/2) |> Enum.reduce(ctx, &step/2)
|> finalize() |> finalize(top_call_gas)
end end
defp step(%{"error" => _}, %{stack: [%{"error" => _} | _]} = ctx), do: ctx defp step(%{"error" => _}, %{stack: [%{"error" => _} | _]} = ctx), do: ctx
@ -78,6 +80,18 @@ defmodule EthereumJSONRPC.Geth.Tracer do
} }
end 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( defp step(
%{"depth" => log_depth} = log, %{"depth" => log_depth} = log,
%{ %{
@ -122,13 +136,28 @@ defmodule EthereumJSONRPC.Geth.Tracer do
defp op(%{"op" => "REVERT"}, ctx), do: revert_op(ctx) defp op(%{"op" => "REVERT"}, ctx), do: revert_op(ctx)
defp op(_, ctx), do: ctx defp op(_, ctx), do: ctx
defp process_return(%{"stack" => log_stack}, %{"type" => "create"} = call) do defp process_return(
%{"stack" => log_stack, "memory" => log_memory},
%{"type" => create, "outputOffset" => out_off, "outputLength" => out_len} = call
)
when create in ~w(create create2) do
[ret | _] = Enum.reverse(log_stack) [ret | _] = Enum.reverse(log_stack)
case quantity_to_integer(ret) do ret
0 -> Map.put(call, "error", call["error"] || "internal failure") |> quantity_to_integer()
_ -> %{call | "createdContractAddressHash" => "0x" <> String.slice(ret, 24, 40)} |> 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 | "createdContractCode" => "0x" <> output, "createdContractAddressHash" => ret}
end end
|> Map.drop(["outputOffset", "outputLength"])
end end
defp process_return( defp process_return(
@ -159,12 +188,12 @@ defmodule EthereumJSONRPC.Geth.Tracer do
%{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx, %{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx,
type \\ "create" type \\ "create"
) do ) do
[value, input_offset, input_length | _] = Enum.reverse(log_stack) [value, input_offset, input_length, output_offset, output_length | _] = Enum.reverse(log_stack)
init = init =
log_memory log_memory
|> IO.iodata_to_binary() |> IO.iodata_to_binary()
|> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) |> String.slice(quantity_to_integer(input_offset) * 2, quantity_to_integer(input_length) * 2)
call = %{ call = %{
"type" => type, "type" => type,
@ -173,7 +202,9 @@ defmodule EthereumJSONRPC.Geth.Tracer do
"init" => "0x" <> init, "init" => "0x" <> init,
"gas" => 0, "gas" => 0,
"gasUsed" => 0, "gasUsed" => 0,
"value" => "0x" <> value, "value" => value,
"outputOffset" => quantity_to_integer(output_offset) * 2,
"outputLength" => quantity_to_integer(output_length) * 2,
"createdContractAddressHash" => nil, "createdContractAddressHash" => nil,
"createdContractCode" => "0x" "createdContractCode" => "0x"
} }
@ -183,7 +214,8 @@ defmodule EthereumJSONRPC.Geth.Tracer do
| depth: stack_depth + 1, | depth: stack_depth + 1,
stack: [call | stack], stack: [call | stack],
trace_address: [0 | trace_address], trace_address: [0 | trace_address],
calls: [[] | calls] calls: [[] | calls],
descended: true
} }
end end
@ -199,7 +231,7 @@ defmodule EthereumJSONRPC.Geth.Tracer do
call = %{ call = %{
"type" => "selfdestruct", "type" => "selfdestruct",
"from" => nil, "from" => nil,
"to" => "0x" <> String.slice(to, 24, 40), "to" => to,
"traceAddress" => Enum.reverse([trace_index | trace_address]), "traceAddress" => Enum.reverse([trace_index | trace_address]),
"gas" => log_gas, "gas" => log_gas,
"gasUsed" => log_gas_cost, "gasUsed" => log_gas_cost,
@ -232,24 +264,24 @@ defmodule EthereumJSONRPC.Geth.Tracer do
_ -> _ ->
[value | rest] = log_stack [value | rest] = log_stack
{"0x" <> value, rest} {value, rest}
end end
input = input =
log_memory log_memory
|> IO.iodata_to_binary() |> IO.iodata_to_binary()
|> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) |> String.slice(quantity_to_integer(input_offset) * 2, quantity_to_integer(input_length) * 2)
call = %{ call = %{
"type" => "call", "type" => "call",
"callType" => call_type, "callType" => call_type,
"from" => nil, "from" => nil,
"to" => "0x" <> String.slice(to, 24, 40), "to" => to,
"traceAddress" => Enum.reverse(trace_address), "traceAddress" => Enum.reverse(trace_address),
"input" => "0x" <> input, "input" => "0x" <> input,
"output" => "0x", "output" => "0x",
"outputOffset" => quantity_to_integer("0x" <> output_offset) * 2, "outputOffset" => quantity_to_integer(output_offset) * 2,
"outputLength" => quantity_to_integer("0x" <> output_length) * 2, "outputLength" => quantity_to_integer(output_length) * 2,
"gas" => 0, "gas" => 0,
"gasUsed" => 0, "gasUsed" => 0,
"value" => value "value" => value
@ -260,7 +292,8 @@ defmodule EthereumJSONRPC.Geth.Tracer do
| depth: stack_depth + 1, | depth: stack_depth + 1,
stack: [call, parent | stack], stack: [call, parent | stack],
trace_address: [0 | trace_address], trace_address: [0 | trace_address],
calls: [[] | calls] calls: [[] | calls],
descended: true
} }
end end
@ -268,17 +301,17 @@ defmodule EthereumJSONRPC.Geth.Tracer do
%{ctx | stack: [Map.put(last, "error", "execution reverted") | stack]} %{ctx | stack: [Map.put(last, "error", "execution reverted") | stack]}
end end
defp finalize(%{stack: [top], calls: [calls]}) do defp finalize(%{stack: [top], calls: [calls]}, top_call_gas) do
calls = calls =
Enum.map(calls, fn Enum.map(calls, fn
subcalls when is_list(subcalls) -> subcalls subcalls when is_list(subcalls) -> subcalls
subcall when is_map(subcall) -> %{subcall | "from" => top["createdContractAddressHash"] || top["to"]} subcall when is_map(subcall) -> %{subcall | "from" => top["createdContractAddressHash"] || top["to"]}
end) end)
[top | Enum.reverse(calls)] [%{top | "gasUsed" => top_call_gas} | Enum.reverse(calls)]
|> List.flatten() |> List.flatten()
|> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call -> |> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call ->
%{call | "gas" => integer_to_quantity(gas), "gasUsed" => integer_to_quantity(gas_used)} %{call | "gas" => integer_to_quantity(gas), "gasUsed" => gas_used |> max(0) |> integer_to_quantity()}
end) end)
end end
end end

@ -3,6 +3,10 @@
// The call stack of the EVM execution. // The call stack of the EVM execution.
callStack: [{}], callStack: [{}],
// Descended tracks whether we've just descended from an outer transaction into
// an inner call.
descended: false,
// step is invoked for every opcode that the VM executes. // step is invoked for every opcode that the VM executes.
step(log, db) { step(log, db) {
// Capture any errors immediately // Capture any errors immediately
@ -85,6 +89,11 @@
success(log, db) { success(log, db) {
const op = log.op.toString(); const op = log.op.toString();
if (this.descended) {
this.topCall().gasBigInt = log.getGas();
this.descended = false;
}
this.beforeOp(log, db); this.beforeOp(log, db);
switch (op) { switch (op) {
@ -163,6 +172,7 @@
valueBigInt: bigInt(stackValue.toString(10)) valueBigInt: bigInt(stackValue.toString(10))
}; };
this.callStack.push(call); this.callStack.push(call);
this.descended = true;
}, },
create2Op(log) { create2Op(log) {
@ -178,6 +188,7 @@
valueBigInt: bigInt(stackValue.toString(10)) valueBigInt: bigInt(stackValue.toString(10))
}; };
this.callStack.push(call); this.callStack.push(call);
this.descended = true;
}, },
selfDestructOp(log, db) { selfDestructOp(log, db) {
@ -208,7 +219,7 @@
const inputOffset = log.stack.peek(2 + stackOffset).valueOf(); const inputOffset = log.stack.peek(2 + stackOffset).valueOf();
const inputLength = log.stack.peek(3 + stackOffset).valueOf(); const inputLength = log.stack.peek(3 + stackOffset).valueOf();
const inputEnd = Math.min(inputOffset + inputLength, log.memory.length()); const inputEnd = Math.min(inputOffset + inputLength, log.memory.length());
const input = (inputLength == 0 ? '0x0' : toHex(log.memory.slice(inputOffset, inputEnd))); const input = (inputLength == 0 ? '0x' : toHex(log.memory.slice(inputOffset, inputEnd)));
const call = { const call = {
type: 'call', type: 'call',
@ -237,6 +248,7 @@
} }
this.callStack.push(call); this.callStack.push(call);
this.descended = true;
}, },
revertOp() { revertOp() {

@ -0,0 +1,53 @@
defmodule EthereumJSONRPC.Geth.TracerTest do
use EthereumJSONRPC.Case, async: false
alias EthereumJSONRPC.Geth
alias EthereumJSONRPC.Geth.{Calls, Tracer}
describe "replay/3" do
test "same as callTracer" do
struct_logs = File.read!(File.cwd!() <> "/test/support/fixture/geth/trace/struct_logger.json") |> Jason.decode!()
tx = "0xa0a5c30c5c5ec22b3346e0ae5ce09f8f41faf54f68a2a113eb15e363af90e9ab"
sl_calls =
Tracer.replay(struct_logs["result"], struct_logs["receipt"], struct_logs["tx"])
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => 0,
"index" => index,
"transactionIndex" => 0,
"transactionHash" => tx
})
end)
|> Calls.to_internal_transactions_params()
init_tracer = Application.get_env(:ethereum_jsonrpc, Geth, :tracer)
Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer")
calls =
File.read!(File.cwd!() <> "/test/support/fixture/geth/trace/calltracer.json")
|> Jason.decode!()
|> Map.get("result")
ct_calls =
calls
|> Geth.prepare_calls()
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => 0,
"index" => index,
"transactionIndex" => 0,
"transactionHash" => tx
})
end)
|> Calls.to_internal_transactions_params()
assert sl_calls == ct_calls
Application.put_env(:ethereum_jsonrpc, Geth, tracer: init_tracer)
end
end
end

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save