Merge pull request #8898 from blockscout/vb-fix-decoding

Enhance method decoding by candidates from DB
pull/8789/head
nikitosing 12 months ago committed by GitHub
commit a03c444e02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 75
      apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex
  4. 8
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  5. 3
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  6. 4
      apps/block_scout_web/priv/gettext/default.pot
  7. 4
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  8. 13
      apps/explorer/lib/explorer/chain/contract_method.ex
  9. 140
      apps/explorer/lib/explorer/chain/log.ex
  10. 4
      apps/explorer/lib/explorer/chain/smart_contract/proxy.ex
  11. 9
      apps/explorer/lib/explorer/chain/transaction.ex

@ -24,5 +24,5 @@ lib/indexer/fetcher/zkevm/transaction_batch.ex:252
lib/block_scout_web/views/api/v2/transaction_view.ex:431
lib/block_scout_web/views/api/v2/transaction_view.ex:472
lib/explorer/chain/transaction.ex:167
lib/explorer/chain/transaction.ex:1457
lib/explorer/chain/transaction.ex:1458
lib/explorer/chain/transaction.ex:1452
lib/explorer/chain/transaction.ex:1453

@ -15,6 +15,7 @@
- [#8917](https://github.com/blockscout/blockscout/pull/8917) - Proxy detection hotfix in API v2
- [#8915](https://github.com/blockscout/blockscout/pull/8915) - smart-contract: delete embeds_many relation on replace
- [#8906](https://github.com/blockscout/blockscout/pull/8906) - Fix abi encoded string argument
- [#8898](https://github.com/blockscout/blockscout/pull/8898) - Enhance method decoding by candidates from DB
- [#8882](https://github.com/blockscout/blockscout/pull/8882) - Change order of proxy contracts patterns detection: existing popular EIPs to the top of the list
- [#8707](https://github.com/blockscout/blockscout/pull/8707) - Fix native coin exchange rate with `EXCHANGE_RATES_COINGECKO_COIN_ID`

@ -27,46 +27,43 @@
) %>
</h3>
</dd>
<%= case decoded_result do %>
<% {:error, :could_not_decode} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<div class="alert alert-danger">
<%= gettext "Failed to decode log data." %>
</div>
<% {:ok, method_id, text, mapping} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<tr>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td>
</tr>
<tr>
<td>Call</td>
<td colspan="3"><code><%= text %></code></td>
</tr>
</table>
<%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %>
<% {:error, :contract_not_verified, results} -> %>
<%= for {:ok, method_id, text, mapping} <- results do %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<tr>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td>
</tr>
<tr>
<td>Call</td>
<td colspan="3"><code><%= text %></code></td>
</tr>
</table>
<%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %>
</div>
<%= case decoded_result do %>
<% {:error, :could_not_decode} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<div class="alert alert-danger">
<%= gettext "Failed to decode log data." %>
</div>
<% {:ok, method_id, text, mapping} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<tr>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td>
</tr>
<tr>
<td>Call</td>
<td colspan="3"><code><%= text %></code></td>
</tr>
</table>
<%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %>
<% {:error, :contract_not_verified, results} -> %>
<%= for {:ok, method_id, text, mapping} <- results do %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<tr>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td>
</tr>
<tr>
<td>Call</td>
<td colspan="3"><code><%= text %></code></td>
</tr>
</table>
<%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %>
<% end %>
<% _ -> %>
<%= nil %>
<% end %>
<dt class="col-md-2"><%= gettext "Topics" %></dt>
<dd class="col-md-10">

@ -497,6 +497,14 @@ defmodule BlockScoutWeb.AddressView do
def contract_interaction_disabled?, do: Application.get_env(:block_scout_web, :contract)[:disable_interaction]
@doc """
Decodes given log
"""
@spec decode(Log.t(), Transaction.t()) ::
{:ok, String.t(), String.t(), map()}
| {:error, atom()}
| {:error, atom(), list()}
| {{:error, :contract_not_verified, list()}, any()}
def decode(log, transaction) do
{result, _contracts_acc, _events_acc} = Log.decode(log, transaction, [], true)
result

@ -184,7 +184,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
end
def decode_logs(logs, skip_sig_provider?) do
defp decode_logs(logs, skip_sig_provider?) do
{result, _, _} =
Enum.reduce(logs, {[], %{}, %{}}, fn log, {results, contracts_acc, events_acc} ->
{result, contracts_acc, events_acc} =
@ -648,7 +648,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
defp format_decoded_input(_), do: nil
defp format_decoded_log_input({:error, :could_not_decode}), do: nil
defp format_decoded_log_input({:error, :no_matching_function}), do: nil
defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded
defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0)

@ -1049,7 +1049,7 @@ msgstr ""
msgid "Daily Transactions"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:101
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7
#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:121
@ -2962,7 +2962,7 @@ msgstr ""
msgid "Topic"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:71
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:91
#, elixir-autogen, elixir-format
msgid "Topics"

@ -1049,7 +1049,7 @@ msgstr ""
msgid "Daily Transactions"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:101
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7
#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:121
@ -2962,7 +2962,7 @@ msgstr ""
msgid "Topic"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:71
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:91
#, elixir-autogen, elixir-format
msgid "Topics"

@ -5,6 +5,7 @@ defmodule Explorer.Chain.ContractMethod do
require Logger
import Ecto.Query, only: [from: 2]
use Explorer.Schema
alias Explorer.Chain.{Hash, MethodIdentifier, SmartContract}
@ -69,6 +70,18 @@ defmodule Explorer.Chain.ContractMethod do
end
end
@doc """
Finds limited number of contract methods by selector id
"""
@spec find_contract_method_query(binary(), integer()) :: Ecto.Query.t()
def find_contract_method_query(method_id, limit) do
from(
contract_method in __MODULE__,
where: contract_method.identifier == ^method_id,
limit: ^limit
)
end
defp abi_element_to_contract_method(element) do
case ABI.parse_specification([element], include_events?: true) do
[selector] ->

@ -7,7 +7,7 @@ defmodule Explorer.Chain.Log do
alias ABI.{Event, FunctionSelector}
alias Explorer.Chain
alias Explorer.Chain.{Address, Block, ContractMethod, Data, Hash, Transaction}
alias Explorer.Chain.{Address, Block, ContractMethod, Data, Hash, Log, Transaction}
alias Explorer.Chain.SmartContract.Proxy
alias Explorer.SmartContract.SigProviderInterface
@ -121,33 +121,47 @@ defmodule Explorer.Chain.Log do
@doc """
Decode transaction log data.
"""
@spec decode(Log.t(), Transaction.t(), any(), boolean, map(), map()) ::
{{:ok, String.t(), String.t(), map()}
| {:error, atom()}
| {:error, atom(), list()}
| {{:error, :contract_not_verified, list()}, any()}, map(), map()}
def decode(log, transaction, options, skip_sig_provider?, contracts_acc \\ %{}, events_acc \\ %{}) do
case check_cache(contracts_acc, log.address_hash, options) do
{nil, contracts_acc} ->
{result, events_acc} = find_candidates(log, transaction, options, events_acc)
{result, contracts_acc, events_acc}
{full_abi, contracts_acc} ->
with {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping) do
{{:ok, identifier, text, mapping}, contracts_acc, events_acc}
else
{:error, :could_not_decode} ->
case find_candidates(log, transaction, options, events_acc) do
{{:error, :contract_not_verified, []}, events_acc} ->
{decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc}
with {full_abi, contracts_acc} <- check_cache(contracts_acc, log.address_hash, options),
{:no_abi, false} <- {:no_abi, is_nil(full_abi)},
{:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction.hash),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping) do
{{:ok, identifier, text, mapping}, contracts_acc, events_acc}
else
{:error, _} = error ->
handle_method_decode_error(error, log, transaction, options, skip_sig_provider?, contracts_acc, events_acc)
{:no_abi, true} ->
handle_method_decode_error(
{:error, :could_not_decode},
log,
transaction,
options,
skip_sig_provider?,
contracts_acc,
events_acc
)
end
end
{{:error, :contract_not_verified, candidates}, events_acc} ->
{{:error, :contract_verified, candidates}, contracts_acc, events_acc}
defp handle_method_decode_error(error, log, transaction, options, skip_sig_provider?, contracts_acc, events_acc) do
case error do
{:error, _reason} ->
case find_method_candidates(log, transaction, options, events_acc, skip_sig_provider?) do
{{:error, :contract_not_verified, []}, events_acc} ->
{decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc}
{_, events_acc} ->
{decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc}
end
{{:error, :contract_not_verified, candidates}, events_acc} ->
{{:error, :contract_not_verified, candidates}, contracts_acc, events_acc}
{:error, reason} ->
{{:error, reason}, contracts_acc, events_acc}
{_, events_acc} ->
{decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc}
end
end
end
@ -175,36 +189,30 @@ defmodule Explorer.Chain.Log do
end
end
defp find_candidates(log, transaction, options, events_acc) do
case log.first_topic do
"0x" <> hex_part ->
case Integer.parse(hex_part, 16) do
{number, ""} ->
<<method_id::binary-size(4), _rest::binary>> = :binary.encode_unsigned(number)
check_events_cache(events_acc, method_id, log, transaction, options)
_ ->
{{:error, :could_not_decode}, events_acc}
end
defp find_method_candidates(log, transaction, options, events_acc, skip_sig_provider?) do
with "0x" <> hex_part <- log.first_topic,
{number, ""} <- Integer.parse(hex_part, 16) do
<<method_id::binary-size(4), _rest::binary>> = :binary.encode_unsigned(number)
_ ->
{{:error, :could_not_decode}, events_acc}
if Map.has_key?(events_acc, method_id) do
{events_acc[method_id], events_acc}
else
result = find_method_candidates_from_db(method_id, log, transaction, options, skip_sig_provider?)
{result, Map.put(events_acc, method_id, result)}
end
else
_ -> {{:error, :could_not_decode}, events_acc}
end
end
defp find_candidates_query(method_id, log, transaction, options) do
candidates_query =
from(
contract_method in ContractMethod,
where: contract_method.identifier == ^method_id,
limit: 3
)
defp find_method_candidates_from_db(method_id, log, transaction, options, skip_sig_provider?) do
candidates_query = ContractMethod.find_contract_method_query(method_id, 3)
candidates =
candidates_query
|> Chain.select_repo(options).all()
|> Enum.flat_map(fn contract_method ->
case find_and_decode([contract_method.abi], log, transaction) do
case find_and_decode([contract_method.abi], log, transaction.hash) do
{:ok, selector, mapping} ->
identifier = Base.encode16(selector.method_id, case: :lower)
text = function_call(selector.function, mapping)
@ -218,21 +226,19 @@ defmodule Explorer.Chain.Log do
|> Enum.take(1)
{:error, :contract_not_verified,
if(candidates == [], do: decode_event_via_sig_provider(log, transaction, true), else: candidates)}
if(candidates == [],
do:
if(skip_sig_provider?,
do: [],
else: decode_event_via_sig_provider(log, transaction, true)
),
else: candidates
)}
end
defp check_events_cache(events_acc, method_id, log, transaction, options) do
if Map.has_key?(events_acc, method_id) do
{events_acc[method_id], events_acc}
else
result = find_candidates_query(method_id, log, transaction, options)
{result, Map.put(events_acc, method_id, result)}
end
end
@spec find_and_decode([map()], __MODULE__.t(), Transaction.t()) ::
@spec find_and_decode([map()], __MODULE__.t(), Hash.t()) ::
{:error, any} | {:ok, ABI.FunctionSelector.t(), any}
def find_and_decode(abi, log, transaction) do
def find_and_decode(abi, log, transaction_hash) do
with {%FunctionSelector{} = selector, mapping} <-
abi
|> ABI.parse_specification(include_events?: true)
@ -249,8 +255,8 @@ defmodule Explorer.Chain.Log do
e ->
Logger.warn(fn ->
[
"Could not decode input data for log from transaction: ",
Hash.to_iodata(transaction.hash),
"Could not decode input data for log from transaction hash: ",
Hash.to_iodata(transaction_hash),
Exception.format(:error, e, __STACKTRACE__)
]
end)
@ -262,12 +268,7 @@ defmodule Explorer.Chain.Log do
text =
mapping
|> Stream.map(fn {name, type, indexed?, _value} ->
indexed_keyword =
if indexed? do
["indexed "]
else
[]
end
indexed_keyword = if indexed?, do: ["indexed "], else: []
[type, " ", indexed_keyword, name]
end)
@ -276,7 +277,12 @@ defmodule Explorer.Chain.Log do
IO.iodata_to_binary([name, "(", text, ")"])
end
defp decode_event_via_sig_provider(log, transaction, only_candidates?, skip_sig_provider? \\ false) do
defp decode_event_via_sig_provider(
log,
transaction,
only_candidates?,
skip_sig_provider? \\ false
) do
with true <- SigProviderInterface.enabled?(),
false <- skip_sig_provider?,
{:ok, result} <-
@ -292,7 +298,7 @@ defmodule Explorer.Chain.Log do
true <- is_list(result),
false <- Enum.empty?(result),
abi <- [result |> List.first() |> Map.put("type", "event")],
{:ok, selector, mapping} <- find_and_decode(abi, log, transaction),
{:ok, selector, mapping} <- find_and_decode(abi, log, transaction.hash),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping) do
if only_candidates? do

@ -267,6 +267,10 @@ defmodule Explorer.Chain.SmartContract.Proxy do
end)
end
@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 \\ [])
def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract, options) when not is_nil(abi) do

@ -774,12 +774,7 @@ defmodule Explorer.Chain.Transaction do
if Map.has_key?(methods_acc, method_id) do
{methods_acc[method_id], methods_acc}
else
candidates_query =
from(
contract_method in ContractMethod,
where: contract_method.identifier == ^method_id,
limit: 1
)
candidates_query = ContractMethod.find_contract_method_query(method_id, 1)
result =
candidates_query
@ -1181,7 +1176,7 @@ defmodule Explorer.Chain.Transaction do
Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc ->
case Log.fetch_log_by_tx_hash_and_first_topic(transaction.hash, @transaction_fee_event_signature, @api_true) do
fee_log when not is_nil(fee_log) ->
{:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction)
{:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction.hash)
[{"token", "address", false, token_address_hash}, _, _, _, _, _] = mapping

Loading…
Cancel
Save