Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Blockscout contract verification #308

Merged
merged 23 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4b9f82b
Implement Blockscout contract verification
DanielSchiavini Sep 11, 2024
c47b7e7
Move verify to deployer
DanielSchiavini Sep 11, 2024
5ce6bfb
fix path resolver
charles-cooper Sep 11, 2024
572ef3f
bring back VyperContract name
charles-cooper Sep 11, 2024
96622c7
roll back further
charles-cooper Sep 11, 2024
22a9c84
remove workaround
charles-cooper Sep 11, 2024
5a72857
fix lint
charles-cooper Sep 11, 2024
f2431ec
Review comments
DanielSchiavini Sep 12, 2024
142c3fb
Merge branch 'master' of github.com:vyperlang/titanoboa into blockscout
DanielSchiavini Sep 12, 2024
97595c8
Get rid of evm_version
DanielSchiavini Sep 12, 2024
57bc002
Remove default value
DanielSchiavini Sep 13, 2024
32ea0ee
roll back line movement
charles-cooper Sep 16, 2024
c8b5a52
fix api_key and address.lower() usages
charles-cooper Sep 16, 2024
5a580c5
fix: use self.timeout instead of constant
charles-cooper Sep 16, 2024
b82cf7c
add set_blockscout function
charles-cooper Sep 16, 2024
7b9042d
Rename to verifier, use it in vyper contract
DanielSchiavini Sep 19, 2024
97bd2b1
boa.verify
DanielSchiavini Sep 19, 2024
a0a8a67
remove BaseVyperContract.verify
charles-cooper Sep 21, 2024
8063277
make is_verified() a function rather than property
charles-cooper Sep 21, 2024
14fbfff
sleep after initial ping, not before
charles-cooper Sep 21, 2024
57c6b0b
clean up some code, add `wait=False` parameter
charles-cooper Sep 21, 2024
60dba56
move boa.verify to verifiers
charles-cooper Sep 21, 2024
72d93d1
update test
charles-cooper Sep 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from boa.precompile import precompile
from boa.test.strategies import fuzz
from boa.util.open_ctx import Open
from boa.verifiers import get_verifier, set_verifier, verify
from boa.vm.py_evm import enable_pyevm_verbose_logging, patch_opcode

# turn off tracebacks if we are in repl
Expand Down
23 changes: 12 additions & 11 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from vyper.codegen.module import generate_ir_for_module
from vyper.compiler import CompilerData
from vyper.compiler import output as compiler_output
from vyper.compiler.output import build_abi_output
from vyper.compiler.output import build_abi_output, build_solc_json
from vyper.compiler.settings import OptimizationLevel, anchor_settings
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
from vyper.exceptions import VyperException
from vyper.ir.optimizer import optimize
Expand Down Expand Up @@ -123,9 +123,15 @@ def at(self, address: Any) -> "VyperContract":
ret._set_bytecode(bytecode)

ret.env.register_contract(address, ret)

return ret

@cached_property
def standard_json(self):
"""
Generates a standard JSON representation of the Vyper contract.
"""
return build_solc_json(self.compiler_data)

@cached_property
def _constants(self):
# Make constants available at compile time. Useful for testing. See #196
Expand Down Expand Up @@ -155,6 +161,10 @@ def __init__(
msg += f"{capabilities.describe_capabilities()}"
raise Exception(msg)

@cached_property
def deployer(self):
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
return VyperDeployer(self.compiler_data, filename=self.filename)

@cached_property
def abi(self):
return build_abi_output(self.compiler_data)
Expand Down Expand Up @@ -204,10 +214,6 @@ def __init__(

self.env.register_blueprint(compiler_data.bytecode, self)

@cached_property
def deployer(self):
return VyperDeployer(self.compiler_data, filename=self.filename)


class FrameDetail(dict):
def __init__(self, fn_name, *args, **kwargs):
Expand Down Expand Up @@ -631,11 +637,6 @@ def __repr__(self):
def _immutables(self):
return ImmutablesModel(self)

@cached_property
def deployer(self):
# TODO add test
return VyperDeployer(self.compiler_data, filename=self.filename)

# is this actually useful?
def at(self, address):
return self.deployer.at(address)
Expand Down
13 changes: 8 additions & 5 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ def compiler_data(
) -> CompilerData:
global _disk_cache, _search_path

path = Path(contract_name)
resolved_path = Path(filename).resolve(strict=False)

file_input = FileInput(
contents=source_code,
source_id=-1,
path=Path(contract_name),
resolved_path=Path(filename),
contents=source_code, source_id=-1, path=path, resolved_path=resolved_path
)

search_paths = get_search_paths(_search_path)
input_bundle = FilesystemInputBundle(search_paths)

Expand Down Expand Up @@ -208,14 +209,16 @@ def loads_partial(
dedent: bool = True,
compiler_args: dict = None,
) -> VyperDeployer:
name = name or "VyperContract" # TODO handle this upstream in CompilerData
name = name or "VyperContract"
filename = filename or "<unknown>"

if dedent:
source_code = textwrap.dedent(source_code)

version = _detect_version(source_code)
if version is not None and version != vyper.__version__:
filename = str(filename) # help mypy
# TODO: pass name to loads_partial_vvm, not filename
return _loads_partial_vvm(source_code, version, filename)

compiler_args = compiler_args or {}
Expand Down
162 changes: 162 additions & 0 deletions boa/verifiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import json
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
from http import HTTPStatus
from typing import Optional

import requests

from boa.util.abi import Address
from boa.util.open_ctx import Open

DEFAULT_BLOCKSCOUT_URI = "https://eth.blockscout.com"


@dataclass
class Blockscout:
"""
Allows users to verify contracts on Blockscout.
This is independent of Vyper contracts, and can be used to verify any smart contract.
"""

uri: str = DEFAULT_BLOCKSCOUT_URI
api_key: Optional[str] = None
timeout: timedelta = timedelta(minutes=2)
backoff: timedelta = timedelta(milliseconds=500)
backoff_factor: float = 1.1
retry_http_codes: tuple[int, ...] = (
HTTPStatus.NOT_FOUND,
HTTPStatus.INTERNAL_SERVER_ERROR,
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
HTTPStatus.SERVICE_UNAVAILABLE,
HTTPStatus.GATEWAY_TIMEOUT,
)

def verify(
self,
address: Address,
contract_name: str,
standard_json: dict,
license_type: str = None,
wait: bool = False,
) -> Optional["VerificationResult"]:
"""
Verify the Vyper contract on Blockscout.
:param address: The address of the contract.
:param contract_name: The name of the contract.
:param standard_json: The standard JSON output of the Vyper compiler.
:param license_type: The license to use for the contract. Defaults to "none".
:param wait: Whether to return a VerificationResult immediately
or wait for verification to complete. Defaults to False
"""
if license_type is None:
license_type = "none"

api_key = self.api_key or ""

url = f"{self.uri}/api/v2/smart-contracts/{address}/"
url += f"verification/via/vyper-standard-input?apikey={api_key}"
data = {
"compiler_version": standard_json["compiler_version"],
"license_type": license_type,
}
files = {
"files[0]": (
contract_name,
json.dumps(standard_json).encode("utf-8"),
"application/json",
)
}

response = requests.post(url, data=data, files=files)
response.raise_for_status()
print(response.json().get("message")) # usually verification started

if not wait:
return VerificationResult(address, self)

self.wait_for_verification(address)
return None

def wait_for_verification(self, address: Address) -> None:
"""
Waits for the contract to be verified on Blockscout.
:param address: The address of the contract.
"""
timeout = datetime.now() + self.timeout
wait_time = self.backoff
while datetime.now() < timeout:
if self.is_verified(address):
msg = "Contract verified!"
msg += f" {self.uri}/address/{address}?tab=contract_code"
print(msg)
return
time.sleep(wait_time.total_seconds())
wait_time *= self.backoff_factor

raise TimeoutError("Timeout waiting for verification to complete")

def is_verified(self, address: Address) -> bool:
api_key = self.api_key or ""
url = f"{self.uri}/api/v2/smart-contracts/{address}?apikey={api_key}"

response = requests.get(url)
if response.status_code in self.retry_http_codes:
return False
response.raise_for_status()
return True


_verifier = Blockscout()


@dataclass
class VerificationResult:
address: Address
verifier: Blockscout

def wait_for_verification(self):
self.verifier.wait_for_verification(self.address)

def is_verified(self):
return self.verifier.is_verified(self.address)


def _set_verifier(verifier):
global _verifier
_verifier = verifier


def get_verifier():
global _verifier
return _verifier


# TODO: maybe allow like `set_verifier("blockscout", *args, **kwargs)`
def set_verifier(verifier):
return Open(get_verifier, _set_verifier, verifier)


def verify(contract, verifier=None, license_type: str = None) -> VerificationResult:
"""
Verifies the contract on a block explorer.
:param contract: The contract to verify.
:param verifier: The block explorer verifier to use.
Defaults to get_verifier().
:param license_type: Optional license to use for the contract.
"""
if verifier is None:
verifier = get_verifier()

if not hasattr(contract, "deployer") or not hasattr(
contract.deployer, "standard_json"
):
raise ValueError(f"Not a contract! {contract}")

address = contract.address
return verifier.verify(
address=address,
standard_json=contract.deployer.standard_json,
contract_name=contract.contract_name,
license_type=license_type,
)
12 changes: 12 additions & 0 deletions tests/integration/network/sepolia/test_sepolia_env.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os

import pytest

import boa
from boa.network import NetworkEnv
from boa.verifiers import Blockscout

# boa.env.anchor() does not work in prod environment
pytestmark = pytest.mark.ignore_isolation
Expand Down Expand Up @@ -32,6 +35,15 @@ def simple_contract():
return boa.loads(code, STARTING_SUPPLY)


def test_verify(simple_contract):
api_key = os.getenv("BLOCKSCOUT_API_KEY")
blockscout = Blockscout("https://eth-sepolia.blockscout.com", api_key)
with boa.set_verifier(blockscout):
result = boa.verify(simple_contract, blockscout)
result.wait_for_verification()
assert result.is_verified()


def test_env_type():
# sanity check
assert isinstance(boa.env, NetworkEnv)
Expand Down
Loading