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
parent
9cce7ab661
commit
e4b7a4ee5f
@ -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 |
Loading…
Reference in new issue