feat: precompiled contracts ABI import (#9899)

* Initial implementation of precompiled contracts ABI import

* Documention added

* address Sobelow finding

* another attempt address Sobelow finding

* Apply evident suggestions from code review

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

* Fix afer merge

* Small inconsistency in spec

* different path for different mix environment

* fix for formatting issue

---------

Co-authored-by: Kirill Fedoseev <kirill@blockscout.com>
pull/9977/head
Alexander Kolotov 7 months ago committed by GitHub
parent 30d0bb5668
commit 6f5dc3b5d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 89
      apps/explorer/lib/explorer/chain/smart_contract.ex
  2. 239
      apps/explorer/lib/explorer/chain_spec/genesis_data.ex
  3. 60
      apps/explorer/lib/explorer/chain_spec/geth/importer.ex
  4. 30
      apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
  5. 130
      config/assets/precompiles-arbitrum.json
  6. 15
      config/runtime.exs
  7. 1
      docker/Dockerfile

@ -861,24 +861,39 @@ defmodule Explorer.Chain.SmartContract do
end end
@doc """ @doc """
Inserts a `t:SmartContract.t/0`. Inserts a new smart contract and associated data into the database.
As part of inserting a new smart contract, an additional record is inserted for This function creates a new smart contract entry in the database. It calculates an MD5 hash of
naming the address for reference. the contract's bytecode, upserts contract methods, and handles the linkage of external libraries and
additional secondary sources. It also updates the associated address to mark the contract as
verified and manages the naming records for the address.
## Parameters
- `attrs`: Attributes for the new smart contract.
- `external_libraries`: A list of external libraries used by the contract.
- `secondary_sources`: Additional source data related to the contract.
## Returns
- `{:ok, smart_contract}` on successful insertion.
- `{:error, data}` on failure, returning the changeset or, if any issues happen during setting the address as verified, an error message.
""" """
@spec create_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} @spec create_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()}
def create_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do def create_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do
new_contract = %__MODULE__{} new_contract = %__MODULE__{}
# Updates contract attributes with calculated MD5 for the contract's bytecode
attrs = attrs =
attrs attrs
|> Helper.add_contract_code_md5() |> Helper.add_contract_code_md5()
# Prepares changeset and extends it with external libraries.
# As part of changeset preparation and verification, contract methods are upserted
smart_contract_changeset = smart_contract_changeset =
new_contract new_contract
|> __MODULE__.changeset(attrs) |> __MODULE__.changeset(attrs)
|> Changeset.put_change(:external_libraries, external_libraries) |> Changeset.put_change(:external_libraries, external_libraries)
# Prepares changesets for additional sources associated with the contract
new_contract_additional_source = %SmartContractAdditionalSource{} new_contract_additional_source = %SmartContractAdditionalSource{}
smart_contract_additional_sources_changesets = smart_contract_additional_sources_changesets =
@ -894,7 +909,10 @@ defmodule Explorer.Chain.SmartContract do
address_hash = Changeset.get_field(smart_contract_changeset, :address_hash) address_hash = Changeset.get_field(smart_contract_changeset, :address_hash)
# Enforce ShareLocks tables order (see docs: sharelocks.md) # Prepares the queries to update Explorer.Chain.Address to mark the contract as
# verified, clear the primary flag for the contract address in
# Explorer.Chain.Address.Name if any (enforce ShareLocks tables order (see
# docs: sharelocks.md)) and insert the contract details.
insert_contract_query = insert_contract_query =
Multi.new() Multi.new()
|> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end) |> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end)
@ -903,6 +921,8 @@ defmodule Explorer.Chain.SmartContract do
end) end)
|> Multi.insert(:smart_contract, smart_contract_changeset) |> Multi.insert(:smart_contract, smart_contract_changeset)
# Updates the queries from the previous step with inserting additional sources
# of the contract
insert_contract_query_with_additional_sources = insert_contract_query_with_additional_sources =
smart_contract_additional_sources_changesets smart_contract_additional_sources_changesets
|> Enum.with_index() |> Enum.with_index()
@ -910,10 +930,12 @@ defmodule Explorer.Chain.SmartContract do
Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset)
end) end)
# Applying the queries to the database
insert_result = insert_result =
insert_contract_query_with_additional_sources insert_contract_query_with_additional_sources
|> Repo.transaction() |> Repo.transaction()
# Set the primary mark for the contract name
AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash)
case insert_result do case insert_result do
@ -929,16 +951,28 @@ defmodule Explorer.Chain.SmartContract do
end end
@doc """ @doc """
Updates a `t:SmartContract.t/0`. Updates an existing smart contract and associated data into the database.
Has the similar logic as create_smart_contract/1. This function is similar to `create_smart_contract/1` but is used for updating an existing smart
Used in cases when you need to update row in DB contains SmartContract, e.g. in case of changing contract, such as changing its verification status from `partially verified` to `fully verified`.
status `partially verified` to `fully verified` (re-verify). It handles the updates including external libraries and secondary sources associated with the contract.
Notably, it updates contract methods based on the new ABI provided: if the new ABI does not contain
some of the previously listed methods, those methods are retained in the database.
## Parameters
- `attrs`: Attributes for the smart contract to be updated.
- `external_libraries`: A list of external libraries associated with the contract.
- `secondary_sources`: A list of secondary source data associated with the contract.
## Returns
- `{:ok, smart_contract}` on successful update.
- `{:error, changeset}` on failure, indicating issues with the data provided for update.
""" """
@spec update_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} @spec update_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()}
def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do
address_hash = Map.get(attrs, :address_hash) address_hash = Map.get(attrs, :address_hash)
# Removes all additional sources associated with the contract
query_sources = query_sources =
from( from(
source in SmartContractAdditionalSource, source in SmartContractAdditionalSource,
@ -947,14 +981,20 @@ defmodule Explorer.Chain.SmartContract do
_delete_sources = Repo.delete_all(query_sources) _delete_sources = Repo.delete_all(query_sources)
# Retrieve the existing smart contract
query = get_smart_contract_query(address_hash) query = get_smart_contract_query(address_hash)
smart_contract = Repo.one(query) smart_contract = Repo.one(query)
# Updates existing changeset and extends it with external libraries.
# As part of changeset preparation and verification, contract methods are
# updated as so if new ABI does not contain some of previous methods, they
# are still kept in the database
smart_contract_changeset = smart_contract_changeset =
smart_contract smart_contract
|> __MODULE__.changeset(attrs) |> __MODULE__.changeset(attrs)
|> Changeset.put_change(:external_libraries, external_libraries) |> Changeset.put_change(:external_libraries, external_libraries)
# Prepares changesets for additional sources associated with the contract
new_contract_additional_source = %SmartContractAdditionalSource{} new_contract_additional_source = %SmartContractAdditionalSource{}
smart_contract_additional_sources_changesets = smart_contract_additional_sources_changesets =
@ -968,7 +1008,9 @@ defmodule Explorer.Chain.SmartContract do
[] []
end end
# Enforce ShareLocks tables order (see docs: sharelocks.md) # Prepares the queries to clear the primary flag for the contract address in
# Explorer.Chain.Address.Name if any (enforce ShareLocks tables order (see
# docs: sharelocks.md)) and updated the contract details.
insert_contract_query = insert_contract_query =
Multi.new() Multi.new()
|> Multi.run(:clear_primary_address_names, fn repo, _ -> |> Multi.run(:clear_primary_address_names, fn repo, _ ->
@ -976,6 +1018,8 @@ defmodule Explorer.Chain.SmartContract do
end) end)
|> Multi.update(:smart_contract, smart_contract_changeset) |> Multi.update(:smart_contract, smart_contract_changeset)
# Updates the queries from the previous step with inserting additional sources
# of the contract
insert_contract_query_with_additional_sources = insert_contract_query_with_additional_sources =
smart_contract_additional_sources_changesets smart_contract_additional_sources_changesets
|> Enum.with_index() |> Enum.with_index()
@ -983,10 +1027,12 @@ defmodule Explorer.Chain.SmartContract do
Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset)
end) end)
# Applying the queries to the database
insert_result = insert_result =
insert_contract_query_with_additional_sources insert_contract_query_with_additional_sources
|> Repo.transaction() |> Repo.transaction()
# Set the primary mark for the contract name
AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash)
case insert_result do case insert_result do
@ -995,9 +1041,6 @@ defmodule Explorer.Chain.SmartContract do
{:error, :smart_contract, changeset, _} -> {:error, :smart_contract, changeset, _} ->
{:error, changeset} {:error, changeset}
{:error, :set_address_verified, message, _} ->
{:error, message}
end end
end end
@ -1059,10 +1102,14 @@ defmodule Explorer.Chain.SmartContract do
end end
@doc """ @doc """
Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the Checks if a `Explorer.Chain.SmartContract` exists for the provided address hash.
`t:Explorer.Chain.Address.t/0` with the provided `hash`.
Returns `true` if found and `false` otherwise. ## Parameters
- `address_hash_str` or `address_hash`: The hash of the address in binary string
form or directly as an address hash.
## Returns
- `boolean()`: `true` if a smart contract exists, `false` otherwise.
""" """
@spec verified?(Hash.Address.t() | String.t()) :: boolean() @spec verified?(Hash.Address.t() | String.t()) :: boolean()
def verified?(address_hash_str) when is_binary(address_hash_str) do def verified?(address_hash_str) when is_binary(address_hash_str) do
@ -1118,7 +1165,13 @@ defmodule Explorer.Chain.SmartContract do
end end
@doc """ @doc """
Gets smart-contract by address hash Composes a query for fetching a smart contract by its address hash.
## Parameters
- `address_hash`: The hash of the smart contract's address.
## Returns
- An `Ecto.Query.t()` that represents the query to fetch the smart contract.
""" """
@spec get_smart_contract_query(Hash.Address.t() | binary) :: Ecto.Query.t() @spec get_smart_contract_query(Hash.Address.t() | binary) :: Ecto.Query.t()
def get_smart_contract_query(address_hash) do def get_smart_contract_query(address_hash) do
@ -1263,6 +1316,8 @@ defmodule Explorer.Chain.SmartContract do
end end
end end
# Checks if a smart contract exists in `Explorer.Chain.SmartContract` for a given
# address hash.
@spec verified_smart_contract_exists?(Hash.Address.t()) :: boolean() @spec verified_smart_contract_exists?(Hash.Address.t()) :: boolean()
defp verified_smart_contract_exists?(address_hash) do defp verified_smart_contract_exists?(address_hash) do
query = get_smart_contract_query(address_hash) query = get_smart_contract_query(address_hash)

@ -1,6 +1,10 @@
defmodule Explorer.ChainSpec.GenesisData do defmodule Explorer.ChainSpec.GenesisData do
@moduledoc """ @moduledoc """
Fetches genesis data. Handles the genesis data import.
This module is responsible for managing the import of genesis data into the
database, which includes pre-mined balances and precompiled smart contract
bytecodes.
""" """
use GenServer use GenServer
@ -10,6 +14,7 @@ defmodule Explorer.ChainSpec.GenesisData do
alias Explorer.ChainSpec.Geth.Importer, as: GethImporter alias Explorer.ChainSpec.Geth.Importer, as: GethImporter
alias Explorer.ChainSpec.Parity.Importer alias Explorer.ChainSpec.Parity.Importer
alias Explorer.Helper alias Explorer.Helper
alias Explorer.SmartContract.Solidity.Publisher, as: SolidityPublisher
alias HTTPoison.Response alias HTTPoison.Response
@interval :timer.minutes(2) @interval :timer.minutes(2)
@ -28,13 +33,23 @@ defmodule Explorer.ChainSpec.GenesisData do
# Callback for errored fetch # Callback for errored fetch
@impl GenServer @impl GenServer
def handle_info({_ref, {:error, reason}}, state) do def handle_info({_ref, {:error, reason}}, state) do
Logger.warn(fn -> "Failed to fetch genesis data '#{reason}'." end) Logger.warn(fn -> "Failed to fetch and import genesis data or precompiled contracts: '#{reason}'." end)
fetch_genesis_data() fetch_genesis_data()
{:noreply, state} {:noreply, state}
end end
# Initiates the import of genesis data.
#
# This function triggers the fetching and importing of genesis data, including pre-mined balances and precompiled smart contract bytecodes.
#
# ## Parameters
# - `:import`: The message that triggers this function.
# - `state`: The current state of the GenServer.
#
# ## Returns
# - `{:noreply, state}`
@impl GenServer @impl GenServer
def handle_info(:import, state) do def handle_info(:import, state) do
Logger.debug(fn -> "Importing genesis data" end) Logger.debug(fn -> "Importing genesis data" end)
@ -57,39 +72,87 @@ defmodule Explorer.ChainSpec.GenesisData do
end end
@doc """ @doc """
Fetches pre-mined balances and pre-compiled smart-contract bytecodes from genesis.json Fetches and processes the genesis data, which includes pre-mined balances and precompiled smart contract bytecodes.
This function retrieves the chain specification and precompiled contracts
configuration from specified paths in the application settings. Then it
asynchronously extends the chain spec with precompiled contracts, imports
genesis accounts, and the precompiled contracts' sources and ABIs.
## Returns
- `Task.t()`: A task handle if the fetch and processing are scheduled successfully.
- `:ok`: Indicates no fetch was attempted due to missing configuration paths.
""" """
@spec fetch_genesis_data() :: Task.t() | :ok @spec fetch_genesis_data() :: Task.t() | :ok
def fetch_genesis_data do def fetch_genesis_data do
path = Application.get_env(:explorer, __MODULE__)[:chain_spec_path] chain_spec_path = get_path(:chain_spec_path)
precompiled_config_path = get_path(:precompiled_config_path)
if path do if is_nil(chain_spec_path) and is_nil(precompiled_config_path) do
Logger.warn(fn -> "Genesis data is not fetched. Neither chain spec path or precompiles config path are set." end)
else
json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments)
variant = Keyword.fetch!(json_rpc_named_arguments, :variant) variant = Keyword.fetch!(json_rpc_named_arguments, :variant)
Task.Supervisor.async_nolink(Explorer.GenesisDataTaskSupervisor, fn -> Task.Supervisor.async_nolink(Explorer.GenesisDataTaskSupervisor, fn ->
case fetch_spec(path) do chain_spec = fetch_chain_spec(chain_spec_path)
{:ok, chain_spec} -> precompiles_config = fetch_precompiles_config(precompiled_config_path)
case variant do
EthereumJSONRPC.Geth ->
{:ok, _} = GethImporter.import_genesis_accounts(chain_spec)
_ -> extended_chain_spec = extend_chain_spec(chain_spec, precompiles_config, variant)
Importer.import_emission_rewards(chain_spec) import_genesis_accounts(extended_chain_spec, variant)
{:ok, _} = Importer.import_genesis_accounts(chain_spec)
import_precompiles_sources_and_abi(precompiles_config)
end)
end
end end
@spec get_path(atom()) :: nil | binary()
defp get_path(key) do
case Application.get_env(:explorer, __MODULE__)[key] do
nil -> nil
value when is_binary(value) -> value
end
end
# Retrieves the chain specification, returning an empty map if unsuccessful.
@spec fetch_chain_spec(binary() | nil) :: map() | list()
defp fetch_chain_spec(path) do
case do_fetch(path, "Failed to fetch chain spec.") do
nil -> %{}
value -> value
end
end
# Retrieves the precompiled contracts configuration, returning an empty list if unsuccessful.
@spec fetch_precompiles_config(binary() | nil) :: list()
defp fetch_precompiles_config(path) do
case do_fetch(path, "Failed to fetch precompiles config.") do
nil -> []
value -> value
end
end
# Fetches JSON data from a specified path.
@spec do_fetch(binary() | nil, binary()) :: list() | map() | nil
defp do_fetch(path, warn_message_prefix) do
if path do
case fetch_spec_as_json(path) do
{:ok, chain_spec} ->
chain_spec
{:error, reason} -> {:error, reason} ->
# credo:disable-for-next-line # credo:disable-for-next-line Credo.Check.Refactor.Nesting
Logger.warn(fn -> "Failed to fetch genesis data. #{inspect(reason)}" end) Logger.warn(fn -> "#{warn_message_prefix} #{inspect(reason)}" end)
nil
end end
end)
else else
Logger.warn(fn -> "Failed to fetch genesis data. Chain spec path is not set." end) nil
end end
end end
defp fetch_spec(path) do # Retrieves a JSON data from either a file or URL based on the source.
@spec fetch_spec_as_json(binary()) :: {:ok, list() | map()} | {:error, any()}
defp fetch_spec_as_json(path) do
if Helper.valid_url?(path) do if Helper.valid_url?(path) do
fetch_from_url(path) fetch_from_url(path)
else else
@ -97,6 +160,8 @@ defmodule Explorer.ChainSpec.GenesisData do
end end
end end
# Reads and parses JSON data from a file.
@spec fetch_from_file(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t()}
# sobelow_skip ["Traversal"] # sobelow_skip ["Traversal"]
defp fetch_from_file(path) do defp fetch_from_file(path) do
with {:ok, data} <- File.read(path) do with {:ok, data} <- File.read(path) do
@ -104,6 +169,8 @@ defmodule Explorer.ChainSpec.GenesisData do
end end
end end
# Fetches JSON data from a provided URL.
@spec fetch_from_url(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
defp fetch_from_url(url) do defp fetch_from_url(url) do
case HTTPoison.get(url) do case HTTPoison.get(url) do
{:ok, %Response{body: body, status_code: 200}} -> {:ok, %Response{body: body, status_code: 200}} ->
@ -113,4 +180,140 @@ defmodule Explorer.ChainSpec.GenesisData do
{:error, reason} {:error, reason}
end end
end end
# Extends the chain specification with precompiled contract information.
#
# This function modifies the chain specification to include precompiled
# contracts that are not originally listed in the spec. It handles different
# formats of chain specs (list or map) according to the `variant` specified
# and adds precompiles.
#
# ## Parameters
# - `chain_spec`: The original chain specification in map or list format.
# - `precompiles_config`: A list of precompiled contracts to be added.
# - `variant`: The client variant (e.g., Geth or Parity), which dictates the
# spec structure.
#
# ## Returns
# - The modified chain specification with precompiled contracts included.
@spec extend_chain_spec(map() | list(), list(), EthereumJSONRPC.Geth | EthereumJSONRPC.Parity) :: map() | list()
defp extend_chain_spec(chain_spec, [], _) do
chain_spec
end
# Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer
defp extend_chain_spec(chain_spec, precompiles_config, variant)
when is_list(chain_spec) and variant == EthereumJSONRPC.Geth do
precompiles_as_map =
precompiles_config
|> Enum.reduce(%{}, fn contract, acc ->
Map.put(acc, contract["address"], %{
"address" => contract["address"],
"balance" => 0,
"bytecode" => contract["bytecode"]
})
end)
filtered_maps_of_precompiles =
chain_spec
|> Enum.reduce(precompiles_as_map, fn account, acc ->
Map.delete(acc, account["address"])
end)
chain_spec ++ Map.values(filtered_maps_of_precompiles)
end
# Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer
defp extend_chain_spec(%{"genesis" => sub_entity} = chain_spec, precompiles_config, variant)
when variant == EthereumJSONRPC.Geth do
updated_sub_entity = extend_chain_spec(sub_entity, precompiles_config, variant)
Map.put(chain_spec, "genesis", updated_sub_entity)
end
# Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer
defp extend_chain_spec(chain_spec, precompiles_config, variant)
when is_map(chain_spec) and variant == EthereumJSONRPC.Geth do
accounts =
case chain_spec["alloc"] do
nil -> %{}
value -> value
end
updated_accounts =
precompiles_config
|> Enum.reduce(accounts, fn contract, acc ->
Map.put_new(acc, contract["address"], %{"balance" => 0, "code" => contract["bytecode"]})
end)
Map.put(chain_spec, "alloc", updated_accounts)
end
# Resulting spec will be handled by Explorer.ChainSpec.Parity.Importer
defp extend_chain_spec(chain_spec, precompiles_config, _) when is_map(chain_spec) do
accounts =
case chain_spec["accounts"] do
nil -> %{}
value -> value
end
updated_accounts =
precompiles_config
|> Enum.reduce(accounts, fn contract, acc ->
Map.put_new(acc, contract["address"], %{"balance" => 0, "constructor" => contract["bytecode"]})
end)
Map.put(chain_spec, "accounts", updated_accounts)
end
# Imports genesis accounts from the specified chain specification and updates
# `Explorer.Chain.Address` and `Explorer.Chain.Address.CoinBalance`, and
# `Explorer.Chain.Address.CoinBalanceDaily`.
@spec import_genesis_accounts(map() | list(), EthereumJSONRPC.Geth | EthereumJSONRPC.Parity) :: any()
defp import_genesis_accounts(chain_spec, variant) do
if not Enum.empty?(chain_spec) do
case variant do
EthereumJSONRPC.Geth ->
{:ok, _} = GethImporter.import_genesis_accounts(chain_spec)
_ ->
Importer.import_emission_rewards(chain_spec)
{:ok, _} = Importer.import_genesis_accounts(chain_spec)
end
end
end
# Iterates through the list of precompiles descriptions, and creating/updating
# each smart contract.
@spec import_precompiles_sources_and_abi([map()]) :: any()
defp import_precompiles_sources_and_abi(precompiles_config) do
precompiles_config
|> Enum.each(fn contract ->
attrs = %{
address_hash: contract["address"],
name: contract["name"],
file_path: nil,
compiler_version: contract["compiler"],
evm_version: nil,
optimization_runs: nil,
optimization: false,
contract_source_code: contract["source"],
constructor_arguments: nil,
external_libraries: [],
secondary_sources: [],
abi: Jason.decode!(contract["abi"]),
verified_via_sourcify: false,
verified_via_eth_bytecode_db: false,
verified_via_verifier_alliance: false,
partially_verified: false,
is_vyper_contract: false,
autodetect_constructor_args: nil,
is_yul: false,
compiler_settings: nil,
license_type: :none
}
SolidityPublisher.create_or_update_smart_contract(contract["address"], attrs)
end)
end
end end

@ -1,4 +1,3 @@
# credo:disable-for-this-file
defmodule Explorer.ChainSpec.Geth.Importer do defmodule Explorer.ChainSpec.Geth.Importer do
@moduledoc """ @moduledoc """
Imports data from Geth genesis.json. Imports data from Geth genesis.json.
@ -10,7 +9,28 @@ defmodule Explorer.ChainSpec.Geth.Importer do
alias Explorer.{Chain, Helper} alias Explorer.{Chain, Helper}
alias Explorer.Chain.Hash.Address alias Explorer.Chain.Hash.Address
@doc """
Imports genesis accounts into the database from a chain specification.
This function extracts genesis account information from a given chain specification,
including initial balances and contract bytecode. It enriches this data with additional
metadata, such as setting the block number to 0 (for genesis accounts) and determining
the day based on the timestamp of the first block. Subsequently, it imports the data
into `Explorer.Chain.Address`, `Explorer.Chain.Address.CoinBalance`, and
`Explorer.Chain.Address.CoinBalanceDaily` tables.
## Parameters
- `chain_spec`: A map or list representing the chain specification that contains
genesis account information. It may be structured directly as an
account list or as part of a larger specification map.
## Returns
- N/A
"""
@spec import_genesis_accounts(map() | list()) :: any()
def import_genesis_accounts(chain_spec) do def import_genesis_accounts(chain_spec) do
# credo:disable-for-previous-line Credo.Check.Design.DuplicatedCode
# It duplicates `import_genesis_accounts/1` from `Explorer.ChainSpec.Parity.Importer`
balance_params = balance_params =
chain_spec chain_spec
|> genesis_accounts() |> genesis_accounts()
@ -50,7 +70,32 @@ defmodule Explorer.ChainSpec.Geth.Importer do
Chain.import(params) Chain.import(params)
end end
@spec genesis_accounts(any()) :: [%{address_hash: Address.t(), value: integer(), contract_code: String.t()}] @doc """
Parses and returns the genesis account information from a chain specification.
It extracts account data such as address hashes, initial balances, and
optionally, contract bytecode for accounts defined in the genesis block of
a blockchain configuration.
## Parameters
- `input`: Can be a list of account maps or a map of the entire chain specification.
## Returns
- A list of maps, each representing an account with keys for the address hash,
balance , and optionally, the contract bytecode. Accounts without defined
balances are omitted.
### Usage
- `genesis_accounts(%{"genesis" => genesis_data})`: Extracts accounts from
a nested genesis key.
- `genesis_accounts(chain_spec)`: Parses accounts from a chain specification that
includes an 'alloc' key.
- `genesis_accounts(list_of_accounts)`: Directly parses a list of account data.
Intended to be called after `genesis_accounts(%{"genesis" => genesis_data})` call.
"""
@spec genesis_accounts(map() | list()) :: [
%{address_hash: Address.t(), value: non_neg_integer(), contract_code: String.t()}
]
def genesis_accounts(%{"genesis" => genesis}) do def genesis_accounts(%{"genesis" => genesis}) do
genesis_accounts(genesis) genesis_accounts(genesis)
end end
@ -80,6 +125,17 @@ defmodule Explorer.ChainSpec.Geth.Importer do
end end
end end
# Parses account data from a provided map to extract address, balance, and optional contract code.
#
# ## Parameters
# - `accounts`: A map with accounts data.
#
# ## Returns
# - A list of maps with accounts data including address hashes, balances,
# and any associated contract code.
@spec parse_accounts(%{binary() => map()}) :: [
%{:address_hash => Address.t(), value: non_neg_integer(), contract_code: String.t() | nil}
]
defp parse_accounts(accounts) do defp parse_accounts(accounts) do
accounts accounts
|> Stream.filter(fn {_address, map} -> |> Stream.filter(fn {_address, map} ->

@ -199,7 +199,35 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
create_or_update_smart_contract(address_hash, attrs) create_or_update_smart_contract(address_hash, attrs)
end end
defp create_or_update_smart_contract(address_hash, attrs) do @doc """
Creates or updates a smart contract record based on its verification status.
This function first checks if a smart contract associated with the provided address hash
is already verified. If verified, it updates the existing smart contract record with the
new attributes provided, such as external libraries and secondary sources. During the update,
the contract methods are also updated: existing methods are preserved, and any new methods
from the provided ABI are added to ensure the contract's integrity and completeness.
If the smart contract is not verified, it creates a new record in the database with the
provided attributes, setting it up for verification. In this case, all contract methods
from the ABI are freshly inserted as part of the new smart contract creation.
## Parameters
- `address_hash`: The hash of the address for the smart contract.
- `attrs`: A map containing attributes such as external libraries and secondary sources.
## Returns
- `{:ok, Explorer.Chain.SmartContract.t()}`: Successfully created or updated smart
contract.
- `{:error, data}`: on failure, returning `Ecto.Changeset.t()` or, if any issues
happen during setting the address as verified, an error message.
"""
@spec create_or_update_smart_contract(binary() | Explorer.Chain.Hash.t(), %{
:external_libraries => list(),
:secondary_sources => list(),
optional(any()) => any()
}) :: {:error, Ecto.Changeset.t() | String.t()} | {:ok, Explorer.Chain.SmartContract.t()}
def create_or_update_smart_contract(address_hash, attrs) do
Logger.info("Publish successfully verified Solidity smart-contract #{address_hash} into the DB") Logger.info("Publish successfully verified Solidity smart-contract #{address_hash} into the DB")
if SmartContract.verified?(address_hash) do if SmartContract.verified?(address_hash) do

File diff suppressed because one or more lines are too long

@ -246,10 +246,23 @@ config :explorer, Explorer.Chain.Events.Listener,
else: true else: true
) )
precompiled_config_base_dir =
case config_env() do
:prod -> "/app/"
_ -> "./"
end
precompiled_config_default_path =
case ConfigHelper.chain_type() do
"arbitrum" -> "#{precompiled_config_base_dir}config/assets/precompiles-arbitrum.json"
_ -> nil
end
config :explorer, Explorer.ChainSpec.GenesisData, config :explorer, Explorer.ChainSpec.GenesisData,
chain_spec_path: System.get_env("CHAIN_SPEC_PATH"), chain_spec_path: System.get_env("CHAIN_SPEC_PATH"),
emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"), emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"),
rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5") rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5"),
precompiled_config_path: System.get_env("PRECOMPILED_CONTRACTS_CONFIG_PATH", precompiled_config_default_path)
address_sum_global_ttl = ConfigHelper.parse_time_env_var("CACHE_ADDRESS_SUM_PERIOD", "1h") address_sum_global_ttl = ConfigHelper.parse_time_env_var("CACHE_ADDRESS_SUM_PERIOD", "1h")

@ -85,3 +85,4 @@ COPY --from=builder /opt/release/blockscout .
COPY --from=builder /app/apps/explorer/node_modules ./node_modules COPY --from=builder /app/apps/explorer/node_modules ./node_modules
COPY --from=builder /app/config/config_helper.exs ./config/config_helper.exs COPY --from=builder /app/config/config_helper.exs ./config/config_helper.exs
COPY --from=builder /app/config/config_helper.exs /app/releases/${RELEASE_VERSION}/config_helper.exs COPY --from=builder /app/config/config_helper.exs /app/releases/${RELEASE_VERSION}/config_helper.exs
COPY --from=builder /app/config/assets/precompiles-arbitrum.json ./config/assets/precompiles-arbitrum.json

Loading…
Cancel
Save