Geth internal transactions

Get internal transactions using debug_traceTransaction and a custom
tracer.  The tracer was based on the callTracer that ships with Geth,
but rewritten to break it into more functions with more explicit purpose
and to patch some holes in callTracer:

* All calls have gas and gasUsed, which patches the hole in callTracer
  for handling `DELEGATECALL`.
* All calls have value, which patches the hole in callTracer for
  handling `DELEGATECALL` and `STATICALL`.

Since the tracer needed to be resubmitted in whole anyway, it was further
customized to match the format needed for internal transactions:

* Instead of returning the `op` as `type`, the `type` and `callType`
  used for internal transactions is returned.
* Instead of returning a call tree, the tree is walked in pre-order,
  displaying the parent call before its nested calls to match the flat
  order returned from parity's trace_transaction.
pull/1072/head
Luke Imhoff 6 years ago
parent 9cce7ab661
commit e4b7a4ee5f
  1. 122
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  2. 487
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex
  3. 233
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/calls.ex
  4. 418
      apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js
  5. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs
  6. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/calls_tests.exs
  7. 75
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs
  8. 1
      apps/indexer/lib/indexer/block/fetcher.ex
  9. 3
      apps/indexer/lib/indexer/block/realtime/fetcher.ex

@ -3,6 +3,10 @@ defmodule EthereumJSONRPC.Geth do
Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth).
"""
import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Geth.Calls
@behaviour EthereumJSONRPC.Variant
@doc """
@ -19,8 +23,16 @@ defmodule EthereumJSONRPC.Geth do
To signal to the caller that fetching is not supported, `:ignore` is returned.
"""
@impl EthereumJSONRPC.Variant
def fetch_internal_transactions(transaction_params, _json_rpc_named_arguments) when is_list(transaction_params),
do: :ignore
def fetch_internal_transactions(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params)
with {:ok, responses} <-
id_to_params
|> debug_trace_transaction_requests()
|> json_rpc(json_rpc_named_arguments) do
debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params)
end
end
@doc """
Pending transaction fetching is not supported currently for Geth.
@ -29,4 +41,110 @@ defmodule EthereumJSONRPC.Geth do
"""
@impl EthereumJSONRPC.Variant
def fetch_pending_transactions(_json_rpc_named_arguments), do: :ignore
defp debug_trace_transaction_requests(id_to_params) 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})
end)
end
@tracer_path "priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js"
@external_resource @tracer_path
@tracer File.read!(@tracer_path)
defp debug_trace_transaction_request(%{id: id, hash_data: hash_data}) do
request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]})
end
defp debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params))
|> reduce_internal_transactions_params()
end
defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, result: calls}, id_to_params)
when is_map(id_to_params) do
%{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} =
Map.fetch!(id_to_params, id)
internal_transaction_params =
calls
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => block_number,
"index" => index,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
end)
|> Calls.to_internal_transactions_params()
{:ok, internal_transaction_params}
end
defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
%{
block_number: block_number,
hash_data: "0x" <> transaction_hash_digits = transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
not_found_message = "transaction " <> transaction_hash_digits <> " not found"
normalized_error =
case error do
%{code: -32_000, message: ^not_found_message} ->
%{message: :not_found}
%{code: -32_000, message: "execution timeout"} ->
%{message: :timeout}
_ ->
error
end
annotated_error =
Map.put(normalized_error, :data, %{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash
})
{:error, annotated_error}
end
defp reduce_internal_transactions_params(internal_transactions_params) when is_list(internal_transactions_params) do
internal_transactions_params
|> Enum.reduce({:ok, []}, &internal_transactions_params_reducer/2)
|> finalize_internal_transactions_params()
end
defp internal_transactions_params_reducer(
{:ok, internal_transactions_params},
{:ok, acc_internal_transactions_params_list}
),
do: {:ok, [internal_transactions_params, acc_internal_transactions_params_list]}
defp internal_transactions_params_reducer({:ok, _}, {:error, _} = acc_error), do: acc_error
defp internal_transactions_params_reducer({:error, reason}, {:ok, _}), do: {:error, [reason]}
defp internal_transactions_params_reducer({:error, reason}, {:error, acc_reasons}) when is_list(acc_reasons),
do: {:error, [reason | acc_reasons]}
defp finalize_internal_transactions_params({:ok, acc_internal_transactions_params_list})
when is_list(acc_internal_transactions_params_list) do
internal_transactions_params =
acc_internal_transactions_params_list
|> Enum.reverse()
|> List.flatten()
{:ok, internal_transactions_params}
end
defp finalize_internal_transactions_params({:error, acc_reasons}) do
{:error, Enum.reverse(acc_reasons)}
end
end

@ -0,0 +1,487 @@
defmodule EthereumJSONRPC.Geth.Call do
@moduledoc """
A single call returned from [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction)
using a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`).
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@doc """
A call can call another another contract:
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3287375,
...> "transactionIndex" => 13,
...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
...> "index" => 0,
...> "type" => "call",
...> "callType" => "call",
...> "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
...> "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
...> "gas" => "0x8600",
...> "gasUsed" => "0x7d37",
...> "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
...> "output" => "0x",
...> "value" => "0x174876e800"
...> }
...> )
%{
block_number: 3287375,
transaction_index: 13,
transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
index: 0,
type: "call",
call_type: "call",
from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
gas: 34304,
gas_used: 32055,
input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
output: "0x",
trace_address: [],
value: 100000000000
}
A call can run out of gas:
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3293221,
...> "callType" => "call",
...> "error" => "out of gas",
...> "from" => "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef",
...> "gas" => "0x4c9",
...> "gasUsed" => "0x4c9",
...> "index" => 0,
...> "input" => "0xa83627de",
...> "to" => "0xaae465ad04b12e90c32291e59b65ca781c57e361",
...> "transactionHash" => "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073",
...> "transactionIndex" => 16,
...> "type" => "call",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3293221,
transaction_index: 16,
transaction_hash: "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073",
index: 0,
type: "call",
call_type: "call",
error: "out of gas",
from_address_hash: "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef",
to_address_hash: "0xaae465ad04b12e90c32291e59b65ca781c57e361",
gas: 1225,
input: "0xa83627de",
trace_address: [],
value: 0
}
A call can reach the stack limit (1024):
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3293621,
...> "transactionIndex" => 7,
...> "transactionHash" => "0xc4f4ba28bf8e6093b3f5932191a7a6af1dd17517c2b0e1be3b76dc445564a9ff",
...> "index" => 64,
...> "type" => "call",
...> "callType" => "call",
...> "from" => "0xaf7cf620c3df1b9ccbc640be903d5ea6cea7bc96",
...> "to" => "0x80629758f88b3f30b7f1244e4588444d6276eef0",
...> "input" => "0x49b46d5d",
...> "error" => "stack limit reached 1024 (1024)",
...> "gas" => "0x160ecc",
...> "gasUsed" => "0x160ecc",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3293621,
transaction_index: 7,
transaction_hash: "0xc4f4ba28bf8e6093b3f5932191a7a6af1dd17517c2b0e1be3b76dc445564a9ff",
index: 64,
type: "call",
call_type: "call",
from_address_hash: "0xaf7cf620c3df1b9ccbc640be903d5ea6cea7bc96",
to_address_hash: "0x80629758f88b3f30b7f1244e4588444d6276eef0",
input: "0x49b46d5d",
error: "stack limit reached 1024 (1024)",
gas: 1445580,
trace_address: [],
value: 0
}
A contract creation:
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3292697,
...> "transactionIndex" => 1,
...> "transactionHash" => "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6",
...> "index" => 0,
...> "type" => "create",
...> "from" => "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c",
...> "createdContractAddressHash" => "0x08d24f568715041e72223cc023e806060de8a2a5",
...> "gas" => "0x5e46ef",
...> "gasUsed" => "0x168a8a",
...> "init" => "0x",
...> "createdContractCode" => "0x",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3292697,
transaction_index: 1,
transaction_hash: "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6",
index: 0,
type: "create",
from_address_hash: "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c",
created_contract_address_hash: "0x08d24f568715041e72223cc023e806060de8a2a5",
gas: 6178543,
gas_used: 1477258,
init: "0x",
created_contract_code: "0x",
trace_address: [],
value: 0
}
A contract creation can fail:
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3299287,
...> "transactionIndex" => 14,
...> "transactionHash" => "0x5c0c728190e593f2bbcbd9d7f851cbfbcaf041e41ce1b1eead97c301deb071fa",
...> "index" => 0,
...> "type" => "create",
...> "from" => "0x0a49007c56c5f9eda04a2ae4229da03a30be892e",
...> "gas" => "0x84068",
...> "gasUsed" => "0x84068",
...> "init" => "0xf49e4745",
...> "error" => "stack underflow (0 <=> 6)",
...> "value" => "0x12c94dd59ce493"
...> }
...> )
%{
block_number: 3299287,
transaction_index: 14,
transaction_hash: "0x5c0c728190e593f2bbcbd9d7f851cbfbcaf041e41ce1b1eead97c301deb071fa",
index: 0,
type: "create",
from_address_hash: "0x0a49007c56c5f9eda04a2ae4229da03a30be892e",
init: "0xf49e4745",
error: "stack underflow (0 <=> 6)",
gas: 540776,
trace_address: [],
value: 5287885714285715
}
A delegate call uses the current contract's state, but the called contract's code:
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3292842,
...> "transactionIndex" => 21,
...> "transactionHash" => "0x6cf0aa434f6500251ce8579d031c821b9fd4b687685b21c368f1c1106e9a49a9",
...> "index" => 1,
...> "type" => "call",
...> "callType" => "delegatecall",
...> "from" => "0x54a298ee9fccbf0ad8e55bc641d3086b81a48c41",
...> "to" => "0x147e7f491ddabc0488edb47f8700633dbaad1fd1",
...> "gas" => "0x40289",
...> "gasUsed" => "0x17df",
...> "input" => "0xeb9d50e46930b3227102b442f93b4aed3dead4ed76f850a76ee7f8b2cbe763428f2790530000000000000000000000000000000000000000000000000926708dfd7272e3",
...> "output" => "0x",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3292842,
transaction_index: 21,
transaction_hash: "0x6cf0aa434f6500251ce8579d031c821b9fd4b687685b21c368f1c1106e9a49a9",
index: 1,
type: "call",
call_type: "delegatecall",
from_address_hash: "0x54a298ee9fccbf0ad8e55bc641d3086b81a48c41",
to_address_hash: "0x147e7f491ddabc0488edb47f8700633dbaad1fd1",
gas: 262793,
gas_used: 6111,
input: "0xeb9d50e46930b3227102b442f93b4aed3dead4ed76f850a76ee7f8b2cbe763428f2790530000000000000000000000000000000000000000000000000926708dfd7272e3",
output: "0x",
trace_address: [],
value: 0
}
A static call calls another contract, but no state can change. This includes no value transfer, so the value for the
call is always `0`. If the called contract does attempt a state change, the call will error.
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3293660,
...> "transactionIndex" => 0,
...> "transactionHash" => "0xb49ac6385dce60e2d88d8b4579f4e70a23cd40b45ecb29eb6c6069efc895325b",
...> "index" => 1,
...> "type" => "call",
...> "callType" => "staticcall",
...> "from" => "0xa4b3886db53bebdabbe17592a57886810b906200",
...> "to" => "0x20f47d830b01c4f4af4b7663a8143d230fcdc0c8",
...> "input" => "0x0f370699",
...> "output" => "0x",
...> "gas" => "0x478d26",
...> "gasUsed" => "0x410",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3293660,
transaction_index: 0,
transaction_hash: "0xb49ac6385dce60e2d88d8b4579f4e70a23cd40b45ecb29eb6c6069efc895325b",
index: 1,
type: "call",
call_type: "staticcall",
from_address_hash: "0xa4b3886db53bebdabbe17592a57886810b906200",
to_address_hash: "0x20f47d830b01c4f4af4b7663a8143d230fcdc0c8",
gas: 4689190,
gas_used: 1040,
input: "0x0f370699",
output: "0x",
trace_address: [],
value: 0
}
A selfdestruct destroys the calling contract and sends any left over balance to the to address.
iex> EthereumJSONRPC.Geth.Call.to_internal_transaction_params(
...> %{
...> "blockNumber" => 3298074,
...> "transactionIndex" => 9,
...> "transactionHash" => "0xe098557c8fa82be6779f5c2b3f248e990e2dc67b6bd60a4fa4a9aa66f6c24c08",
...> "index" => 32,
...> "type" => "selfdestruct",
...> "from" => "0x9317da7be8e05f36f329a95f004a44552effb968",
...> "to" => "0xff77830c100623316736b45c4983df970423aaf4",
...> "gas" => "0xb52c8",
...> "gasUsed" => "0xaf6b5",
...> "value" => "0x0"
...> }
...> )
%{
block_number: 3298074,
from_address_hash: "0x9317da7be8e05f36f329a95f004a44552effb968",
gas: 742088,
gas_used: 718517,
index: 32,
to_address_hash: "0xff77830c100623316736b45c4983df970423aaf4",
transaction_hash: "0xe098557c8fa82be6779f5c2b3f248e990e2dc67b6bd60a4fa4a9aa66f6c24c08",
transaction_index: 9,
type: "selfdestruct",
value: 0
}
"""
def to_internal_transaction_params(call) when is_map(call) do
call
|> to_elixir()
|> elixir_to_internal_transaction_params()
end
defp to_elixir(call) when is_map(call) do
Enum.into(call, %{}, &entry_to_elixir/1)
end
defp entry_to_elixir({key, value} = entry)
when key in ~w(callType createdContractAddressHash createdContractCode error from init input output to transactionHash type) and
is_binary(value),
do: entry
defp entry_to_elixir({key, value} = entry) when key in ~w(blockNumber index transactionIndex) and is_integer(value),
do: entry
defp entry_to_elixir({key, quantity}) when key in ~w(gas gasUsed value) and is_binary(quantity) do
{key, quantity_to_integer(quantity)}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "call" = type,
"callType" => call_type,
"from" => from_address_hash,
"to" => to_address_hash,
"gas" => gas,
"gasUsed" => gas_used,
"input" => input,
"output" => output,
"value" => value
})
when call_type in ~w(call callcode delegatecall) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: type,
call_type: call_type,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
gas: gas,
gas_used: gas_used,
input: input,
output: output,
trace_address: [],
value: value
}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "call" = type,
"callType" => call_type,
"from" => from_address_hash,
"to" => to_address_hash,
"gas" => gas,
"input" => input,
"error" => error,
"value" => value
})
when call_type in ~w(call callcode delegatecall) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: type,
call_type: call_type,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
gas: gas,
input: input,
error: error,
trace_address: [],
value: value
}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "call" = type,
"callType" => "staticcall" = call_type,
"from" => from_address_hash,
"to" => to_address_hash,
"input" => input,
"output" => output,
"gas" => gas,
"gasUsed" => gas_used,
"value" => 0 = value
}) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: type,
call_type: call_type,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
gas: gas,
gas_used: gas_used,
input: input,
output: output,
trace_address: [],
value: value
}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "create",
"from" => from_address_hash,
"createdContractAddressHash" => created_contract_address_hash,
"gas" => gas,
"gasUsed" => gas_used,
"init" => init,
"createdContractCode" => created_contract_code,
"value" => value
}) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: "create",
from_address_hash: from_address_hash,
gas: gas,
gas_used: gas_used,
created_contract_address_hash: created_contract_address_hash,
init: init,
created_contract_code: created_contract_code,
trace_address: [],
value: value
}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "create" = type,
"from" => from_address_hash,
"error" => error,
"gas" => gas,
"init" => init,
"value" => value
}) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: type,
from_address_hash: from_address_hash,
gas: gas,
error: error,
init: init,
trace_address: [],
value: value
}
end
defp elixir_to_internal_transaction_params(%{
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash,
"index" => index,
"type" => "selfdestruct" = type,
"from" => from_address_hash,
"to" => to_address_hash,
"gas" => gas,
"gasUsed" => gas_used,
"value" => value
}) do
%{
block_number: block_number,
transaction_index: transaction_index,
transaction_hash: transaction_hash,
index: index,
type: type,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
gas: gas,
gas_used: gas_used,
value: value
}
end
end

@ -0,0 +1,233 @@
defmodule EthereumJSONRPC.Geth.Calls do
@moduledoc """
Calls returned from [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction)
using a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`).
"""
alias EthereumJSONRPC.Geth.Call
@doc """
Converts a sequence of calls to internal transaction params.
A sequence of calls:
iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params(
...> [
...> %{
...> "blockNumber" => 3287375,
...> "callType" => "call",
...> "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
...> "gas" => "0x8600",
...> "gasUsed" => "0x7d37",
...> "index" => 0,
...> "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
...> "output" => "0x",
...> "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
...> "transactionIndex" => 13,
...> "type" => "call",
...> "value" => "0x174876e800"
...> },
...> %{
...> "blockNumber" => 3287375,
...> "callType" => "call",
...> "from" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
...> "gas" => "0x25e4",
...> "gasUsed" => "0x1ce8",
...> "index" => 1,
...> "input" => "0x",
...> "output" => "0x",
...> "to" => "0xf8d67a2d17b7936bda99585d921fd7276fc5cac7",
...> "transactionHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
...> "transactionIndex" => 13,
...> "type" => "call",
...> "value" => "0x174876e800"
...> }
...> ]
...> )
[
%{
block_number: 3287375,
call_type: "call",
from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
gas: 34304,
gas_used: 32055,
index: 0,
input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
output: "0x",
to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
trace_address: [],
transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
transaction_index: 13,
type: "call",
value: 100000000000
},
%{
block_number: 3287375,
call_type: "call",
from_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
gas: 9700,
gas_used: 7400,
index: 1,
input: "0x",
output: "0x",
to_address_hash: "0xf8d67a2d17b7936bda99585d921fd7276fc5cac7",
trace_address: [],
transaction_hash: "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c",
transaction_index: 13,
type: "call",
value: 100000000000
}
]
A call can run out of gas:
iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params(
...> [
...> %{
...> "blockNumber" => 3293221,
...> "callType" => "call",
...> "error" => "out of gas",
...> "from" => "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef",
...> "gas" => "0x4c9",
...> "gasUsed" => "0x4c9",
...> "index" => 0,
...> "input" => "0xa83627de",
...> "to" => "0xaae465ad04b12e90c32291e59b65ca781c57e361",
...> "transactionHash" => "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073",
...> "transactionIndex" => 16,
...> "type" => "call",
...> "value" => "0x0"
...> }
...> ]
...> )
[
%{
block_number: 3293221,
transaction_index: 16,
transaction_hash: "0xa9a893fe2f019831496cec9777ad25ff940823b9b47a3969299ea139e42b2073",
index: 0,
type: "call",
call_type: "call",
error: "out of gas",
from_address_hash: "0x8ec75ef3adf6c953775d0738e0e7bd60e647e5ef",
to_address_hash: "0xaae465ad04b12e90c32291e59b65ca781c57e361",
gas: 1225,
input: "0xa83627de",
trace_address: [],
value: 0
}
]
A contract creation:
iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params(
...> [
...> %{
...> "blockNumber" => 3292697,
...> "type" => "create",
...> "transactionIndex" => 1,
...> "transactionHash" => "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6",
...> "index" => 0,
...> "from" => "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c",
...> "createdContractAddressHash" => "0x08d24f568715041e72223cc023e806060de8a2a5",
...> "gas" => "0x5e46ef",
...> "gasUsed" => "0x168a8a",
...> "init" => "0x",
...> "createdContractCode" => "0x",
...> "value" => "0x0"
...> }
...> ]
...> )
[
%{
block_number: 3292697,
transaction_index: 1,
transaction_hash: "0x248a832af263a298b9869ee9a669c2c86a3676799b0b8b566c6dd452daaedbf6",
index: 0,
type: "create",
from_address_hash: "0xb95754d27da16a0f17aba278fc10a69e1c9fee1c",
created_contract_address_hash: "0x08d24f568715041e72223cc023e806060de8a2a5",
gas: 6178543,
gas_used: 1477258,
init: "0x",
created_contract_code: "0x",
trace_address: [],
value: 0
}
]
Contract creation can happen indirectly through a call:
iex> EthereumJSONRPC.Geth.Calls.to_internal_transactions_params(
...> [
...> %{
...> "blockNumber" => 3293393,
...> "transactionIndex" => 13,
...> "transactionHash" => "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461",
...> "index" => 0,
...> "callType" => "call",
...> "from" => "0x129f447137b03ee3d8bbad62ef5d89021d944324",
...> "to" => "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518",
...> "gas" => "0x18d2c2",
...> "gasUsed" => "0x106e24",
...> "input" => "0xe9696f54",
...> "output" => "0x0000000000000000000000009b5a1dcfd53caa108ef83cf2ff0e17db27facf0f",
...> "type" => "call",
...> "value" => "0x0"
...> },
...> %{
...> "blockNumber" => 3293393,
...> "transactionIndex" => 13,
...> "transactionHash" => "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461",
...> "index" => 1,
...> "type" => "create",
...> "from" => "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518",
...> "createdContractAddressHash" => "0x9b5a1dcfd53caa108ef83cf2ff0e17db27facf0f",
...> "gas" => "0x18c869",
...> "gasUsed" => "0xfe428",
...> "init" => "0x6080604",
...> "createdContractCode" => "0x608060",
...> "value" => "0x0"
...> }
...> ]
...> )
[
%{
block_number: 3293393,
transaction_index: 13,
transaction_hash: "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461",
index: 0,
type: "call",
call_type: "call",
from_address_hash: "0x129f447137b03ee3d8bbad62ef5d89021d944324",
to_address_hash: "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518",
gas: 1626818,
gas_used: 1076772,
input: "0xe9696f54",
output: "0x0000000000000000000000009b5a1dcfd53caa108ef83cf2ff0e17db27facf0f",
trace_address: [],
value: 0
},
%{
block_number: 3293393,
transaction_index: 13,
transaction_hash: "0x19379505cd9fcd16f19d92f23dc323ee921991da1f169df2af1d93fdb8bca461",
index: 1,
type: "create",
created_contract_address_hash: "0x9b5a1dcfd53caa108ef83cf2ff0e17db27facf0f",
from_address_hash: "0x2c8a58ddba2dc097ea0f95db6cd51ac7d31d1518",
gas: 1624169,
gas_used: 1041448,
init: "0x6080604",
created_contract_code: "0x608060",
trace_address: [],
value: 0
}
]
"""
def to_internal_transactions_params(calls) when is_list(calls) do
Enum.map(calls, &Call.to_internal_transaction_params/1)
end
end

@ -0,0 +1,418 @@
// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`
{
// The call stack of the EVM execution.
callStack: [{}],
// step is invoked for every opcode that the VM executes.
step(log, db) {
// Capture any errors immediately
const error = log.getError();
if (error !== undefined) {
this.fault(log, db);
} else {
this.success(log, db);
}
},
// fault is invoked when the actual execution of an opcode fails.
fault(log, db) {
// If the topmost call already reverted, don't handle the additional fault again
if (this.topCall().error === undefined) {
this.putError(log);
}
},
putError(log) {
if (this.callStack.length > 1) {
this.putErrorInTopCall(log);
} else {
this.putErrorInBottomCall(log);
}
},
putErrorInTopCall(log) {
// Pop off the just failed call
const call = this.callStack.pop();
this.putErrorInCall(log, call);
this.pushChildCall(call);
},
putErrorInBottomCall(log) {
const call = this.bottomCall();
this.putErrorInCall(log, call);
},
putErrorInCall(log, call) {
call.error = log.getError();
// Consume all available gas and clean any leftovers
if (call.gasBigInt !== undefined) {
call.gasUsedBigInt = call.gasBigInt;
}
delete call.outputOffset;
delete call.outputLength;
},
topCall() {
return this.callStack[this.callStack.length - 1];
},
bottomCall() {
return this.callStack[0];
},
pushChildCall(childCall) {
const topCall = this.topCall();
if (topCall.calls === undefined) {
topCall.calls = [];
}
topCall.calls.push(childCall);
},
success(log, db) {
const op = log.op.toString();
this.beforeOp(log, db);
switch (op) {
case 'CREATE':
this.createOp(log);
break;
case 'SELFDESTRUCT':
this.selfDestructOp(log, db);
break;
case 'CALL':
case 'CALLCODE':
case 'DELEGATECALL':
case 'STATICCALL':
this.callOp(log, op);
break;
case 'REVERT':
this.revertOp();
break;
}
},
beforeOp(log, db) {
/**
* Depths
* 0 - `ctx`. Never shows up in `log.getDepth()`
* 1 - first level of `log.getDepth()`
*
* callStack indexes
*
* 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)
* 1 - first callOp inside of `ctx`
*/
const logDepth = log.getDepth();
const callStackDepth = this.callStack.length;
if (logDepth < callStackDepth) {
// Pop off the last call and get the execution results
const call = this.callStack.pop();
call.gasUsedBigInt = call.gasBigInt.subtract(log.getGas());
const ret = log.stack.peek(0);
if (!ret.equals(0)) {
if (call.type === 'create') {
call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));
call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));
} else {
call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen));
}
} else if (call.error === undefined) {
call.error = 'internal failure';
}
delete call.outputOffset;
delete call.outputLength;
this.pushChildCall(call);
}
},
createOp(log) {
const inputOffset = log.stack.peek(1).valueOf();
const inputLength = log.stack.peek(2).valueOf();
const inputEnd = inputOffset + inputLength;
const stackValue = log.stack.peek(0);
const call = {
type: 'create',
from: toHex(log.contract.getAddress()),
init: toHex(log.memory.slice(inputOffset, inputEnd)),
gasBigInt: bigInt(log.getGas()),
valueBigInt: bigInt(stackValue.toString(10))
};
this.callStack.push(call);
},
selfDestructOp(log, db) {
const contractAddress = log.contract.getAddress();
this.pushChildCall({
type: 'selfdestruct',
from: toHex(contractAddress),
to: toHex(toAddress(log.stack.peek(0).toString(16))),
gasBigInt: bigInt(log.getGas()),
valueBigInt: db.getBalance(contractAddress)
});
},
callOp(log, op) {
const to = toAddress(log.stack.peek(1).toString(16));
// Skip any pre-compile invocations, those are just fancy opcodes
if (!isPrecompiled(to)) {
this.callCustomOp(log, op, to);
}
},
callCustomOp(log, op, to) {
const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);
const inputOffset = log.stack.peek(2 + stackOffset).valueOf();
const inputLength = log.stack.peek(3 + stackOffset).valueOf();
const inputEnd = inputOffset + inputLength;
const call = {
type: 'call',
callType: op.toLowerCase(),
from: toHex(log.contract.getAddress()),
to: toHex(to),
gasBigInt: bigInt(log.getGas()),
input: toHex(log.memory.slice(inputOffset, inputEnd)),
outputOffset: log.stack.peek(4 + stackOffset).valueOf(),
outputLength: log.stack.peek(5 + stackOffset).valueOf()
};
switch (op) {
case 'CALL':
case 'CALLCODE':
call.valueBigInt = bigInt(log.stack.peek(2));
break;
case 'DELEGATECALL':
// value inherited from scope during call sequencing
break;
case 'STATICCALL':
// by definition static calls transfer no value
call.valueBigInt = bigInt.zero;
break;
default:
throw "Unknown custom call op " + op;
}
this.callStack.push(call);
},
revertOp() {
this.topCall().error = 'execution reverted';
},
// result is invoked when all the opcodes have been iterated over and returns
// the final result of the tracing.
result(ctx, db) {
const result = this.ctxToResult(ctx, db);
const filtered = this.filterNotUndefined(result);
const callSequence = this.sequence(filtered, [], filtered.valueBigInt, filtered.gasUsedBigInt).callSequence;
return this.encodeCallSequence(callSequence);
},
ctxToResult(ctx, db) {
var result;
switch (ctx.type) {
case 'CALL':
result = this.ctxToCall(ctx);
break;
case 'CREATE':
result = this.ctxToCreate(ctx, db);
break;
}
return result;
},
ctxToCall(ctx) {
const result = {
type: 'call',
callType: 'call',
from: toHex(ctx.from),
to: toHex(ctx.to),
valueBigInt: bigInt(ctx.value.toString(10)),
gasBigInt: bigInt(ctx.gas),
gasUsedBigInt: bigInt(ctx.gasUsed),
input: toHex(ctx.input)
};
this.putBottomChildCalls(result);
this.putErrorOrOutput(result, ctx);
return result;
},
putErrorOrOutput(result, ctx) {
const error = this.error(ctx);
if (error !== undefined) {
result.error = error;
} else {
result.output = toHex(ctx.output);
}
},
ctxToCreate(ctx, db) {
const result = {
type: 'create',
from: toHex(ctx.from),
init: toHex(ctx.input),
valueBigInt: bigInt(ctx.value.toString(10)),
gasBigInt: bigInt(ctx.gas),
gasUsedBigInt: bigInt(ctx.gasUsed)
};
this.putBottomChildCalls(result);
this.putErrorOrCreatedContract(result, ctx, db);
return result;
},
putBottomChildCalls(result) {
const bottomCall = this.bottomCall();
const bottomChildCalls = bottomCall.calls;
if (bottomChildCalls !== undefined) {
result.calls = bottomChildCalls;
}
},
putErrorOrCreatedContract(result, ctx, db) {
const error = this.error(ctx);
if (error !== undefined) {
result.error = error
} else {
result.createdContractAddressHash = toHex(ctx.to);
result.createdContractCode = toHex(db.getCode(ctx.to));
}
},
error(ctx) {
var error;
const bottomCall = this.bottomCall();
const bottomCallError = bottomCall.error;
if (bottomCallError !== undefined) {
error = bottomCallError;
} else {
const ctxError = ctx.error;
if (ctxError !== undefined) {
error = ctxError;
}
}
return error;
},
filterNotUndefined(call) {
for (var key in call) {
if (call[key] === undefined) {
delete call[key];
}
}
if (call.calls !== undefined) {
for (var i = 0; i < call.calls.length; i++) {
call.calls[i] = this.filterNotUndefined(call.calls[i]);
}
}
return call;
},
// sequence converts the finalized calls from a call tree to a call sequence
sequence(call, callSequence, availableValueBigInt, availableGasBigInt) {
const subcalls = call.calls;
delete call.calls;
if (call.type === 'call' && call.callType === 'delegatecall') {
call.valueBigInt = availableValueBigInt;
} else if (call.type === 'selfdestruct') {
call.gasUsedBigInt = availableGasBigInt
}
var newCallSequence = callSequence.concat([call]);
if (subcalls !== undefined) {
var nestedAvailableValueBigInt = availableValueBigInt;
var nestedAvailableGasBigInt = availableGasBigInt;
for (var i = 0; i < subcalls.length; i++) {
const nestedSequenced = this.sequence(subcalls[i], newCallSequence, nestedAvailableValueBigInt, availableGasBigInt);
newCallSequence = nestedSequenced.callSequence;
nestedAvailableValueBigInt = nestedSequenced.availableValueBigInt;
nestedAvailableGasBigInt = nestedSequenced.availableGasBigInt;
}
}
const newAvailableValueBigInt = availableValueBigInt.subtract(call.valueBigInt);
const newAvailableGasUsedBigInt = availableGasBigInt.subtract(call.gasUsedBigInt);
return {callSequence: newCallSequence, availableValueBigInt: newAvailableValueBigInt, availableGasBigInt: newAvailableGasUsedBigInt};
},
encodeCallSequence(calls) {
for (var i = 0; i < calls.length; i++) {
this.encodeCall(calls[i]);
}
return calls;
},
encodeCall(call) {
this.putValue(call);
this.putGas(call);
this.putGasUsed(call);
return call;
},
putValue(call) {
const valueBigInt = call.valueBigInt;
delete call.valueBigInt;
call.value = '0x' + valueBigInt.toString(16);
},
putGas(call) {
const gasBigInt = call.gasBigInt;
delete call.gasBigInt;
if (gasBigInt === undefined) {
throw "gasBigInt undefined in " + JSON.stringify(call);
}
call.gas = '0x' + gasBigInt.toString(16);
},
putGasUsed(call) {
const gasUsedBigInt = call.gasUsedBigInt;
delete call.gasUsedBigInt;
if (gasUsedBigInt === undefined) {
throw "gasUsedBigInt undefined in " + JSON.stringify(call);
}
call.gasUsed = '0x' + gasUsedBigInt.toString(16);
}
}

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.Geth.CallTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Geth.Call
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.Geth.CallsTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Geth.Calls
end

@ -1,18 +1,79 @@
defmodule EthereumJSONRPC.GethTest do
use EthereumJSONRPC.Case, async: false
import Mox
alias EthereumJSONRPC.Geth
@moduletag :no_parity
describe "fetch_internal_transactions/2" do
test "is not supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do
Geth.fetch_internal_transactions(
[
"0x2ec382949ba0b22443aa4cb38267b1fb5e68e188109ac11f7a82f67571a0adf3"
],
json_rpc_named_arguments
)
# Infura Mainnet does not support debug_traceTransaction, so this cannot be tested expect in Mox
setup do
EthereumJSONRPC.Case.Geth.Mox.setup()
end
setup :verify_on_exit!
# Data taken from Rinkeby
test "is supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do
block_number = 3_287_375
transaction_index = 13
transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c"
tracer = File.read!("priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js")
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^transaction_hash, %{tracer: ^tracer}]}], _ ->
{:ok,
[
%{
id: id,
result: [
%{
"traceAddress" => [],
"type" => "call",
"callType" => "call",
"from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
"to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
"gas" => "0x8600",
"gasUsed" => "0x7d37",
"input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
"output" => "0x",
"value" => "0x174876e800"
}
]
}
]}
end)
assert {:ok,
[
%{
block_number: ^block_number,
transaction_index: ^transaction_index,
transaction_hash: ^transaction_hash,
index: 0,
trace_address: [],
type: "call",
call_type: "call",
from_address_hash: "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4",
to_address_hash: "0x1469b17ebf82fedf56f04109e5207bdc4554288c",
gas: 34304,
gas_used: 32055,
input: "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008",
output: "0x",
value: 100_000_000_000
}
]} =
Geth.fetch_internal_transactions(
[
%{
block_number: block_number,
transaction_index: transaction_index,
hash_data: transaction_hash
}
],
json_rpc_named_arguments
)
end
end

@ -140,6 +140,7 @@ defmodule Indexer.Block.Fetcher do
{:ok, {inserted, next}}
else
{step, {:error, reason}} -> {:error, {step, reason}}
{:error, :timeout} = error -> error
{:error, changesets} = error when is_list(changesets) -> error
{:error, step, failed_value, changes_so_far} -> {:error, {step, failed_value, changes_so_far}}
end

@ -10,6 +10,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1]
import Indexer.Block.Fetcher, only: [async_import_tokens: 1, async_import_uncles: 1, fetch_and_import_range: 2]
alias Ecto.Changeset
alias EthereumJSONRPC.Subscription
alias Explorer.Chain
alias Indexer.{AddressExtraction, Block, TokenBalances}
@ -146,7 +147,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
]
end)
{:error, changesets} when is_list(changesets) ->
{:error, [%Changeset{} | _] = changesets} ->
params = %{
changesets: changesets,
block_number_to_fetch: block_number_to_fetch,

Loading…
Cancel
Save