Integrate Eth Bytecode DB (#7187)
* Add Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand * Add smart_contract_was_verified broadcast * Changelog * Rollback rename * Refactoring * Fix warning on rendering contracts without constructor args * Add metadata to the verification request to microservice * Add ETH_BYTECODE_DB_INTERVAL_BETWEEN_LOOKUPS * Add MICROSERVICE_ prefix to MICROSERVICE_ETH_BYTECODE_DB_INTERVAL_BETWEEN_LOOKUPS; Add MICROSERVICE_SC_VERIFIER_TYPE * Fix tests * Update runtime.exs * Update common-blockscout.env --------- Co-authored-by: Victor Baranov <baranov.viktor.27@gmail.com>pull/7245/head
parent
46f77791e3
commit
3633b7d273
@ -0,0 +1,17 @@ |
||||
{ |
||||
"sources": [ |
||||
{ |
||||
"fileName": "Test.sol", |
||||
"contractName": "Test", |
||||
"compilerVersion": "v0.8.17+commit.8df45f5f", |
||||
"compilerSettings": "{\"libraries\":{\"Test.sol\":{}},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":199},\"outputSelection\":{\"*\":{\"\":[\"ast\"],\"*\":[\"abi\",\"evm.bytecode\",\"evm.deployedBytecode\",\"evm.methodIdentifiers\"]}}}", |
||||
"sourceType": "SOLIDITY", |
||||
"sourceFiles": { |
||||
"Test.sol": "// SPDX-License-Identifier: MIT\r\n\r\npragma solidity 0.8.17;\r\n\r\ncontract Test {\r\n enum E {\r\n V1, V2, V3, V4\r\n }\r\n struct A {\r\n E a;\r\n uint256[] b;\r\n B[] c;\r\n }\r\n\r\n struct B {\r\n uint256 d;\r\n uint256 e;\r\n }\r\n\r\n function get(uint256 x) external pure returns (A memory) {\r\n uint256[] memory b = new uint256[](3);\r\n b[0] = 1;\r\n b[1] = 2;\r\n b[2] = 3;\r\n B[] memory c = new B[](3);\r\n c[0] = B(1, 2);\r\n c[1] = B(3, 4);\r\n c[2] = B(5, 6);\r\n return A(E.V3, b, c);\r\n }\r\n}" |
||||
}, |
||||
"abi": "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"x\",\"type\":\"uint256\"}],\"name\":\"get\",\"outputs\":[{\"components\":[{\"type\":\"uint8\"},{\"type\":\"uint256[]\"},{\"components\":[{\"type\":\"uint256\"},{\"type\":\"uint256\"}],\"type\":\"tuple[]\"}],\"internalType\":\"struct Test.A\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}]", |
||||
"constructorArguments": null, |
||||
"matchType": "PARTIAL" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,112 @@ |
||||
defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do |
||||
@moduledoc """ |
||||
On demand fetcher sources for unverified smart contract from [Ethereum Bytecode DB](https://github.com/blockscout/blockscout-rs/tree/main/eth-bytecode-db/eth-bytecode-db) |
||||
""" |
||||
|
||||
use GenServer |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.{Address, Data, SmartContract} |
||||
alias Explorer.Chain.Events.Publisher |
||||
alias Explorer.SmartContract.EthBytecodeDBInterface |
||||
alias Explorer.SmartContract.Solidity.Publisher, as: SolidityPublisher |
||||
alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher |
||||
|
||||
import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, contract_creation_input: 1] |
||||
|
||||
@cache_name :smart_contracts_sources_fetching |
||||
|
||||
def trigger_fetch(nil, _) do |
||||
:ignore |
||||
end |
||||
|
||||
def trigger_fetch(_address, %SmartContract{}) do |
||||
:ignore |
||||
end |
||||
|
||||
def trigger_fetch(address, _) do |
||||
GenServer.cast(__MODULE__, {:fetch, address}) |
||||
end |
||||
|
||||
defp fetch_sources(address) do |
||||
creation_tx_input = contract_creation_input(address.hash) |
||||
|
||||
with {:ok, %{"sourceType" => type} = source} <- |
||||
%{} |
||||
|> prepare_bytecode_for_microservice(creation_tx_input, Data.to_string(address.contract_code)) |
||||
|> EthBytecodeDBInterface.search_contract(), |
||||
{:ok, _} <- process_contract_source(type, source, address.hash) do |
||||
Publisher.broadcast(%{smart_contract_was_verified: [address.hash]}, :on_demand) |
||||
else |
||||
_ -> |
||||
false |
||||
end |
||||
|
||||
:ets.insert(@cache_name, {to_string(address.hash), DateTime.utc_now()}) |
||||
end |
||||
|
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(opts) do |
||||
:ets.new(@cache_name, [ |
||||
:set, |
||||
:named_table, |
||||
:public |
||||
]) |
||||
|
||||
{:ok, opts} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_cast({:fetch, address}, state) do |
||||
if need_to_fetch_sources?(address) && check_interval(to_string(address.hash)) do |
||||
fetch_sources(address) |
||||
end |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
defp need_to_fetch_sources?(%Address{smart_contract: nil}), do: true |
||||
|
||||
defp need_to_fetch_sources?(%Address{hash: hash}) do |
||||
case Chain.address_hash_to_one_smart_contract(hash) do |
||||
nil -> |
||||
true |
||||
|
||||
_ -> |
||||
false |
||||
end |
||||
end |
||||
|
||||
defp check_interval(address_string) do |
||||
fetch_interval = |
||||
Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand)[:fetch_interval] |
||||
|
||||
case :ets.lookup(@cache_name, address_string) do |
||||
[{_, datetime}] -> |
||||
datetime |
||||
|> DateTime.add(fetch_interval, :millisecond) |
||||
|> DateTime.compare(DateTime.utc_now()) != :gt |
||||
|
||||
_ -> |
||||
true |
||||
end |
||||
end |
||||
|
||||
def process_contract_source("SOLIDITY", source, address_hash) do |
||||
SolidityPublisher.process_rust_verifier_response(source, address_hash, true, true) |
||||
end |
||||
|
||||
def process_contract_source("VYPER", source, address_hash) do |
||||
VyperPublisher.process_rust_verifier_response(source, address_hash, true) |
||||
end |
||||
|
||||
def process_contract_source("YUL", source, address_hash) do |
||||
SolidityPublisher.process_rust_verifier_response(source, address_hash, true, true) |
||||
end |
||||
|
||||
def process_contract_source(_, _source, _address_hash), do: false |
||||
end |
@ -0,0 +1,21 @@ |
||||
defmodule Explorer.SmartContract.EthBytecodeDBInterface do |
||||
@moduledoc """ |
||||
Adapter for interaction with https://github.com/blockscout/blockscout-rs/tree/main/eth-bytecode-db |
||||
""" |
||||
|
||||
def search_contract(%{"bytecode" => _, "bytecodeType" => _} = body) do |
||||
http_post_request(bytecode_search_sources_url(), body) |
||||
end |
||||
|
||||
def process_verifier_response(%{"sources" => [src | _]}) do |
||||
{:ok, src} |
||||
end |
||||
|
||||
def process_verifier_response(%{"sources" => []}) do |
||||
{:ok, nil} |
||||
end |
||||
|
||||
def bytecode_search_sources_url, do: "#{base_api_url()}" <> "/bytecodes/sources:search" |
||||
|
||||
use Explorer.SmartContract.RustVerifierInterfaceBehaviour |
||||
end |
@ -0,0 +1,158 @@ |
||||
defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do |
||||
@moduledoc """ |
||||
This behaviour module was created in order to add possibility to extend the functionality of RustVerifierInterface |
||||
""" |
||||
defmacro __using__(_) do |
||||
quote([]) do |
||||
alias Explorer.Utility.RustService |
||||
alias HTTPoison.Response |
||||
require Logger |
||||
|
||||
@post_timeout :timer.seconds(120) |
||||
@request_error_msg "Error while sending request to verification microservice" |
||||
|
||||
def verify_multi_part( |
||||
%{ |
||||
"bytecode" => _, |
||||
"bytecodeType" => _, |
||||
"compilerVersion" => _, |
||||
"sourceFiles" => _, |
||||
"evmVersion" => _, |
||||
"optimizationRuns" => _, |
||||
"libraries" => _ |
||||
} = body, |
||||
address_hash |
||||
) do |
||||
http_post_request(multiple_files_verification_url(), append_metadata(body, address_hash)) |
||||
end |
||||
|
||||
def verify_standard_json_input( |
||||
%{ |
||||
"bytecode" => _, |
||||
"bytecodeType" => _, |
||||
"compilerVersion" => _, |
||||
"input" => _ |
||||
} = body, |
||||
address_hash |
||||
) do |
||||
http_post_request(standard_json_input_verification_url(), append_metadata(body, address_hash)) |
||||
end |
||||
|
||||
def vyper_verify_multipart( |
||||
%{ |
||||
"bytecode" => _, |
||||
"bytecodeType" => _, |
||||
"compilerVersion" => _, |
||||
"sourceFiles" => _ |
||||
} = body, |
||||
address_hash |
||||
) do |
||||
http_post_request(vyper_multiple_files_verification_url(), append_metadata(body, address_hash)) |
||||
end |
||||
|
||||
def 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} -> |
||||
old_truncate = Application.get_env(:logger, :truncate) |
||||
Logger.configure(truncate: :infinity) |
||||
|
||||
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) |
||||
|
||||
Logger.configure(truncate: old_truncate) |
||||
{:error, @request_error_msg} |
||||
end |
||||
end |
||||
|
||||
def 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} -> |
||||
old_truncate = Application.get_env(:logger, :truncate) |
||||
Logger.configure(truncate: :infinity) |
||||
|
||||
Logger.error(fn -> |
||||
[ |
||||
"Error while sending request to verification microservice url: #{url}: ", |
||||
inspect(error, limit: :infinity, printable_limit: :infinity) |
||||
] |
||||
end) |
||||
|
||||
Logger.configure(truncate: old_truncate) |
||||
{:error, @request_error_msg} |
||||
end |
||||
end |
||||
|
||||
def get_versions_list do |
||||
http_get_request(versions_list_url()) |
||||
end |
||||
|
||||
def vyper_get_versions_list do |
||||
http_get_request(vyper_versions_list_url()) |
||||
end |
||||
|
||||
def process_verifier_response(body) when is_binary(body) do |
||||
case Jason.decode(body) do |
||||
{:ok, decoded} -> |
||||
process_verifier_response(decoded) |
||||
|
||||
_ -> |
||||
{:error, body} |
||||
end |
||||
end |
||||
|
||||
def process_verifier_response(%{"status" => "SUCCESS", "source" => source}) do |
||||
{:ok, source} |
||||
end |
||||
|
||||
def process_verifier_response(%{"status" => "FAILURE", "message" => error}) do |
||||
{:error, error} |
||||
end |
||||
|
||||
def process_verifier_response(%{"compilerVersions" => versions}), do: {:ok, versions} |
||||
|
||||
def process_verifier_response(other), do: {:error, other} |
||||
|
||||
def multiple_files_verification_url, do: "#{base_api_url()}" <> "/verifier/solidity/sources:verify-multi-part" |
||||
|
||||
def vyper_multiple_files_verification_url, do: "#{base_api_url()}" <> "/verifier/vyper/sources:verify-multi-part" |
||||
|
||||
def standard_json_input_verification_url, |
||||
do: "#{base_api_url()}" <> "/verifier/solidity/sources:verify-standard-json" |
||||
|
||||
def versions_list_url, do: "#{base_api_url()}" <> "/verifier/solidity/versions" |
||||
|
||||
def vyper_versions_list_url, do: "#{base_api_url()}" <> "/verifier/vyper/versions" |
||||
|
||||
def base_api_url, do: "#{base_url()}" <> "/api/v2" |
||||
|
||||
def base_url do |
||||
RustService.base_url(Explorer.SmartContract.RustVerifierInterfaceBehaviour) |
||||
end |
||||
|
||||
def enabled?, do: Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:enabled] |
||||
|
||||
defp append_metadata(body, address_hash) when is_map(body) do |
||||
body |
||||
|> Map.put("metadata", %{ |
||||
"chainId" => Application.get_env(:block_scout_web, :chain_id), |
||||
"contractAddress" => to_string(address_hash) |
||||
}) |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue