From 6d8aa194c3eea56c23493149691546a7ae88da41 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Mon, 21 Oct 2024 19:16:48 +0400 Subject: [PATCH] perf: refactor tx data decoding with fewer DB queries (#10842) * perf: refactor tx data decoding with fewer DB queries * fix: tests * chore: more refactor * chore: fix merge conflicts --- .../api/v2/advanced_filter_controller.ex | 17 +- .../api/v2/token_transfer_controller.ex | 2 +- .../controllers/api/v2/utils_controller.ex | 2 +- .../transaction_interpretation.ex | 4 +- .../views/api/v2/suave_view.ex | 2 +- .../views/api/v2/transaction_view.ex | 16 +- .../block_scout_web/views/transaction_view.ex | 3 +- ...saction_token_transfer_controller_test.exs | 2 + apps/explorer/lib/explorer/chain.ex | 6 +- .../lib/explorer/chain/contract_method.ex | 9 +- .../address_transaction_csv_exporter.ex | 1 - apps/explorer/lib/explorer/chain/log.ex | 2 +- .../explorer/chain/smart_contract/proxy.ex | 49 +-- .../proxy/models/implementation.ex | 8 +- .../lib/explorer/chain/transaction.ex | 313 ++++++++---------- .../proxy/models/implementation_test.exs | 7 + .../chain/smart_contract/proxy_test.exs | 46 ++- .../test/explorer/chain/transaction_test.exs | 16 +- 18 files changed, 224 insertions(+), 281 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 35975e90ec..d0696c4aff 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -57,7 +57,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do {advanced_filters, next_page} = split_list_by_page(advanced_filters_plus_one) - {decoded_transactions, _abi_acc, methods_acc} = + decoded_transactions = advanced_filters |> Enum.map(fn af -> %Transaction{to_address: af.to_address, input: af.input, hash: af.hash} end) |> Transaction.decode_transactions(true, @api_true) @@ -69,7 +69,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do advanced_filters: advanced_filters, decoded_transactions: decoded_transactions, search_params: %{ - method_ids: method_id_to_name_from_params(full_options[:methods] || [], methods_acc), + method_ids: method_id_to_name_from_params(full_options[:methods] || [], decoded_transactions), tokens: contract_address_hash_to_token_from_params(full_options[:token_contract_address_hashes]) }, next_page_params: next_page_params @@ -142,22 +142,19 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do render(conn, :methods, methods: @methods) end - defp method_id_to_name_from_params(prepared_method_ids, methods_acc) do + defp method_id_to_name_from_params(prepared_method_ids, decoded_transactions) do {decoded_method_ids, method_ids_to_find} = Enum.reduce(prepared_method_ids, {%{}, []}, fn method_id, {decoded, to_decode} -> {:ok, method_id_hash} = Data.cast(method_id) + trimmed_method_id = method_id_hash.bytes |> Base.encode16(case: :lower) case {Map.get(@methods_id_to_name_map, method_id), - methods_acc - |> Map.get(method_id_hash.bytes, []) - |> Enum.find( - &match?(%ContractMethod{abi: %{"type" => "function", "name" => name}} when is_binary(name), &1) - )} do + decoded_transactions |> Enum.find(&match?({:ok, ^trimmed_method_id, _, _}, &1))} do {name, _} when is_binary(name) -> {Map.put(decoded, method_id, name), to_decode} - {_, %ContractMethod{abi: %{"type" => "function", "name" => name}}} when is_binary(name) -> - {Map.put(decoded, method_id, name), to_decode} + {_, {:ok, _, function_signature, _}} when is_binary(function_signature) -> + {Map.put(decoded, method_id, function_signature |> String.split("(") |> Enum.at(0)), to_decode} {nil, nil} -> {decoded, [method_id_hash.bytes | to_decode]} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index 05234b6ec7..f353bcc8bc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -58,7 +58,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do end) |> Enum.uniq() - {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) + decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) decoded_transactions_map = transactions diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex index 1826c67598..233066aaff 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.API.V2.UtilsController do updated_smart_contract end - {decoded_input, _abi_acc, _methods_acc} = + decoded_input = Transaction.decoded_input_data( %Transaction{ input: data, diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 11eb245814..c2c1c8aa7b 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -140,7 +140,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do end) skip_sig_provider? = false - {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true) + decoded_input = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true) decoded_input_data = decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input() @@ -383,7 +383,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do skip_sig_provider? = false - {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true) + decoded_input = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true) {mock_tx, decoded_input, decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input()} end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex index 5d334ee06c..42b3612532 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex @@ -27,7 +27,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do wrapped_max_fee_per_gas = Map.get(transaction, :wrapped_max_fee_per_gas) wrapped_value = Map.get(transaction, :wrapped_value) - {[wrapped_decoded_input], _, _} = + [wrapped_decoded_input] = Transaction.decode_transactions( [ %Transaction{ diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 2c921347f1..9b2b3a18dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do watchlist_names: watchlist_names }) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) + decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) %{ "items" => @@ -49,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do watchlist_names: watchlist_names }) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) + decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) transactions |> chain_type_transformations() @@ -61,7 +61,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transactions.json", %{transactions: transactions, next_page_params: next_page_params, conn: conn}) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) + decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) %{ "items" => @@ -81,7 +81,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transactions.json", %{transactions: transactions, conn: conn}) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) + decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) transactions |> chain_type_transformations() @@ -91,7 +91,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transaction.json", %{transaction: transaction, conn: conn}) do block_height = Chain.block_height(@api_true) - {[decoded_input], _, _} = Transaction.decode_transactions([transaction], false, @api_true) + [decoded_input] = Transaction.decode_transactions([transaction], false, @api_true) transaction |> chain_type_transformations() @@ -115,7 +115,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do - {decoded_transactions, _, _} = + decoded_transactions = Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true) %{ @@ -128,7 +128,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do - {decoded_transactions, _, _} = + decoded_transactions = Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true) token_transfers @@ -137,7 +137,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do - {[decoded_transaction], _, _} = Transaction.decode_transactions([token_transfer.transaction], true, @api_true) + [decoded_transaction] = Transaction.decode_transactions([token_transfer.transaction], true, @api_true) TokenTransferView.prepare_token_transfer(token_transfer, conn, decoded_transaction) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 07e6f73967..81b1b06ad1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -395,8 +395,7 @@ defmodule BlockScoutWeb.TransactionView do end def decoded_input_data(transaction) do - {result, _, _} = Transaction.decoded_input_data(transaction, []) - result + Transaction.decoded_input_data(transaction, []) end def decoded_revert_reason(revert_reason, transaction, options) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs index 96fcbe6889..de82d6c155 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs @@ -159,6 +159,8 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do end test "preloads to_address smart contract verified", %{conn: conn} do + TestHelper.get_eip1967_implementation_zero_addresses() + transaction = insert(:transaction_to_verified_contract) conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 346b2dd4fa..6472fe13b0 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1095,7 +1095,11 @@ defmodule Explorer.Chain do """ @spec hashes_to_addresses([Hash.Address.t()], [necessity_by_association_option | api?]) :: [Address.t()] - def hashes_to_addresses(hashes, options \\ []) when is_list(hashes) do + def hashes_to_addresses(hashes, options \\ []) + + def hashes_to_addresses([], _), do: [] + + def hashes_to_addresses(hashes, options) when is_list(hashes) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) query = diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 8d1c381571..021e62d4f7 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -109,14 +109,19 @@ defmodule Explorer.Chain.ContractMethod do @doc """ Finds contract methods by selector id """ - @spec find_contract_methods(binary(), [Chain.api?()]) :: [__MODULE__.t()] + @spec find_contract_methods([binary()], [Chain.api?()]) :: [__MODULE__.t()] + def find_contract_methods(method_ids, options) + + def find_contract_methods([], _), do: [] + def find_contract_methods(method_ids, options) do query = from( contract_method in __MODULE__, distinct: contract_method.identifier, where: contract_method.abi["type"] == "function", - where: contract_method.identifier in ^method_ids + where: contract_method.identifier in ^method_ids, + order_by: [asc: contract_method.identifier, asc: contract_method.inserted_at] ) Chain.select_repo(options).all(query) diff --git a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex index 88496d4cd3..f9676e1868 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex @@ -19,7 +19,6 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do transactions |> Transaction.decode_transactions(true, api?: true) - |> elem(0) |> Enum.zip(transactions) |> to_csv_format(address_hash, exchange_rate) |> Helper.dump_to_stream() diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 8fdb03cb8a..7ef3588844 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -224,7 +224,7 @@ defmodule Explorer.Chain.Log do else case Chain.find_contract_address(address_hash, address_options, false) do {:ok, %{smart_contract: smart_contract}} -> - full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, %{}, true, options) + full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, options) {full_abi, Map.put(acc, address_hash, full_abi)} _ -> diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex index 7e1e26d869..2412874ccb 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -498,54 +498,15 @@ defmodule Explorer.Chain.SmartContract.Proxy do @doc """ Returns combined ABI from proxy and implementation smart-contracts """ - @spec combine_proxy_implementation_abi(any(), map(), boolean(), any()) :: SmartContract.abi() + @spec combine_proxy_implementation_abi(any(), any()) :: SmartContract.abi() def combine_proxy_implementation_abi( smart_contract, - proxy_implementation_addresses_map \\ %{}, - fetch_proxy?, options \\ [] - ) - - def combine_proxy_implementation_abi( - %SmartContract{abi: abi} = smart_contract, - proxy_implementation_addresses_map, - fetch_proxy?, - options - ) - when not is_nil(abi) do - implementation_abi = - get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?) - - if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi - end - - def combine_proxy_implementation_abi(smart_contract, proxy_implementation_addresses_map, fetch_proxy?, options) do - get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?) - end - - defp get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?) do - if fetch_proxy? do - Proxy.get_implementation_abi_from_proxy(smart_contract, options) - else - implementations = - proxy_implementation_addresses_map - |> Map.get(smart_contract.address_hash) - - parse_abi_from_proxy_implementations(implementations) - end - end - - defp parse_abi_from_proxy_implementations(nil), do: [] + ) do + proxy_abi = (smart_contract && smart_contract.abi) || [] + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, options) - defp parse_abi_from_proxy_implementations(implementations) do - implementations - |> Enum.reduce([], fn implementation, acc -> - if implementation.smart_contract && implementation.smart_contract.abi do - acc ++ implementation.smart_contract.abi - else - acc - end - end) + proxy_abi ++ implementation_abi end defp find_input_by_name(inputs, name) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index fd5387e35a..a60fb55887 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -94,8 +94,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do @doc """ Returns all implementations for the given smart-contract address hashes """ - @spec get_proxy_implementations_for_multiple_proxies([Hash.Address.t()], Keyword.t()) :: __MODULE__.t() | nil - def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options \\ []) do + @spec get_proxy_implementations_for_multiple_proxies([Hash.Address.t()], Keyword.t()) :: [__MODULE__.t()] + def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options \\ []) + + def get_proxy_implementations_for_multiple_proxies([], _), do: [] + + def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options) do proxy_address_hashes |> get_proxy_implementations_by_multiple_hashes_query() |> select_repo(options).all() diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index f80a199eed..75883500c0 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -303,7 +303,6 @@ defmodule Explorer.Chain.Transaction do Data, DenormalizationHelper, Hash, - SmartContract, SmartContract.Proxy, TokenTransfer, Transaction, @@ -737,17 +736,14 @@ defmodule Explorer.Chain.Transaction do {:ok, identifier, text, mapping} _ -> - {result, _, _} = - decoded_input_data( - %Transaction{ - to_address: smart_contract, - hash: hash, - input: %Data{bytes: binary_revert_reason} - }, - options - ) - - result + decoded_input_data( + %Transaction{ + to_address: smart_contract, + hash: hash, + input: %Data{bytes: binary_revert_reason} + }, + options + ) end _ -> @@ -760,23 +756,19 @@ defmodule Explorer.Chain.Transaction do NotLoaded.t() | Transaction.t(), boolean(), [Chain.api?()], - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map - ) :: - {error_type | success_type, full_abi_acc, methods_acc} - when full_abi_acc: map(), - methods_acc: map(), - proxy_implementation_addresses_map: map(), + methods_map, + proxy_implementation_abi_map + ) :: error_type | success_type + when methods_map: map(), + proxy_implementation_abi_map: map(), error_type: {:error, any()} | {:error, :contract_not_verified | :contract_verified, list()}, success_type: {:ok | binary(), any()} | {:ok, binary(), binary(), list()} def decoded_input_data( tx, skip_sig_provider? \\ false, options, - full_abi_acc \\ %{}, - methods_acc \\ %{}, - proxy_implementation_addresses_map \\ %{} + methods_map \\ %{}, + proxy_implementation_abi_map \\ %{} ) # skip decoding if there is no to_address @@ -784,27 +776,25 @@ defmodule Explorer.Chain.Transaction do %__MODULE__{to_address: nil}, _, _, - full_abi_acc, - methods_acc, - _proxy_implementation_addresses_map + _, + _ ), - do: {{:error, :no_to_address}, full_abi_acc, methods_acc} + do: {:error, :no_to_address} # skip decoding if transaction is not loaded - def decoded_input_data(%NotLoaded{}, _, _, full_abi_acc, methods_acc, _proxy_implementation_addresses_map), - do: {{:error, :not_loaded}, full_abi_acc, methods_acc} + def decoded_input_data(%NotLoaded{}, _, _, _, _), + do: {:error, :not_loaded} # skip decoding if input is empty def decoded_input_data( %__MODULE__{input: %{bytes: bytes}}, _, _, - full_abi_acc, - methods_acc, - _proxy_implementation_addresses_map + _, + _ ) when bytes in [nil, <<>>] do - {{:error, :no_input_data}, full_abi_acc, methods_acc} + {:error, :no_input_data} end # skip decoding if to_address is not a contract unless DECODE_NOT_A_CONTRACT_CALLS is set @@ -813,11 +803,10 @@ defmodule Explorer.Chain.Transaction do %__MODULE__{to_address: %{contract_code: nil}}, _, _, - full_abi_acc, - methods_acc, - _proxy_implementation_addresses_map + _, + _ ), - do: {{:error, :not_a_contract_call}, full_abi_acc, methods_acc} + do: {:error, :not_a_contract_call} end # if to_address's smart_contract is nil reduce to the case when to_address is not loaded @@ -829,9 +818,8 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) do decoded_input_data( %__MODULE__{ @@ -841,9 +829,8 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) end @@ -856,9 +843,8 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) do decoded_input_data( %__MODULE__{ @@ -868,9 +854,8 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) end @@ -883,33 +868,26 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + _proxy_implementation_abi_map ) do - {methods, methods_acc} = - method_id - |> check_methods_cache(methods_acc, options) + methods = check_methods_cache(method_id, methods_map, options) candidates = methods |> Enum.flat_map(fn candidate -> case do_decoded_input_data( data, - %SmartContract{abi: [candidate.abi], address_hash: nil}, - hash, - options, - %{}, - proxy_implementation_addresses_map + [candidate.abi], + hash ) do - {{:ok, _, _, _} = decoded, _} -> [decoded] + {:ok, _, _, _} = decoded -> [decoded] _ -> [] end end) - {{:error, :contract_not_verified, - if(candidates == [], do: decode_function_call_via_sig_provider(input, hash, skip_sig_provider?), else: candidates)}, - full_abi_acc, methods_acc} + {:error, :contract_not_verified, + if(candidates == [], do: decode_function_call_via_sig_provider(input, hash, skip_sig_provider?), else: candidates)} end # if to_address is not loaded and input is not a method call return error @@ -917,11 +895,10 @@ defmodule Explorer.Chain.Transaction do %__MODULE__{to_address: %NotLoaded{}}, _, _, - full_abi_acc, - methods_acc, - _proxy_implementation_addresses_map + _, + _ ) do - {{:error, :contract_not_verified, []}, full_abi_acc, methods_acc} + {:error, :contract_not_verified, []} end def decoded_input_data( @@ -932,20 +909,14 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) do - case do_decoded_input_data( - data, - smart_contract, - hash, - options, - full_abi_acc, - proxy_implementation_addresses_map - ) do + full_abi = check_full_abi_cache(smart_contract, proxy_implementation_abi_map, options) + + case do_decoded_input_data(data, full_abi, hash) do # In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB - {{:error, error}, full_abi_acc} when error in [:could_not_decode, :no_matching_function] -> + {:error, error} when error in [:could_not_decode, :no_matching_function] -> case decoded_input_data( %__MODULE__{ to_address: %NotLoaded{}, @@ -954,22 +925,21 @@ defmodule Explorer.Chain.Transaction do }, skip_sig_provider?, options, - full_abi_acc, - methods_acc, - proxy_implementation_addresses_map + methods_map, + proxy_implementation_abi_map ) do - {{:error, :contract_not_verified, []}, full_abi_acc, methods_acc} -> - {decode_function_call_via_sig_provider_wrapper(input, hash, skip_sig_provider?), full_abi_acc, methods_acc} + {:error, :contract_not_verified, []} -> + decode_function_call_via_sig_provider_wrapper(input, hash, skip_sig_provider?) - {{:error, :contract_not_verified, candidates}, full_abi_acc, methods_acc} -> - {{:error, :contract_verified, candidates}, full_abi_acc, methods_acc} + {:error, :contract_not_verified, candidates} -> + {:error, :contract_verified, candidates} - {_, full_abi_acc, methods_acc} -> - {{:error, :could_not_decode}, full_abi_acc, methods_acc} + _ -> + {:error, :could_not_decode} end - {output, full_abi_acc} -> - {output, full_abi_acc, methods_acc} + output -> + output end end @@ -983,24 +953,13 @@ defmodule Explorer.Chain.Transaction do end end - defp do_decoded_input_data( - data, - smart_contract, - hash, - options, - full_abi_acc, - proxy_implementation_addresses_map \\ %{} - ) do - {full_abi, full_abi_acc} = - check_full_abi_cache(smart_contract, full_abi_acc, options, proxy_implementation_addresses_map) - - {with( - {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash), - {:ok, mapping} <- selector_mapping(selector, values, hash), - identifier <- Base.encode16(selector.method_id, case: :lower), - text <- function_call(selector.function, mapping), - do: {:ok, identifier, text, mapping} - ), full_abi_acc} + defp do_decoded_input_data(data, full_abi, hash) do + with {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash), + {:ok, mapping} <- selector_mapping(selector, values, hash), + identifier <- Base.encode16(selector.method_id, case: :lower), + text <- function_call(selector.function, mapping) do + {:ok, identifier, text, mapping} + end end defp decode_function_call_via_sig_provider(%{bytes: data} = input, hash, skip_sig_provider?) do @@ -1010,8 +969,7 @@ defmodule Explorer.Chain.Transaction do true <- is_list(result), false <- Enum.empty?(result), abi <- [result |> List.first() |> Map.put("outputs", []) |> Map.put("type", "function")], - {{:ok, _, _, _} = candidate, _} <- - do_decoded_input_data(data, %SmartContract{abi: abi, address_hash: nil}, hash, [], %{}) do + {:ok, _, _, _} = candidate <- do_decoded_input_data(data, abi, hash) do [candidate] else _ -> @@ -1019,39 +977,22 @@ defmodule Explorer.Chain.Transaction do end end - defp check_methods_cache(method_id, methods_acc, options) do - if Map.has_key?(methods_acc, method_id) do - {methods_acc[method_id], methods_acc} - else - candidates_query = ContractMethod.find_contract_method_query(method_id, 1) - - result = - candidates_query - |> Chain.select_repo(options).all() - - {result, Map.put(methods_acc, method_id, result)} - end + defp check_methods_cache(method_id, methods_map, options) do + Map.get_lazy(methods_map, method_id, fn -> + method_id + |> ContractMethod.find_contract_method_query(1) + |> Chain.select_repo(options).all() + end) end defp check_full_abi_cache( - %{address_hash: address_hash} = smart_contract, - full_abi_acc, - options, - proxy_implementation_addresses_map + smart_contract, + proxy_implementation_abi_map, + options ) do - if !is_nil(address_hash) && Map.has_key?(full_abi_acc, address_hash) do - {full_abi_acc[address_hash], full_abi_acc} - else - full_abi = - Proxy.combine_proxy_implementation_abi( - smart_contract, - proxy_implementation_addresses_map, - false, - options - ) - - {full_abi, Map.put(full_abi_acc, address_hash, full_abi)} - end + Map.get_lazy(proxy_implementation_abi_map, smart_contract, fn -> + Proxy.combine_proxy_implementation_abi(smart_contract, options) + end) end def get_method_name( @@ -1071,10 +1012,10 @@ defmodule Explorer.Chain.Transaction do true, [] ) do - {{:error, :contract_not_verified, [{:ok, _method_id, decoded_func, _}]}, _, _} -> + {:error, :contract_not_verified, [{:ok, _method_id, decoded_func, _}]} -> parse_method_name(decoded_func) - {{:error, :contract_not_verified, []}, _, _} -> + {:error, :contract_not_verified, []} -> "0x" <> Base.encode16(method_id, case: :lower) _ -> @@ -2000,35 +1941,59 @@ defmodule Explorer.Chain.Transaction do end @doc """ - Receives as input list of transactions and returns tuple {decoded_input_data, abi_acc, methods_acc} + Receives as input list of transactions and returns decoded_input_data Where - `decoded_input_data` is list of results: either `{:ok, _identifier, _text, _mapping}` or `nil` - - `abi_acc` is list of all smart contracts ABIs fetched during decoding - - `methods_acc` is list of all smart contracts methods fetched from `contract_methods` table during decoding """ - @spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: {[any()], map(), map()} + @spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: [nil | {:ok, String.t(), String.t(), map()}] def decode_transactions(transactions, skip_sig_provider?, opts) do - proxy_implementation_addresses_map = combine_proxy_implementation_addresses_map(transactions) - - {results, abi_acc, methods_acc} = - Enum.reduce(transactions, {[], %{}, %{}}, fn transaction, {results, abi_acc, methods_acc} -> - {result, abi_acc, methods_acc} = - decoded_input_data( - transaction, - skip_sig_provider?, - opts, - abi_acc, - methods_acc, - proxy_implementation_addresses_map - ) + proxy_implementation_abi_map = combine_proxy_implementation_abi_map(transactions) - {[format_decoded_input(result) | results], abi_acc, methods_acc} + # first we assemble an empty methods map, so that decoded_input_data will skip ContractMethod.t() lookup and decoding + empty_methods_map = + transactions + |> Enum.flat_map(fn + %{input: <>} -> [method_id] + _ -> [] end) + |> Enum.into(%{}, &{&1, []}) - {Enum.reverse(results), abi_acc, methods_acc} + # try to decode transaction using full abi data from proxy_implementation_abi_map + decoded_transactions = + transactions + |> Enum.map(fn transaction -> + transaction + |> decoded_input_data(skip_sig_provider?, opts, empty_methods_map, proxy_implementation_abi_map) + |> format_decoded_input() + end) + |> Enum.zip(transactions) + + # assemble a new methods map from methods in non-decoded transactions + methods_map = + decoded_transactions + |> Enum.flat_map(fn + {nil, %{input: <>}} -> [method_id] + _ -> [] + end) + |> Enum.uniq() + |> ContractMethod.find_contract_methods(opts) + |> Enum.into(%{}, &{&1.identifier, [&1]}) + + # decode remaining transaction using methods map + decoded_transactions + |> Enum.map(fn + {nil, transaction} -> + transaction + |> Map.put(:to_address, %NotLoaded{}) + |> decoded_input_data(skip_sig_provider?, opts, methods_map, proxy_implementation_abi_map) + |> format_decoded_input() + + {decoded, _} -> + decoded + end) end - defp combine_proxy_implementation_addresses_map(transactions) do + defp combine_proxy_implementation_abi_map(transactions) do # parse unique address hashes of smart-contracts from to_address and created_contract_address properties of the transactions list unique_to_address_hashes = transactions @@ -2043,32 +2008,34 @@ defmodule Explorer.Chain.Transaction do multiple_proxy_implementations = Implementation.get_proxy_implementations_for_multiple_proxies(unique_to_address_hashes) - # query from the DB address objects with smart_contract preload for all found above implementation addresses - implementation_addresses_with_smart_contracts = + # query from the DB address objects with smart_contract preload for all found above proxy and implementation addresses + addresses_with_smart_contracts = multiple_proxy_implementations |> Enum.flat_map(fn proxy_implementations -> proxy_implementations.address_hashes end) + |> Enum.concat(unique_to_address_hashes) |> Chain.hashes_to_addresses(necessity_by_association: %{smart_contract: :optional}) |> Enum.into(%{}, &{&1.hash, &1}) - # combine map %{proxy_address_hash => the list of implementations as Address.t() object with preloaded SmartContract.t()} + # combine map %{proxy_address_hash => combined proxy abi} multiple_proxy_implementations - |> Enum.reduce(%{}, fn proxy_implementations, proxy_implementation_addresses_map -> - implementation_addresses_with_smart_contract_preload = - proxy_implementations.address_hashes - |> Enum.map(fn implementation_address_hash -> - Map.get(implementation_addresses_with_smart_contracts, implementation_address_hash) + |> Enum.into(%{}, fn proxy_implementations -> + full_abi = + [proxy_implementations.proxy_address_hash | proxy_implementations.address_hashes] + |> Enum.map(&Map.get(addresses_with_smart_contracts, &1)) + |> Enum.flat_map(fn + %{smart_contract: %{abi: abi}} when is_list(abi) -> abi + _ -> [] end) |> Enum.filter(&(!is_nil(&1))) - proxy_implementation_addresses_map - |> Map.put(proxy_implementations.proxy_address_hash, implementation_addresses_with_smart_contract_preload) + {proxy_implementations.proxy_address_hash, full_abi} end) end @doc """ Receives as input result of decoded_input_data/5, returns either nil or decoded input in format: {:ok, _identifier, _text, _mapping} """ - @spec format_decoded_input(any()) :: nil | tuple() + @spec format_decoded_input(any()) :: nil | {:ok, String.t(), String.t(), map()} def format_decoded_input({:error, _, []}), do: nil def format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) def format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs index 7893139d5b..506ba65a1f 100644 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs @@ -102,6 +102,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do assert implementation_1.updated_at == implementation_2.updated_at && contract_1.updated_at == contract_2.updated_at + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) end test "get_implementation/1 for twins contract" do diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs index 1e7ea53887..b38977f85b 100644 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs @@ -131,36 +131,36 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do } ] - test "combine_proxy_implementation_abi/4 returns empty [] abi if proxy abi is null" do + test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do proxy_contract_address = insert(:contract_address) - assert Proxy.combine_proxy_implementation_abi( - %SmartContract{address_hash: proxy_contract_address.hash, abi: nil}, - %{}, - false - ) == + assert Proxy.combine_proxy_implementation_abi(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) == [] end - test "combine_proxy_implementation_abi/4 returns [] abi for unverified proxy" do + test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do + TestHelper.get_eip1967_implementation_zero_addresses() + proxy_contract_address = insert(:contract_address) smart_contract = insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == [] + assert Proxy.combine_proxy_implementation_abi(smart_contract) == [] end - test "combine_proxy_implementation_abi/4 returns proxy abi if implementation is not verified" do + test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do + TestHelper.get_eip1967_implementation_zero_addresses() + proxy_contract_address = insert(:contract_address) smart_contract = insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == @proxy_abi + assert Proxy.combine_proxy_implementation_abi(smart_contract) == @proxy_abi end - test "combine_proxy_implementation_abi/4 returns proxy + implementation abi if implementation is verified" do + test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do proxy_contract_address = insert(:contract_address) proxy_smart_contract = @@ -176,9 +176,6 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do name: "impl" ) - implementation_contract_address_with_smart_contract_preload = - implementation_contract_address |> Repo.preload(:smart_contract) - insert(:proxy_implementation, proxy_address_hash: proxy_contract_address.hash, proxy_type: "eip1167", @@ -186,19 +183,7 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do names: [implementation_smart_contract.name] ) - _implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - proxy_implementation_addresses_map = - %{} - |> Map.put(proxy_contract_address.hash, [implementation_contract_address_with_smart_contract_preload]) - - combined_abi = - Proxy.combine_proxy_implementation_abi( - proxy_smart_contract, - proxy_implementation_addresses_map, - false - ) + combined_abi = Proxy.combine_proxy_implementation_abi(proxy_smart_contract) assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false @@ -482,6 +467,13 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) end defp eip_1967_beacon_proxy_mock_requests( diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index febe9e703f..69a456402e 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -252,7 +252,7 @@ defmodule Explorer.Chain.TransactionTest do test "that a transaction that is not a contract call returns a commensurate error" do transaction = insert(:transaction) - assert {{:error, :not_a_contract_call}, _, _} = Transaction.decoded_input_data(transaction, []) + assert {:error, :not_a_contract_call} = Transaction.decoded_input_data(transaction, []) end test "that a contract call transaction that has no verified contract returns a commensurate error" do @@ -261,20 +261,24 @@ defmodule Explorer.Chain.TransactionTest do |> insert(to_address: insert(:contract_address), input: "0x1234567891") |> Repo.preload(to_address: :smart_contract) - assert {{:error, :contract_not_verified, []}, _, _} = Transaction.decoded_input_data(transaction, []) + assert {:error, :contract_not_verified, []} = Transaction.decoded_input_data(transaction, []) end test "that a contract call transaction that has a verified contract returns the decoded input data" do + TestHelper.get_eip1967_implementation_zero_addresses() + transaction = :transaction_to_verified_contract |> insert() |> Repo.preload(to_address: :smart_contract) - assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]}, _, _} = + assert {:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]} = Transaction.decoded_input_data(transaction, []) end test "that a contract call will look up a match in contract_methods table" do + TestHelper.get_eip1967_implementation_zero_addresses() + :transaction_to_verified_contract |> insert() |> Repo.preload(to_address: :smart_contract) @@ -291,11 +295,13 @@ defmodule Explorer.Chain.TransactionTest do |> insert(to_address: contract.address, input: "0x" <> input_data) |> Repo.preload(to_address: :smart_contract) - assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]}, _, _} = + assert {:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]} = Transaction.decoded_input_data(transaction, []) end test "arguments name in function call replaced with argN if it's empty string" do + TestHelper.get_eip1967_implementation_zero_addresses() + contract = insert(:smart_contract, contract_code_md5: "123", @@ -323,7 +329,7 @@ defmodule Explorer.Chain.TransactionTest do |> insert(to_address: contract.address, input: "0x" <> input_data) |> Repo.preload(to_address: :smart_contract) - assert {{:ok, "60fe47b1", "set(uint256 arg0)", [{"arg0", "uint256", 10}]}, _, _} = + assert {:ok, "60fe47b1", "set(uint256 arg0)", [{"arg0", "uint256", 10}]} = Transaction.decoded_input_data(transaction, []) end end