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