From 6f1a521ca733f7167f3947a90b3ad139306822dd Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 18 Feb 2019 10:33:00 +0300 Subject: [PATCH 1/7] do not try to convert nils in logs --- .../lib/ethereum_jsonrpc/log.ex | 6 ++++- .../test/ethereum_jsonrpc/log_test.exs | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex index 55b327ee0e..c9a6fd08e6 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex @@ -171,7 +171,11 @@ defmodule EthereumJSONRPC.Log do do: entry defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber logIndex transactionIndex transactionLogIndex) do - {key, quantity_to_integer(quantity)} + if is_nil(quantity) do + {key, nil} + else + {key, quantity_to_integer(quantity)} + end end defp put_topics(params, topics) when is_map(params) and is_list(topics) do diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs index a2c43064cf..1ea07960da 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs @@ -2,4 +2,29 @@ defmodule EthereumJSONRPC.LogTest do use ExUnit.Case, async: true doctest EthereumJSONRPC.Log + + alias EthereumJSONRPC.Log + + describe "to_elixir/1" do + test "does not tries convert nils to integer" do + input = %{ + "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55", + "blockHash" => nil, + "blockNumber" => nil, + "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + "logIndex" => "0x0", + "removed" => false, + "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"], + "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e", + "transactionIndex" => "0x0" + } + + result = Log.to_elixir(input) + + assert result["blockNumber"] == nil + assert result["blockHash"] == nil + end + end end From d1ec793889de9d3cb1867aba7b6a03e7f293bcd7 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 18 Feb 2019 10:39:28 +0300 Subject: [PATCH 2/7] mix format --- apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs index 1ea07960da..3237ea8303 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs @@ -14,9 +14,11 @@ defmodule EthereumJSONRPC.LogTest do "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", "logIndex" => "0x0", "removed" => false, - "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", - "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", - "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"], + "topics" => [ + "0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed" + ], "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e", "transactionIndex" => "0x0" } From 366f9b7aa60a0b5e0ddcdc283f48c07c73208d38 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 18 Feb 2019 15:24:42 +0300 Subject: [PATCH 3/7] do not create contracts on failed parent transactions --- .../indexer/internal_transaction/fetcher.ex | 35 ++++++++- .../internal_transaction/fetcher_test.exs | 77 ++++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/apps/indexer/lib/indexer/internal_transaction/fetcher.ex b/apps/indexer/lib/indexer/internal_transaction/fetcher.ex index 5cf6df501e..01675cfb9d 100644 --- a/apps/indexer/lib/indexer/internal_transaction/fetcher.ex +++ b/apps/indexer/lib/indexer/internal_transaction/fetcher.ex @@ -113,7 +113,12 @@ defmodule Indexer.InternalTransaction.Fetcher do |> EthereumJSONRPC.fetch_internal_transactions(json_rpc_named_arguments) |> case do {:ok, internal_transactions_params} -> - addresses_params = AddressExtraction.extract_addresses(%{internal_transactions: internal_transactions_params}) + internal_transactions_params_without_failed_creations = remove_failed_creations(internal_transactions_params) + + addresses_params = + AddressExtraction.extract_addresses(%{ + internal_transactions: internal_transactions_params_without_failed_creations + }) address_hash_to_block_number = Enum.into(addresses_params, %{}, fn %{fetched_coin_balance_block_number: block_number, hash: hash} -> @@ -123,7 +128,7 @@ defmodule Indexer.InternalTransaction.Fetcher do with {:ok, imported} <- Chain.import(%{ addresses: %{params: addresses_params}, - internal_transactions: %{params: internal_transactions_params}, + internal_transactions: %{params: internal_transactions_params_without_failed_creations}, timeout: :infinity }) do async_import_coin_balances(imported, %{ @@ -197,4 +202,30 @@ defmodule Indexer.InternalTransaction.Fetcher do end end) end + + defp remove_failed_creations(internal_transactions_params) do + internal_transactions_params + |> Enum.map(fn internal_transaction_params -> + internal_transaction_params[:trace_address] + + failed_parent_index = + Enum.find(internal_transaction_params[:trace_address], fn trace_address -> + parent = Enum.at(internal_transactions_params, trace_address) + + !is_nil(parent[:error]) + end) + + failed_parent = failed_parent_index && Enum.at(internal_transactions_params, failed_parent_index) + + if failed_parent do + internal_transaction_params + |> Map.delete(:created_contract_address_hash) + |> Map.delete(:created_contract_code) + |> Map.delete(:gas_used) + |> Map.put(:error, failed_parent[:error]) + else + internal_transaction_params + end + end) + end end diff --git a/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs b/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs index e81a5b4954..c49e8dbbef 100644 --- a/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs +++ b/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs @@ -5,7 +5,7 @@ defmodule Indexer.InternalTransaction.FetcherTest do import ExUnit.CaptureLog import Mox - alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.{Address, Hash, Transaction} alias Indexer.{CoinBalance, InternalTransaction, PendingTransaction} @@ -151,6 +151,81 @@ defmodule Indexer.InternalTransaction.FetcherTest do """ end + test "internal transactions with failed parent does not create a new address", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: %{ + "output" => "0x", + "stateDiff" => nil, + "trace" => [ + %{ + "action" => %{ + "callType" => "call", + "from" => "0xc73add416e2119d20ce80e0904fc1877e33ef246", + "gas" => "0x13388", + "input" => "0xc793bf97", + "to" => "0x2d07e106b5d280e4ccc2d10deee62441c91d4340", + "value" => "0x0" + }, + "error" => "Reverted", + "subtraces" => 1, + "traceAddress" => [], + "type" => "call" + }, + %{ + "action" => %{ + "from" => "0x2d07e106b5d280e4ccc2d10deee62441c91d4340", + "gas" => "0xb2ab", + "init" => + "0x608060405234801561001057600080fd5b5060d38061001f6000396000f3fe6080604052600436106038577c010000000000000000000000000000000000000000000000000000000060003504633ccfd60b8114604f575b336000908152602081905260409020805434019055005b348015605a57600080fd5b5060616063565b005b33600081815260208190526040808220805490839055905190929183156108fc02918491818181858888f1935050505015801560a3573d6000803e3d6000fd5b505056fea165627a7a72305820e9a226f249def650de957dd8b4127b85a3049d6bfa818cadc4e2d3c44b6a53530029", + "value" => "0x0" + }, + "result" => %{ + "address" => "0xf4a5afe28b91cf928c2568805cfbb36d477f0b75", + "code" => + "0x6080604052600436106038577c010000000000000000000000000000000000000000000000000000000060003504633ccfd60b8114604f575b336000908152602081905260409020805434019055005b348015605a57600080fd5b5060616063565b005b33600081815260208190526040808220805490839055905190929183156108fc02918491818181858888f1935050505015801560a3573d6000803e3d6000fd5b505056fea165627a7a72305820e9a226f249def650de957dd8b4127b85a3049d6bfa818cadc4e2d3c44b6a53530029", + "gasUsed" => "0xa535" + }, + "subtraces" => 0, + "traceAddress" => [0], + "type" => "create" + } + ], + "vmTrace" => nil + } + } + ]} + end) + + CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + %Transaction{hash: %Hash{bytes: bytes}} = + insert(:transaction, hash: "0x03cd5899a63b6f6222afda8705d059fd5a7d126bcabe962fb654d9736e6bcafa") + |> with_block() + + :ok = + InternalTransaction.Fetcher.run( + [ + {7_202_692, bytes, 0} + ], + json_rpc_named_arguments + ) + + address = "0xf4a5afe28b91cf928c2568805cfbb36d477f0b75" + + fetched_address = Repo.one(from(address in Address, where: address.hash == ^address)) + + assert is_nil(fetched_address) + end + end + test "duplicate transaction hashes only retry uniques", %{json_rpc_named_arguments: json_rpc_named_arguments} do if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options -> From f76ea69cff17deccd11c3785666dfaa16c4cf870 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 18 Feb 2019 16:18:14 +0300 Subject: [PATCH 4/7] exclude test from parity --- apps/indexer/test/indexer/internal_transaction/fetcher_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs b/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs index c49e8dbbef..38213458e7 100644 --- a/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs +++ b/apps/indexer/test/indexer/internal_transaction/fetcher_test.exs @@ -151,6 +151,7 @@ defmodule Indexer.InternalTransaction.FetcherTest do """ end + @tag :no_parity test "internal transactions with failed parent does not create a new address", %{ json_rpc_named_arguments: json_rpc_named_arguments } do From 5bacac716ada0a8e64e949349ed08f710f3ccb7c Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 18 Feb 2019 17:51:08 +0300 Subject: [PATCH 5/7] remove output field from internal transaction --- apps/indexer/lib/indexer/internal_transaction/fetcher.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/indexer/lib/indexer/internal_transaction/fetcher.ex b/apps/indexer/lib/indexer/internal_transaction/fetcher.ex index 01675cfb9d..d103e054c8 100644 --- a/apps/indexer/lib/indexer/internal_transaction/fetcher.ex +++ b/apps/indexer/lib/indexer/internal_transaction/fetcher.ex @@ -222,6 +222,7 @@ defmodule Indexer.InternalTransaction.Fetcher do |> Map.delete(:created_contract_address_hash) |> Map.delete(:created_contract_code) |> Map.delete(:gas_used) + |> Map.delete(:output) |> Map.put(:error, failed_parent[:error]) else internal_transaction_params From 1616266194447bf95651fc26e13aece54fe4d7f9 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Tue, 19 Feb 2019 15:48:35 +0300 Subject: [PATCH 6/7] check if to_address_hash is contract address to fetch internal transactions --- apps/explorer/lib/explorer/chain.ex | 42 +++++++++++ apps/explorer/test/explorer/chain_test.exs | 70 +++++++++++++++++++ .../lib/indexer/block/realtime/fetcher.ex | 9 ++- 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index f6c8d9eb51..2f6e2f071b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -17,6 +17,8 @@ defmodule Explorer.Chain do where: 3 ] + import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias Ecto.Adapters.SQL alias Ecto.{Changeset, Multi} @@ -1868,6 +1870,46 @@ defmodule Explorer.Chain do |> Data.to_string() end + @doc """ + Checks if an address is a contract + """ + @spec contract_address?(String.t(), non_neg_integer(), Keyword.t()) :: boolean() | :json_rpc_error + def contract_address?(address_hash, block_number, json_rpc_named_arguments \\ []) do + {:ok, binary_hash} = Explorer.Chain.Hash.Address.cast(address_hash) + + query = + from( + address in Address, + where: address.hash == ^binary_hash + ) + + address = Repo.one(query) + + cond do + is_nil(address) -> + block_quantity = integer_to_quantity(block_number) + + case EthereumJSONRPC.fetch_codes( + [%{block_quantity: block_quantity, address: address_hash}], + json_rpc_named_arguments + ) do + {:ok, %EthereumJSONRPC.FetchedCodes{params_list: fetched_codes}} -> + result = List.first(fetched_codes) + + result && !(is_nil(result[:code]) || result[:code] == "" || result[:code] == "0x") + + _ -> + :json_rpc_error + end + + is_nil(address.contract_code) -> + false + + true -> + true + end + end + @doc """ Fetches contract creation input data. """ diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 566eb3ad40..118a42f2f8 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1,10 +1,12 @@ defmodule Explorer.ChainTest do use Explorer.DataCase + use EthereumJSONRPC.Case, async: true require Ecto.Query import Ecto.Query import Explorer.Factory + import Mox alias Explorer.{Chain, Factory, PagingOptions, Repo} @@ -27,6 +29,10 @@ defmodule Explorer.ChainTest do doctest Explorer.Chain + setup :set_mox_global + + setup :verify_on_exit! + describe "count_addresses_with_balance_from_cache/0" do test "returns the number of addresses with fetched_coin_balance > 0" do insert(:address, fetched_coin_balance: 0) @@ -3631,4 +3637,68 @@ defmodule Explorer.ChainTest do assert found_creation_data == "" end end + + describe "contract_address?/2" do + test "returns true if address has contract code" do + code = %Data{ + bytes: <<1, 2, 3, 4, 5>> + } + + address = insert(:address, contract_code: code) + + assert Chain.contract_address?(to_string(address.hash), 1) + end + + test "returns false if address has not contract code" do + address = insert(:address) + + refute Chain.contract_address?(to_string(address.hash), 1) + end + + @tag :no_parity + @tag :no_geth + test "returns true if fetched code from json rpc", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + hash = "0x71300d93a8CdF93385Af9635388cF2D00b95a480" + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _arguments, _options -> + {:ok, + [ + %{ + id: 0, + result: "0x0102030405" + } + ]} + end) + end + + assert Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments) + end + + @tag :no_parity + @tag :no_geth + test "returns false if no fetched code from json rpc", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + hash = "0x71300d93a8CdF93385Af9635388cF2D00b95a480" + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _arguments, _options -> + {:ok, + [ + %{ + id: 0, + result: "0x" + } + ]} + end) + end + + refute Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments) + end + end end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 7f684d8510..a825b90b77 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -435,8 +435,13 @@ defmodule Indexer.Block.Realtime.Fetcher do end end - # Input-less transactions are value-transfers only, so their internal transactions do not need to be indexed - defp fetch_internal_transactions?(%{status: :ok, created_contract_address_hash: nil, input: "0x"}, _), do: false + defp fetch_internal_transactions?( + %{status: :ok, created_contract_address_hash: nil, input: "0x", to_address_hash: from_address_hash}, + _ + ) do + Chain.contract_address?(from_address_hash) + end + # Token transfers not transferred during contract creation don't need internal transactions as the token transfers # derive completely from the logs. defp fetch_internal_transactions?(%{status: :ok, created_contract_address_hash: nil}, true), do: false From 1f88620ce9348b15f8a5d4b15ad01edcebf54713 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Tue, 19 Feb 2019 16:01:44 +0300 Subject: [PATCH 7/7] pass json_rpc_arguments to Chain.contract_address? --- .../lib/indexer/block/realtime/fetcher.ex | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index a825b90b77..61def67c82 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -368,7 +368,7 @@ defmodule Indexer.Block.Realtime.Fetcher do } ) do case transactions_params - |> transactions_params_to_fetch_internal_transactions_params(token_transfers_params) + |> transactions_params_to_fetch_internal_transactions_params(token_transfers_params, json_rpc_named_arguments) |> EthereumJSONRPC.fetch_internal_transactions(json_rpc_named_arguments) do {:ok, internal_transactions_params} -> merged_addresses_params = @@ -387,23 +387,32 @@ defmodule Indexer.Block.Realtime.Fetcher do end end - defp transactions_params_to_fetch_internal_transactions_params(transactions_params, token_transfers_params) do + defp transactions_params_to_fetch_internal_transactions_params( + transactions_params, + token_transfers_params, + json_rpc_named_arguments + ) do token_transfer_transaction_hash_set = MapSet.new(token_transfers_params, & &1.transaction_hash) Enum.flat_map( transactions_params, - &transaction_params_to_fetch_internal_transaction_params_list(&1, token_transfer_transaction_hash_set) + &transaction_params_to_fetch_internal_transaction_params_list( + &1, + token_transfer_transaction_hash_set, + json_rpc_named_arguments + ) ) end defp transaction_params_to_fetch_internal_transaction_params_list( %{block_number: block_number, transaction_index: transaction_index, hash: hash} = transaction_params, - token_transfer_transaction_hash_set + token_transfer_transaction_hash_set, + json_rpc_named_arguments ) when is_integer(block_number) and is_integer(transaction_index) and is_binary(hash) do token_transfer? = hash in token_transfer_transaction_hash_set - if fetch_internal_transactions?(transaction_params, token_transfer?) do + if fetch_internal_transactions?(transaction_params, token_transfer?, json_rpc_named_arguments) do [%{block_number: block_number, transaction_index: transaction_index, hash_data: hash}] else [] @@ -419,6 +428,7 @@ defmodule Indexer.Block.Realtime.Fetcher do input: unquote(TokenTransfer.transfer_function_signature()) <> params, value: 0 }, + _, _ ) do types = [:address, {:uint, 256}] @@ -436,16 +446,23 @@ defmodule Indexer.Block.Realtime.Fetcher do end defp fetch_internal_transactions?( - %{status: :ok, created_contract_address_hash: nil, input: "0x", to_address_hash: from_address_hash}, - _ + %{ + status: :ok, + created_contract_address_hash: nil, + input: "0x", + to_address_hash: to_address_hash, + block_number: block_number + }, + _, + json_rpc_named_arguments ) do - Chain.contract_address?(from_address_hash) + Chain.contract_address?(to_address_hash, block_number, json_rpc_named_arguments) end # Token transfers not transferred during contract creation don't need internal transactions as the token transfers # derive completely from the logs. - defp fetch_internal_transactions?(%{status: :ok, created_contract_address_hash: nil}, true), do: false - defp fetch_internal_transactions?(_, _), do: true + defp fetch_internal_transactions?(%{status: :ok, created_contract_address_hash: nil}, true, _), do: false + defp fetch_internal_transactions?(_, _, _), do: true defp balances( %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments},