feat: Add Stylus verificaiton support (#11183)

* feat: Add Stylus verificaiton support

* Fix tests

* Apply suggestions from code review

Co-authored-by: Alexander Kolotov <alexandr.kolotov@gmail.com>

* Refactoring + handle snake case

* Add env to docker-compose/envs/common-blockscout.env

* Apply suggestions from code review

Co-authored-by: Alexander Kolotov <alexandr.kolotov@gmail.com>

* Fix cspell

* Drop MICROSERVICE_STYLUS_VERIFIER_ENABLED

* Fix tests

* Fix tests

* Apply suggestions from code review

Co-authored-by: Alexander Kolotov <alexandr.kolotov@gmail.com>

* Process review comments

* Fix dialyzer

---------

Co-authored-by: Alexander Kolotov <alexandr.kolotov@gmail.com>
master
nikitosing 11 hours ago committed by GitHub
parent c4e6f9c384
commit d5709dfda9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .dialyzer-ignore
  2. 71
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex
  3. 4
      apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex
  4. 9
      apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex
  5. 37
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  6. 20
      apps/explorer/lib/explorer/chain/smart_contract.ex
  7. 81
      apps/explorer/lib/explorer/smart_contract/compiler_version.ex
  8. 3
      apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
  9. 7
      apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex
  10. 235
      apps/explorer/lib/explorer/smart_contract/stylus/publisher.ex
  11. 72
      apps/explorer/lib/explorer/smart_contract/stylus/publisher_worker.ex
  12. 108
      apps/explorer/lib/explorer/smart_contract/stylus/verifier.ex
  13. 165
      apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex
  14. 3
      apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex
  15. 7
      apps/explorer/lib/explorer/smart_contract/vyper/publisher_worker.ex
  16. 10
      apps/explorer/priv/arbitrum/migrations/20241111195112_add_stylus_fields.exs
  17. 9
      apps/explorer/priv/repo/migrations/20241111200520_add_language_field.exs
  18. 38
      config/config_helper.exs
  19. 3
      config/runtime.exs
  20. 1
      docker-compose/envs/common-blockscout.env

@ -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

@ -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) ->

@ -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

@ -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.

@ -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

@ -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()

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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")

@ -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=

Loading…
Cancel
Save