From 17649bb3284d4fd513588b75b487a66f6656b2a4 Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Tue, 17 Sep 2024 15:01:03 +0300 Subject: [PATCH 01/10] feat: support EIP-7702 transactions --- .../api/v2/transaction_controller.ex | 4 +- .../lib/block_scout_web/paging_helper.ex | 3 +- .../views/api/v2/transaction_view.ex | 43 ++++++- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 27 +++++ .../lib/ethereum_jsonrpc/transaction.ex | 13 ++- .../import/runner/signed_authorizations.ex | 110 ++++++++++++++++++ .../chain/import/stage/block_referencing.ex | 3 +- .../explorer/chain/signed_authorization.ex | 53 +++++++++ .../lib/explorer/chain/transaction.ex | 6 + ...904161254_create_signed_authorizations.exs | 23 ++++ apps/indexer/lib/indexer/block/fetcher.ex | 48 +++++++- 11 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex create mode 100644 apps/explorer/lib/explorer/chain/signed_authorization.ex create mode 100644 apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 43179c26c823..b1c2a19d87da 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -109,7 +109,9 @@ defmodule BlockScoutWeb.API.V2.TransactionController do @spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do necessity_by_association_with_actions = - Map.put(@transaction_necessity_by_association, :transaction_actions, :optional) + @transaction_necessity_by_association + |> Map.put(:transaction_actions, :optional) + |> Map.put(:signed_authorizations, :optional) necessity_by_association = case Application.get_env(:explorer, :chain_type) do diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 25dec818db16..8d565201e538 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -19,7 +19,8 @@ defmodule BlockScoutWeb.PagingHelper do "contract_creation", "token_transfer", "token_creation", - "blob_transaction" + "blob_transaction", + "set_code_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 c950a69fa53c..f1e8186610bc 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 @@ -210,6 +210,12 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end + def render("authorization_list.json", %{signed_authorizations: signed_authorizations}) do + signed_authorizations + |> Enum.sort_by(& &1.index, :asc) + |> Enum.map(&prepare_signed_authorization/1) + end + @doc """ Decodes list of logs """ @@ -336,6 +342,18 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end + def prepare_signed_authorization(signed_authorization) do + %{ + "address" => signed_authorization.address, + "chain_id" => signed_authorization.chain_id, + "nonce" => signed_authorization.nonce, + "r" => signed_authorization.r, + "s" => signed_authorization.s, + "v" => signed_authorization.v, + "authority" => signed_authorization.authority + } + end + defp get_tx_hash(%Transaction{} = tx), do: to_string(tx.hash) defp get_tx_hash(hash), do: to_string(hash) @@ -450,7 +468,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "method" => Transaction.method_name(transaction, decoded_input), "tx_types" => tx_types(transaction), "tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_tx? && conn)), - "has_error_in_internal_txs" => transaction.has_error_in_internal_txs + "has_error_in_internal_txs" => transaction.has_error_in_internal_txs, + "authorizationList" => authorization_list(transaction.signed_authorizations) } result @@ -482,6 +501,13 @@ defmodule BlockScoutWeb.API.V2.TransactionView do render("transaction_actions.json", %{actions: actions}) end + def authorization_list(nil), do: [] + def authorization_list(%NotLoaded{}), do: [] + + def authorization_list(signed_authorizations) do + render("authorization_list.json", %{signed_authorizations: signed_authorizations}) + end + defp burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) do if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do if Decimal.compare(max_fee_per_gas.value, 0) == :eq do @@ -608,7 +634,20 @@ defmodule BlockScoutWeb.API.V2.TransactionView do | :token_creation | :token_transfer | :blob_transaction - def tx_types(tx, types \\ [], stage \\ :blob_transaction) + | :set_code_transaction + def tx_types(tx, types \\ [], stage \\ :set_code_transaction) + + def tx_types(%Transaction{type: type} = tx, types, :set_code_transaction) do + # EIP-7702 set code transaction type + types = + if type == 4 do + [:set_code_transaction | types] + else + types + end + + tx_types(tx, types, :blob_transaction) + end def tx_types(%Transaction{type: type} = tx, types, :blob_transaction) do # EIP-2718 blob transaction type diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 954036218db5..fc31bb2aff29 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -144,6 +144,18 @@ defmodule EthereumJSONRPC do """ @type request_id :: String.t() | non_neg_integer() + @typedoc """ + EIP-7702 signed authorization data. + """ + @type signed_authorization :: %{ + chain_id: non_neg_integer(), + address: address(), + nonce: non_neg_integer(), + r: non_neg_integer(), + s: non_neg_integer(), + v: non_neg_integer() + } + @doc """ Execute smart contract functions. @@ -508,6 +520,21 @@ defmodule EthereumJSONRPC do integer end + @doc """ + Converts `map()/0` to `t:signed_authorization/0` + """ + @spec to_signed_authorization(map()) :: signed_authorization() + def to_signed_authorization(map) do + %{ + chain_id: quantity_to_integer(map["chainId"]), + address: map["address"], + nonce: quantity_to_integer(map["nonce"]), + r: quantity_to_integer(map["r"]), + s: quantity_to_integer(map["s"]), + v: quantity_to_integer(map["v"]) + } + end + @doc """ A request payload for a JSONRPC. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index f835c4fb6fbf..2d76f023cec6 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -105,6 +105,7 @@ defmodule EthereumJSONRPC.Transaction do * `"maxPriorityFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max priority fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"maxFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"type"` - `t:EthereumJSONRPC.quantity/0` denotes transaction type. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) + * `"authorizationList"` - `t:list/0` of `t:EthereumJSONRPC.signed_authorization/0` authorization tuples. Introduced in [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) #{case Application.compile_env(:explorer, :chain_type) do :ethereum -> """ * `"maxFeePerBlobGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of blob gas used. Introduced in [EIP-4844](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md) @@ -150,7 +151,8 @@ defmodule EthereumJSONRPC.Transaction do transaction_index: non_neg_integer(), max_priority_fee_per_gas: non_neg_integer(), max_fee_per_gas: non_neg_integer(), - type: non_neg_integer() + type: non_neg_integer(), + authorization_list: [EthereumJSONRPC.signed_authorization()] } @doc """ @@ -322,7 +324,8 @@ defmodule EthereumJSONRPC.Transaction do {"block_timestamp", :block_timestamp}, {"r", :r}, {"s", :s}, - {"v", :v} + {"v", :v}, + {"authorizationList", :authorization_list} ]) end @@ -368,7 +371,8 @@ defmodule EthereumJSONRPC.Transaction do {"block_timestamp", :block_timestamp}, {"r", :r}, {"s", :s}, - {"v", :v} + {"v", :v}, + {"authorizationList", :authorization_list} ]) end @@ -700,6 +704,9 @@ defmodule EthereumJSONRPC.Transaction do end end + defp entry_to_elixir({"authorizationList" = key, value}), + do: {key, value |> Enum.map(&EthereumJSONRPC.to_signed_authorization/1)} + # Celo-specific fields if Application.compile_env(:explorer, :chain_type) == :celo do defp entry_to_elixir({key, value}) diff --git a/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex new file mode 100644 index 000000000000..01a804eee619 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex @@ -0,0 +1,110 @@ +defmodule Explorer.Chain.Import.Runner.SignedAuthorizations do + @moduledoc """ + Bulk imports `t:Explorer.Chain.SignedAuthorization.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.{Import, SignedAuthorization} + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [SignedAuthorization.t()] + + @impl Import.Runner + def ecto_schema_module, do: SignedAuthorization + + @impl Import.Runner + def option_key, do: :signed_authorizations + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :signed_authorizations, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :logs, + :logs + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [SignedAuthorization.t()]} + | {:error, [Changeset.t()]} + defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + conflict_target = [:transaction_hash, :index] + + {:ok, _} = + Import.insert_changes_list( + repo, + changes_list, + for: SignedAuthorization, + on_conflict: on_conflict, + conflict_target: conflict_target, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + from( + authorization in SignedAuthorization, + update: [ + set: [ + chain_id: fragment("EXCLUDED.chain_id"), + address: fragment("EXCLUDED.address"), + nonce: fragment("EXCLUDED.nonce"), + r: fragment("EXCLUDED.r"), + s: fragment("EXCLUDED.s"), + v: fragment("EXCLUDED.v"), + authority: fragment("EXCLUDED.authority"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", authorization.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", authorization.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.chain_id, EXCLUDED.address, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.v, EXCLUDED.authority) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + authorization.chain_id, + authorization.address, + authorization.nonce, + authorization.r, + authorization.s, + authorization.v, + authorization.authority + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex index df3b0c264d79..621a712168b4 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex @@ -14,7 +14,8 @@ defmodule Explorer.Chain.Import.Stage.BlockReferencing do Runner.Tokens, Runner.TokenInstances, Runner.TransactionActions, - Runner.Withdrawals + Runner.Withdrawals, + Runner.SignedAuthorizations ] @extra_runners_by_chain_type %{ diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex new file mode 100644 index 000000000000..d57b6a7990e4 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -0,0 +1,53 @@ +defmodule Explorer.Chain.SignedAuthorization do + @moduledoc "Models a transaction extension with authorization tuples from eip7702 set code transactions." + + use Explorer.Schema + + alias Explorer.Chain.{Hash, Transaction} + + @required_attrs ~w(transaction_hash index chain_id address nonce r s v authority)a + + @type t :: %__MODULE__{ + transaction_hash: Hash.Full, + index: :integer, + chain_id: :integer, + address: Hash.Address, + nonce: :integer, + r: :decimal, + s: :decimal, + v: :integer, + authority: Hash.Address + } + + @primary_key false + schema "signed_authorizations" do + field(:index, :integer) + field(:chain_id, :integer) + field(:address, Hash.Address) + field(:nonce, :integer) + field(:r, :decimal) + field(:s, :decimal) + field(:v, :integer) + field(:authority, Hash.Address) + + belongs_to(:transaction, Transaction, + foreign_key: :transaction_hash, + primary_key: true, + references: :hash, + type: Hash.Full + ) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = struct, attrs \\ %{}) do + struct + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:transaction_hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 59f0ec68f40b..742201650e9b 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -16,6 +16,7 @@ defmodule Explorer.Chain.Transaction.Schema do Hash, InternalTransaction, Log, + SignedAuthorization, TokenTransfer, TransactionAction, Wei @@ -270,6 +271,11 @@ defmodule Explorer.Chain.Transaction.Schema do type: Hash.Address ) + has_many(:signed_authorizations, SignedAuthorization, + foreign_key: :transaction_hash, + references: :hash + ) + unquote_splicing(@chain_type_fields) end end diff --git a/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs b/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs new file mode 100644 index 000000000000..d41817bccbeb --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Migrations.CreateSignedAuthorizations do + use Ecto.Migration + + def change do + create table(:signed_authorizations, primary_key: false) do + add(:transaction_hash, references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), + null: false, + primary_key: true + ) + + add(:index, :integer, null: false, primary_key: true) + add(:chain_id, :bigint, null: false) + add(:address, :bytea, null: false) + add(:nonce, :integer, null: false) + add(:v, :integer, null: false) + add(:r, :numeric, precision: 100, null: false) + add(:s, :numeric, precision: 100, null: false) + add(:authority, :bytea, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index b8a4d716b201..a71d9586cc64 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -234,7 +234,8 @@ defmodule Indexer.Block.Fetcher do tokens: %{params: tokens}, transactions: %{params: transactions_with_receipts}, withdrawals: %{params: withdrawals_params}, - token_instances: %{params: token_instances} + token_instances: %{params: token_instances}, + signed_authorizations: %{params: extract_signed_authorizations(transactions_with_receipts)} }, chain_type_import_options = %{ transactions_with_receipts: transactions_with_receipts, @@ -739,4 +740,49 @@ defmodule Indexer.Block.Fetcher do Map.put(token_transfer, :token, token) end) end + + defp extract_signed_authorizations(transactions_with_receipts) do + transactions_with_receipts + |> Enum.filter(&Map.has_key?(&1, :authorization_list)) + |> Enum.map( + &(&1.authorization_list + |> Enum.with_index() + |> Enum.map(fn {authorization, index} -> + authorization + |> Map.merge(%{ + transaction_hash: &1.hash, + index: index, + authority: recover_authority(authorization) + }) + end)) + ) + |> List.flatten() + end + + defp recover_authority(signed_authorization) do + eip7702_magic = 0x5 + {:ok, %{bytes: address}} = Hash.Address.cast(signed_authorization.address) + + signed_message = + ExKeccak.hash_256( + <> <> ExRLP.encode([signed_authorization.chain_id, address, signed_authorization.nonce]) + ) + + authority = + ec_recover(signed_message, signed_authorization.r, signed_authorization.s, signed_authorization.v) + + authority + end + + defp ec_recover(signed_message, r, s, v) do + r_bytes = <> + s_bytes = <> + + {:ok, <<_compression::bytes-size(1), public_key::binary>>} = + ExSecp256k1.recover(signed_message, r_bytes, s_bytes, v) + + <<_::bytes-size(12), hash::binary>> = ExKeccak.hash_256(public_key) + address = Base.encode16(hash, case: :lower) + "0x" <> address + end end From 38c57fda56bed816a75fc0c9c7f89ade8f4ddeac Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Wed, 18 Sep 2024 15:37:06 +0300 Subject: [PATCH 02/10] fix: handle invalid signatures --- .../lib/explorer/chain/signed_authorization.ex | 2 +- ...20240904161254_create_signed_authorizations.exs | 2 +- apps/indexer/lib/indexer/block/fetcher.ex | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex index d57b6a7990e4..aec1eb058c9d 100644 --- a/apps/explorer/lib/explorer/chain/signed_authorization.ex +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -5,7 +5,7 @@ defmodule Explorer.Chain.SignedAuthorization do alias Explorer.Chain.{Hash, Transaction} - @required_attrs ~w(transaction_hash index chain_id address nonce r s v authority)a + @required_attrs ~w(transaction_hash index chain_id address nonce r s v)a @type t :: %__MODULE__{ transaction_hash: Hash.Full, diff --git a/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs b/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs index d41817bccbeb..be633ffbe42a 100644 --- a/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs +++ b/apps/explorer/priv/repo/migrations/20240904161254_create_signed_authorizations.exs @@ -15,7 +15,7 @@ defmodule Explorer.Repo.Migrations.CreateSignedAuthorizations do add(:v, :integer, null: false) add(:r, :numeric, precision: 100, null: false) add(:s, :numeric, precision: 100, null: false) - add(:authority, :bytea, null: false) + add(:authority, :bytea, null: true) timestamps(null: false, type: :utc_datetime_usec) end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index a71d9586cc64..9e61b5c9c6a6 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -778,11 +778,13 @@ defmodule Indexer.Block.Fetcher do r_bytes = <> s_bytes = <> - {:ok, <<_compression::bytes-size(1), public_key::binary>>} = - ExSecp256k1.recover(signed_message, r_bytes, s_bytes, v) - - <<_::bytes-size(12), hash::binary>> = ExKeccak.hash_256(public_key) - address = Base.encode16(hash, case: :lower) - "0x" <> address + with {:ok, <<_compression::bytes-size(1), public_key::binary>>} <- + ExSecp256k1.recover(signed_message, r_bytes, s_bytes, v), + <<_::bytes-size(12), hash::binary>> <- ExKeccak.hash_256(public_key) do + address = Base.encode16(hash, case: :lower) + "0x" <> address + else + _ -> nil + end end end From 50b65270c8d6b307216a4cb86cc39a60a7309a3c Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Wed, 18 Sep 2024 16:21:00 +0300 Subject: [PATCH 03/10] fix: save authority --- apps/explorer/lib/explorer/chain/signed_authorization.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex index aec1eb058c9d..e638c88a0fd2 100644 --- a/apps/explorer/lib/explorer/chain/signed_authorization.ex +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -5,6 +5,7 @@ defmodule Explorer.Chain.SignedAuthorization do alias Explorer.Chain.{Hash, Transaction} + @optional_attrs ~w(authority)a @required_attrs ~w(transaction_hash index chain_id address nonce r s v)a @type t :: %__MODULE__{ @@ -46,7 +47,11 @@ defmodule Explorer.Chain.SignedAuthorization do @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() def changeset(%__MODULE__{} = struct, attrs \\ %{}) do struct - |> cast(attrs, @required_attrs) + |> cast( + attrs, + @required_attrs ++ + @optional_attrs + ) |> validate_required(@required_attrs) |> foreign_key_constraint(:transaction_hash) end From 4bfa2c9e7847722bbbdc1c71ad9ddfa392228dfe Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Mon, 23 Sep 2024 11:54:50 +0300 Subject: [PATCH 04/10] Update apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex Co-authored-by: Kirill Fedoseev --- .../lib/block_scout_web/views/api/v2/transaction_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f1e8186610bc..645e0d0f56f8 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 @@ -469,7 +469,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "tx_types" => tx_types(transaction), "tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_tx? && conn)), "has_error_in_internal_txs" => transaction.has_error_in_internal_txs, - "authorizationList" => authorization_list(transaction.signed_authorizations) + "authorization_list" => authorization_list(transaction.signed_authorizations) } result From 5ab3bebada28229d356930212ab4dbfdea9b282a Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Mon, 23 Sep 2024 11:55:20 +0300 Subject: [PATCH 05/10] Update apps/explorer/lib/explorer/chain/signed_authorization.ex Co-authored-by: Kirill Fedoseev --- apps/explorer/lib/explorer/chain/signed_authorization.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex index e638c88a0fd2..a479e7cdb6a7 100644 --- a/apps/explorer/lib/explorer/chain/signed_authorization.ex +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -22,7 +22,7 @@ defmodule Explorer.Chain.SignedAuthorization do @primary_key false schema "signed_authorizations" do - field(:index, :integer) + field(:index, :integer, primary_key: true) field(:chain_id, :integer) field(:address, Hash.Address) field(:nonce, :integer) From 64be8b2986a5aa37bc11d61e57d7c939ab3fa62f Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Mon, 23 Sep 2024 11:56:18 +0300 Subject: [PATCH 06/10] Update apps/indexer/lib/indexer/block/fetcher.ex Co-authored-by: Kirill Fedoseev --- apps/indexer/lib/indexer/block/fetcher.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 9e61b5c9c6a6..180f12de6915 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -744,7 +744,7 @@ defmodule Indexer.Block.Fetcher do defp extract_signed_authorizations(transactions_with_receipts) do transactions_with_receipts |> Enum.filter(&Map.has_key?(&1, :authorization_list)) - |> Enum.map( + |> Enum.flat_map( &(&1.authorization_list |> Enum.with_index() |> Enum.map(fn {authorization, index} -> @@ -756,7 +756,6 @@ defmodule Indexer.Block.Fetcher do }) end)) ) - |> List.flatten() end defp recover_authority(signed_authorization) do From 086c8bdc2137aca92690851b7446ad8f1f939102 Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Mon, 23 Sep 2024 15:34:55 +0300 Subject: [PATCH 07/10] fix: remove set_code_transaction from @allowed_type_labels --- apps/block_scout_web/lib/block_scout_web/paging_helper.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 8d565201e538..25dec818db16 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -19,8 +19,7 @@ defmodule BlockScoutWeb.PagingHelper do "contract_creation", "token_transfer", "token_creation", - "blob_transaction", - "set_code_transaction" + "blob_transaction" ] _ -> From 80b6a5bcf6d009ca7663663000bb9af5f75170d7 Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Tue, 1 Oct 2024 11:38:00 +0300 Subject: [PATCH 08/10] Update apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex Co-authored-by: Kirill Fedoseev --- .../lib/explorer/chain/import/runner/signed_authorizations.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex index 01a804eee619..147f5c821dc7 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex @@ -45,8 +45,8 @@ defmodule Explorer.Chain.Import.Runner.SignedAuthorizations do Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, :block_referencing, - :logs, - :logs + :signed_authorizations, + :signed_authorizations ) end) end From d3eff0dfd41cebc52ea35651a435c0297ae94b37 Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Tue, 1 Oct 2024 15:02:11 +0300 Subject: [PATCH 09/10] fix: move signed_authorization to a separate module --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 27 ------------ .../ethereum_jsonrpc/signed_authorization.ex | 44 +++++++++++++++++++ .../lib/ethereum_jsonrpc/transaction.ex | 22 +++++++--- 3 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/signed_authorization.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index fc31bb2aff29..954036218db5 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -144,18 +144,6 @@ defmodule EthereumJSONRPC do """ @type request_id :: String.t() | non_neg_integer() - @typedoc """ - EIP-7702 signed authorization data. - """ - @type signed_authorization :: %{ - chain_id: non_neg_integer(), - address: address(), - nonce: non_neg_integer(), - r: non_neg_integer(), - s: non_neg_integer(), - v: non_neg_integer() - } - @doc """ Execute smart contract functions. @@ -520,21 +508,6 @@ defmodule EthereumJSONRPC do integer end - @doc """ - Converts `map()/0` to `t:signed_authorization/0` - """ - @spec to_signed_authorization(map()) :: signed_authorization() - def to_signed_authorization(map) do - %{ - chain_id: quantity_to_integer(map["chainId"]), - address: map["address"], - nonce: quantity_to_integer(map["nonce"]), - r: quantity_to_integer(map["r"]), - s: quantity_to_integer(map["s"]), - v: quantity_to_integer(map["v"]) - } - end - @doc """ A request payload for a JSONRPC. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/signed_authorization.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/signed_authorization.ex new file mode 100644 index 000000000000..85559fe3c54d --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/signed_authorization.ex @@ -0,0 +1,44 @@ +defmodule EthereumJSONRPC.SignedAuthorization do + @moduledoc """ + The format of authorization tuples returned for + set code transactions [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). + """ + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + @typedoc """ + * `"chainId"` - specifies the chain for which the authorization was created `t:EthereumJSONRPC.quantity/0`. + * `"address"` - `t:EthereumJSONRPC.address/0` of the delegate contract. + * `"nonce"` - signature nonce `t:EthereumJSONRPC.quantity/0`. + * `"v"` - v component of the signature `t:EthereumJSONRPC.quantity/0`. + * `"r"` - r component of the signature `t:EthereumJSONRPC.quantity/0`. + * `"s"` - s component of the signature `t:EthereumJSONRPC.quantity/0`. + """ + @type t :: %{ + String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.quantity() + } + + @type params :: %{ + chain_id: non_neg_integer(), + address: EthereumJSONRPC.address(), + nonce: non_neg_integer(), + r: non_neg_integer(), + s: non_neg_integer(), + v: non_neg_integer() + } + + @doc """ + Converts `t:t/0` to `t:params/0` + """ + @spec to_params(t()) :: params() + def to_params(raw) do + %{ + chain_id: quantity_to_integer(raw["chainId"]), + address: raw["address"], + nonce: quantity_to_integer(raw["nonce"]), + r: quantity_to_integer(raw["r"]), + s: quantity_to_integer(raw["s"]), + v: quantity_to_integer(raw["v"]) + } + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 2d76f023cec6..c9351e6d7b71 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -16,6 +16,7 @@ defmodule EthereumJSONRPC.Transaction do ] alias EthereumJSONRPC + alias EthereumJSONRPC.SignedAuthorization case Application.compile_env(:explorer, :chain_type) do :ethereum -> @@ -75,7 +76,13 @@ defmodule EthereumJSONRPC.Transaction do end @type elixir :: %{ - String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.hash() | String.t() | non_neg_integer() | nil + String.t() => + EthereumJSONRPC.address() + | EthereumJSONRPC.hash() + | String.t() + | non_neg_integer() + | [SignedAuthorization.params()] + | nil } @typedoc """ @@ -105,7 +112,7 @@ defmodule EthereumJSONRPC.Transaction do * `"maxPriorityFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max priority fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"maxFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"type"` - `t:EthereumJSONRPC.quantity/0` denotes transaction type. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) - * `"authorizationList"` - `t:list/0` of `t:EthereumJSONRPC.signed_authorization/0` authorization tuples. Introduced in [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) + * `"authorizationList"` - `t:list/0` of `t:EthereumJSONRPC.SignedAuthorization.t/0` authorization tuples. Introduced in [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) #{case Application.compile_env(:explorer, :chain_type) do :ethereum -> """ * `"maxFeePerBlobGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of blob gas used. Introduced in [EIP-4844](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md) @@ -129,7 +136,12 @@ defmodule EthereumJSONRPC.Transaction do """ @type t :: %{ String.t() => - EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil + EthereumJSONRPC.address() + | EthereumJSONRPC.hash() + | EthereumJSONRPC.quantity() + | String.t() + | [SignedAuthorization.t()] + | nil } @type params :: %{ @@ -152,7 +164,7 @@ defmodule EthereumJSONRPC.Transaction do max_priority_fee_per_gas: non_neg_integer(), max_fee_per_gas: non_neg_integer(), type: non_neg_integer(), - authorization_list: [EthereumJSONRPC.signed_authorization()] + authorization_list: [SignedAuthorization.params()] } @doc """ @@ -705,7 +717,7 @@ defmodule EthereumJSONRPC.Transaction do end defp entry_to_elixir({"authorizationList" = key, value}), - do: {key, value |> Enum.map(&EthereumJSONRPC.to_signed_authorization/1)} + do: {key, value |> Enum.map(&SignedAuthorization.to_params/1)} # Celo-specific fields if Application.compile_env(:explorer, :chain_type) == :celo do From b8d427c4e83f776079f5d39c899493196aa59994 Mon Sep 17 00:00:00 2001 From: Alexander Filippov Date: Wed, 2 Oct 2024 18:17:36 +0300 Subject: [PATCH 10/10] add todo --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index c9351e6d7b71..1ad8b544cc83 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -75,6 +75,8 @@ defmodule EthereumJSONRPC.Transaction do @chain_type_fields quote(do: []) end + # todo: Check if it's possible to simplify by avoiding t -> elixir -> params conversions + # and directly convert t -> params. @type elixir :: %{ String.t() => EthereumJSONRPC.address()