perf: Improve performance of transactions list page (#10734)

* perf: Improve performance of the transactions list page

* Update apps/explorer/lib/explorer/chain/transaction.ex

Co-authored-by: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>

* Mix format

* Update apps/explorer/lib/explorer/chain/smart_contract/proxy.ex

Co-authored-by: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>

* Refactoring

* Process review comment

* Second refactoring

* Revert latest refactoring

* Fix nil address_hash

* Update apps/explorer/lib/explorer/chain/transaction.ex

Co-authored-by: Kirill Fedoseev <kirill@blockscout.com>

* Remove credo comment

* Update apps/explorer/lib/explorer/chain/smart_contract/proxy.ex

Co-authored-by: Kirill Fedoseev <kirill@blockscout.com>

* tuple list to map

---------

Co-authored-by: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>
Co-authored-by: Kirill Fedoseev <kirill@blockscout.com>
pull/10764/head
Victor Baranov 2 months ago committed by GitHub
parent 889d511737
commit 680bb0960e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
  2. 2
      apps/explorer/lib/explorer/chain/log.ex
  3. 49
      apps/explorer/lib/explorer/chain/smart_contract/proxy.ex
  4. 22
      apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex
  5. 177
      apps/explorer/lib/explorer/chain/transaction.ex
  6. 83
      apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs
  7. 6
      apps/explorer/test/explorer/chain/transaction_test.exs

@ -159,8 +159,6 @@ 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))

@ -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, options)
full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, %{}, true, options)
{full_abi, Map.put(acc, address_hash, full_abi)}
_ ->

@ -480,17 +480,54 @@ defmodule Explorer.Chain.SmartContract.Proxy do
@doc """
Returns combined ABI from proxy and implementation smart-contracts
"""
@spec combine_proxy_implementation_abi(any(), any()) :: SmartContract.abi()
def combine_proxy_implementation_abi(smart_contract, options \\ [])
@spec combine_proxy_implementation_abi(any(), map(), boolean(), 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, options) when not is_nil(abi) do
implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, 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(_, _) do
[]
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: []
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)
end
defp find_input_by_name(inputs, name) do

@ -17,7 +17,6 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
alias Explorer.Chain.{Address, Hash, SmartContract}
alias Explorer.Chain.SmartContract.Proxy
alias Explorer.Chain.SmartContract.Proxy.Models.Implementation
alias Explorer.Counters.AverageBlockTime
alias Explorer.Repo
alias Timex.Duration
@ -91,6 +90,16 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
|> select_repo(options).one()
end
@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
proxy_address_hashes
|> get_proxy_implementations_by_multiple_hashes_query()
|> select_repo(options).all()
end
@doc """
Returns the last implementation updated_at for the given smart-contract address hash
"""
@ -109,6 +118,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
)
end
defp get_proxy_implementations_by_multiple_hashes_query(proxy_address_hashes) do
from(
p in __MODULE__,
where: p.proxy_address_hash in ^proxy_address_hashes
)
end
@doc """
Returns implementation address, name and proxy type for the given SmartContract
"""
@ -210,7 +226,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
{:ok, :error} ->
format_proxy_implementations_response(proxy_implementations)
{:ok, %Implementation{} = result} ->
{:ok, %__MODULE__{} = result} ->
format_proxy_implementations_response(result)
_ ->
@ -289,7 +305,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
Saves proxy's implementation into the DB
"""
@spec save_implementation_data([String.t()], Hash.Address.t(), atom() | nil, Keyword.t()) ::
Implementation.t() | :empty | :error
__MODULE__.t() | :empty | :error
def save_implementation_data(:error, _proxy_address_hash, _proxy_type, _options) do
:error
end

@ -6,6 +6,8 @@ defmodule Explorer.Chain.Transaction.Schema do
- Explorer.Chain.Import.Runner.Transactions
"""
alias Explorer.Chain
alias Explorer.Chain.{
Address,
Beacon.BlobTransaction,
@ -302,6 +304,8 @@ defmodule Explorer.Chain.Transaction do
Wei
}
alias Explorer.Chain.SmartContract.Proxy.Models.Implementation
alias Explorer.SmartContract.SigProviderInterface
@optional_attrs ~w(max_priority_fee_per_gas max_fee_per_gas block_hash block_number
@ -751,28 +755,59 @@ defmodule Explorer.Chain.Transaction do
boolean(),
[Chain.api?()],
full_abi_acc,
methods_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(),
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 \\ %{})
def decoded_input_data(
tx,
skip_sig_provider? \\ false,
options,
full_abi_acc \\ %{},
methods_acc \\ %{},
proxy_implementation_addresses_map \\ %{}
)
def decoded_input_data(%__MODULE__{to_address: nil}, _, _, full_abi_acc, methods_acc),
do: {{:error, :no_to_address}, full_abi_acc, methods_acc}
def decoded_input_data(
%__MODULE__{to_address: nil},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
),
do: {{:error, :no_to_address}, full_abi_acc, methods_acc}
def decoded_input_data(%NotLoaded{}, _, _, full_abi_acc, methods_acc),
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(%__MODULE__{input: %{bytes: bytes}}, _, _, full_abi_acc, methods_acc)
when bytes in [nil, <<>>],
do: {{:error, :no_input_data}, full_abi_acc, methods_acc}
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}
end
if not Application.compile_env(:explorer, :decode_not_a_contract_calls) do
def decoded_input_data(%__MODULE__{to_address: %{contract_code: nil}}, _, _, full_abi_acc, methods_acc),
do: {{:error, :not_a_contract_call}, full_abi_acc, methods_acc}
def decoded_input_data(
%__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}
end
def decoded_input_data(
@ -784,7 +819,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
) do
decoded_input_data(
%__MODULE__{
@ -795,7 +831,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
)
end
@ -808,7 +845,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
) do
decoded_input_data(
%__MODULE__{
@ -819,7 +857,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
)
end
@ -832,7 +871,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
) do
{methods, methods_acc} =
method_id
@ -841,7 +881,14 @@ defmodule Explorer.Chain.Transaction do
candidates =
methods
|> Enum.flat_map(fn candidate ->
case do_decoded_input_data(data, %SmartContract{abi: [candidate.abi], address_hash: nil}, hash, options, %{}) do
case do_decoded_input_data(
data,
%SmartContract{abi: [candidate.abi], address_hash: nil},
hash,
options,
%{},
proxy_implementation_addresses_map
) do
{{:ok, _, _, _} = decoded, _} -> [decoded]
_ -> []
end
@ -852,7 +899,14 @@ defmodule Explorer.Chain.Transaction do
full_abi_acc, methods_acc}
end
def decoded_input_data(%__MODULE__{to_address: %NotLoaded{}}, _, _, full_abi_acc, methods_acc) do
def decoded_input_data(
%__MODULE__{to_address: %NotLoaded{}},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
) do
{{:error, :contract_not_verified, []}, full_abi_acc, methods_acc}
end
@ -865,9 +919,17 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_map
) do
case do_decoded_input_data(data, smart_contract, hash, options, full_abi_acc) do
case do_decoded_input_data(
data,
smart_contract,
hash,
options,
full_abi_acc,
proxy_implementation_addresses_map
) 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, :could_not_decode}, full_abi_acc} ->
case decoded_input_data(
@ -879,7 +941,8 @@ defmodule Explorer.Chain.Transaction do
skip_sig_provider?,
options,
full_abi_acc,
methods_acc
methods_acc,
proxy_implementation_addresses_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}
@ -906,8 +969,16 @@ defmodule Explorer.Chain.Transaction do
end
end
defp do_decoded_input_data(data, smart_contract, hash, options, full_abi_acc) do
{full_abi, full_abi_acc} = check_full_abi_cache(smart_contract, full_abi_acc, options)
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),
@ -948,11 +1019,22 @@ defmodule Explorer.Chain.Transaction do
end
end
defp check_full_abi_cache(%{address_hash: address_hash} = smart_contract, full_abi_acc, options) do
defp check_full_abi_cache(
%{address_hash: address_hash} = smart_contract,
full_abi_acc,
options,
proxy_implementation_addresses_map
) 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, options)
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
@ -1911,10 +1993,19 @@ defmodule Explorer.Chain.Transaction do
"""
@spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: {[any()], map(), 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)
decoded_input_data(
transaction,
skip_sig_provider?,
opts,
abi_acc,
methods_acc,
proxy_implementation_addresses_map
)
{[format_decoded_input(result) | results], abi_acc, methods_acc}
end)
@ -1922,6 +2013,42 @@ defmodule Explorer.Chain.Transaction do
{Enum.reverse(results), abi_acc, methods_acc}
end
defp combine_proxy_implementation_addresses_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
|> Enum.flat_map(fn
%Transaction{to_address: %Address{hash: hash}} -> [hash]
%Transaction{created_contract_address: %Address{hash: hash}} -> [hash]
_ -> []
end)
|> Enum.uniq()
# query from the DB proxy implementation objects for those address hashes
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 =
multiple_proxy_implementations
|> Enum.flat_map(fn proxy_implementations -> proxy_implementations.address_hashes end)
|> 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()}
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)
end)
proxy_implementation_addresses_map
|> Map.put(proxy_implementations.proxy_address_hash, implementation_addresses_with_smart_contract_preload)
end)
end
@doc """
Receives as input result of decoded_input_data/5, returns either nil or decoded input in format: {:ok, _identifier, _text, _mapping}
"""

@ -131,70 +131,74 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
}
]
test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do
test "combine_proxy_implementation_abi/4 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}) ==
assert Proxy.combine_proxy_implementation_abi(
%SmartContract{address_hash: proxy_contract_address.hash, abi: nil},
%{},
false
) ==
[]
end
test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do
test "combine_proxy_implementation_abi/4 returns [] abi for unverified proxy" do
proxy_contract_address = insert(:contract_address)
smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
TestHelper.get_eip1967_implementation_zero_addresses()
assert Proxy.combine_proxy_implementation_abi(smart_contract) == []
assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == []
end
test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do
test "combine_proxy_implementation_abi/4 returns proxy abi if implementation is not verified" do
proxy_contract_address = insert(:contract_address)
smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
TestHelper.get_eip1967_implementation_zero_addresses()
assert Proxy.combine_proxy_implementation_abi(smart_contract) == @proxy_abi
assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == @proxy_abi
end
test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do
test "combine_proxy_implementation_abi/4 returns proxy + implementation abi if implementation is verified" do
proxy_contract_address = insert(:contract_address)
smart_contract =
proxy_smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
implementation_contract_address = insert(:contract_address)
insert(:smart_contract,
address_hash: implementation_contract_address.hash,
abi: @implementation_abi,
contract_code_md5: "123"
implementation_smart_contract =
insert(:smart_contract,
address_hash: implementation_contract_address.hash,
abi: @implementation_abi,
contract_code_md5: "123",
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",
address_hashes: [implementation_contract_address.hash],
names: [implementation_smart_contract.name]
)
implementation_contract_address_hash_string =
_implementation_contract_address_hash_string =
Base.encode16(implementation_contract_address.hash.bytes, case: :lower)
TestHelper.get_eip1967_implementation_zero_addresses()
proxy_implementation_addresses_map =
%{}
|> Map.put(proxy_contract_address.hash, [implementation_contract_address_with_smart_contract_preload])
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x000000000000000000000000" <> implementation_contract_address_hash_string
}
]}
end
)
combined_abi = Proxy.combine_proxy_implementation_abi(smart_contract)
combined_abi =
Proxy.combine_proxy_implementation_abi(
proxy_smart_contract,
proxy_implementation_addresses_map,
false
)
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
@ -212,17 +216,6 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
[]
end
test "get_implementation_abi_from_proxy/2 returns [] abi for unverified proxy" do
proxy_contract_address = insert(:contract_address)
smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
TestHelper.get_eip1967_implementation_zero_addresses()
assert Proxy.combine_proxy_implementation_abi(smart_contract) == []
end
test "get_implementation_abi_from_proxy/2 returns [] if implementation is not verified" do
proxy_contract_address = insert(:contract_address)

@ -270,8 +270,6 @@ defmodule Explorer.Chain.TransactionTest do
|> insert()
|> Repo.preload(to_address: :smart_contract)
TestHelper.get_eip1967_implementation_zero_addresses()
assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]}, _, _} =
Transaction.decoded_input_data(transaction, [])
end
@ -293,8 +291,6 @@ defmodule Explorer.Chain.TransactionTest do
|> insert(to_address: contract.address, input: "0x" <> input_data)
|> Repo.preload(to_address: :smart_contract)
TestHelper.get_eip1967_implementation_zero_addresses()
assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]}, _, _} =
Transaction.decoded_input_data(transaction, [])
end
@ -327,8 +323,6 @@ defmodule Explorer.Chain.TransactionTest do
|> insert(to_address: contract.address, input: "0x" <> input_data)
|> Repo.preload(to_address: :smart_contract)
TestHelper.get_eip1967_implementation_zero_addresses()
assert {{:ok, "60fe47b1", "set(uint256 arg0)", [{"arg0", "uint256", 10}]}, _, _} =
Transaction.decoded_input_data(transaction, [])
end

Loading…
Cancel
Save