From d5709dfda9e5b8184b192cbafafb890d5891a93e Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:40:28 +0300 Subject: [PATCH] feat: Add Stylus verificaiton support (#11183) * feat: Add Stylus verificaiton support * Fix tests * Apply suggestions from code review Co-authored-by: Alexander Kolotov * Refactoring + handle snake case * Add env to docker-compose/envs/common-blockscout.env * Apply suggestions from code review Co-authored-by: Alexander Kolotov * Fix cspell * Drop MICROSERVICE_STYLUS_VERIFIER_ENABLED * Fix tests * Fix tests * Apply suggestions from code review Co-authored-by: Alexander Kolotov * Process review comments * Fix dialyzer --------- Co-authored-by: Alexander Kolotov --- .dialyzer-ignore | 2 + .../api/v2/verification_controller.ex | 71 +++++- .../routers/smart_contracts_api_v2_router.ex | 4 + .../views/api/v2/filecoin_view.ex | 9 +- .../views/api/v2/smart_contract_view.ex | 37 ++- .../lib/explorer/chain/smart_contract.ex | 20 +- .../smart_contract/compiler_version.ex | 81 +++--- .../smart_contract/solidity/publisher.ex | 3 +- .../solidity/publisher_worker.ex | 7 +- .../smart_contract/stylus/publisher.ex | 235 ++++++++++++++++++ .../smart_contract/stylus/publisher_worker.ex | 72 ++++++ .../smart_contract/stylus/verifier.ex | 108 ++++++++ .../stylus_verifier_interface.ex | 165 ++++++++++++ .../smart_contract/vyper/publisher.ex | 3 +- .../smart_contract/vyper/publisher_worker.ex | 7 +- .../20241111195112_add_stylus_fields.exs | 10 + .../20241111200520_add_language_field.exs | 9 + config/config_helper.exs | 38 +++ config/runtime.exs | 3 + docker-compose/envs/common-blockscout.env | 1 + 20 files changed, 833 insertions(+), 52 deletions(-) create mode 100644 apps/explorer/lib/explorer/smart_contract/stylus/publisher.ex create mode 100644 apps/explorer/lib/explorer/smart_contract/stylus/publisher_worker.ex create mode 100644 apps/explorer/lib/explorer/smart_contract/stylus/verifier.ex create mode 100644 apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex create mode 100644 apps/explorer/priv/arbitrum/migrations/20241111195112_add_stylus_fields.exs create mode 100644 apps/explorer/priv/repo/migrations/20241111200520_add_language_field.exs diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 056ffa415c..941f77a1f1 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -1,6 +1,8 @@ lib/ethereum_jsonrpc/rolling_window.ex:171 lib/explorer/smart_contract/solidity/publisher_worker.ex:1 lib/explorer/smart_contract/vyper/publisher_worker.ex:1 +lib/explorer/smart_contract/stylus/publisher_worker.ex:1 lib/explorer/smart_contract/solidity/publisher_worker.ex:8 lib/explorer/smart_contract/vyper/publisher_worker.ex:8 +lib/explorer/smart_contract/stylus/publisher_worker.ex:8 lib/phoenix/router.ex:402 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex index 3176bde5a8..5450b97173 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex @@ -11,8 +11,9 @@ defmodule BlockScoutWeb.API.V2.VerificationController do alias Explorer.Chain.SmartContract alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Explorer.SmartContract.Solidity.PublishHelper + alias Explorer.SmartContract.Stylus.PublisherWorker, as: StylusPublisherWorker alias Explorer.SmartContract.Vyper.PublisherWorker, as: VyperPublisherWorker - alias Explorer.SmartContract.{CompilerVersion, RustVerifierInterface, Solidity.CodeCompiler} + alias Explorer.SmartContract.{CompilerVersion, RustVerifierInterface, Solidity.CodeCompiler, StylusVerifierInterface} action_fallback(BlockScoutWeb.API.V2.FallbackController) @@ -46,6 +47,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do config = base_config |> maybe_add_zk_options() + |> maybe_add_stylus_options() conn |> json(config) @@ -64,6 +66,10 @@ defmodule BlockScoutWeb.API.V2.VerificationController do do: ["multi-part", "vyper-multi-part", "vyper-standard-input"] ++ &1, else: &1 )).() + |> (&if(StylusVerifierInterface.enabled?(), + do: ["stylus-github-repository" | &1], + else: &1 + )).() end end @@ -79,6 +85,16 @@ defmodule BlockScoutWeb.API.V2.VerificationController do end end + # Adds Stylus compiler versions to config if Stylus verification is enabled + defp maybe_add_stylus_options(config) do + if StylusVerifierInterface.enabled?() do + config + |> Map.put(:stylus_compiler_versions, CompilerVersion.fetch_version_list(:stylus)) + else + config + end + end + def verification_via_flattened_code( conn, %{"address_hash" => address_hash_string, "compiler_version" => compiler_version, "source_code" => source_code} = @@ -291,6 +307,59 @@ defmodule BlockScoutWeb.API.V2.VerificationController do end end + @doc """ + Initiates verification of a Stylus smart contract using its GitHub repository source code. + + Validates the request parameters and queues the verification job to be processed + asynchronously by the Stylus publisher worker. + + ## Parameters + - `conn`: The connection struct + - `params`: A map containing: + - `address_hash`: Contract address to verify + - `cargo_stylus_version`: Version of cargo-stylus used for deployment + - `repository_url`: GitHub repository URL containing contract code + - `commit`: Git commit hash used for deployment + - `path_prefix`: Optional path prefix if contract is not in repository root + + ## Returns + - JSON response with: + - Success message if verification request is queued successfully + - Error message if: + - Stylus verification is not enabled + - Address format is invalid + - Contract is already verified + - Access is restricted + """ + @spec verification_via_stylus_github_repository(Plug.Conn.t(), %{String.t() => any()}) :: + {:already_verified, true} + | {:format, :error} + | {:not_found, false | nil} + | {:restricted_access, true} + | Plug.Conn.t() + def verification_via_stylus_github_repository( + conn, + %{ + "address_hash" => address_hash_string, + "cargo_stylus_version" => _, + "repository_url" => _, + "commit" => _, + "path_prefix" => _ + } = params + ) do + Logger.info("API v2 stylus smart-contract #{address_hash_string} verification via github repository") + + with {:not_found, true} <- {:not_found, StylusVerifierInterface.enabled?()}, + :validated <- validate_address(params) do + log_sc_verification_started(address_hash_string) + Que.add(StylusPublisherWorker, {"github_repository", params}) + + conn + |> put_view(ApiView) + |> render(:message, %{message: @sc_verification_started}) + end + end + defp parse_interfaces(interfaces) do cond do is_binary(interfaces) -> diff --git a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex index 479c83d247..aee2f7363b 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex @@ -79,5 +79,9 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do post("/vyper-multi-part", V2.VerificationController, :verification_via_vyper_multipart) post("/vyper-standard-input", V2.VerificationController, :verification_via_vyper_standard_input) end + + if Application.compile_env(:explorer, :chain_type) === :arbitrum do + post("/stylus-github-repository", V2.VerificationController, :verification_via_stylus_github_repository) + end end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex index 398bc56f67..82a2270b65 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex @@ -26,8 +26,9 @@ if Application.compile_env(:explorer, :chain_type) == :filecoin do end @spec preload_and_put_filecoin_robust_address(map(), %{ - address_hash: String.t() | nil, - field_prefix: String.t() | nil + optional(:address_hash) => String.t() | nil, + optional(:field_prefix) => String.t() | nil, + optional(any) => any }) :: map() def preload_and_put_filecoin_robust_address(result, %{address_hash: address_hash} = params) do @@ -36,6 +37,10 @@ if Application.compile_env(:explorer, :chain_type) == :filecoin do put_filecoin_robust_address(result, Map.put(params, :address, address)) end + def preload_and_put_filecoin_robust_address(result, _params) do + result + end + @doc """ Adds a Filecoin robust address to the given result. diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index d895db108f..70da14e7fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -230,8 +230,11 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "is_blueprint" => if(smart_contract.is_blueprint, do: smart_contract.is_blueprint, else: false) } |> Map.merge(bytecode_info(address)) - |> add_zksync_info(target_contract) - |> chain_type_fields(%{address_hash: verified_twin_address_hash, field_prefix: "verified_twin"}) + |> chain_type_fields(%{ + address_hash: verified_twin_address_hash, + field_prefix: "verified_twin", + target_contract: target_contract + }) end def prepare_smart_contract(%Address{proxy_implementations: implementations} = address, conn) do @@ -284,16 +287,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do end end - defp add_zksync_info(smart_contract_info, target_contract) do - if Application.get_env(:explorer, :chain_type) == :zksync do - Map.merge(smart_contract_info, %{ - "zk_compiler_version" => target_contract.zk_compiler_version - }) - else - smart_contract_info - end - end - defp prepare_external_libraries(libraries) when is_list(libraries) do Enum.map(libraries, fn %Explorer.Chain.SmartContract.ExternalLibrary{name: name, address_hash: address_hash} -> {:ok, hash} = Chain.string_to_address_hash(address_hash) @@ -366,7 +359,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do } smart_contract_info - |> add_zksync_info(smart_contract) + |> chain_type_fields(%{ + target_contract: smart_contract + }) end defp smart_contract_language(smart_contract) do @@ -374,6 +369,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do smart_contract.is_vyper_contract -> "vyper" + not is_nil(smart_contract.language) -> + smart_contract.language + is_nil(smart_contract.abi) -> "yul" @@ -452,6 +450,19 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do BlockScoutWeb.API.V2.FilecoinView.preload_and_put_filecoin_robust_address(result, params) end + :arbitrum -> + defp chain_type_fields(result, %{target_contract: target_contract}) do + result + |> Map.put("package_name", target_contract.package_name) + |> Map.put("github_repository_metadata", target_contract.github_repository_metadata) + end + + :zksync -> + defp chain_type_fields(result, %{target_contract: target_contract}) do + result + |> Map.put("zk_compiler_version", target_contract.zk_compiler_version) + end + _ -> defp chain_type_fields(result, _address) do result diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 09f8d297bb..1fc439ee34 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -20,6 +20,15 @@ defmodule Explorer.Chain.SmartContract.Schema do ] ) + :arbitrum -> + @chain_type_fields quote( + do: [ + field(:package_name, :string), + field(:github_repository_metadata, :map), + field(:optimization_runs, :integer) + ] + ) + _ -> @chain_type_fields quote(do: [field(:optimization_runs, :integer)]) end @@ -51,6 +60,7 @@ defmodule Explorer.Chain.SmartContract.Schema do field(:license_type, Ecto.Enum, values: @license_enum, default: :none) field(:certified, :boolean) field(:is_blueprint, :boolean) + field(:language, Ecto.Enum, values: [solidity: 1, vyper: 2, yul: 3, stylus_rust: 4], default: :solidity) has_many( :decompiled_smart_contracts, @@ -123,7 +133,7 @@ defmodule Explorer.Chain.SmartContract do @burn_address_hash_string "0x0000000000000000000000000000000000000000" @dead_address_hash_string "0x000000000000000000000000000000000000dEaD" - @required_attrs ~w(compiler_version optimization address_hash contract_code_md5)a + @required_attrs ~w(compiler_version optimization address_hash contract_code_md5 language)a @optional_common_attrs ~w(name contract_source_code evm_version optimization_runs constructor_arguments verified_via_sourcify verified_via_eth_bytecode_db verified_via_verifier_alliance partially_verified file_path is_vyper_contract is_changed_bytecode bytecode_checked_at autodetect_constructor_args license_type certified is_blueprint)a @@ -134,6 +144,9 @@ defmodule Explorer.Chain.SmartContract do :zksync -> ~w(zk_compiler_version)a + :arbitrum -> + ~w(package_name github_repository_metadata)a + _ -> ~w()a end) @@ -388,6 +401,10 @@ defmodule Explorer.Chain.SmartContract do :zksync -> """ * `zk_compiler_version` - the version of ZkSolc or ZkVyper compilers. """ + :arbitrum -> """ + * `package_name` - package name of stylus contract. + * `github_repository_metadata` - map with repository details. + """ _ -> "" end} * `optimization` - whether optimizations were turned on when compiling `contract_source_code` into `address` @@ -410,6 +427,7 @@ defmodule Explorer.Chain.SmartContract do * `is_yul` - field was added for storing user's choice * `certified` - boolean flag, which can be set for set of smart-contracts via runtime env variable to prioritize those smart-contracts in the search. * `is_blueprint` - boolean flag, determines if contract is ERC-5202 compatible blueprint contract or not. + * `language` - enum for smart contract language tracking, stands for getting rid of is_vyper_contract/is_yul bool flags. """ Explorer.Chain.SmartContract.Schema.generate() diff --git a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex index 0151ee24bd..99f164c43e 100644 --- a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex +++ b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex @@ -4,7 +4,7 @@ defmodule Explorer.SmartContract.CompilerVersion do """ alias Explorer.Helper - alias Explorer.SmartContract.RustVerifierInterface + alias Explorer.SmartContract.{RustVerifierInterface, StylusVerifierInterface} @unsupported_solc_versions ~w(0.1.1 0.1.2) @unsupported_vyper_versions ~w(v0.2.9 v0.2.10) @@ -12,15 +12,38 @@ defmodule Explorer.SmartContract.CompilerVersion do @doc """ Fetches a list of compilers from the Ethereum Solidity API. """ - @spec fetch_versions(:solc | :vyper | :zk) :: {atom, [map]} - def fetch_versions(compiler) do - case compiler do - :solc -> fetch_solc_versions() - :vyper -> fetch_vyper_versions() - :zk -> fetch_zk_versions() - end + @spec fetch_versions(:solc | :vyper | :zk | :stylus) :: {atom, [binary()]} + def fetch_versions(compiler) + + def fetch_versions(:solc) do + fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :solc) + end + + def fetch_versions(:vyper) do + fetch_compiler_versions(&RustVerifierInterface.vyper_get_versions_list/0, :vyper) + end + + def fetch_versions(:zk) do + fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :zk) + end + + def fetch_versions(:stylus) do + fetch_compiler_versions(&StylusVerifierInterface.get_versions_list/0, :stylus) end + @doc """ + Fetches the list of compiler versions for the given compiler. + + ## Parameters + + - compiler: The name of the compiler for which to fetch the version list. + + ## Returns + + - A list of available compiler versions. + + """ + @spec fetch_version_list(:solc | :vyper | :zk | :stylus) :: [binary()] def fetch_version_list(compiler) do case fetch_versions(compiler) do {:ok, compiler_versions} -> @@ -31,38 +54,38 @@ defmodule Explorer.SmartContract.CompilerVersion do end end - defp fetch_solc_versions do - fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :solc) - end - - defp fetch_zk_versions do - fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :zk) + defp fetch_compiler_versions(compiler_list_fn, :stylus = compiler_type) do + if StylusVerifierInterface.enabled?() do + fetch_compiler_versions_sc_verified_enabled(compiler_list_fn, compiler_type) + else + {:ok, []} + end end - defp fetch_vyper_versions do - fetch_compiler_versions(&RustVerifierInterface.vyper_get_versions_list/0, :vyper) + defp fetch_compiler_versions(compiler_list_fn, :zk = compiler_type) do + if RustVerifierInterface.enabled?() do + fetch_compiler_versions_sc_verified_enabled(compiler_list_fn, compiler_type) + else + {:ok, []} + end end defp fetch_compiler_versions(compiler_list_fn, compiler_type) do if RustVerifierInterface.enabled?() do fetch_compiler_versions_sc_verified_enabled(compiler_list_fn, compiler_type) else - if compiler_type == :zk do - {:ok, []} - else - headers = [{"Content-Type", "application/json"}] + headers = [{"Content-Type", "application/json"}] - # credo:disable-for-next-line - case HTTPoison.get(source_url(compiler_type), headers) do - {:ok, %{status_code: 200, body: body}} -> - {:ok, format_data(body, compiler_type)} + # credo:disable-for-next-line + case HTTPoison.get(source_url(compiler_type), headers) do + {:ok, %{status_code: 200, body: body}} -> + {:ok, format_data(body, compiler_type)} - {:ok, %{status_code: _status_code, body: body}} -> - {:error, Helper.decode_json(body)["error"]} + {:ok, %{status_code: _status_code, body: body}} -> + {:error, Helper.decode_json(body)["error"]} - {:error, %{reason: reason}} -> - {:error, reason} - end + {:error, %{reason: reason}} -> + {:error, reason} end end end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex index cf24dcae9a..908f90241a 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex @@ -405,7 +405,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do is_yul: params["is_yul"] || false, compiler_settings: clean_compiler_settings, license_type: prepare_license_type(params["license_type"]) || :none, - is_blueprint: params["is_blueprint"] || false + is_blueprint: params["is_blueprint"] || false, + language: (is_nil(abi) && :yul) || :solidity } base_attributes diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex index ae2caacc5e..f1d4dff9de 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex @@ -74,9 +74,12 @@ defmodule Explorer.SmartContract.Solidity.PublisherWorker do Logger.info("Smart-contract #{address_hash} verification: broadcast verification results") if conn do - EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result, conn}}], :on_demand) + EventsPublisher.broadcast( + [{:contract_verification_result, {String.downcase(address_hash), result, conn}}], + :on_demand + ) else - EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result}}], :on_demand) + EventsPublisher.broadcast([{:contract_verification_result, {String.downcase(address_hash), result}}], :on_demand) end end diff --git a/apps/explorer/lib/explorer/smart_contract/stylus/publisher.ex b/apps/explorer/lib/explorer/smart_contract/stylus/publisher.ex new file mode 100644 index 0000000000..06dc5bfc6c --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/stylus/publisher.ex @@ -0,0 +1,235 @@ +defmodule Explorer.SmartContract.Stylus.Publisher do + @moduledoc """ + Module responsible for verifying and publishing Stylus smart contracts. + + The verification process includes: + 1. Initiating verification through a microservice that compares GitHub repository + source code against deployed bytecode + 2. Processing the verification response, including ABI and source files + 3. Creating or updating the smart contract record in the database + 4. Handling verification failures by creating invalid changesets with error messages + """ + + require Logger + + alias Explorer.Chain.SmartContract + alias Explorer.SmartContract.Helper + alias Explorer.SmartContract.Stylus.Verifier + + @default_file_name "src/lib.rs" + + @sc_verification_via_github_repository_started "Smart-contract verification via Github repository started" + + @doc """ + Verifies and publishes a Stylus smart contract using GitHub repository source code. + + Initiates verification of a contract through the verification microservice. On + successful verification, processes and stores the contract details in the + database. On failure, creates an invalid changeset with appropriate error + messages. + + ## Parameters + - `address_hash`: The contract's address hash as binary or `t:Explorer.Chain.Hash.t/0` + - `params`: Map containing verification parameters: + - `"cargo_stylus_version"`: Version of cargo-stylus used for deployment + - `"repository_url"`: GitHub repository URL containing contract code + - `"commit"`: Git commit hash used for deployment + - `"path_prefix"`: Optional path prefix if contract is not in repository root + + ## Returns + - `{:ok, smart_contract}` if verification and database storage succeed + - `{:error, changeset}` if verification fails or there are validation errors + """ + @spec publish(binary() | Explorer.Chain.Hash.t(), %{String.t() => any()}) :: + {:error, Ecto.Changeset.t()} | {:ok, Explorer.Chain.SmartContract.t()} + def publish(address_hash, params) do + Logger.info(@sc_verification_via_github_repository_started) + + case Verifier.evaluate_authenticity(address_hash, params) do + { + :ok, + %{ + "abi" => _, + "cargo_stylus_version" => _, + "contract_name" => _, + "files" => _, + "package_name" => _, + "github_repository_metadata" => _ + } = result_params + } -> + process_verifier_response(result_params, address_hash) + + {:error, error} -> + {:error, unverified_smart_contract(address_hash, params, error, nil)} + + _ -> + {:error, unverified_smart_contract(address_hash, params, "Unexpected error", nil)} + end + end + + # Processes successful Stylus contract verification response and stores contract data. + # + # Takes the verification response from `evaluate_authenticity/2` containing verified contract + # details and prepares them for storage in the database. The main source file is extracted + # from `files` map using the default filename, while other files are stored as secondary + # sources. + # + # ## Parameters + # - `response`: Verification response map containing: + # - `abi`: Contract ABI as JSON string + # - `cargo_stylus_version`: Version of cargo-stylus used + # - `contract_name`: Name of the contract + # - `files`: Map of file paths to source code contents + # - `package_name`: Package name of the contract + # - `github_repository_metadata`: Repository metadata + # - `address_hash`: The contract's address hash as binary or `t:Explorer.Chain.Hash.t/0` + # + # ## Returns + # - `{:ok, smart_contract}` if database storage succeeds + # - `{:error, changeset}` if there are validation errors + # - `{:error, message}` if the database operation fails + @spec process_verifier_response(%{String.t() => any()}, binary() | Explorer.Chain.Hash.t()) :: + {:ok, Explorer.Chain.SmartContract.t()} | {:error, Ecto.Changeset.t() | String.t()} + defp process_verifier_response( + %{ + "abi" => abi_string, + "cargo_stylus_version" => cargo_stylus_version, + "contract_name" => contract_name, + "files" => files, + "package_name" => package_name, + "github_repository_metadata" => github_repository_metadata + }, + address_hash + ) do + secondary_sources = + for {file, code} <- files, + file != @default_file_name, + do: %{"file_name" => file, "contract_source_code" => code, "address_hash" => address_hash} + + contract_source_code = files[@default_file_name] + + prepared_params = + %{} + |> Map.put("compiler_version", cargo_stylus_version) + |> Map.put("contract_source_code", contract_source_code) + |> Map.put("name", contract_name) + |> Map.put("file_path", contract_source_code && @default_file_name) + |> Map.put("secondary_sources", secondary_sources) + |> Map.put("package_name", package_name) + |> Map.put("github_repository_metadata", github_repository_metadata) + + publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null")) + end + + # Stores information about a verified Stylus smart contract in the database. + # + # ## Parameters + # - `address_hash`: The contract's address hash as binary or `t:Explorer.Chain.Hash.t/0` + # - `params`: Map containing contract details: + # - `name`: Contract name + # - `file_path`: Path to the contract source file + # - `compiler_version`: Version of the Stylus compiler + # - `contract_source_code`: Source code of the contract + # - `secondary_sources`: Additional source files + # - `package_name`: Package name for Stylus contract + # - `github_repository_metadata`: Repository metadata + # - `abi`: Contract's ABI (Application Binary Interface) + # + # ## Returns + # - `{:ok, smart_contract}` if publishing succeeds + # - `{:error, changeset}` if there are validation errors + # - `{:error, message}` if the database operation fails + @spec publish_smart_contract(binary() | Explorer.Chain.Hash.t(), %{String.t() => any()}, map()) :: + {:error, Ecto.Changeset.t() | String.t()} | {:ok, Explorer.Chain.SmartContract.t()} + defp publish_smart_contract(address_hash, params, abi) do + attrs = address_hash |> attributes(params, abi) + + create_or_update_smart_contract(address_hash, attrs) + end + + # This function first checks if a smart contract already exists in the database + # at the given address. If it exists, updates the contract with new attributes. + # Otherwise, creates a new smart contract record. + @spec create_or_update_smart_contract(binary() | Explorer.Chain.Hash.t(), map()) :: + {:error, Ecto.Changeset.t() | String.t()} | {:ok, Explorer.Chain.SmartContract.t()} + defp create_or_update_smart_contract(address_hash, attrs) do + Logger.info("Publish successfully verified Stylus smart-contract #{address_hash} into the DB") + + if SmartContract.verified?(address_hash) do + SmartContract.update_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) + else + SmartContract.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) + end + end + + # Creates an invalid changeset for a Stylus smart contract that failed verification. + # + # Prepares contract attributes with MD5 hash of bytecode and creates an invalid changeset + # with appropriate error messages. The changeset is marked with `:insert` action to + # indicate a failed verification attempt. + # + # ## Parameters + # - `address_hash`: The contract's address hash + # - `params`: Map containing contract details from verification attempt + # - `error`: The verification error that occurred + # - `error_message`: Optional custom error message + # - `verification_with_files?`: Boolean indicating if verification used source files. + # Defaults to `false` + # + # ## Returns + # An invalid `t:Ecto.Changeset.t/0` with: + # - Contract attributes including MD5 hash of bytecode + # - Error message attached to appropriate field + # - Action set to `:insert` + @spec unverified_smart_contract(binary() | Explorer.Chain.Hash.t(), %{String.t() => any()}, any(), any(), boolean()) :: + Ecto.Changeset.t() + defp unverified_smart_contract(address_hash, params, error, error_message, verification_with_files? \\ false) do + attrs = + address_hash + |> attributes(params |> Map.put("compiler_version", params["cargo_stylus_version"])) + |> Helper.add_contract_code_md5() + + changeset = + SmartContract.invalid_contract_changeset( + %SmartContract{address_hash: address_hash}, + attrs, + error, + error_message, + verification_with_files? + ) + + Logger.error("Stylus smart-contract verification #{address_hash} failed because of the error #{inspect(error)}") + + %{changeset | action: :insert} + end + + defp attributes(address_hash, params, abi \\ %{}) do + %{ + address_hash: address_hash, + name: params["name"], + file_path: params["file_path"], + compiler_version: params["compiler_version"], + evm_version: nil, + optimization_runs: nil, + optimization: false, + contract_source_code: params["contract_source_code"], + constructor_arguments: nil, + external_libraries: [], + secondary_sources: params["secondary_sources"], + abi: 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: false, + is_yul: false, + compiler_settings: nil, + license_type: :none, + is_blueprint: false, + language: :stylus_rust, + package_name: params["package_name"], + github_repository_metadata: params["github_repository_metadata"] + } + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/stylus/publisher_worker.ex b/apps/explorer/lib/explorer/smart_contract/stylus/publisher_worker.ex new file mode 100644 index 0000000000..ad5357978c --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/stylus/publisher_worker.ex @@ -0,0 +1,72 @@ +defmodule Explorer.SmartContract.Stylus.PublisherWorker do + @moduledoc """ + Processes Stylus smart contract verification requests asynchronously in the background. + + This module implements a worker that handles verification of Stylus smart contracts + through their GitHub repository source code. It uses a job queue system to: + - Receive verification requests containing contract address and GitHub details + - Delegate verification to the Publisher module + - Broadcast verification results through the events system + """ + + require Logger + + use Que.Worker, concurrency: 5 + + alias Explorer.Chain.Events.Publisher, as: EventsPublisher + alias Explorer.SmartContract.Stylus.Publisher + + @doc """ + Processes a Stylus smart contract verification request. + + Initiates the verification process by broadcasting the verification request to + the module responsible for the actual verification and consequent update of + the database. This function is called automatically by the job queue system. + + ## Parameters + - `{"github_repository", params}`: Tuple containing: + - First element: `"github_repository"` indicating the verification source + - Second element: Map containing: + - `"address_hash"`: The contract's address hash to verify + + ## Returns + - Result of the broadcast operation + """ + @spec perform({binary(), %{String.t() => any()}}) :: any() + def perform({"github_repository", %{"address_hash" => address_hash} = params}) do + broadcast(:publish, address_hash, [address_hash, params]) + end + + # Broadcasts the result of a Stylus smart contract verification attempt. + # + # Executes the specified verification method in the `Publisher` module and + # broadcasts the result through the events publisher. + # + # ## Parameters + # - `method`: The verification method to execute + # - `address_hash`: Contract address + # - `args`: Arguments to pass to the verification method + # + # ## Returns + # - `{:ok, contract}` if verification succeeds + # - `{:error, changeset}` if verification fails + @spec broadcast(atom(), binary() | Explorer.Chain.Hash.t(), any()) :: any() + defp broadcast(method, address_hash, args) do + result = + case apply(Publisher, method, args) do + {:ok, _contract} = result -> + result + + {:error, changeset} -> + Logger.error( + "Stylus smart-contract verification #{address_hash} failed because of the error: #{inspect(changeset)}" + ) + + {:error, changeset} + end + + Logger.info("Smart-contract #{address_hash} verification: broadcast verification results") + + EventsPublisher.broadcast([{:contract_verification_result, {String.downcase(address_hash), result}}], :on_demand) + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/stylus/verifier.ex b/apps/explorer/lib/explorer/smart_contract/stylus/verifier.ex new file mode 100644 index 0000000000..4d90ee37d2 --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/stylus/verifier.ex @@ -0,0 +1,108 @@ +defmodule Explorer.SmartContract.Stylus.Verifier do + @moduledoc """ + Verifies Stylus smart contracts by comparing their source code against deployed bytecode. + + This module handles verification of Stylus smart contracts through their GitHub repository + source code. It interfaces with a verification microservice that: + - Fetches source code from the specified GitHub repository and commit + - Compiles the code using the specified cargo-stylus version + - Compares the resulting bytecode against the deployed contract bytecode + - Returns verification details including ABI and contract metadata + """ + alias Explorer.Chain.{Hash, SmartContract} + alias Explorer.SmartContract.StylusVerifierInterface + + require Logger + + @doc """ + Verifies a Stylus smart contract by comparing source code from a GitHub repository against the deployed bytecode using a verification microservice. + + ## Parameters + - `address_hash`: Contract address + - `params`: Map containing verification parameters: + - `cargo_stylus_version`: Version of cargo-stylus used for deployment + - `repository_url`: GitHub repository URL containing contract code + - `commit`: Git commit hash used for deployment + - `path_prefix`: Optional path prefix if contract is not in repository root + + ## Returns + - `{:ok, map}` with verification details: + - `abi`: Contract ABI (optional) + - `contract_name`: Contract name (optional) + - `package_name`: Package name + - `files`: Map of file paths to contents used in verification + - `cargo_stylus_version`: Version of cargo-stylus used + - `github_repository_metadata`: Repository metadata (optional) + - `{:error, any}` if verification fails or is disabled + """ + @spec evaluate_authenticity(EthereumJSONRPC.address() | Hash.Address.t(), map()) :: + {:ok, map()} | {:error, any()} + def evaluate_authenticity(address_hash, params) do + evaluate_authenticity_inner(StylusVerifierInterface.enabled?(), address_hash, params) + rescue + exception -> + Logger.error(fn -> + [ + "Error while verifying smart-contract address: #{address_hash}, params: #{inspect(params, limit: :infinity, printable_limit: :infinity)}: ", + Exception.format(:error, exception, __STACKTRACE__) + ] + end) + end + + # Verifies the authenticity of a Stylus smart contract using GitHub repository source code. + # + # This function retrieves the contract creation transaction and blockchain RPC endpoint, + # which together with passed parameters are required by the verification microservice to + # validate the contract deployment and verify the source code against the deployed + # bytecode. + # + # ## Parameters + # - `true`: Required boolean flag to proceed with verification + # - `address_hash`: Contract address + # - `params`: Map containing verification parameters + # + # ## Returns + # - `{:ok, map}` with verification details including ABI, contract name, and source files + # - `{:error, any}` if verification fails + @spec evaluate_authenticity_inner(boolean(), EthereumJSONRPC.address() | Hash.Address.t(), map()) :: + {:ok, map()} | {:error, any()} + defp evaluate_authenticity_inner(true, address_hash, params) do + transaction_hash = fetch_data_for_stylus_verification(address_hash) + rpc_endpoint = Application.get_env(:explorer, :json_rpc_named_arguments)[:transport_options][:url] + + params + |> Map.take(["cargo_stylus_version", "repository_url", "commit", "path_prefix"]) + |> Map.put("rpc_endpoint", rpc_endpoint) + |> Map.put("deployment_transaction", transaction_hash) + |> StylusVerifierInterface.verify_github_repository() + end + + defp evaluate_authenticity_inner(false, _address_hash, _params) do + {:error, "Stylus verification is disabled"} + end + + # Retrieves the transaction hash that created a Stylus smart contract. + + # Looks up the creation transaction for the given contract address and returns its hash. + # Checks both regular transactions and internal transactions. + + # ## Parameters + # - `address_hash`: The address hash of the smart contract as a binary or `t:Hash.Address.t/0` + + # ## Returns + # - `t:Hash.t/0` - The transaction hash if found + # - `nil` - If no creation transaction exists + @spec fetch_data_for_stylus_verification(binary() | Hash.Address.t()) :: Hash.t() | nil + defp fetch_data_for_stylus_verification(address_hash) do + case SmartContract.creation_transaction_with_bytecode(address_hash) do + %{transaction: transaction} -> + transaction.hash + + %{internal_transaction: internal_transaction} -> + internal_transaction.transaction_hash + + _ -> + nil + end + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex b/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex new file mode 100644 index 0000000000..596ebc6b2c --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex @@ -0,0 +1,165 @@ +defmodule Explorer.SmartContract.StylusVerifierInterface do + @moduledoc """ + Provides an interface for verifying Stylus smart contracts by interacting with a verification + microservice. + + Handles verification requests for Stylus contracts deployed from GitHub repositories by + communicating with an external verification service. + """ + alias HTTPoison.Response + require Logger + + @post_timeout :timer.minutes(5) + @request_error_msg "Error while sending request to stylus verification microservice" + + @doc """ + Verifies a Stylus smart contract using source code from a GitHub repository. + + Sends verification request to the verification microservice with repository details + and deployment information. + + ## Parameters + - `body`: A map containing: + - `deployment_transaction`: Transaction hash where contract was deployed + - `rpc_endpoint`: RPC endpoint URL for the chain + - `cargo_stylus_version`: Version of cargo-stylus used for deployment + - `repository_url`: GitHub repository URL containing contract code + - `commit`: Git commit hash used for deployment + - `path_prefix`: Optional path prefix if contract is not in repository root + + ## Returns + - `{:ok, map}` with verification details: + - `abi`: Contract ABI (optional) + - `contract_name`: Contract name (optional) + - `package_name`: Package name + - `files`: Map of file paths to contents used in verification + - `cargo_stylus_version`: Version of cargo-stylus used + - `github_repository_metadata`: Repository metadata (optional) + - `{:error, any}` if verification fails + """ + @spec verify_github_repository(map()) :: {:ok, map()} | {:error, any()} + def verify_github_repository( + %{ + "deployment_transaction" => _, + "rpc_endpoint" => _, + "cargo_stylus_version" => _, + "repository_url" => _, + "commit" => _, + "path_prefix" => _ + } = body + ) do + http_post_request(github_repository_verification_url(), body) + end + + @spec http_post_request(String.t(), map()) :: {:ok, map()} | {:error, any()} + defp http_post_request(url, body) do + headers = [{"Content-Type", "application/json"}] + + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %Response{body: body, status_code: _}} -> + process_verifier_response(body) + + {:error, error} -> + Logger.error(fn -> + [ + "Error while sending request to verification microservice url: #{url}, body: #{inspect(body, limit: :infinity, printable_limit: :infinity)}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + {:error, @request_error_msg} + end + end + + @spec http_get_request(String.t()) :: {:ok, [String.t()]} | {:error, any()} + defp http_get_request(url) do + case HTTPoison.get(url) do + {:ok, %Response{body: body, status_code: 200}} -> + process_verifier_response(body) + + {:ok, %Response{body: body, status_code: _}} -> + {:error, body} + + {:error, error} -> + Logger.error(fn -> + [ + "Error while sending request to verification microservice url: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + {:error, @request_error_msg} + end + end + + @doc """ + Retrieves a list of supported versions of Cargo Stylus package from the verification microservice. + + ## Returns + - `{:ok, [String.t()]}` - List of versions on success + - `{:error, any()}` - Error message if the request fails + """ + @spec get_versions_list() :: {:ok, [String.t()]} | {:error, any()} + def get_versions_list do + http_get_request(versions_list_url()) + end + + @spec process_verifier_response(binary()) :: {:ok, map() | [String.t()]} | {:error, any()} + defp process_verifier_response(body) when is_binary(body) do + case Jason.decode(body) do + {:ok, decoded} -> + process_verifier_response(decoded) + + _ -> + {:error, body} + end + end + + # Handles response from `stylus-sdk-rs/verify-github-repository` of stylus verifier microservice + @spec process_verifier_response(map()) :: {:ok, map()} + defp process_verifier_response(%{"verification_success" => source}) do + {:ok, source} + end + + # Handles response from `stylus-sdk-rs/verify-github-repository` of stylus verifier microservice + @spec process_verifier_response(map()) :: {:ok, map()} + defp process_verifier_response(%{"verificationSuccess" => source}) do + {:ok, source} + end + + # Handles response from `stylus-sdk-rs/verify-github-repository` of stylus verifier microservice + @spec process_verifier_response(map()) :: {:error, String.t()} + defp process_verifier_response(%{"verification_failure" => %{"message" => error_message}}) do + {:error, error_message} + end + + # Handles response from `stylus-sdk-rs/verify-github-repository` of stylus verifier microservice + @spec process_verifier_response(map()) :: {:error, String.t()} + defp process_verifier_response(%{"verificationFailure" => %{"message" => error_message}}) do + {:error, error_message} + end + + # Handles response from `stylus-sdk-rs/cargo-stylus-versions` of stylus verifier microservice + @spec process_verifier_response(map()) :: {:ok, [String.t()]} + defp process_verifier_response(%{"versions" => versions}), do: {:ok, Enum.map(versions, &Map.fetch!(&1, "version"))} + + @spec process_verifier_response(any()) :: {:error, any()} + defp process_verifier_response(other) do + {:error, other} + end + + # Uses url encoded ("%3A") version of ':', as ':' symbol breaks `Bypass` library during tests. + # https://github.com/PSPDFKit-labs/bypass/issues/122 + + defp github_repository_verification_url, + do: base_api_url() <> "%3Averify-github-repository" + + defp versions_list_url, do: base_api_url() <> "/cargo-stylus-versions" + + defp base_api_url, do: "#{base_url()}" <> "/api/v1/stylus-sdk-rs" + + defp base_url, do: Application.get_env(:explorer, __MODULE__)[:service_url] + + def enabled?, + do: !is_nil(base_url()) && Application.get_env(:explorer, :chain_type) == :arbitrum +end diff --git a/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex b/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex index da7c92854a..c2fcfa9768 100644 --- a/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex @@ -220,7 +220,8 @@ defmodule Explorer.SmartContract.Vyper.Publisher do file_path: params["file_path"], compiler_settings: clean_compiler_settings, license_type: prepare_license_type(params["license_type"]) || :none, - is_blueprint: params["is_blueprint"] || false + is_blueprint: params["is_blueprint"] || false, + language: :vyper } end diff --git a/apps/explorer/lib/explorer/smart_contract/vyper/publisher_worker.ex b/apps/explorer/lib/explorer/smart_contract/vyper/publisher_worker.ex index 690efc3466..a060197d2f 100644 --- a/apps/explorer/lib/explorer/smart_contract/vyper/publisher_worker.ex +++ b/apps/explorer/lib/explorer/smart_contract/vyper/publisher_worker.ex @@ -39,9 +39,12 @@ defmodule Explorer.SmartContract.Vyper.PublisherWorker do Logger.info("Smart-contract #{address_hash} verification: broadcast verification results") if conn do - EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result, conn}}], :on_demand) + EventsPublisher.broadcast( + [{:contract_verification_result, {String.downcase(address_hash), result, conn}}], + :on_demand + ) else - EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result}}], :on_demand) + EventsPublisher.broadcast([{:contract_verification_result, {String.downcase(address_hash), result}}], :on_demand) end end end diff --git a/apps/explorer/priv/arbitrum/migrations/20241111195112_add_stylus_fields.exs b/apps/explorer/priv/arbitrum/migrations/20241111195112_add_stylus_fields.exs new file mode 100644 index 0000000000..fb9ae49174 --- /dev/null +++ b/apps/explorer/priv/arbitrum/migrations/20241111195112_add_stylus_fields.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Arbitrum.Migrations.AddStylusFields do + use Ecto.Migration + + def change do + alter table(:smart_contracts) do + add(:package_name, :string, null: true) + add(:github_repository_metadata, :jsonb, null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20241111200520_add_language_field.exs b/apps/explorer/priv/repo/migrations/20241111200520_add_language_field.exs new file mode 100644 index 0000000000..28a811359d --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20241111200520_add_language_field.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.AddLanguageField do + use Ecto.Migration + + def change do + alter table(:smart_contracts) do + add(:language, :int2, null: true) + end + end +end diff --git a/config/config_helper.exs b/config/config_helper.exs index 512291e8b0..22bf43f264 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -346,4 +346,42 @@ defmodule ConfigHelper do urls -> urls end end + + @doc """ + Parses and validates a microservice URL from an environment variable, removing any trailing slash. + + ## Parameters + - `env_name`: The name of the environment variable containing the URL + + ## Returns + - The validated URL string with any trailing slash removed + - `nil` if the URL is invalid or missing required components + """ + @spec parse_microservice_url(String.t()) :: String.t() | nil + def parse_microservice_url(env_name) do + url = System.get_env(env_name) + + cond do + not valid_url?(url) -> + nil + + String.ends_with?(url, "/") -> + url + |> String.slice(0..(String.length(url) - 2)) + + true -> + url + end + end + + # Validates if the given string is a valid URL by checking if it has both scheme (like http, + # https, ftp) and host components. + @spec valid_url?(String.t()) :: boolean() + defp valid_url?(string) when is_binary(string) do + uri = URI.parse(string) + + !is_nil(uri.scheme) && !is_nil(uri.host) + end + + defp valid_url?(_), do: false end diff --git a/config/runtime.exs b/config/runtime.exs index 1c69447732..263bf4b34a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -538,6 +538,9 @@ config :explorer, Explorer.MicroserviceInterfaces.Metadata, service_url: System.get_env("MICROSERVICE_METADATA_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_METADATA_ENABLED") +config :explorer, Explorer.SmartContract.StylusVerifierInterface, + service_url: ConfigHelper.parse_microservice_url("MICROSERVICE_STYLUS_VERIFIER_URL") + config :explorer, :air_table_public_tags, table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index c3608980e2..1782375eb0 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -398,6 +398,7 @@ MICROSERVICE_ACCOUNT_ABSTRACTION_ENABLED=false MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ # MICROSERVICE_METADATA_URL= # MICROSERVICE_METADATA_ENABLED= +# MICROSERVICE_STYLUS_VERIFIER_URL= DECODE_NOT_A_CONTRACT_CALLS=true # DATABASE_READ_ONLY_API_URL= # ACCOUNT_DATABASE_URL=