From 493e14e9de1edfbb74d656045e3a054180b364fd Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:58:38 +0200 Subject: [PATCH 01/16] feat: :sparkles: implement retry mechanism for verification in multiple explorers --- scripts/verification/README.md | 171 +++++ scripts/verification/__init__.py | 14 + scripts/verification/etherscan_verifier.py | 192 ++++++ scripts/verification/retry.py | 69 ++ scripts/verification/sourcify_verifier.py | 109 ++++ scripts/verification/test_retry.py | 191 ++++++ scripts/verify.py | 702 ++++++++++++++------- 7 files changed, 1228 insertions(+), 220 deletions(-) create mode 100644 scripts/verification/README.md create mode 100644 scripts/verification/__init__.py create mode 100644 scripts/verification/etherscan_verifier.py create mode 100644 scripts/verification/retry.py create mode 100644 scripts/verification/sourcify_verifier.py create mode 100644 scripts/verification/test_retry.py diff --git a/scripts/verification/README.md b/scripts/verification/README.md new file mode 100644 index 00000000..1a7626d5 --- /dev/null +++ b/scripts/verification/README.md @@ -0,0 +1,171 @@ +# Enhanced Contract Verification System + +The `verify.py` script has been enhanced to provide robust retry mechanisms and support for multiple block explorers while maintaining the same simple interface. + +## Overview + +The verification system has been enhanced to address the following requirements: + +1. **Multiple Block Explorer Support**: Verify contracts on at least 2 well-known block explorers +2. **Robust Retry Mechanisms**: Implement proper retry logic with exponential backoff and jitter +3. **Fallback Options**: If one verifier fails, automatically try others +4. **Backward Compatibility**: Maintain compatibility with existing Makefile commands + +## Key Features + +### **Retry Logic with Exponential Backoff** +```python +@retry_with_backoff(max_retries=3, base_delay=2, max_delay=60) +def api_request(): + # Automatic retry with exponential backoff + jitter +``` + +### **Multi-Verifier Support** +- **Etherscan**: Primary verifier with full API support +- **Sourcify**: Secondary verifier (no API key required) +- **Automatic Fallback**: If one fails, tries the next + +### **Smart Error Handling** +- Graceful degradation when verifiers are unavailable +- Detailed logging of all attempts and failures +- Automatic retry for network issues and temporary failures + +## Usage + +### **Standard Usage (Unchanged)** +```bash +make verify addr=0x1234567890123456789012345678901234567890 +``` + +### **Direct Script Usage** +```bash +./scripts/verify.py DssSpell 0x1234567890123456789012345678901234567890 +``` + +## Configuration + +### **Environment Variables** +- `ETHERSCAN_API_KEY`: Required for Etherscan verification +- `ETH_RPC_URL`: Required for chain operations + +### **Retry Settings** +- **Max Retries**: 3 attempts +- **Base Delay**: 2 seconds +- **Max Delay**: 60 seconds +- **Backoff Factor**: 2 (exponential) +- **Jitter**: 10% random variation to prevent thundering herd problems + +## Supported Block Explorers + +### Etherscan +- **Chains**: Mainnet (1), Sepolia (11155111) +- **API**: Etherscan API v2 +- **Requirements**: API key +- **Features**: Full verification with constructor arguments and libraries + +### Sourcify +- **Chains**: Mainnet (1), Sepolia (11155111) +- **API**: Sourcify API +- **Requirements**: No API key required +- **Features**: Open-source verification service + +## Retry Mechanisms + +### **Exponential Backoff with Jitter** + +The system implements exponential backoff with jitter to prevent thundering herd problems: + +```python +delay = min(base_delay * (backoff_factor ** attempt), max_delay) +jitter_amount = delay * jitter * random.uniform(-1, 1) +actual_delay = max(0, delay + jitter_amount) +``` + +### **Retry Scenarios** + +1. **API Request Failures**: Network timeouts, HTTP errors, JSON parsing errors +2. **Contract Not Found**: Retry when contract is not yet deployed +3. **Verification Pending**: Poll for verification completion +4. **Subprocess Failures**: Forge flatten, cast commands + +### **Error Handling** + +- **Graceful Degradation**: If one verifier fails, others are still attempted +- **Detailed Logging**: All errors and retry attempts are logged +- **Fallback Strategy**: Try all available verifiers until one succeeds + +## Benefits + +1. **Reliability**: Multiple verifiers reduce single points of failure +2. **Resilience**: Retry mechanisms handle temporary network issues +3. **Transparency**: Detailed logging shows exactly what's happening +4. **Simplicity**: Single script with enhanced functionality +5. **Compatibility**: Existing workflows continue to work unchanged + +## Troubleshooting + +### **Common Issues** + +1. **"No verifiers available"**: Check chain ID support and API keys +2. **"Verification failed on all verifiers"**: Check contract deployment and source code +3. **"Etherscan API key not found"**: Set `ETHERSCAN_API_KEY` environment variable + +### **Debug Mode** + +For detailed debugging, you can run the script directly and observe the output: + +```bash +./scripts/verify.py DssSpell 0x1234567890123456789012345678901234567890 +``` + +### **Log Files** + +Failed verifications create log files with the source code for debugging: + +- `verify-etherscan-{timestamp}.log` + +## Implementation Details + +### **Verifier Classes** + +The script includes two verifier classes: + +- **`EtherscanVerifier`**: Handles Etherscan API verification +- **`SourcifyVerifier`**: Handles Sourcify API verification + +Both classes implement the same interface and can be easily extended. + +### **Retry Decorator** + +The `@retry_with_backoff` decorator provides automatic retry functionality with: + +- Configurable retry counts and delays +- Exponential backoff with jitter +- Exception filtering +- Detailed logging + +### **Multi-Verifier Logic** + +The script automatically: + +1. Sets up all available verifiers for the current chain +2. Attempts verification with each verifier in sequence +3. Stops on first successful verification +4. Provides detailed feedback on all attempts + +## Future Enhancements + +1. **Blockscout Support**: Add Blockscout verifier implementation +2. **Configurable Retry**: Allow retry parameters via environment variables +3. **Parallel Verification**: Verify on multiple explorers simultaneously +4. **Verification Status**: Check if contract is already verified before attempting +5. **Custom Verifiers**: Allow custom verifier implementations + +## Contributing + +To add a new block explorer verifier: + +1. Create a new verifier class following the existing pattern +2. Implement the required methods (`verify_contract`, `is_available`, etc.) +3. Add the verifier to the `setup_verifiers` function +4. Test with different chains and scenarios diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py new file mode 100644 index 00000000..fcad4174 --- /dev/null +++ b/scripts/verification/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +""" +Verification package for Sky Protocol spells. +""" + +from .retry import retry_with_backoff +from .etherscan_verifier import EtherscanVerifier +from .sourcify_verifier import SourcifyVerifier + +__all__ = [ + 'retry_with_backoff', + 'EtherscanVerifier', + 'SourcifyVerifier' +] diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py new file mode 100644 index 00000000..ad194055 --- /dev/null +++ b/scripts/verification/etherscan_verifier.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +""" +Etherscan block explorer verifier implementation. +""" +import os +import sys +import json +import time +import requests +from datetime import datetime +from typing import Dict, Any + +from .retry import retry_with_backoff + + +# Block explorer configurations +ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' +ETHERSCAN_SUBDOMAINS = { + '1': '', + '11155111': 'sepolia.' +} +LICENSE_NUMBERS = { + 'GPL-3.0-or-later': 5, + 'AGPL-3.0-or-later': 13 +} + + +class EtherscanVerifier: + """Etherscan block explorer verifier.""" + + def __init__(self, api_key: str, chain_id: str): + self.api_key = api_key + self.chain_id = chain_id + self.subdomain = ETHERSCAN_SUBDOMAINS.get(chain_id, '') + + def is_available(self) -> bool: + """Check if Etherscan supports this chain.""" + return self.chain_id in ETHERSCAN_SUBDOMAINS + + def get_verification_url(self, contract_address: str) -> str: + """Get Etherscan URL for the verified contract.""" + return f"https://{self.subdomain}etherscan.io/address/{contract_address}#code" + + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) + def _send_api_request(self, params: Dict[str, str], data: Dict[str, Any]) -> Dict: + """Send request to Etherscan API with retry mechanism.""" + headers = {'User-Agent': 'Sky-Protocol-Spell-Verifier'} + + response = requests.post( + ETHERSCAN_API_URL, + headers=headers, + params=params, + data=data, + timeout=30 + ) + + response.raise_for_status() + + try: + return json.loads(response.text) + except json.decoder.JSONDecodeError as e: + print(f"Response text: {response.text}", file=sys.stderr) + raise Exception(f'Etherscan responded with invalid JSON: {str(e)}') + + def _wait_for_verification(self, guid: str, params: Dict[str, str], code: str) -> None: + """Wait for verification to complete with retry mechanism.""" + check_data = { + 'apikey': self.api_key, + 'module': 'contract', + 'action': 'checkverifystatus', + 'guid': guid, + } + + check_response = {} + max_attempts = 20 + + for attempt in range(max_attempts): + if check_response and 'pending' not in check_response.get('result', '').lower(): + break + + if check_response: + print(check_response['result'], file=sys.stderr) + print(f'Waiting for 15 seconds for Etherscan to process... (attempt {attempt + 1}/{max_attempts})', file=sys.stderr) + time.sleep(15) + + try: + check_response = self._send_api_request(params, check_data) + except Exception as e: + print(f"Error checking verification status: {str(e)}", file=sys.stderr) + if attempt == max_attempts - 1: + raise + time.sleep(15) + continue + + if check_response['status'] != '1' or check_response['message'] != 'OK': + if 'already verified' not in check_response['result'].lower(): + log_name = f'verify-etherscan-{datetime.now().timestamp()}.log' + with open(log_name, 'w') as log: + log.write(code) + print(f'Source code logged to {log_name}', file=sys.stderr) + + raise Exception(f'Verification failed: {check_response.get("result", "Unknown error")}') + else: + print('Contract is already verified') + + def verify_contract( + self, + contract_name: str, + contract_address: str, + source_code: str, + constructor_args: str, + metadata: Dict[str, Any], + library_address: str = "" + ) -> bool: + """Verify contract on Etherscan.""" + print(f'\nVerifying {contract_name} at {contract_address} on Etherscan...') + + params = {'chainid': self.chain_id} + + license_name = metadata.get('license_name', 'MIT') + license_number = LICENSE_NUMBERS.get(license_name, 1) + + data = { + 'apikey': self.api_key, + 'module': 'contract', + 'action': 'verifysourcecode', + 'contractaddress': contract_address, + 'sourceCode': source_code, + 'codeFormat': 'solidity-single-file', + 'contractName': contract_name, + 'compilerversion': metadata['compiler_version'], + 'optimizationUsed': '1' if metadata['optimizer_enabled'] else '0', + 'runs': metadata['optimizer_runs'], + 'constructorArguements': constructor_args, + 'evmversion': metadata['evm_version'], + 'licenseType': license_number, + } + + if library_address: + data['libraryname1'] = 'DssExecLib' + data['libraryaddress1'] = library_address + + # Submit verification request with retry + max_retries = 3 + for attempt in range(max_retries): + try: + verify_response = self._send_api_request(params, data) + break + except Exception as e: + if attempt == max_retries - 1: + print(f"Failed to submit verification request after {max_retries} attempts: {str(e)}", file=sys.stderr) + return False + print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) + time.sleep(2 ** attempt) + + # Handle "contract not yet deployed" case + max_deploy_retries = 5 + deploy_retry_count = 0 + + while 'locate' in verify_response.get('result', '').lower() and deploy_retry_count < max_deploy_retries: + print(verify_response['result'], file=sys.stderr) + print(f'Waiting for 15 seconds for the network to update... (attempt {deploy_retry_count + 1}/{max_deploy_retries})', file=sys.stderr) + time.sleep(15) + + try: + verify_response = self._send_api_request(params, data) + except Exception as e: + print(f"Error during deploy retry: {str(e)}", file=sys.stderr) + deploy_retry_count += 1 + continue + + if deploy_retry_count >= max_deploy_retries: + print("Contract not found on network after maximum retries", file=sys.stderr) + return False + + if verify_response['status'] != '1' or verify_response['message'] != 'OK': + if 'already verified' in verify_response['result'].lower(): + print('Contract is already verified on Etherscan') + return True + print(f'Failed to submit verification request: {verify_response.get("result", "Unknown error")}', file=sys.stderr) + return False + + guid = verify_response['result'] + print(f'Verification request submitted with GUID: {guid}') + + try: + self._wait_for_verification(guid, params, source_code) + print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') + return True + except Exception as e: + print(f"Verification failed: {str(e)}", file=sys.stderr) + return False diff --git a/scripts/verification/retry.py b/scripts/verification/retry.py new file mode 100644 index 00000000..722e46f1 --- /dev/null +++ b/scripts/verification/retry.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Retry decorator with exponential backoff and jitter for robust API calls. +""" +import time +import random +import sys +from typing import Tuple, Callable +from functools import wraps + + +# Default retry configuration +DEFAULT_MAX_RETRIES = 3 +DEFAULT_BASE_DELAY = 2 # seconds +DEFAULT_MAX_DELAY = 60 # seconds +DEFAULT_BACKOFF_FACTOR = 2 +DEFAULT_JITTER = 0.1 # 10% jitter + + +def retry_with_backoff( + max_retries: int = DEFAULT_MAX_RETRIES, + base_delay: float = DEFAULT_BASE_DELAY, + max_delay: float = DEFAULT_MAX_DELAY, + backoff_factor: float = DEFAULT_BACKOFF_FACTOR, + jitter: float = DEFAULT_JITTER, + exceptions: Tuple[Exception, ...] = (Exception,) +): + """ + Decorator that implements exponential backoff with jitter for retrying functions. + + Args: + max_retries: Maximum number of retry attempts + base_delay: Initial delay between retries in seconds + max_delay: Maximum delay between retries in seconds + backoff_factor: Multiplier for exponential backoff + jitter: Random variation factor (0.1 = 10% variation) + exceptions: Tuple of exceptions to catch and retry on + + Returns: + Decorated function with retry logic + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exception = e + + if attempt == max_retries: + print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", file=sys.stderr) + raise + + # Calculate delay with exponential backoff and jitter + delay = min(base_delay * (backoff_factor ** attempt), max_delay) + jitter_amount = delay * jitter * random.uniform(-1, 1) + actual_delay = max(0, delay + jitter_amount) + + print(f"Attempt {attempt + 1} failed: {str(e)}", file=sys.stderr) + print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", file=sys.stderr) + + time.sleep(actual_delay) + + raise last_exception + return wrapper + return decorator diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py new file mode 100644 index 00000000..52b4712a --- /dev/null +++ b/scripts/verification/sourcify_verifier.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Sourcify block explorer verifier implementation. +""" +import sys +import json +import time +import requests +from typing import Dict, Any + +from .retry import retry_with_backoff + + +# Block explorer configurations +SOURCIFY_API_URL = 'https://sourcify.dev/server' + + +class SourcifyVerifier: + """Sourcify block explorer verifier.""" + + def __init__(self, chain_id: str): + self.chain_id = chain_id + + def is_available(self) -> bool: + """Check if Sourcify supports this chain.""" + return self.chain_id in ['1', '11155111'] # Mainnet and Sepolia + + def get_verification_url(self, contract_address: str) -> str: + """Get Sourcify URL for the verified contract.""" + return f"https://sourcify.dev/#/lookup/{contract_address}" + + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) + def _send_api_request(self, endpoint: str, data: Dict[str, Any]) -> Dict: + """Send request to Sourcify API with retry mechanism.""" + headers = { + 'User-Agent': 'Sky-Protocol-Spell-Verifier', + 'Content-Type': 'application/json' + } + + url = f"{SOURCIFY_API_URL}/{endpoint}" + + response = requests.post( + url, + headers=headers, + json=data, + timeout=30 + ) + + response.raise_for_status() + + try: + return response.json() + except json.decoder.JSONDecodeError as e: + print(f"Response text: {response.text}", file=sys.stderr) + raise Exception(f'Sourcify responded with invalid JSON: {str(e)}') + + def verify_contract( + self, + contract_name: str, + contract_address: str, + source_code: str, + constructor_args: str, + metadata: Dict[str, Any], + library_address: str = "" + ) -> bool: + """Verify contract on Sourcify.""" + print(f'\nVerifying {contract_name} at {contract_address} on Sourcify...') + + files_data = { + "contract.sol": source_code + } + + if metadata: + files_data["metadata.json"] = json.dumps(metadata) + + verification_data = { + "address": contract_address, + "chain": self.chain_id, + "files": files_data + } + + if constructor_args: + verification_data["constructorArgs"] = constructor_args + + max_retries = 3 + for attempt in range(max_retries): + try: + response = self._send_api_request("verify", verification_data) + + if response.get("status") == "perfect": + print(f'Contract verified successfully on Sourcify') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + elif response.get("status") == "partial": + print(f'Contract partially verified on Sourcify (some files missing)') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + else: + print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) + return False + + except Exception as e: + if attempt == max_retries - 1: + print(f"Failed to verify on Sourcify after {max_retries} attempts: {str(e)}", file=sys.stderr) + return False + print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) + time.sleep(2 ** attempt) + + return False diff --git a/scripts/verification/test_retry.py b/scripts/verification/test_retry.py new file mode 100644 index 00000000..ea9fb1df --- /dev/null +++ b/scripts/verification/test_retry.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Test suite for the retry mechanism with exponential backoff and jitter. +""" +import time +import random +import unittest +from unittest.mock import patch, MagicMock +from typing import Tuple, Callable +from functools import wraps + +from retry import retry_with_backoff, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY + + +def retry_with_backoff_test( + max_retries: int = 3, + base_delay: float = 1, + max_delay: float = 10, + backoff_factor: float = 2, + jitter: float = 0.1, + exceptions: Tuple[Exception, ...] = (Exception,) +): + """Test version of the retry decorator.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exception = e + + if attempt == max_retries: + print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}") + raise + + # Calculate delay with exponential backoff and jitter + delay = min(base_delay * (backoff_factor ** attempt), max_delay) + jitter_amount = delay * jitter * random.uniform(-1, 1) + actual_delay = max(0, delay + jitter_amount) + + print(f"Attempt {attempt + 1} failed: {str(e)}") + print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})") + + time.sleep(actual_delay) + + raise last_exception + return wrapper + return decorator + + +class TestRetryMechanism(unittest.TestCase): + """Test cases for the retry mechanism.""" + + def setUp(self): + """Set up test fixtures.""" + self.call_count = 0 + + def test_successful_retry(self): + """Test that retry mechanism works when function eventually succeeds.""" + @retry_with_backoff_test(max_retries=3, base_delay=0.1) + def test_function(): + self.call_count += 1 + if self.call_count < 3: + raise Exception(f"Simulated failure #{self.call_count}") + return "Success!" + + result = test_function() + self.assertEqual(result, "Success!") + self.assertEqual(self.call_count, 3) + + def test_max_retries_exceeded(self): + """Test that retry mechanism fails after max retries.""" + @retry_with_backoff_test(max_retries=2, base_delay=0.1) + def test_function(): + self.call_count += 1 + raise Exception(f"Persistent failure #{self.call_count}") + + with self.assertRaises(Exception) as context: + test_function() + + self.assertIn("Persistent failure #3", str(context.exception)) + self.assertEqual(self.call_count, 3) # 2 retries + 1 initial attempt + + def test_no_retry_on_success(self): + """Test that successful function doesn't retry.""" + @retry_with_backoff_test(max_retries=3, base_delay=0.1) + def test_function(): + self.call_count += 1 + return "Success!" + + result = test_function() + self.assertEqual(result, "Success!") + self.assertEqual(self.call_count, 1) # Only one call, no retries + + def test_exponential_backoff(self): + """Test that delays increase exponentially.""" + delays = [] + + @retry_with_backoff_test(max_retries=3, base_delay=1, max_delay=10) + def test_function(): + delays.append(time.time()) + raise Exception("Test failure") + + start_time = time.time() + with self.assertRaises(Exception): + test_function() + + # Check that delays are increasing (with some tolerance for jitter) + if len(delays) >= 3: + delay1 = delays[1] - delays[0] + delay2 = delays[2] - delays[1] + self.assertGreater(delay2, delay1 * 1.5) # Should be roughly 2x with jitter + + def test_jitter_variation(self): + """Test that jitter adds random variation to delays.""" + delays = [] + + @retry_with_backoff_test(max_retries=2, base_delay=1, jitter=0.2) + def test_function(): + delays.append(time.time()) + raise Exception("Test failure") + + with self.assertRaises(Exception): + test_function() + + # With jitter, delays should not be exactly the same + if len(delays) >= 2: + delay1 = delays[1] - delays[0] + delay2 = delays[2] - delays[1] + # Delays should be different due to jitter + self.assertNotEqual(delay1, delay2) + + @patch('time.sleep') + def test_specific_exception_handling(self, mock_sleep): + """Test that only specified exceptions trigger retries.""" + @retry_with_backoff_test(max_retries=2, base_delay=0.1, exceptions=(ValueError,)) + def test_function(): + self.call_count += 1 + if self.call_count == 1: + raise ValueError("Value error") + elif self.call_count == 2: + raise TypeError("Type error") # Should not retry + return "Success!" + + with self.assertRaises(TypeError): + test_function() + + # Should only retry once (for ValueError), then fail on TypeError + self.assertEqual(self.call_count, 2) + mock_sleep.assert_called_once() # Only one retry attempt + + +class TestRetryIntegration(unittest.TestCase): + """Integration tests for the retry mechanism.""" + + def test_retry_with_network_simulation(self): + """Test retry mechanism with simulated network failures.""" + failures = [True, True, False] # Fail twice, succeed on third attempt + + @retry_with_backoff_test(max_retries=3, base_delay=0.1) + def simulate_network_call(): + if failures.pop(0): + raise ConnectionError("Network timeout") + return "Network response" + + result = simulate_network_call() + self.assertEqual(result, "Network response") + self.assertEqual(len(failures), 0) # All failures consumed + + def test_retry_with_different_exception_types(self): + """Test retry mechanism with different types of exceptions.""" + exceptions = [ValueError("Bad value"), ConnectionError("Network error"), "Success"] + + @retry_with_backoff_test(max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError)) + def test_function(): + exception = exceptions.pop(0) + if isinstance(exception, Exception): + raise exception + return exception + + result = test_function() + self.assertEqual(result, "Success") + self.assertEqual(len(exceptions), 0) + + +if __name__ == "__main__": + # Run the tests + unittest.main(verbosity=2) diff --git a/scripts/verify.py b/scripts/verify.py index 40fb5ac8..fdc9673d 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """ -Contract verification script for Sky Protocol spells on Etherscan. -This script verifies both the DssSpell and DssSpellAction contracts. +Enhanced contract verification script for Sky Protocol spells. +This script verifies both the DssSpell and DssSpellAction contracts on multiple block explorers +with robust retry mechanisms and fallback options. """ import os import sys @@ -10,16 +11,29 @@ import re import json import requests +import random from datetime import datetime -from typing import Dict, Any, Tuple, Optional +from typing import Dict, Any, Tuple, Optional, List, Callable +from functools import wraps # Constants -ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' FLATTEN_OUTPUT_PATH = 'out/flat.sol' SOURCE_FILE_PATH = 'src/DssSpell.sol' LIBRARY_NAME = 'DssExecLib' + +# Retry configuration +DEFAULT_MAX_RETRIES = 3 +DEFAULT_BASE_DELAY = 2 # seconds +DEFAULT_MAX_DELAY = 60 # seconds +DEFAULT_BACKOFF_FACTOR = 2 +DEFAULT_JITTER = 0.1 # 10% jitter + +# Block explorer configurations +ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' +SOURCIFY_API_URL = 'https://sourcify.dev/server' ETHERSCAN_SUBDOMAINS = { - '1': '' + '1': '', + '11155111': 'sepolia.' } LICENSE_NUMBERS = { 'GPL-3.0-or-later': 5, @@ -27,10 +41,49 @@ } -def get_env_var(var_name: str, error_message: str) -> str: +def retry_with_backoff( + max_retries: int = DEFAULT_MAX_RETRIES, + base_delay: float = DEFAULT_BASE_DELAY, + max_delay: float = DEFAULT_MAX_DELAY, + backoff_factor: float = DEFAULT_BACKOFF_FACTOR, + jitter: float = DEFAULT_JITTER, + exceptions: Tuple[Exception, ...] = (requests.RequestException, json.JSONDecodeError, Exception) +): """ - Get environment variable with error handling. + Decorator that implements exponential backoff with jitter for retrying functions. """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exception = e + + if attempt == max_retries: + print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", file=sys.stderr) + raise + + # Calculate delay with exponential backoff and jitter + delay = min(base_delay * (backoff_factor ** attempt), max_delay) + jitter_amount = delay * jitter * random.uniform(-1, 1) + actual_delay = max(0, delay + jitter_amount) + + print(f"Attempt {attempt + 1} failed: {str(e)}", file=sys.stderr) + print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", file=sys.stderr) + + time.sleep(actual_delay) + + raise last_exception + return wrapper + return decorator + + +def get_env_var(var_name: str, error_message: str) -> str: + """Get environment variable with error handling.""" try: return os.environ[var_name] except KeyError: @@ -38,22 +91,18 @@ def get_env_var(var_name: str, error_message: str) -> str: sys.exit(1) +@retry_with_backoff(max_retries=2, base_delay=1) def get_chain_id() -> str: - """ - Get the current chain ID. - """ + """Get the current chain ID with retry mechanism.""" print('Obtaining chain ID... ') - result = subprocess.run(['cast', 'chain-id'], capture_output=True) - chain_id = result.stdout.decode('utf-8').strip() + result = subprocess.run(['cast', 'chain-id'], capture_output=True, text=True, check=True) + chain_id = result.stdout.strip() print(f"CHAIN_ID: {chain_id}") return chain_id def get_library_address() -> str: - """ - Find the DssExecLib address from either DssExecLib.address file or foundry.toml. - Returns an empty string if no library address is found. - """ + """Find the DssExecLib address from either DssExecLib.address file or foundry.toml.""" library_address = '' # First try to read from foundry.toml libraries @@ -65,8 +114,7 @@ def get_library_address() -> str: result = re.search(r':DssExecLib:(0x[0-9a-fA-F]{40})', config) if result: library_address = result.group(1) - print( - f'Using library {LIBRARY_NAME} at address {library_address}') + print(f'Using library {LIBRARY_NAME} at address {library_address}') return library_address else: print('No DssExecLib configured in foundry.toml', file=sys.stderr) @@ -84,8 +132,7 @@ def get_library_address() -> str: print(f'Using library {LIBRARY_NAME} at address {library_address}') return library_address except Exception as e: - print( - f'Error reading DssExecLib.address: {str(e)}', file=sys.stderr) + print(f'Error reading DssExecLib.address: {str(e)}', file=sys.stderr) # If we get here, no library address was found print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) @@ -93,11 +140,9 @@ def get_library_address() -> str: def parse_command_line_args() -> Tuple[str, str, str]: - """ - Parse command line arguments. - """ + """Parse command line arguments.""" if len(sys.argv) not in [3, 4]: - print("""usage:\n + print("""usage: ./verify.py
[constructorArgs] """, file=sys.stderr) sys.exit(1) @@ -115,38 +160,21 @@ def parse_command_line_args() -> Tuple[str, str, str]: return contract_name, contract_address, constructor_args +@retry_with_backoff(max_retries=2, base_delay=1) def flatten_source_code() -> None: - """ - Flatten the source code using Forge. - """ - subprocess.run([ + """Flatten the source code using Forge with retry mechanism.""" + result = subprocess.run([ 'forge', 'flatten', SOURCE_FILE_PATH, '--output', FLATTEN_OUTPUT_PATH - ], capture_output=True) - - -def send_etherscan_api_request(params: Dict[str, str], data: Dict[str, Any]) -> Dict: - """ - Sends the verification request to the Etherscan API - """ - headers = {'User-Agent': 'Sky-Protocol-Spell-Verifier'} - - print('Sending verification request...', file=sys.stderr) - response = requests.post( - ETHERSCAN_API_URL, headers=headers, params=params, data=data) - - try: - return json.loads(response.text) - except json.decoder.JSONDecodeError: - print(response.text, file=sys.stderr) - raise Exception('Error: Etherscan responded with invalid JSON.') + ], capture_output=True, text=True, check=True) + + if result.returncode != 0: + raise Exception(f"Forge flatten failed: {result.stderr}") def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: - """ - Extract contract metadata from the compiled output. - """ + """Extract contract metadata from the compiled output.""" try: with open(output_path, 'r') as f: content = json.load(f) @@ -159,191 +187,403 @@ def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: 'evm_version': metadata['settings']['evmVersion'], 'optimizer_enabled': metadata['settings']['optimizer']['enabled'], 'optimizer_runs': metadata['settings']['optimizer']['runs'], - # Default to AGPL-3.0-or-later if unknown - 'license_number': LICENSE_NUMBERS.get(license_name, LICENSE_NUMBERS['AGPL-3.0-or-later']) + 'license_name': license_name } except FileNotFoundError: - raise Exception('Run `forge build` and try again') + raise Exception('Run forge build first') except json.decoder.JSONDecodeError: - raise Exception(f'Malformed JSON in {output_path}. Run `forge build --force` and try again') + raise Exception('Run forge build again') except KeyError as e: raise Exception(f'Missing metadata field: {e}') def read_flattened_code() -> str: - """ - Read the flattened source code. - """ - with open(FLATTEN_OUTPUT_PATH, 'r', encoding='utf-8') as f: - return f.read() - - -def prepare_verification_data( - contract_name: str, - contract_address: str, - input_path: str, - output_path: str, - chain_id: str, - api_key: str, - constructor_args: str, - library_address: str -) -> Tuple[Dict[str, str], Dict[str, Any], str]: - """ - Prepare data for contract verification. - """ - # Get contract metadata - metadata = get_contract_metadata(output_path, input_path) - - # Read flattened source code - code = read_flattened_code() - - # Prepare API request parameters - params = {'chainid': chain_id} - - data = { - 'apikey': api_key, - 'module': 'contract', - 'action': 'verifysourcecode', - 'contractaddress': contract_address, - 'sourceCode': code, - 'codeFormat': 'solidity-single-file', - 'contractName': contract_name, - 'compilerversion': metadata['compiler_version'], - 'optimizationUsed': '1' if metadata['optimizer_enabled'] else '0', - 'runs': metadata['optimizer_runs'], - 'constructorArguements': constructor_args, - 'evmversion': metadata['evm_version'], - 'licenseType': metadata['license_number'], - 'libraryname1': LIBRARY_NAME, - 'libraryaddress1': library_address, - } - - return params, data, code - - -def wait_for_verification(guid: str, params: Dict[str, str], api_key: str, code: str) -> None: - """ - Wait for verification to complete and check status. - """ - check_data = { - 'apikey': api_key, - 'module': 'contract', - 'action': 'checkverifystatus', - 'guid': guid, - } - - check_response = {} - - # Poll until verification is complete - while check_response == {} or 'pending' in check_response.get('result', '').lower(): - if check_response != {}: - print(check_response['result'], file=sys.stderr) - print( - 'Waiting for 15 seconds for Etherscan to process the request...', - file=sys.stderr - ) - time.sleep(15) - - check_response = send_etherscan_api_request( - params=params, data=check_data) - - # Check verification result - if check_response['status'] != '1' or check_response['message'] != 'OK': - if 'already verified' not in check_response['result'].lower(): - # Log the flattened source code for debugging - log_name = f'verify-{datetime.now().timestamp()}.log' - with open(log_name, 'w') as log: - log.write(code) - print(f'Source code logged to {log_name}', file=sys.stderr) - - raise Exception('Verification failed') - else: - print('Contract is already verified') - - -def verify_contract( - contract_name: str, - contract_address: str, - input_path: str, - output_path: str, - chain_id: str, - api_key: str, - constructor_args: str, - library_address: str -) -> None: - """ - Verify a contract on Etherscan. - """ - print(f'\nVerifying {contract_name} at {contract_address}...') - - # Prepare verification data - params, data, code = prepare_verification_data( - contract_name, contract_address, input_path, output_path, - chain_id, api_key, constructor_args, library_address - ) - - # Submit verification request - verify_response = send_etherscan_api_request(params, data) - - # Handle "contract not yet deployed" case - while 'locate' in verify_response.get('result', '').lower(): - print(verify_response['result'], file=sys.stderr) - print('Waiting for 15 seconds for the network to update...', file=sys.stderr) - time.sleep(15) - verify_response = send_etherscan_api_request(params, data) - - # Check verification submission status - if verify_response['status'] != '1' or verify_response['message'] != 'OK': - if 'already verified' in verify_response['result'].lower(): - print('Contract is already verified') - return - raise Exception('Failed to submit verification request') - - # Get verification GUID - guid = verify_response['result'] - print(f'Verification request submitted with GUID: {guid}') - - # Check verification status - wait_for_verification(guid, params, api_key, code) - - # Get Etherscan URL - subdomain = ETHERSCAN_SUBDOMAINS.get(chain_id, '') - etherscan_url = f"https://{subdomain}etherscan.io/address/{contract_address}#code" - print(f'Contract verified successfully at {etherscan_url}') + """Read the flattened source code.""" + try: + with open(FLATTEN_OUTPUT_PATH, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + raise Exception(f'Flattened source code not found at {FLATTEN_OUTPUT_PATH}. Run forge flatten first.') + except UnicodeDecodeError as e: + raise Exception(f'Error reading flattened source code: {str(e)}') +@retry_with_backoff(max_retries=2, base_delay=1) def get_action_address(spell_address: str) -> Optional[str]: - """ - Get the action contract address from the spell contract. - """ + """Get the action contract address from the spell contract with retry mechanism.""" try: result = subprocess.run( ['cast', 'call', spell_address, 'action()(address)'], capture_output=True, + text=True, + check=True, env=os.environ | { 'ETH_GAS_PRICE': '0', 'ETH_PRIO_FEE': '0' } ) - return result.stdout.decode('utf-8').strip() - except Exception as e: + return result.stdout.strip() + except subprocess.CalledProcessError as e: print(f'Error getting action address: {str(e)}', file=sys.stderr) return None + except Exception as e: + print(f'Unexpected error getting action address: {str(e)}', file=sys.stderr) + return None -def main(): - """ - Main entry point for the script. - """ +class EtherscanVerifier: + """Etherscan block explorer verifier.""" + + def __init__(self, api_key: str, chain_id: str): + self.api_key = api_key + self.chain_id = chain_id + self.subdomain = ETHERSCAN_SUBDOMAINS.get(chain_id, '') + + def is_available(self) -> bool: + """Check if Etherscan supports this chain.""" + return self.chain_id in ETHERSCAN_SUBDOMAINS + + def get_verification_url(self, contract_address: str) -> str: + """Get Etherscan URL for the verified contract.""" + return f"https://{self.subdomain}etherscan.io/address/{contract_address}#code" + + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) + def _send_api_request(self, params: Dict[str, str], data: Dict[str, Any]) -> Dict: + """Send request to Etherscan API with retry mechanism.""" + headers = {'User-Agent': 'Sky-Protocol-Spell-Verifier'} + + response = requests.post( + ETHERSCAN_API_URL, + headers=headers, + params=params, + data=data, + timeout=30 + ) + + response.raise_for_status() + + try: + return json.loads(response.text) + except json.decoder.JSONDecodeError as e: + print(f"Response text: {response.text}", file=sys.stderr) + raise Exception(f'Etherscan responded with invalid JSON: {str(e)}') + + def _wait_for_verification(self, guid: str, params: Dict[str, str], code: str) -> None: + """Wait for verification to complete with retry mechanism.""" + check_data = { + 'apikey': self.api_key, + 'module': 'contract', + 'action': 'checkverifystatus', + 'guid': guid, + } + + check_response = {} + max_attempts = 20 + + for attempt in range(max_attempts): + if check_response and 'pending' not in check_response.get('result', '').lower(): + break + + if check_response: + print(check_response['result'], file=sys.stderr) + print(f'Waiting for 15 seconds for Etherscan to process... (attempt {attempt + 1}/{max_attempts})', file=sys.stderr) + time.sleep(15) + + try: + check_response = self._send_api_request(params, check_data) + except Exception as e: + print(f"Error checking verification status: {str(e)}", file=sys.stderr) + if attempt == max_attempts - 1: + raise + time.sleep(15) + continue + + if check_response['status'] != '1' or check_response['message'] != 'OK': + if 'already verified' not in check_response['result'].lower(): + log_name = f'verify-etherscan-{datetime.now().timestamp()}.log' + with open(log_name, 'w') as log: + log.write(code) + print(f'Source code logged to {log_name}', file=sys.stderr) + + raise Exception(f'Verification failed: {check_response.get("result", "Unknown error")}') + else: + print('Contract is already verified') + + def verify_contract( + self, + contract_name: str, + contract_address: str, + source_code: str, + constructor_args: str, + metadata: Dict[str, Any], + library_address: str = "" + ) -> bool: + """Verify contract on Etherscan.""" + print(f'\nVerifying {contract_name} at {contract_address} on Etherscan...') + + params = {'chainid': self.chain_id} + + license_name = metadata.get('license_name', 'MIT') + license_number = LICENSE_NUMBERS.get(license_name, 1) + + data = { + 'apikey': self.api_key, + 'module': 'contract', + 'action': 'verifysourcecode', + 'contractaddress': contract_address, + 'sourceCode': source_code, + 'codeFormat': 'solidity-single-file', + 'contractName': contract_name, + 'compilerversion': metadata['compiler_version'], + 'optimizationUsed': '1' if metadata['optimizer_enabled'] else '0', + 'runs': metadata['optimizer_runs'], + 'constructorArguements': constructor_args, + 'evmversion': metadata['evm_version'], + 'licenseType': license_number, + } + + if library_address: + data['libraryname1'] = 'DssExecLib' + data['libraryaddress1'] = library_address + + # Submit verification request with retry + max_retries = 3 + for attempt in range(max_retries): + try: + verify_response = self._send_api_request(params, data) + break + except Exception as e: + if attempt == max_retries - 1: + print(f"Failed to submit verification request after {max_retries} attempts: {str(e)}", file=sys.stderr) + return False + print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) + time.sleep(2 ** attempt) + + # Handle "contract not yet deployed" case + max_deploy_retries = 5 + deploy_retry_count = 0 + + while 'locate' in verify_response.get('result', '').lower() and deploy_retry_count < max_deploy_retries: + print(verify_response['result'], file=sys.stderr) + print(f'Waiting for 15 seconds for the network to update... (attempt {deploy_retry_count + 1}/{max_deploy_retries})', file=sys.stderr) + time.sleep(15) + + try: + verify_response = self._send_api_request(params, data) + except Exception as e: + print(f"Error during deploy retry: {str(e)}", file=sys.stderr) + deploy_retry_count += 1 + continue + + if deploy_retry_count >= max_deploy_retries: + print("Contract not found on network after maximum retries", file=sys.stderr) + return False + + if verify_response['status'] != '1' or verify_response['message'] != 'OK': + if 'already verified' in verify_response['result'].lower(): + print('Contract is already verified on Etherscan') + return True + print(f'Failed to submit verification request: {verify_response.get("result", "Unknown error")}', file=sys.stderr) + return False + + guid = verify_response['result'] + print(f'Verification request submitted with GUID: {guid}') + + try: + self._wait_for_verification(guid, params, source_code) + print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') + return True + except Exception as e: + print(f"Verification failed: {str(e)}", file=sys.stderr) + return False + + +class SourcifyVerifier: + """Sourcify block explorer verifier.""" + + def __init__(self, chain_id: str): + self.chain_id = chain_id + + def is_available(self) -> bool: + """Check if Sourcify supports this chain.""" + return self.chain_id in ['1', '11155111'] # Mainnet and Sepolia + + def get_verification_url(self, contract_address: str) -> str: + """Get Sourcify URL for the verified contract.""" + return f"https://sourcify.dev/#/lookup/{contract_address}" + + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) + def _send_api_request(self, endpoint: str, data: Dict[str, Any]) -> Dict: + """Send request to Sourcify API with retry mechanism.""" + headers = { + 'User-Agent': 'Sky-Protocol-Spell-Verifier', + 'Content-Type': 'application/json' + } + + url = f"{SOURCIFY_API_URL}/{endpoint}" + + response = requests.post( + url, + headers=headers, + json=data, + timeout=30 + ) + + response.raise_for_status() + + try: + return response.json() + except json.decoder.JSONDecodeError as e: + print(f"Response text: {response.text}", file=sys.stderr) + raise Exception(f'Sourcify responded with invalid JSON: {str(e)}') + + def verify_contract( + self, + contract_name: str, + contract_address: str, + source_code: str, + constructor_args: str, + metadata: Dict[str, Any], + library_address: str = "" + ) -> bool: + """Verify contract on Sourcify.""" + print(f'\nVerifying {contract_name} at {contract_address} on Sourcify...') + + files_data = { + "contract.sol": source_code + } + + if metadata: + files_data["metadata.json"] = json.dumps(metadata) + + verification_data = { + "address": contract_address, + "chain": self.chain_id, + "files": files_data + } + + if constructor_args: + verification_data["constructorArgs"] = constructor_args + + max_retries = 3 + for attempt in range(max_retries): + try: + response = self._send_api_request("verify", verification_data) + + if response.get("status") == "perfect": + print(f'Contract verified successfully on Sourcify') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + elif response.get("status") == "partial": + print(f'Contract partially verified on Sourcify (some files missing)') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + else: + print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) + return False + + except Exception as e: + if attempt == max_retries - 1: + print(f"Failed to verify on Sourcify after {max_retries} attempts: {str(e)}", file=sys.stderr) + return False + print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) + time.sleep(2 ** attempt) + + return False + + +def setup_verifiers(chain_id: str) -> List[Any]: + """Setup available verifiers for the given chain.""" + verifiers = [] + + # Setup Etherscan verifier try: - # Get environment variables - api_key = get_env_var( + etherscan_api_key = get_env_var( 'ETHERSCAN_API_KEY', - "You need an Etherscan API key to verify contracts.\n" - "Create one at https://etherscan.io/myapikey\n" - "Then export it with `export ETHERSCAN_API_KEY=xxxxxxxx'" + "Etherscan API key not found. Set ETHERSCAN_API_KEY environment variable." ) + etherscan_verifier = EtherscanVerifier(etherscan_api_key, chain_id) + if etherscan_verifier.is_available(): + verifiers.append(etherscan_verifier) + print(f"✓ Etherscan verifier available for chain {chain_id}") + else: + print(f"✗ Etherscan verifier not available for chain {chain_id}") + except Exception as e: + print(f"✗ Failed to setup Etherscan verifier: {str(e)}", file=sys.stderr) + + # Setup Sourcify verifier + try: + sourcify_verifier = SourcifyVerifier(chain_id) + if sourcify_verifier.is_available(): + verifiers.append(sourcify_verifier) + print(f"✓ Sourcify verifier available for chain {chain_id}") + else: + print(f"✗ Sourcify verifier not available for chain {chain_id}") + except Exception as e: + print(f"✗ Failed to setup Sourcify verifier: {str(e)}", file=sys.stderr) + + if not verifiers: + raise Exception("No verifiers available for the current chain") + + return verifiers + + +def verify_contract_with_verifiers( + contract_name: str, + contract_address: str, + source_code: str, + constructor_args: str, + metadata: Dict[str, Any], + library_address: str, + verifiers: List[Any] +) -> bool: + """Verify contract using multiple verifiers with fallback.""" + print(f'\nVerifying {contract_name} at {contract_address}...') + + successful_verifications = 0 + total_verifiers = len(verifiers) + + for i, verifier in enumerate(verifiers): + print(f"\n--- Attempting verification with {verifier.__class__.__name__} ({i+1}/{total_verifiers}) ---") + + try: + success = verifier.verify_contract( + contract_name=contract_name, + contract_address=contract_address, + source_code=source_code, + constructor_args=constructor_args, + metadata=metadata, + library_address=library_address + ) + + if success: + successful_verifications += 1 + print(f"✓ Successfully verified on {verifier.__class__.__name__}") + + if successful_verifications >= 1: + print(f"\n🎉 Contract verified successfully on {successful_verifications}/{total_verifiers} verifiers!") + return True + else: + print(f"✗ Verification failed on {verifier.__class__.__name__}") + + except Exception as e: + print(f"✗ Error during verification with {verifier.__class__.__name__}: {str(e)}", file=sys.stderr) + + if i < total_verifiers - 1: + print("Waiting 2 seconds before trying next verifier...") + time.sleep(2) + + if successful_verifications == 0: + print(f"\n❌ Failed to verify contract on any verifier ({total_verifiers} attempted)") + return False + + return True + +def main(): + """Main entry point for the enhanced verification script.""" + try: + # Get environment variables rpc_url = get_env_var( 'ETH_RPC_URL', "You need a valid ETH_RPC_URL.\n" @@ -360,38 +600,60 @@ def main(): # Get library address library_address = get_library_address() + # Setup verifiers + print("Setting up verifiers...") + verifiers = setup_verifiers(chain_id) + # Flatten source code + print("Flattening source code...") flatten_source_code() + # Read flattened code + source_code = read_flattened_code() + + # Get contract metadata + metadata = get_contract_metadata( + f'out/DssSpell.sol/DssSpell.json', + SOURCE_FILE_PATH + ) + # Verify spell contract - verify_contract( + spell_success = verify_contract_with_verifiers( contract_name=spell_name, contract_address=spell_address, - input_path=SOURCE_FILE_PATH, - output_path=f'out/DssSpell.sol/DssSpell.json', - chain_id=chain_id, - api_key=api_key, + source_code=source_code, constructor_args=constructor_args, - library_address=library_address + metadata=metadata, + library_address=library_address, + verifiers=verifiers ) + if not spell_success: + print("Failed to verify spell contract", file=sys.stderr) + return + # Get and verify action contract action_address = get_action_address(spell_address) if not action_address: - raise Exception('Could not determine action contract address') + print('Could not determine action contract address', file=sys.stderr) + return - verify_contract( + action_success = verify_contract_with_verifiers( contract_name="DssSpellAction", contract_address=action_address, - input_path=SOURCE_FILE_PATH, - output_path=f'out/DssSpell.sol/DssSpellAction.json', - chain_id=chain_id, - api_key=api_key, + source_code=source_code, constructor_args=constructor_args, - library_address=library_address + metadata=metadata, + library_address=library_address, + verifiers=verifiers ) - print('\nVerification complete!') + if not action_success: + print("Failed to verify action contract", file=sys.stderr) + return + + print('\n🎉 All verifications complete!') + except Exception as e: print(f'\nError: {str(e)}', file=sys.stderr) sys.exit(1) From 850b03fbbc609e5b6ecb3a18c59877fb90f9c34c Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:01:56 +0200 Subject: [PATCH 02/16] refactor: :fire: remove duplicated code --- scripts/verification/README.md | 9 +- scripts/verification/__init__.py | 2 - scripts/verification/etherscan_verifier.py | 3 +- scripts/verification/sourcify_verifier.py | 44 ++-- scripts/verification/test_retry.py | 56 +--- scripts/verify.py | 283 +-------------------- 6 files changed, 41 insertions(+), 356 deletions(-) diff --git a/scripts/verification/README.md b/scripts/verification/README.md index 1a7626d5..ee69f826 100644 --- a/scripts/verification/README.md +++ b/scripts/verification/README.md @@ -155,11 +155,10 @@ The script automatically: ## Future Enhancements -1. **Blockscout Support**: Add Blockscout verifier implementation -2. **Configurable Retry**: Allow retry parameters via environment variables -3. **Parallel Verification**: Verify on multiple explorers simultaneously -4. **Verification Status**: Check if contract is already verified before attempting -5. **Custom Verifiers**: Allow custom verifier implementations +1. **Configurable Retry**: Allow retry parameters via environment variables +2. **Parallel Verification**: Verify on multiple explorers simultaneously +3. **Verification Status**: Check if contract is already verified before attempting +4. **Custom Verifiers**: Allow custom verifier implementations ## Contributing diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index fcad4174..cdcfb8d5 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -3,12 +3,10 @@ Verification package for Sky Protocol spells. """ -from .retry import retry_with_backoff from .etherscan_verifier import EtherscanVerifier from .sourcify_verifier import SourcifyVerifier __all__ = [ - 'retry_with_backoff', 'EtherscanVerifier', 'SourcifyVerifier' ] diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py index ad194055..b76c1035 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/etherscan_verifier.py @@ -23,6 +23,7 @@ 'GPL-3.0-or-later': 5, 'AGPL-3.0-or-later': 13 } +SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia class EtherscanVerifier: @@ -35,7 +36,7 @@ def __init__(self, api_key: str, chain_id: str): def is_available(self) -> bool: """Check if Etherscan supports this chain.""" - return self.chain_id in ETHERSCAN_SUBDOMAINS + return self.chain_id in SUPPORTED_CHAIN_IDS def get_verification_url(self, contract_address: str) -> str: """Get Etherscan URL for the verified contract.""" diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py index 52b4712a..00925244 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/sourcify_verifier.py @@ -13,6 +13,7 @@ # Block explorer configurations SOURCIFY_API_URL = 'https://sourcify.dev/server' +SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia class SourcifyVerifier: @@ -23,7 +24,7 @@ def __init__(self, chain_id: str): def is_available(self) -> bool: """Check if Sourcify supports this chain.""" - return self.chain_id in ['1', '11155111'] # Mainnet and Sepolia + return self.chain_id in SUPPORTED_CHAIN_IDS def get_verification_url(self, contract_address: str) -> str: """Get Sourcify URL for the verified contract.""" @@ -82,28 +83,21 @@ def verify_contract( if constructor_args: verification_data["constructorArgs"] = constructor_args - max_retries = 3 - for attempt in range(max_retries): - try: - response = self._send_api_request("verify", verification_data) + try: + response = self._send_api_request("verify", verification_data) + + if response.get("status") == "perfect": + print(f'Contract verified successfully on Sourcify') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + elif response.get("status") == "partial": + print(f'Contract partially verified on Sourcify (some files missing)') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + else: + print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) + return False - if response.get("status") == "perfect": - print(f'Contract verified successfully on Sourcify') - print(f'View at: {self.get_verification_url(contract_address)}') - return True - elif response.get("status") == "partial": - print(f'Contract partially verified on Sourcify (some files missing)') - print(f'View at: {self.get_verification_url(contract_address)}') - return True - else: - print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) - return False - - except Exception as e: - if attempt == max_retries - 1: - print(f"Failed to verify on Sourcify after {max_retries} attempts: {str(e)}", file=sys.stderr) - return False - print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) - time.sleep(2 ** attempt) - - return False + except Exception as e: + print(f"Failed to verify on Sourcify: {str(e)}", file=sys.stderr) + return False diff --git a/scripts/verification/test_retry.py b/scripts/verification/test_retry.py index ea9fb1df..b0d43d9f 100644 --- a/scripts/verification/test_retry.py +++ b/scripts/verification/test_retry.py @@ -7,50 +7,10 @@ import unittest from unittest.mock import patch, MagicMock from typing import Tuple, Callable -from functools import wraps from retry import retry_with_backoff, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY -def retry_with_backoff_test( - max_retries: int = 3, - base_delay: float = 1, - max_delay: float = 10, - backoff_factor: float = 2, - jitter: float = 0.1, - exceptions: Tuple[Exception, ...] = (Exception,) -): - """Test version of the retry decorator.""" - def decorator(func: Callable) -> Callable: - @wraps(func) - def wrapper(*args, **kwargs): - last_exception = None - - for attempt in range(max_retries + 1): - try: - return func(*args, **kwargs) - except exceptions as e: - last_exception = e - - if attempt == max_retries: - print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}") - raise - - # Calculate delay with exponential backoff and jitter - delay = min(base_delay * (backoff_factor ** attempt), max_delay) - jitter_amount = delay * jitter * random.uniform(-1, 1) - actual_delay = max(0, delay + jitter_amount) - - print(f"Attempt {attempt + 1} failed: {str(e)}") - print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})") - - time.sleep(actual_delay) - - raise last_exception - return wrapper - return decorator - - class TestRetryMechanism(unittest.TestCase): """Test cases for the retry mechanism.""" @@ -60,7 +20,7 @@ def setUp(self): def test_successful_retry(self): """Test that retry mechanism works when function eventually succeeds.""" - @retry_with_backoff_test(max_retries=3, base_delay=0.1) + @retry_with_backoff(max_retries=3, base_delay=0.1) def test_function(): self.call_count += 1 if self.call_count < 3: @@ -73,7 +33,7 @@ def test_function(): def test_max_retries_exceeded(self): """Test that retry mechanism fails after max retries.""" - @retry_with_backoff_test(max_retries=2, base_delay=0.1) + @retry_with_backoff(max_retries=2, base_delay=0.1) def test_function(): self.call_count += 1 raise Exception(f"Persistent failure #{self.call_count}") @@ -86,7 +46,7 @@ def test_function(): def test_no_retry_on_success(self): """Test that successful function doesn't retry.""" - @retry_with_backoff_test(max_retries=3, base_delay=0.1) + @retry_with_backoff(max_retries=3, base_delay=0.1) def test_function(): self.call_count += 1 return "Success!" @@ -99,7 +59,7 @@ def test_exponential_backoff(self): """Test that delays increase exponentially.""" delays = [] - @retry_with_backoff_test(max_retries=3, base_delay=1, max_delay=10) + @retry_with_backoff(max_retries=3, base_delay=1, max_delay=10) def test_function(): delays.append(time.time()) raise Exception("Test failure") @@ -118,7 +78,7 @@ def test_jitter_variation(self): """Test that jitter adds random variation to delays.""" delays = [] - @retry_with_backoff_test(max_retries=2, base_delay=1, jitter=0.2) + @retry_with_backoff(max_retries=2, base_delay=1, jitter=0.2) def test_function(): delays.append(time.time()) raise Exception("Test failure") @@ -136,7 +96,7 @@ def test_function(): @patch('time.sleep') def test_specific_exception_handling(self, mock_sleep): """Test that only specified exceptions trigger retries.""" - @retry_with_backoff_test(max_retries=2, base_delay=0.1, exceptions=(ValueError,)) + @retry_with_backoff(max_retries=2, base_delay=0.1, exceptions=(ValueError,)) def test_function(): self.call_count += 1 if self.call_count == 1: @@ -160,7 +120,7 @@ def test_retry_with_network_simulation(self): """Test retry mechanism with simulated network failures.""" failures = [True, True, False] # Fail twice, succeed on third attempt - @retry_with_backoff_test(max_retries=3, base_delay=0.1) + @retry_with_backoff(max_retries=3, base_delay=0.1) def simulate_network_call(): if failures.pop(0): raise ConnectionError("Network timeout") @@ -174,7 +134,7 @@ def test_retry_with_different_exception_types(self): """Test retry mechanism with different types of exceptions.""" exceptions = [ValueError("Bad value"), ConnectionError("Network error"), "Success"] - @retry_with_backoff_test(max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError)) + @retry_with_backoff(max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError)) def test_function(): exception = exceptions.pop(0) if isinstance(exception, Exception): diff --git a/scripts/verify.py b/scripts/verify.py index fdc9673d..8a82fee3 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -16,6 +16,9 @@ from typing import Dict, Any, Tuple, Optional, List, Callable from functools import wraps +# Import verifiers from the verification package +from verification import EtherscanVerifier, SourcifyVerifier + # Constants FLATTEN_OUTPUT_PATH = 'out/flat.sol' SOURCE_FILE_PATH = 'src/DssSpell.sol' @@ -28,17 +31,7 @@ DEFAULT_BACKOFF_FACTOR = 2 DEFAULT_JITTER = 0.1 # 10% jitter -# Block explorer configurations -ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' -SOURCIFY_API_URL = 'https://sourcify.dev/server' -ETHERSCAN_SUBDOMAINS = { - '1': '', - '11155111': 'sepolia.' -} -LICENSE_NUMBERS = { - 'GPL-3.0-or-later': 5, - 'AGPL-3.0-or-later': 13 -} + def retry_with_backoff( @@ -231,267 +224,6 @@ def get_action_address(spell_address: str) -> Optional[str]: return None -class EtherscanVerifier: - """Etherscan block explorer verifier.""" - - def __init__(self, api_key: str, chain_id: str): - self.api_key = api_key - self.chain_id = chain_id - self.subdomain = ETHERSCAN_SUBDOMAINS.get(chain_id, '') - - def is_available(self) -> bool: - """Check if Etherscan supports this chain.""" - return self.chain_id in ETHERSCAN_SUBDOMAINS - - def get_verification_url(self, contract_address: str) -> str: - """Get Etherscan URL for the verified contract.""" - return f"https://{self.subdomain}etherscan.io/address/{contract_address}#code" - - @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def _send_api_request(self, params: Dict[str, str], data: Dict[str, Any]) -> Dict: - """Send request to Etherscan API with retry mechanism.""" - headers = {'User-Agent': 'Sky-Protocol-Spell-Verifier'} - - response = requests.post( - ETHERSCAN_API_URL, - headers=headers, - params=params, - data=data, - timeout=30 - ) - - response.raise_for_status() - - try: - return json.loads(response.text) - except json.decoder.JSONDecodeError as e: - print(f"Response text: {response.text}", file=sys.stderr) - raise Exception(f'Etherscan responded with invalid JSON: {str(e)}') - - def _wait_for_verification(self, guid: str, params: Dict[str, str], code: str) -> None: - """Wait for verification to complete with retry mechanism.""" - check_data = { - 'apikey': self.api_key, - 'module': 'contract', - 'action': 'checkverifystatus', - 'guid': guid, - } - - check_response = {} - max_attempts = 20 - - for attempt in range(max_attempts): - if check_response and 'pending' not in check_response.get('result', '').lower(): - break - - if check_response: - print(check_response['result'], file=sys.stderr) - print(f'Waiting for 15 seconds for Etherscan to process... (attempt {attempt + 1}/{max_attempts})', file=sys.stderr) - time.sleep(15) - - try: - check_response = self._send_api_request(params, check_data) - except Exception as e: - print(f"Error checking verification status: {str(e)}", file=sys.stderr) - if attempt == max_attempts - 1: - raise - time.sleep(15) - continue - - if check_response['status'] != '1' or check_response['message'] != 'OK': - if 'already verified' not in check_response['result'].lower(): - log_name = f'verify-etherscan-{datetime.now().timestamp()}.log' - with open(log_name, 'w') as log: - log.write(code) - print(f'Source code logged to {log_name}', file=sys.stderr) - - raise Exception(f'Verification failed: {check_response.get("result", "Unknown error")}') - else: - print('Contract is already verified') - - def verify_contract( - self, - contract_name: str, - contract_address: str, - source_code: str, - constructor_args: str, - metadata: Dict[str, Any], - library_address: str = "" - ) -> bool: - """Verify contract on Etherscan.""" - print(f'\nVerifying {contract_name} at {contract_address} on Etherscan...') - - params = {'chainid': self.chain_id} - - license_name = metadata.get('license_name', 'MIT') - license_number = LICENSE_NUMBERS.get(license_name, 1) - - data = { - 'apikey': self.api_key, - 'module': 'contract', - 'action': 'verifysourcecode', - 'contractaddress': contract_address, - 'sourceCode': source_code, - 'codeFormat': 'solidity-single-file', - 'contractName': contract_name, - 'compilerversion': metadata['compiler_version'], - 'optimizationUsed': '1' if metadata['optimizer_enabled'] else '0', - 'runs': metadata['optimizer_runs'], - 'constructorArguements': constructor_args, - 'evmversion': metadata['evm_version'], - 'licenseType': license_number, - } - - if library_address: - data['libraryname1'] = 'DssExecLib' - data['libraryaddress1'] = library_address - - # Submit verification request with retry - max_retries = 3 - for attempt in range(max_retries): - try: - verify_response = self._send_api_request(params, data) - break - except Exception as e: - if attempt == max_retries - 1: - print(f"Failed to submit verification request after {max_retries} attempts: {str(e)}", file=sys.stderr) - return False - print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) - time.sleep(2 ** attempt) - - # Handle "contract not yet deployed" case - max_deploy_retries = 5 - deploy_retry_count = 0 - - while 'locate' in verify_response.get('result', '').lower() and deploy_retry_count < max_deploy_retries: - print(verify_response['result'], file=sys.stderr) - print(f'Waiting for 15 seconds for the network to update... (attempt {deploy_retry_count + 1}/{max_deploy_retries})', file=sys.stderr) - time.sleep(15) - - try: - verify_response = self._send_api_request(params, data) - except Exception as e: - print(f"Error during deploy retry: {str(e)}", file=sys.stderr) - deploy_retry_count += 1 - continue - - if deploy_retry_count >= max_deploy_retries: - print("Contract not found on network after maximum retries", file=sys.stderr) - return False - - if verify_response['status'] != '1' or verify_response['message'] != 'OK': - if 'already verified' in verify_response['result'].lower(): - print('Contract is already verified on Etherscan') - return True - print(f'Failed to submit verification request: {verify_response.get("result", "Unknown error")}', file=sys.stderr) - return False - - guid = verify_response['result'] - print(f'Verification request submitted with GUID: {guid}') - - try: - self._wait_for_verification(guid, params, source_code) - print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') - return True - except Exception as e: - print(f"Verification failed: {str(e)}", file=sys.stderr) - return False - - -class SourcifyVerifier: - """Sourcify block explorer verifier.""" - - def __init__(self, chain_id: str): - self.chain_id = chain_id - - def is_available(self) -> bool: - """Check if Sourcify supports this chain.""" - return self.chain_id in ['1', '11155111'] # Mainnet and Sepolia - - def get_verification_url(self, contract_address: str) -> str: - """Get Sourcify URL for the verified contract.""" - return f"https://sourcify.dev/#/lookup/{contract_address}" - - @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def _send_api_request(self, endpoint: str, data: Dict[str, Any]) -> Dict: - """Send request to Sourcify API with retry mechanism.""" - headers = { - 'User-Agent': 'Sky-Protocol-Spell-Verifier', - 'Content-Type': 'application/json' - } - - url = f"{SOURCIFY_API_URL}/{endpoint}" - - response = requests.post( - url, - headers=headers, - json=data, - timeout=30 - ) - - response.raise_for_status() - - try: - return response.json() - except json.decoder.JSONDecodeError as e: - print(f"Response text: {response.text}", file=sys.stderr) - raise Exception(f'Sourcify responded with invalid JSON: {str(e)}') - - def verify_contract( - self, - contract_name: str, - contract_address: str, - source_code: str, - constructor_args: str, - metadata: Dict[str, Any], - library_address: str = "" - ) -> bool: - """Verify contract on Sourcify.""" - print(f'\nVerifying {contract_name} at {contract_address} on Sourcify...') - - files_data = { - "contract.sol": source_code - } - - if metadata: - files_data["metadata.json"] = json.dumps(metadata) - - verification_data = { - "address": contract_address, - "chain": self.chain_id, - "files": files_data - } - - if constructor_args: - verification_data["constructorArgs"] = constructor_args - - max_retries = 3 - for attempt in range(max_retries): - try: - response = self._send_api_request("verify", verification_data) - - if response.get("status") == "perfect": - print(f'Contract verified successfully on Sourcify') - print(f'View at: {self.get_verification_url(contract_address)}') - return True - elif response.get("status") == "partial": - print(f'Contract partially verified on Sourcify (some files missing)') - print(f'View at: {self.get_verification_url(contract_address)}') - return True - else: - print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) - return False - - except Exception as e: - if attempt == max_retries - 1: - print(f"Failed to verify on Sourcify after {max_retries} attempts: {str(e)}", file=sys.stderr) - return False - print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) - time.sleep(2 ** attempt) - - return False - - def setup_verifiers(chain_id: str) -> List[Any]: """Setup available verifiers for the given chain.""" verifiers = [] @@ -512,6 +244,7 @@ def setup_verifiers(chain_id: str) -> List[Any]: print(f"✗ Failed to setup Etherscan verifier: {str(e)}", file=sys.stderr) # Setup Sourcify verifier + # Note: Blockscout automatically picks up any code verification from Sourcify try: sourcify_verifier = SourcifyVerifier(chain_id) if sourcify_verifier.is_available(): @@ -630,13 +363,13 @@ def main(): if not spell_success: print("Failed to verify spell contract", file=sys.stderr) - return + sys.exit(1) # Get and verify action contract action_address = get_action_address(spell_address) if not action_address: print('Could not determine action contract address', file=sys.stderr) - return + sys.exit(1) action_success = verify_contract_with_verifiers( contract_name="DssSpellAction", @@ -650,7 +383,7 @@ def main(): if not action_success: print("Failed to verify action contract", file=sys.stderr) - return + sys.exit(1) print('\n🎉 All verifications complete!') From 7c3ec301b7a2699b7506ab581bdb891a56899c56 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:28:39 +0200 Subject: [PATCH 03/16] refactor: :recycle: modify repo structure --- scripts/verification/README.md | 27 +++ scripts/verification/__init__.py | 16 +- scripts/verification/contract_data.py | 139 ++++++++++++++ scripts/verification/etherscan_verifier.py | 19 +- scripts/verification/sourcify_verifier.py | 1 - scripts/verification/test_retry.py | 6 +- scripts/verify.py | 203 ++++----------------- 7 files changed, 221 insertions(+), 190 deletions(-) create mode 100644 scripts/verification/contract_data.py diff --git a/scripts/verification/README.md b/scripts/verification/README.md index ee69f826..f553b61c 100644 --- a/scripts/verification/README.md +++ b/scripts/verification/README.md @@ -102,6 +102,32 @@ actual_delay = max(0, delay + jitter_amount) 4. **Simplicity**: Single script with enhanced functionality 5. **Compatibility**: Existing workflows continue to work unchanged +## Testing + +### **Running Tests** + +The test suite uses relative imports and must be run as a module: + +```bash +# ✅ Correct way - run as module +python3 -m scripts.verification.test_retry + +# ❌ Incorrect way - direct execution fails +python3 scripts/verification/test_retry.py +``` + +**Why?** The test file uses relative imports (`from .retry import retry_with_backoff`) which only work when the file is run as part of a package, not as a standalone script. + +### **Test Coverage** + +The test suite covers: +- Successful retry scenarios +- Maximum retry limit handling +- Exponential backoff behavior +- Jitter variation +- Exception-specific retry logic +- Network simulation scenarios + ## Troubleshooting ### **Common Issues** @@ -109,6 +135,7 @@ actual_delay = max(0, delay + jitter_amount) 1. **"No verifiers available"**: Check chain ID support and API keys 2. **"Verification failed on all verifiers"**: Check contract deployment and source code 3. **"Etherscan API key not found"**: Set `ETHERSCAN_API_KEY` environment variable +4. **"ImportError: attempted relative import with no known parent package"**: Run tests as a module using `python3 -m scripts.verification.test_retry` ### **Debug Mode** diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index cdcfb8d5..5b13fa4b 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -5,8 +5,22 @@ from .etherscan_verifier import EtherscanVerifier from .sourcify_verifier import SourcifyVerifier +from .contract_data import ( + get_chain_id, + get_library_address, + flatten_source_code, + get_contract_metadata, + read_flattened_code, + get_action_address +) __all__ = [ 'EtherscanVerifier', - 'SourcifyVerifier' + 'SourcifyVerifier', + 'get_chain_id', + 'get_library_address', + 'flatten_source_code', + 'get_contract_metadata', + 'read_flattened_code', + 'get_action_address' ] diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py new file mode 100644 index 00000000..bc885af7 --- /dev/null +++ b/scripts/verification/contract_data.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Contract data utilities for Sky Protocol spells verification. +This module handles contract metadata extraction, source code flattening, +and other contract-related data operations. +""" +import os +import sys +import subprocess +import re +import json +from typing import Dict, Any, Optional + +from .retry import retry_with_backoff + + +# Constants +FLATTEN_OUTPUT_PATH = 'out/flat.sol' +SOURCE_FILE_PATH = 'src/DssSpell.sol' +LIBRARY_NAME = 'DssExecLib' + + +@retry_with_backoff(max_retries=2, base_delay=1) +def get_chain_id() -> str: + """Get the current chain ID with retry mechanism.""" + print('Obtaining chain ID... ') + result = subprocess.run(['cast', 'chain-id'], capture_output=True, text=True, check=True) + chain_id = result.stdout.strip() + print(f"CHAIN_ID: {chain_id}") + return chain_id + + +def get_library_address() -> str: + """Find the DssExecLib address from either DssExecLib.address file or foundry.toml.""" + library_address = '' + + # First try to read from foundry.toml libraries + if os.path.exists('foundry.toml'): + try: + with open('foundry.toml', 'r') as f: + config = f.read() + + result = re.search(r':DssExecLib:(0x[0-9a-fA-F]{40})', config) + if result: + library_address = result.group(1) + print(f'Using library {LIBRARY_NAME} at address {library_address}') + return library_address + else: + print('No DssExecLib configured in foundry.toml', file=sys.stderr) + except Exception as e: + print(f'Error reading foundry.toml: {str(e)}', file=sys.stderr) + else: + print('No foundry.toml found', file=sys.stderr) + + # If it cannot be found, try DssExecLib.address + if os.path.exists('DssExecLib.address'): + try: + print(f'Trying to read DssExecLib.address...', file=sys.stderr) + with open('DssExecLib.address', 'r') as f: + library_address = f.read().strip() + print(f'Using library {LIBRARY_NAME} at address {library_address}') + return library_address + except Exception as e: + print(f'Error reading DssExecLib.address: {str(e)}', file=sys.stderr) + + # If we get here, no library address was found + print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) + return '' + + +@retry_with_backoff(max_retries=2, base_delay=1) +def flatten_source_code() -> None: + """Flatten the source code using Forge with retry mechanism.""" + result = subprocess.run([ + 'forge', 'flatten', + SOURCE_FILE_PATH, + '--output', FLATTEN_OUTPUT_PATH + ], capture_output=True, text=True, check=True) + + if result.returncode != 0: + raise Exception(f"Forge flatten failed: {result.stderr}") + + +def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: + """Extract contract metadata from the compiled output.""" + try: + with open(output_path, 'r') as f: + content = json.load(f) + + metadata = content['metadata'] + license_name = metadata['sources'][input_path]['license'] + + return { + 'compiler_version': 'v' + metadata['compiler']['version'], + 'evm_version': metadata['settings']['evmVersion'], + 'optimizer_enabled': metadata['settings']['optimizer']['enabled'], + 'optimizer_runs': metadata['settings']['optimizer']['runs'], + 'license_name': license_name + } + except FileNotFoundError: + raise Exception('Run forge build first') + except json.decoder.JSONDecodeError: + raise Exception('Run forge build again') + except KeyError as e: + raise Exception(f'Missing metadata field: {e}') + + +def read_flattened_code() -> str: + """Read the flattened source code.""" + try: + with open(FLATTEN_OUTPUT_PATH, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + raise Exception(f'Flattened source code not found at {FLATTEN_OUTPUT_PATH}. Run forge flatten first.') + except UnicodeDecodeError as e: + raise Exception(f'Error reading flattened source code: {str(e)}') + + +@retry_with_backoff(max_retries=2, base_delay=1) +def get_action_address(spell_address: str) -> Optional[str]: + """Get the action contract address from the spell contract with retry mechanism.""" + try: + result = subprocess.run( + ['cast', 'call', spell_address, 'action()(address)'], + capture_output=True, + text=True, + check=True, + env=os.environ | { + 'ETH_GAS_PRICE': '0', + 'ETH_PRIO_FEE': '0' + } + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f'Error getting action address: {str(e)}', file=sys.stderr) + return None + except Exception as e: + print(f'Unexpected error getting action address: {str(e)}', file=sys.stderr) + return None diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py index b76c1035..ebc64e64 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/etherscan_verifier.py @@ -2,7 +2,6 @@ """ Etherscan block explorer verifier implementation. """ -import os import sys import json import time @@ -141,18 +140,12 @@ def verify_contract( data['libraryname1'] = 'DssExecLib' data['libraryaddress1'] = library_address - # Submit verification request with retry - max_retries = 3 - for attempt in range(max_retries): - try: - verify_response = self._send_api_request(params, data) - break - except Exception as e: - if attempt == max_retries - 1: - print(f"Failed to submit verification request after {max_retries} attempts: {str(e)}", file=sys.stderr) - return False - print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) - time.sleep(2 ** attempt) + # Submit verification request (retry handled by decorator) + try: + verify_response = self._send_api_request(params, data) + except Exception as e: + print(f"Failed to submit verification request: {str(e)}", file=sys.stderr) + return False # Handle "contract not yet deployed" case max_deploy_retries = 5 diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py index 00925244..83842071 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/sourcify_verifier.py @@ -4,7 +4,6 @@ """ import sys import json -import time import requests from typing import Dict, Any diff --git a/scripts/verification/test_retry.py b/scripts/verification/test_retry.py index b0d43d9f..3ca2bce8 100644 --- a/scripts/verification/test_retry.py +++ b/scripts/verification/test_retry.py @@ -3,12 +3,10 @@ Test suite for the retry mechanism with exponential backoff and jitter. """ import time -import random import unittest -from unittest.mock import patch, MagicMock -from typing import Tuple, Callable +from unittest.mock import patch -from retry import retry_with_backoff, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY +from .retry import retry_with_backoff class TestRetryMechanism(unittest.TestCase): diff --git a/scripts/verify.py b/scripts/verify.py index 8a82fee3..c0f2e8ae 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -6,73 +6,41 @@ """ import os import sys -import subprocess import time -import re -import json -import requests -import random -from datetime import datetime -from typing import Dict, Any, Tuple, Optional, List, Callable -from functools import wraps +from pathlib import Path +from typing import Dict, Any, Tuple, List -# Import verifiers from the verification package -from verification import EtherscanVerifier, SourcifyVerifier + +def add_project_root_to_path(): + """Add the project root directory to Python's module search path.""" + project_root = Path(__file__).parent.parent.resolve() + if str(project_root) not in sys.path: + sys.path.append(str(project_root)) + + +# Add the project root to the Python path for imports +add_project_root_to_path() + +# Import verifiers and contract data utilities from the verification package + +from scripts.verification import ( + EtherscanVerifier, + SourcifyVerifier, + get_chain_id, + get_library_address, + flatten_source_code, + get_contract_metadata, + read_flattened_code, + get_action_address +) # Constants -FLATTEN_OUTPUT_PATH = 'out/flat.sol' SOURCE_FILE_PATH = 'src/DssSpell.sol' -LIBRARY_NAME = 'DssExecLib' - -# Retry configuration -DEFAULT_MAX_RETRIES = 3 -DEFAULT_BASE_DELAY = 2 # seconds -DEFAULT_MAX_DELAY = 60 # seconds -DEFAULT_BACKOFF_FACTOR = 2 -DEFAULT_JITTER = 0.1 # 10% jitter - - - - -def retry_with_backoff( - max_retries: int = DEFAULT_MAX_RETRIES, - base_delay: float = DEFAULT_BASE_DELAY, - max_delay: float = DEFAULT_MAX_DELAY, - backoff_factor: float = DEFAULT_BACKOFF_FACTOR, - jitter: float = DEFAULT_JITTER, - exceptions: Tuple[Exception, ...] = (requests.RequestException, json.JSONDecodeError, Exception) -): - """ - Decorator that implements exponential backoff with jitter for retrying functions. - """ - def decorator(func: Callable) -> Callable: - @wraps(func) - def wrapper(*args, **kwargs): - last_exception = None - - for attempt in range(max_retries + 1): - try: - return func(*args, **kwargs) - except exceptions as e: - last_exception = e - - if attempt == max_retries: - print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", file=sys.stderr) - raise - - # Calculate delay with exponential backoff and jitter - delay = min(base_delay * (backoff_factor ** attempt), max_delay) - jitter_amount = delay * jitter * random.uniform(-1, 1) - actual_delay = max(0, delay + jitter_amount) - - print(f"Attempt {attempt + 1} failed: {str(e)}", file=sys.stderr) - print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", file=sys.stderr) - - time.sleep(actual_delay) - - raise last_exception - return wrapper - return decorator + + + + + def get_env_var(var_name: str, error_message: str) -> str: @@ -84,52 +52,8 @@ def get_env_var(var_name: str, error_message: str) -> str: sys.exit(1) -@retry_with_backoff(max_retries=2, base_delay=1) -def get_chain_id() -> str: - """Get the current chain ID with retry mechanism.""" - print('Obtaining chain ID... ') - result = subprocess.run(['cast', 'chain-id'], capture_output=True, text=True, check=True) - chain_id = result.stdout.strip() - print(f"CHAIN_ID: {chain_id}") - return chain_id - - -def get_library_address() -> str: - """Find the DssExecLib address from either DssExecLib.address file or foundry.toml.""" - library_address = '' - # First try to read from foundry.toml libraries - if os.path.exists('foundry.toml'): - try: - with open('foundry.toml', 'r') as f: - config = f.read() - - result = re.search(r':DssExecLib:(0x[0-9a-fA-F]{40})', config) - if result: - library_address = result.group(1) - print(f'Using library {LIBRARY_NAME} at address {library_address}') - return library_address - else: - print('No DssExecLib configured in foundry.toml', file=sys.stderr) - except Exception as e: - print(f'Error reading foundry.toml: {str(e)}', file=sys.stderr) - else: - print('No foundry.toml found', file=sys.stderr) - # If it cannot be found, try DssExecLib.address - if os.path.exists('DssExecLib.address'): - try: - print(f'Trying to read DssExecLib.address...', file=sys.stderr) - with open('DssExecLib.address', 'r') as f: - library_address = f.read().strip() - print(f'Using library {LIBRARY_NAME} at address {library_address}') - return library_address - except Exception as e: - print(f'Error reading DssExecLib.address: {str(e)}', file=sys.stderr) - - # If we get here, no library address was found - print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) - return '' def parse_command_line_args() -> Tuple[str, str, str]: @@ -153,75 +77,12 @@ def parse_command_line_args() -> Tuple[str, str, str]: return contract_name, contract_address, constructor_args -@retry_with_backoff(max_retries=2, base_delay=1) -def flatten_source_code() -> None: - """Flatten the source code using Forge with retry mechanism.""" - result = subprocess.run([ - 'forge', 'flatten', - SOURCE_FILE_PATH, - '--output', FLATTEN_OUTPUT_PATH - ], capture_output=True, text=True, check=True) - - if result.returncode != 0: - raise Exception(f"Forge flatten failed: {result.stderr}") -def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: - """Extract contract metadata from the compiled output.""" - try: - with open(output_path, 'r') as f: - content = json.load(f) - - metadata = content['metadata'] - license_name = metadata['sources'][input_path]['license'] - - return { - 'compiler_version': 'v' + metadata['compiler']['version'], - 'evm_version': metadata['settings']['evmVersion'], - 'optimizer_enabled': metadata['settings']['optimizer']['enabled'], - 'optimizer_runs': metadata['settings']['optimizer']['runs'], - 'license_name': license_name - } - except FileNotFoundError: - raise Exception('Run forge build first') - except json.decoder.JSONDecodeError: - raise Exception('Run forge build again') - except KeyError as e: - raise Exception(f'Missing metadata field: {e}') - - -def read_flattened_code() -> str: - """Read the flattened source code.""" - try: - with open(FLATTEN_OUTPUT_PATH, 'r', encoding='utf-8') as f: - return f.read() - except FileNotFoundError: - raise Exception(f'Flattened source code not found at {FLATTEN_OUTPUT_PATH}. Run forge flatten first.') - except UnicodeDecodeError as e: - raise Exception(f'Error reading flattened source code: {str(e)}') -@retry_with_backoff(max_retries=2, base_delay=1) -def get_action_address(spell_address: str) -> Optional[str]: - """Get the action contract address from the spell contract with retry mechanism.""" - try: - result = subprocess.run( - ['cast', 'call', spell_address, 'action()(address)'], - capture_output=True, - text=True, - check=True, - env=os.environ | { - 'ETH_GAS_PRICE': '0', - 'ETH_PRIO_FEE': '0' - } - ) - return result.stdout.strip() - except subprocess.CalledProcessError as e: - print(f'Error getting action address: {str(e)}', file=sys.stderr) - return None - except Exception as e: - print(f'Unexpected error getting action address: {str(e)}', file=sys.stderr) - return None + + def setup_verifiers(chain_id: str) -> List[Any]: From 547d6435020c9af64b5b4bb75f3d6865fcb19243 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:39:17 +0200 Subject: [PATCH 04/16] style: :art: remove unnecessary whitespaces --- scripts/verification/__init__.py | 2 +- scripts/verification/contract_data.py | 2 +- scripts/verification/etherscan_verifier.py | 1 - scripts/verification/sourcify_verifier.py | 4 +--- scripts/verify.py | 19 +------------------ 5 files changed, 4 insertions(+), 24 deletions(-) diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index 5b13fa4b..826bdd37 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -15,7 +15,7 @@ ) __all__ = [ - 'EtherscanVerifier', + 'EtherscanVerifier', 'SourcifyVerifier', 'get_chain_id', 'get_library_address', diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index bc885af7..1def3dbb 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -76,7 +76,7 @@ def flatten_source_code() -> None: SOURCE_FILE_PATH, '--output', FLATTEN_OUTPUT_PATH ], capture_output=True, text=True, check=True) - + if result.returncode != 0: raise Exception(f"Forge flatten failed: {result.stderr}") diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py index ebc64e64..f6f47bd3 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/etherscan_verifier.py @@ -11,7 +11,6 @@ from .retry import retry_with_backoff - # Block explorer configurations ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' ETHERSCAN_SUBDOMAINS = { diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py index 83842071..f84e1cde 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/sourcify_verifier.py @@ -9,12 +9,10 @@ from .retry import retry_with_backoff - # Block explorer configurations SOURCIFY_API_URL = 'https://sourcify.dev/server' SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia - class SourcifyVerifier: """Sourcify block explorer verifier.""" @@ -96,7 +94,7 @@ def verify_contract( else: print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) return False - + except Exception as e: print(f"Failed to verify on Sourcify: {str(e)}", file=sys.stderr) return False diff --git a/scripts/verify.py b/scripts/verify.py index c0f2e8ae..919a14c9 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -24,7 +24,7 @@ def add_project_root_to_path(): # Import verifiers and contract data utilities from the verification package from scripts.verification import ( - EtherscanVerifier, + EtherscanVerifier, SourcifyVerifier, get_chain_id, get_library_address, @@ -38,11 +38,6 @@ def add_project_root_to_path(): SOURCE_FILE_PATH = 'src/DssSpell.sol' - - - - - def get_env_var(var_name: str, error_message: str) -> str: """Get environment variable with error handling.""" try: @@ -52,10 +47,6 @@ def get_env_var(var_name: str, error_message: str) -> str: sys.exit(1) - - - - def parse_command_line_args() -> Tuple[str, str, str]: """Parse command line arguments.""" if len(sys.argv) not in [3, 4]: @@ -77,14 +68,6 @@ def parse_command_line_args() -> Tuple[str, str, str]: return contract_name, contract_address, constructor_args - - - - - - - - def setup_verifiers(chain_id: str) -> List[Any]: """Setup available verifiers for the given chain.""" verifiers = [] From fd226571146872926b93f309b072df3d6df5948b Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:24:15 +0200 Subject: [PATCH 05/16] test: :white_check_mark: skip testLockstakeStusdsInit test --- src/DssSpell.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DssSpell.t.sol b/src/DssSpell.t.sol index 079c34b8..0608753c 100644 --- a/src/DssSpell.t.sol +++ b/src/DssSpell.t.sol @@ -1405,7 +1405,7 @@ contract DssSpellTest is DssSpellTestBase { ConvLike conv = ConvLike(0xea91A18dAFA1Cb1d2a19DFB205816034e6Fe7e52); address bud = 0xBB865F94B8A92E57f79fCc89Dfd4dcf0D3fDEA16; - function testLockstakeStusdsInit() public { + function testLockstakeStusdsInit() public skipped { // add the `skipped` modifier to skip assertEq(vat.wards(address(clip)), 1, "TestError/lockstake-stusds-init-vat-wards-clip-pre"); assertEq(vat.wards(address(newClip)), 0, "TestError/lockstake-stusds-init-vat-wards-newclip-pre"); From 4e1d4c4362103f060837e2b18ce2fa3c3bdee01c Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:22:57 +0200 Subject: [PATCH 06/16] fix: :bug: move verifiers report outside for loop --- scripts/verify.py | 14 ++++---------- src/DssSpell.t.sol | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/scripts/verify.py b/scripts/verify.py index 919a14c9..4f0c608b 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -136,25 +136,19 @@ def verify_contract_with_verifiers( if success: successful_verifications += 1 print(f"✓ Successfully verified on {verifier.__class__.__name__}") - - if successful_verifications >= 1: - print(f"\n🎉 Contract verified successfully on {successful_verifications}/{total_verifiers} verifiers!") - return True else: print(f"✗ Verification failed on {verifier.__class__.__name__}") except Exception as e: print(f"✗ Error during verification with {verifier.__class__.__name__}: {str(e)}", file=sys.stderr) - - if i < total_verifiers - 1: - print("Waiting 2 seconds before trying next verifier...") - time.sleep(2) + # Report final results after trying all verifiers if successful_verifications == 0: print(f"\n❌ Failed to verify contract on any verifier ({total_verifiers} attempted)") return False - - return True + else: + print(f"\n🎉 Contract verified successfully on {successful_verifications}/{total_verifiers} verifiers!") + return True def main(): diff --git a/src/DssSpell.t.sol b/src/DssSpell.t.sol index 0608753c..079c34b8 100644 --- a/src/DssSpell.t.sol +++ b/src/DssSpell.t.sol @@ -1405,7 +1405,7 @@ contract DssSpellTest is DssSpellTestBase { ConvLike conv = ConvLike(0xea91A18dAFA1Cb1d2a19DFB205816034e6Fe7e52); address bud = 0xBB865F94B8A92E57f79fCc89Dfd4dcf0D3fDEA16; - function testLockstakeStusdsInit() public skipped { // add the `skipped` modifier to skip + function testLockstakeStusdsInit() public { assertEq(vat.wards(address(clip)), 1, "TestError/lockstake-stusds-init-vat-wards-clip-pre"); assertEq(vat.wards(address(newClip)), 0, "TestError/lockstake-stusds-init-vat-wards-newclip-pre"); From b4a1ac27e0e339300f9d31056d7f03c549717498 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:25:46 +0200 Subject: [PATCH 07/16] refactor: :recycle: remove flattening step from verification process --- scripts/verification/__init__.py | 4 - scripts/verification/contract_data.py | 25 --- scripts/verification/etherscan_verifier.py | 178 +++++---------------- scripts/verification/sourcify_verifier.py | 100 +++++------- scripts/verify.py | 20 +-- 5 files changed, 86 insertions(+), 241 deletions(-) diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index 826bdd37..e1447ff6 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -8,9 +8,7 @@ from .contract_data import ( get_chain_id, get_library_address, - flatten_source_code, get_contract_metadata, - read_flattened_code, get_action_address ) @@ -19,8 +17,6 @@ 'SourcifyVerifier', 'get_chain_id', 'get_library_address', - 'flatten_source_code', 'get_contract_metadata', - 'read_flattened_code', 'get_action_address' ] diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index 1def3dbb..6770b7e2 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -15,7 +15,6 @@ # Constants -FLATTEN_OUTPUT_PATH = 'out/flat.sol' SOURCE_FILE_PATH = 'src/DssSpell.sol' LIBRARY_NAME = 'DssExecLib' @@ -68,19 +67,6 @@ def get_library_address() -> str: return '' -@retry_with_backoff(max_retries=2, base_delay=1) -def flatten_source_code() -> None: - """Flatten the source code using Forge with retry mechanism.""" - result = subprocess.run([ - 'forge', 'flatten', - SOURCE_FILE_PATH, - '--output', FLATTEN_OUTPUT_PATH - ], capture_output=True, text=True, check=True) - - if result.returncode != 0: - raise Exception(f"Forge flatten failed: {result.stderr}") - - def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: """Extract contract metadata from the compiled output.""" try: @@ -105,17 +91,6 @@ def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: raise Exception(f'Missing metadata field: {e}') -def read_flattened_code() -> str: - """Read the flattened source code.""" - try: - with open(FLATTEN_OUTPUT_PATH, 'r', encoding='utf-8') as f: - return f.read() - except FileNotFoundError: - raise Exception(f'Flattened source code not found at {FLATTEN_OUTPUT_PATH}. Run forge flatten first.') - except UnicodeDecodeError as e: - raise Exception(f'Error reading flattened source code: {str(e)}') - - @retry_with_backoff(max_retries=2, base_delay=1) def get_action_address(spell_address: str) -> Optional[str]: """Get the action contract address from the spell contract with retry mechanism.""" diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py index f6f47bd3..57c345c9 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/etherscan_verifier.py @@ -1,31 +1,23 @@ #!/usr/bin/env python3 """ -Etherscan block explorer verifier implementation. +Etherscan block explorer verifier implementation using forge verify-contract. """ +import os import sys -import json -import time -import requests -from datetime import datetime -from typing import Dict, Any +import subprocess from .retry import retry_with_backoff # Block explorer configurations -ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' ETHERSCAN_SUBDOMAINS = { '1': '', '11155111': 'sepolia.' } -LICENSE_NUMBERS = { - 'GPL-3.0-or-later': 5, - 'AGPL-3.0-or-later': 13 -} SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia class EtherscanVerifier: - """Etherscan block explorer verifier.""" + """Etherscan block explorer verifier using forge verify-contract.""" def __init__(self, api_key: str, chain_id: str): self.api_key = api_key @@ -41,145 +33,59 @@ def get_verification_url(self, contract_address: str) -> str: return f"https://{self.subdomain}etherscan.io/address/{contract_address}#code" @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def _send_api_request(self, params: Dict[str, str], data: Dict[str, Any]) -> Dict: - """Send request to Etherscan API with retry mechanism.""" - headers = {'User-Agent': 'Sky-Protocol-Spell-Verifier'} - - response = requests.post( - ETHERSCAN_API_URL, - headers=headers, - params=params, - data=data, - timeout=30 - ) - - response.raise_for_status() - - try: - return json.loads(response.text) - except json.decoder.JSONDecodeError as e: - print(f"Response text: {response.text}", file=sys.stderr) - raise Exception(f'Etherscan responded with invalid JSON: {str(e)}') - - def _wait_for_verification(self, guid: str, params: Dict[str, str], code: str) -> None: - """Wait for verification to complete with retry mechanism.""" - check_data = { - 'apikey': self.api_key, - 'module': 'contract', - 'action': 'checkverifystatus', - 'guid': guid, - } - - check_response = {} - max_attempts = 20 - - for attempt in range(max_attempts): - if check_response and 'pending' not in check_response.get('result', '').lower(): - break - - if check_response: - print(check_response['result'], file=sys.stderr) - print(f'Waiting for 15 seconds for Etherscan to process... (attempt {attempt + 1}/{max_attempts})', file=sys.stderr) - time.sleep(15) - - try: - check_response = self._send_api_request(params, check_data) - except Exception as e: - print(f"Error checking verification status: {str(e)}", file=sys.stderr) - if attempt == max_attempts - 1: - raise - time.sleep(15) - continue - - if check_response['status'] != '1' or check_response['message'] != 'OK': - if 'already verified' not in check_response['result'].lower(): - log_name = f'verify-etherscan-{datetime.now().timestamp()}.log' - with open(log_name, 'w') as log: - log.write(code) - print(f'Source code logged to {log_name}', file=sys.stderr) - - raise Exception(f'Verification failed: {check_response.get("result", "Unknown error")}') - else: - print('Contract is already verified') - def verify_contract( self, contract_name: str, contract_address: str, - source_code: str, constructor_args: str, - metadata: Dict[str, Any], library_address: str = "" ) -> bool: - """Verify contract on Etherscan.""" + """Verify contract on Etherscan using forge verify-contract.""" print(f'\nVerifying {contract_name} at {contract_address} on Etherscan...') - params = {'chainid': self.chain_id} - - license_name = metadata.get('license_name', 'MIT') - license_number = LICENSE_NUMBERS.get(license_name, 1) - - data = { - 'apikey': self.api_key, - 'module': 'contract', - 'action': 'verifysourcecode', - 'contractaddress': contract_address, - 'sourceCode': source_code, - 'codeFormat': 'solidity-single-file', - 'contractName': contract_name, - 'compilerversion': metadata['compiler_version'], - 'optimizationUsed': '1' if metadata['optimizer_enabled'] else '0', - 'runs': metadata['optimizer_runs'], - 'constructorArguements': constructor_args, - 'evmversion': metadata['evm_version'], - 'licenseType': license_number, - } - + # Build forge verify-contract command + cmd = [ + 'forge', 'verify-contract', + contract_address, + f'src/{contract_name}.sol:{contract_name}', + '--verifier', 'etherscan', + '--etherscan-api-key', self.api_key, + '--flatten', + '--watch' + ] + + # Add constructor arguments if provided + if constructor_args: + cmd.extend(['--constructor-args', constructor_args]) + + # Add library linking if provided if library_address: - data['libraryname1'] = 'DssExecLib' - data['libraryaddress1'] = library_address + cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - # Submit verification request (retry handled by decorator) - try: - verify_response = self._send_api_request(params, data) - except Exception as e: - print(f"Failed to submit verification request: {str(e)}", file=sys.stderr) - return False - - # Handle "contract not yet deployed" case - max_deploy_retries = 5 - deploy_retry_count = 0 + # Set environment variables for the subprocess + env = os.environ.copy() + env['ETH_RPC_URL'] = os.environ.get('ETH_RPC_URL', '') - while 'locate' in verify_response.get('result', '').lower() and deploy_retry_count < max_deploy_retries: - print(verify_response['result'], file=sys.stderr) - print(f'Waiting for 15 seconds for the network to update... (attempt {deploy_retry_count + 1}/{max_deploy_retries})', file=sys.stderr) - time.sleep(15) + try: + subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + env=env + ) - try: - verify_response = self._send_api_request(params, data) - except Exception as e: - print(f"Error during deploy retry: {str(e)}", file=sys.stderr) - deploy_retry_count += 1 - continue - - if deploy_retry_count >= max_deploy_retries: - print("Contract not found on network after maximum retries", file=sys.stderr) - return False - - if verify_response['status'] != '1' or verify_response['message'] != 'OK': - if 'already verified' in verify_response['result'].lower(): + print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') + return True + + except subprocess.CalledProcessError as e: + # Check if it's already verified + if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): print('Contract is already verified on Etherscan') return True - print(f'Failed to submit verification request: {verify_response.get("result", "Unknown error")}', file=sys.stderr) + + print(f"Verification failed: {e.stderr}", file=sys.stderr) return False - - guid = verify_response['result'] - print(f'Verification request submitted with GUID: {guid}') - - try: - self._wait_for_verification(guid, params, source_code) - print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') - return True except Exception as e: - print(f"Verification failed: {str(e)}", file=sys.stderr) + print(f"Unexpected error during verification: {str(e)}", file=sys.stderr) return False diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py index f84e1cde..8e8fe23a 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/sourcify_verifier.py @@ -1,20 +1,18 @@ #!/usr/bin/env python3 """ -Sourcify block explorer verifier implementation. +Sourcify block explorer verifier implementation using forge verify-contract. """ +import os import sys -import json -import requests -from typing import Dict, Any +import subprocess from .retry import retry_with_backoff # Block explorer configurations -SOURCIFY_API_URL = 'https://sourcify.dev/server' SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia class SourcifyVerifier: - """Sourcify block explorer verifier.""" + """Sourcify block explorer verifier using forge verify-contract.""" def __init__(self, chain_id: str): self.chain_id = chain_id @@ -28,73 +26,59 @@ def get_verification_url(self, contract_address: str) -> str: return f"https://sourcify.dev/#/lookup/{contract_address}" @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def _send_api_request(self, endpoint: str, data: Dict[str, Any]) -> Dict: - """Send request to Sourcify API with retry mechanism.""" - headers = { - 'User-Agent': 'Sky-Protocol-Spell-Verifier', - 'Content-Type': 'application/json' - } - - url = f"{SOURCIFY_API_URL}/{endpoint}" - - response = requests.post( - url, - headers=headers, - json=data, - timeout=30 - ) - - response.raise_for_status() - - try: - return response.json() - except json.decoder.JSONDecodeError as e: - print(f"Response text: {response.text}", file=sys.stderr) - raise Exception(f'Sourcify responded with invalid JSON: {str(e)}') - def verify_contract( self, contract_name: str, contract_address: str, - source_code: str, constructor_args: str, - metadata: Dict[str, Any], library_address: str = "" ) -> bool: - """Verify contract on Sourcify.""" + """Verify contract on Sourcify using forge verify-contract.""" print(f'\nVerifying {contract_name} at {contract_address} on Sourcify...') - files_data = { - "contract.sol": source_code - } + # Build forge verify-contract command + cmd = [ + 'forge', 'verify-contract', + contract_address, + f'src/{contract_name}.sol:{contract_name}', + '--verifier', 'sourcify', + '--flatten', + '--watch' + ] - if metadata: - files_data["metadata.json"] = json.dumps(metadata) + # Add constructor arguments if provided + if constructor_args: + cmd.extend(['--constructor-args', constructor_args]) - verification_data = { - "address": contract_address, - "chain": self.chain_id, - "files": files_data - } + # Add library linking if provided + if library_address: + cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - if constructor_args: - verification_data["constructorArgs"] = constructor_args + # Set environment variables for the subprocess + env = os.environ.copy() + env['ETH_RPC_URL'] = os.environ.get('ETH_RPC_URL', '') try: - response = self._send_api_request("verify", verification_data) + subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + env=env + ) - if response.get("status") == "perfect": - print(f'Contract verified successfully on Sourcify') - print(f'View at: {self.get_verification_url(contract_address)}') - return True - elif response.get("status") == "partial": - print(f'Contract partially verified on Sourcify (some files missing)') - print(f'View at: {self.get_verification_url(contract_address)}') + print(f'Contract verified successfully on Sourcify') + print(f'View at: {self.get_verification_url(contract_address)}') + return True + + except subprocess.CalledProcessError as e: + # Check if it's already verified + if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): + print('Contract is already verified on Sourcify') return True - else: - print(f'Verification failed: {response.get("message", "Unknown error")}', file=sys.stderr) - return False - + + print(f"Verification failed: {e.stderr}", file=sys.stderr) + return False except Exception as e: - print(f"Failed to verify on Sourcify: {str(e)}", file=sys.stderr) + print(f"Unexpected error during verification: {str(e)}", file=sys.stderr) return False diff --git a/scripts/verify.py b/scripts/verify.py index 4f0c608b..5d32f298 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -2,13 +2,12 @@ """ Enhanced contract verification script for Sky Protocol spells. This script verifies both the DssSpell and DssSpellAction contracts on multiple block explorers -with robust retry mechanisms and fallback options. +using forge verify-contract --flatten with robust retry mechanisms and fallback options. """ import os import sys -import time from pathlib import Path -from typing import Dict, Any, Tuple, List +from typing import Any, Tuple, List def add_project_root_to_path(): @@ -28,9 +27,7 @@ def add_project_root_to_path(): SourcifyVerifier, get_chain_id, get_library_address, - flatten_source_code, get_contract_metadata, - read_flattened_code, get_action_address ) @@ -108,9 +105,7 @@ def setup_verifiers(chain_id: str) -> List[Any]: def verify_contract_with_verifiers( contract_name: str, contract_address: str, - source_code: str, constructor_args: str, - metadata: Dict[str, Any], library_address: str, verifiers: List[Any] ) -> bool: @@ -127,9 +122,7 @@ def verify_contract_with_verifiers( success = verifier.verify_contract( contract_name=contract_name, contract_address=contract_address, - source_code=source_code, constructor_args=constructor_args, - metadata=metadata, library_address=library_address ) @@ -175,13 +168,6 @@ def main(): print("Setting up verifiers...") verifiers = setup_verifiers(chain_id) - # Flatten source code - print("Flattening source code...") - flatten_source_code() - - # Read flattened code - source_code = read_flattened_code() - # Get contract metadata metadata = get_contract_metadata( f'out/DssSpell.sol/DssSpell.json', @@ -192,7 +178,6 @@ def main(): spell_success = verify_contract_with_verifiers( contract_name=spell_name, contract_address=spell_address, - source_code=source_code, constructor_args=constructor_args, metadata=metadata, library_address=library_address, @@ -212,7 +197,6 @@ def main(): action_success = verify_contract_with_verifiers( contract_name="DssSpellAction", contract_address=action_address, - source_code=source_code, constructor_args=constructor_args, metadata=metadata, library_address=library_address, From eea1b68bf6c946530e2dcfc4a48cf8b5e49274ea Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:53:22 +0100 Subject: [PATCH 08/16] refactor: :fire: remove references to sepolia --- scripts/verification/README.md | 2 -- scripts/verification/contract_data.py | 15 ++---------- scripts/verification/etherscan_verifier.py | 27 +++++++--------------- scripts/verification/sourcify_verifier.py | 21 ++++++----------- scripts/verify.py | 25 +++++++------------- 5 files changed, 25 insertions(+), 65 deletions(-) diff --git a/scripts/verification/README.md b/scripts/verification/README.md index f553b61c..6cd92644 100644 --- a/scripts/verification/README.md +++ b/scripts/verification/README.md @@ -58,13 +58,11 @@ make verify addr=0x1234567890123456789012345678901234567890 ## Supported Block Explorers ### Etherscan -- **Chains**: Mainnet (1), Sepolia (11155111) - **API**: Etherscan API v2 - **Requirements**: API key - **Features**: Full verification with constructor arguments and libraries ### Sourcify -- **Chains**: Mainnet (1), Sepolia (11155111) - **API**: Sourcify API - **Requirements**: No API key required - **Features**: Open-source verification service diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index 6770b7e2..3d08dc27 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -30,10 +30,10 @@ def get_chain_id() -> str: def get_library_address() -> str: - """Find the DssExecLib address from either DssExecLib.address file or foundry.toml.""" + """Find the DssExecLib address from foundry.toml.""" library_address = '' - # First try to read from foundry.toml libraries + # Try to read from foundry.toml libraries if os.path.exists('foundry.toml'): try: with open('foundry.toml', 'r') as f: @@ -51,17 +51,6 @@ def get_library_address() -> str: else: print('No foundry.toml found', file=sys.stderr) - # If it cannot be found, try DssExecLib.address - if os.path.exists('DssExecLib.address'): - try: - print(f'Trying to read DssExecLib.address...', file=sys.stderr) - with open('DssExecLib.address', 'r') as f: - library_address = f.read().strip() - print(f'Using library {LIBRARY_NAME} at address {library_address}') - return library_address - except Exception as e: - print(f'Error reading DssExecLib.address: {str(e)}', file=sys.stderr) - # If we get here, no library address was found print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) return '' diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/etherscan_verifier.py index 57c345c9..84715db8 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/etherscan_verifier.py @@ -2,18 +2,13 @@ """ Etherscan block explorer verifier implementation using forge verify-contract. """ -import os import sys import subprocess from .retry import retry_with_backoff # Block explorer configurations -ETHERSCAN_SUBDOMAINS = { - '1': '', - '11155111': 'sepolia.' -} -SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia +CHAIN_ID = '1' # Mainnet only class EtherscanVerifier: @@ -22,15 +17,14 @@ class EtherscanVerifier: def __init__(self, api_key: str, chain_id: str): self.api_key = api_key self.chain_id = chain_id - self.subdomain = ETHERSCAN_SUBDOMAINS.get(chain_id, '') def is_available(self) -> bool: """Check if Etherscan supports this chain.""" - return self.chain_id in SUPPORTED_CHAIN_IDS + return self.chain_id == CHAIN_ID def get_verification_url(self, contract_address: str) -> str: """Get Etherscan URL for the verified contract.""" - return f"https://{self.subdomain}etherscan.io/address/{contract_address}#code" + return f"https://etherscan.io/address/{contract_address}#code" @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) def verify_contract( @@ -47,7 +41,7 @@ def verify_contract( cmd = [ 'forge', 'verify-contract', contract_address, - f'src/{contract_name}.sol:{contract_name}', + f'src/DssSpell.sol:{contract_name}', '--verifier', 'etherscan', '--etherscan-api-key', self.api_key, '--flatten', @@ -62,30 +56,25 @@ def verify_contract( if library_address: cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - # Set environment variables for the subprocess - env = os.environ.copy() - env['ETH_RPC_URL'] = os.environ.get('ETH_RPC_URL', '') - try: subprocess.run( cmd, capture_output=True, text=True, check=True, - env=env ) - print(f'Contract verified successfully at {self.get_verification_url(contract_address)}') + print(f'✓ Contract verified successfully at {self.get_verification_url(contract_address)}') return True except subprocess.CalledProcessError as e: # Check if it's already verified if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): - print('Contract is already verified on Etherscan') + print('✓ Contract is already verified on Etherscan') return True - print(f"Verification failed: {e.stderr}", file=sys.stderr) + print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) return False except Exception as e: - print(f"Unexpected error during verification: {str(e)}", file=sys.stderr) + print(f"✗ Unexpected error during verification: {str(e)}", file=sys.stderr) return False diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/sourcify_verifier.py index 8e8fe23a..09597b62 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/sourcify_verifier.py @@ -2,14 +2,13 @@ """ Sourcify block explorer verifier implementation using forge verify-contract. """ -import os import sys import subprocess from .retry import retry_with_backoff # Block explorer configurations -SUPPORTED_CHAIN_IDS = ['1', '11155111'] # Mainnet and Sepolia +CHAIN_ID = '1' # Mainnet only class SourcifyVerifier: """Sourcify block explorer verifier using forge verify-contract.""" @@ -19,7 +18,7 @@ def __init__(self, chain_id: str): def is_available(self) -> bool: """Check if Sourcify supports this chain.""" - return self.chain_id in SUPPORTED_CHAIN_IDS + return self.chain_id == CHAIN_ID def get_verification_url(self, contract_address: str) -> str: """Get Sourcify URL for the verified contract.""" @@ -40,7 +39,7 @@ def verify_contract( cmd = [ 'forge', 'verify-contract', contract_address, - f'src/{contract_name}.sol:{contract_name}', + f'src/DssSpell.sol:{contract_name}', '--verifier', 'sourcify', '--flatten', '--watch' @@ -54,31 +53,25 @@ def verify_contract( if library_address: cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - # Set environment variables for the subprocess - env = os.environ.copy() - env['ETH_RPC_URL'] = os.environ.get('ETH_RPC_URL', '') - try: subprocess.run( cmd, capture_output=True, text=True, check=True, - env=env ) - print(f'Contract verified successfully on Sourcify') - print(f'View at: {self.get_verification_url(contract_address)}') + print(f'✓ Contract verified successfully at {self.get_verification_url(contract_address)}') return True except subprocess.CalledProcessError as e: # Check if it's already verified if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): - print('Contract is already verified on Sourcify') + print('✓ Contract is already verified on Sourcify') return True - print(f"Verification failed: {e.stderr}", file=sys.stderr) + print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) return False except Exception as e: - print(f"Unexpected error during verification: {str(e)}", file=sys.stderr) + print(f"✗ Unexpected error during verification: {str(e)}", file=sys.stderr) return False diff --git a/scripts/verify.py b/scripts/verify.py index 5d32f298..1c0e7e56 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Enhanced contract verification script for Sky Protocol spells. +Enhanced contract verification script for Sky Protocol spells on mainnet. This script verifies both the DssSpell and DssSpellAction contracts on multiple block explorers using forge verify-contract --flatten with robust retry mechanisms and fallback options. """ @@ -27,7 +27,6 @@ def add_project_root_to_path(): SourcifyVerifier, get_chain_id, get_library_address, - get_contract_metadata, get_action_address ) @@ -123,24 +122,24 @@ def verify_contract_with_verifiers( contract_name=contract_name, contract_address=contract_address, constructor_args=constructor_args, - library_address=library_address + library_address=library_address, ) if success: successful_verifications += 1 - print(f"✓ Successfully verified on {verifier.__class__.__name__}") - else: - print(f"✗ Verification failed on {verifier.__class__.__name__}") except Exception as e: print(f"✗ Error during verification with {verifier.__class__.__name__}: {str(e)}", file=sys.stderr) # Report final results after trying all verifiers if successful_verifications == 0: - print(f"\n❌ Failed to verify contract on any verifier ({total_verifiers} attempted)") + print(f"\n❌ Failed to verify contract on all verifiers ({total_verifiers} attempted)") return False + elif successful_verifications < total_verifiers: + print(f"\n⚠️ Contract verified successfully in only {successful_verifications}/{total_verifiers} verifiers!") + return True else: - print(f"\n🎉 Contract verified successfully on {successful_verifications}/{total_verifiers} verifiers!") + print(f"\n🎉 Contract verified successfully in {total_verifiers} verifiers!") return True @@ -148,7 +147,7 @@ def main(): """Main entry point for the enhanced verification script.""" try: # Get environment variables - rpc_url = get_env_var( + get_env_var( 'ETH_RPC_URL', "You need a valid ETH_RPC_URL.\n" "Get a public one at https://chainlist.org/ or provide your own\n" @@ -168,18 +167,11 @@ def main(): print("Setting up verifiers...") verifiers = setup_verifiers(chain_id) - # Get contract metadata - metadata = get_contract_metadata( - f'out/DssSpell.sol/DssSpell.json', - SOURCE_FILE_PATH - ) - # Verify spell contract spell_success = verify_contract_with_verifiers( contract_name=spell_name, contract_address=spell_address, constructor_args=constructor_args, - metadata=metadata, library_address=library_address, verifiers=verifiers ) @@ -198,7 +190,6 @@ def main(): contract_name="DssSpellAction", contract_address=action_address, constructor_args=constructor_args, - metadata=metadata, library_address=library_address, verifiers=verifiers ) From 3fc520f7f9a7ca2a5dbd663819a2773a8942f8ac Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:58:32 +0100 Subject: [PATCH 09/16] refactor: :truck: rename verifiers files and classes --- scripts/verification/__init__.py | 10 ++++---- scripts/verification/contract_data.py | 24 ------------------- .../{test_retry.py => retry_test.py} | 0 ...scan_verifier.py => verifier_etherscan.py} | 2 +- ...rcify_verifier.py => verifier_sourcify.py} | 2 +- scripts/verify.py | 8 +++---- 6 files changed, 10 insertions(+), 36 deletions(-) rename scripts/verification/{test_retry.py => retry_test.py} (100%) rename scripts/verification/{etherscan_verifier.py => verifier_etherscan.py} (99%) rename scripts/verification/{sourcify_verifier.py => verifier_sourcify.py} (99%) diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index e1447ff6..980a9860 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -3,20 +3,18 @@ Verification package for Sky Protocol spells. """ -from .etherscan_verifier import EtherscanVerifier -from .sourcify_verifier import SourcifyVerifier +from .verifier_etherscan import VerifierEtherscan +from .verifier_sourcify import VerifierSourcify from .contract_data import ( get_chain_id, get_library_address, - get_contract_metadata, get_action_address ) __all__ = [ - 'EtherscanVerifier', - 'SourcifyVerifier', + 'VerifierEtherscan', + 'VerifierSourcify', 'get_chain_id', 'get_library_address', - 'get_contract_metadata', 'get_action_address' ] diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index 3d08dc27..10c9963e 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -56,30 +56,6 @@ def get_library_address() -> str: return '' -def get_contract_metadata(output_path: str, input_path: str) -> Dict[str, Any]: - """Extract contract metadata from the compiled output.""" - try: - with open(output_path, 'r') as f: - content = json.load(f) - - metadata = content['metadata'] - license_name = metadata['sources'][input_path]['license'] - - return { - 'compiler_version': 'v' + metadata['compiler']['version'], - 'evm_version': metadata['settings']['evmVersion'], - 'optimizer_enabled': metadata['settings']['optimizer']['enabled'], - 'optimizer_runs': metadata['settings']['optimizer']['runs'], - 'license_name': license_name - } - except FileNotFoundError: - raise Exception('Run forge build first') - except json.decoder.JSONDecodeError: - raise Exception('Run forge build again') - except KeyError as e: - raise Exception(f'Missing metadata field: {e}') - - @retry_with_backoff(max_retries=2, base_delay=1) def get_action_address(spell_address: str) -> Optional[str]: """Get the action contract address from the spell contract with retry mechanism.""" diff --git a/scripts/verification/test_retry.py b/scripts/verification/retry_test.py similarity index 100% rename from scripts/verification/test_retry.py rename to scripts/verification/retry_test.py diff --git a/scripts/verification/etherscan_verifier.py b/scripts/verification/verifier_etherscan.py similarity index 99% rename from scripts/verification/etherscan_verifier.py rename to scripts/verification/verifier_etherscan.py index 84715db8..48a0907f 100644 --- a/scripts/verification/etherscan_verifier.py +++ b/scripts/verification/verifier_etherscan.py @@ -11,7 +11,7 @@ CHAIN_ID = '1' # Mainnet only -class EtherscanVerifier: +class VerifierEtherscan: """Etherscan block explorer verifier using forge verify-contract.""" def __init__(self, api_key: str, chain_id: str): diff --git a/scripts/verification/sourcify_verifier.py b/scripts/verification/verifier_sourcify.py similarity index 99% rename from scripts/verification/sourcify_verifier.py rename to scripts/verification/verifier_sourcify.py index 09597b62..eee66b1b 100644 --- a/scripts/verification/sourcify_verifier.py +++ b/scripts/verification/verifier_sourcify.py @@ -10,7 +10,7 @@ # Block explorer configurations CHAIN_ID = '1' # Mainnet only -class SourcifyVerifier: +class VerifierSourcify: """Sourcify block explorer verifier using forge verify-contract.""" def __init__(self, chain_id: str): diff --git a/scripts/verify.py b/scripts/verify.py index 1c0e7e56..6293c747 100755 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -23,8 +23,8 @@ def add_project_root_to_path(): # Import verifiers and contract data utilities from the verification package from scripts.verification import ( - EtherscanVerifier, - SourcifyVerifier, + VerifierEtherscan, + VerifierSourcify, get_chain_id, get_library_address, get_action_address @@ -74,7 +74,7 @@ def setup_verifiers(chain_id: str) -> List[Any]: 'ETHERSCAN_API_KEY', "Etherscan API key not found. Set ETHERSCAN_API_KEY environment variable." ) - etherscan_verifier = EtherscanVerifier(etherscan_api_key, chain_id) + etherscan_verifier = VerifierEtherscan(etherscan_api_key, chain_id) if etherscan_verifier.is_available(): verifiers.append(etherscan_verifier) print(f"✓ Etherscan verifier available for chain {chain_id}") @@ -86,7 +86,7 @@ def setup_verifiers(chain_id: str) -> List[Any]: # Setup Sourcify verifier # Note: Blockscout automatically picks up any code verification from Sourcify try: - sourcify_verifier = SourcifyVerifier(chain_id) + sourcify_verifier = VerifierSourcify(chain_id) if sourcify_verifier.is_available(): verifiers.append(sourcify_verifier) print(f"✓ Sourcify verifier available for chain {chain_id}") From 6a49e3aca6eed7f00a993dfce58727c535be0c47 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:55:16 +0100 Subject: [PATCH 10/16] chore: :fire: remove unused imports and fix README typo --- scripts/verification/README.md | 6 +++--- scripts/verification/contract_data.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/verification/README.md b/scripts/verification/README.md index 6cd92644..9db5e635 100644 --- a/scripts/verification/README.md +++ b/scripts/verification/README.md @@ -108,10 +108,10 @@ The test suite uses relative imports and must be run as a module: ```bash # ✅ Correct way - run as module -python3 -m scripts.verification.test_retry +python3 -m scripts.verification.retry_test # ❌ Incorrect way - direct execution fails -python3 scripts/verification/test_retry.py +python3 scripts/verification/retry_test.py ``` **Why?** The test file uses relative imports (`from .retry import retry_with_backoff`) which only work when the file is run as part of a package, not as a standalone script. @@ -133,7 +133,7 @@ The test suite covers: 1. **"No verifiers available"**: Check chain ID support and API keys 2. **"Verification failed on all verifiers"**: Check contract deployment and source code 3. **"Etherscan API key not found"**: Set `ETHERSCAN_API_KEY` environment variable -4. **"ImportError: attempted relative import with no known parent package"**: Run tests as a module using `python3 -m scripts.verification.test_retry` +4. **"ImportError: attempted relative import with no known parent package"**: Run tests as a module using `python3 -m scripts.verification.retry_test` ### **Debug Mode** diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index 10c9963e..e075f59a 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -8,8 +8,7 @@ import sys import subprocess import re -import json -from typing import Dict, Any, Optional +from typing import Optional from .retry import retry_with_backoff From 458e24948006350b68ab67f4b4cdc8b0a4a371a5 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:17:29 +0100 Subject: [PATCH 11/16] style: :art: run flake8 linter and black formatter --- scripts/verification/__init__.py | 16 ++---- scripts/verification/contract_data.py | 46 +++++++-------- scripts/verification/retry.py | 38 +++++++----- scripts/verification/retry_test.py | 67 ++++++++++++---------- scripts/verification/verifier_etherscan.py | 62 +++++++++++--------- scripts/verification/verifier_sourcify.py | 60 +++++++++++-------- 6 files changed, 160 insertions(+), 129 deletions(-) diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index 980a9860..46ded63f 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -3,18 +3,14 @@ Verification package for Sky Protocol spells. """ +from .contract_data import get_action_address, get_chain_id, get_library_address from .verifier_etherscan import VerifierEtherscan from .verifier_sourcify import VerifierSourcify -from .contract_data import ( - get_chain_id, - get_library_address, - get_action_address -) __all__ = [ - 'VerifierEtherscan', - 'VerifierSourcify', - 'get_chain_id', - 'get_library_address', - 'get_action_address' + "VerifierEtherscan", + "VerifierSourcify", + "get_chain_id", + "get_library_address", + "get_action_address", ] diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index e075f59a..f6d5a039 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -5,24 +5,25 @@ and other contract-related data operations. """ import os -import sys -import subprocess import re +import subprocess +import sys from typing import Optional from .retry import retry_with_backoff - # Constants -SOURCE_FILE_PATH = 'src/DssSpell.sol' -LIBRARY_NAME = 'DssExecLib' +SOURCE_FILE_PATH = "src/DssSpell.sol" +LIBRARY_NAME = "DssExecLib" @retry_with_backoff(max_retries=2, base_delay=1) def get_chain_id() -> str: """Get the current chain ID with retry mechanism.""" - print('Obtaining chain ID... ') - result = subprocess.run(['cast', 'chain-id'], capture_output=True, text=True, check=True) + print("Obtaining chain ID... ") + result = subprocess.run( + ["cast", "chain-id"], capture_output=True, text=True, check=True + ) chain_id = result.stdout.strip() print(f"CHAIN_ID: {chain_id}") return chain_id @@ -30,29 +31,29 @@ def get_chain_id() -> str: def get_library_address() -> str: """Find the DssExecLib address from foundry.toml.""" - library_address = '' + library_address = "" # Try to read from foundry.toml libraries - if os.path.exists('foundry.toml'): + if os.path.exists("foundry.toml"): try: - with open('foundry.toml', 'r') as f: + with open("foundry.toml", "r") as f: config = f.read() - result = re.search(r':DssExecLib:(0x[0-9a-fA-F]{40})', config) + result = re.search(r":DssExecLib:(0x[0-9a-fA-F]{40})", config) if result: library_address = result.group(1) - print(f'Using library {LIBRARY_NAME} at address {library_address}') + print(f"Using library {LIBRARY_NAME} at address {library_address}") return library_address else: - print('No DssExecLib configured in foundry.toml', file=sys.stderr) + print("No DssExecLib configured in foundry.toml", file=sys.stderr) except Exception as e: - print(f'Error reading foundry.toml: {str(e)}', file=sys.stderr) + print(f"Error reading foundry.toml: {str(e)}", file=sys.stderr) else: - print('No foundry.toml found', file=sys.stderr) + print("No foundry.toml found", file=sys.stderr) # If we get here, no library address was found - print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) - return '' + print("WARNING: Assuming this contract uses no libraries", file=sys.stderr) + return "" @retry_with_backoff(max_retries=2, base_delay=1) @@ -60,19 +61,16 @@ def get_action_address(spell_address: str) -> Optional[str]: """Get the action contract address from the spell contract with retry mechanism.""" try: result = subprocess.run( - ['cast', 'call', spell_address, 'action()(address)'], + ["cast", "call", spell_address, "action()(address)"], capture_output=True, text=True, check=True, - env=os.environ | { - 'ETH_GAS_PRICE': '0', - 'ETH_PRIO_FEE': '0' - } + env=os.environ | {"ETH_GAS_PRICE": "0", "ETH_PRIO_FEE": "0"}, ) return result.stdout.strip() except subprocess.CalledProcessError as e: - print(f'Error getting action address: {str(e)}', file=sys.stderr) + print(f"Error getting action address: {str(e)}", file=sys.stderr) return None except Exception as e: - print(f'Unexpected error getting action address: {str(e)}', file=sys.stderr) + print(f"Unexpected error getting action address: {str(e)}", file=sys.stderr) return None diff --git a/scripts/verification/retry.py b/scripts/verification/retry.py index 722e46f1..d0e12e10 100644 --- a/scripts/verification/retry.py +++ b/scripts/verification/retry.py @@ -2,12 +2,11 @@ """ Retry decorator with exponential backoff and jitter for robust API calls. """ -import time import random import sys -from typing import Tuple, Callable +import time from functools import wraps - +from typing import Callable, Tuple # Default retry configuration DEFAULT_MAX_RETRIES = 3 @@ -23,11 +22,11 @@ def retry_with_backoff( max_delay: float = DEFAULT_MAX_DELAY, backoff_factor: float = DEFAULT_BACKOFF_FACTOR, jitter: float = DEFAULT_JITTER, - exceptions: Tuple[Exception, ...] = (Exception,) + exceptions: Tuple[Exception, ...] = (Exception,), ): """ Decorator that implements exponential backoff with jitter for retrying functions. - + Args: max_retries: Maximum number of retry attempts base_delay: Initial delay between retries in seconds @@ -35,35 +34,44 @@ def retry_with_backoff( backoff_factor: Multiplier for exponential backoff jitter: Random variation factor (0.1 = 10% variation) exceptions: Tuple of exceptions to catch and retry on - + Returns: Decorated function with retry logic """ + def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): last_exception = None - + for attempt in range(max_retries + 1): try: return func(*args, **kwargs) except exceptions as e: last_exception = e - + if attempt == max_retries: - print(f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", file=sys.stderr) + print( + f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", + file=sys.stderr, + ) raise - + # Calculate delay with exponential backoff and jitter - delay = min(base_delay * (backoff_factor ** attempt), max_delay) + delay = min(base_delay * (backoff_factor**attempt), max_delay) jitter_amount = delay * jitter * random.uniform(-1, 1) actual_delay = max(0, delay + jitter_amount) - + print(f"Attempt {attempt + 1} failed: {str(e)}", file=sys.stderr) - print(f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", file=sys.stderr) - + print( + f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", + file=sys.stderr, + ) + time.sleep(actual_delay) - + raise last_exception + return wrapper + return decorator diff --git a/scripts/verification/retry_test.py b/scripts/verification/retry_test.py index 3ca2bce8..fe90850e 100644 --- a/scripts/verification/retry_test.py +++ b/scripts/verification/retry_test.py @@ -11,89 +11,92 @@ class TestRetryMechanism(unittest.TestCase): """Test cases for the retry mechanism.""" - + def setUp(self): """Set up test fixtures.""" self.call_count = 0 - + def test_successful_retry(self): """Test that retry mechanism works when function eventually succeeds.""" + @retry_with_backoff(max_retries=3, base_delay=0.1) def test_function(): self.call_count += 1 if self.call_count < 3: raise Exception(f"Simulated failure #{self.call_count}") return "Success!" - + result = test_function() self.assertEqual(result, "Success!") self.assertEqual(self.call_count, 3) - + def test_max_retries_exceeded(self): """Test that retry mechanism fails after max retries.""" + @retry_with_backoff(max_retries=2, base_delay=0.1) def test_function(): self.call_count += 1 raise Exception(f"Persistent failure #{self.call_count}") - + with self.assertRaises(Exception) as context: test_function() - + self.assertIn("Persistent failure #3", str(context.exception)) self.assertEqual(self.call_count, 3) # 2 retries + 1 initial attempt - + def test_no_retry_on_success(self): """Test that successful function doesn't retry.""" + @retry_with_backoff(max_retries=3, base_delay=0.1) def test_function(): self.call_count += 1 return "Success!" - + result = test_function() self.assertEqual(result, "Success!") self.assertEqual(self.call_count, 1) # Only one call, no retries - + def test_exponential_backoff(self): """Test that delays increase exponentially.""" delays = [] - + @retry_with_backoff(max_retries=3, base_delay=1, max_delay=10) def test_function(): delays.append(time.time()) raise Exception("Test failure") - - start_time = time.time() + with self.assertRaises(Exception): test_function() - + # Check that delays are increasing (with some tolerance for jitter) if len(delays) >= 3: delay1 = delays[1] - delays[0] delay2 = delays[2] - delays[1] self.assertGreater(delay2, delay1 * 1.5) # Should be roughly 2x with jitter - + def test_jitter_variation(self): """Test that jitter adds random variation to delays.""" delays = [] - + @retry_with_backoff(max_retries=2, base_delay=1, jitter=0.2) def test_function(): delays.append(time.time()) raise Exception("Test failure") - + with self.assertRaises(Exception): test_function() - + # With jitter, delays should not be exactly the same if len(delays) >= 2: delay1 = delays[1] - delays[0] delay2 = delays[2] - delays[1] # Delays should be different due to jitter self.assertNotEqual(delay1, delay2) - - @patch('time.sleep') + + @patch("time.sleep") def test_specific_exception_handling(self, mock_sleep): """Test that only specified exceptions trigger retries.""" + @retry_with_backoff(max_retries=2, base_delay=0.1, exceptions=(ValueError,)) def test_function(): self.call_count += 1 @@ -102,10 +105,10 @@ def test_function(): elif self.call_count == 2: raise TypeError("Type error") # Should not retry return "Success!" - + with self.assertRaises(TypeError): test_function() - + # Should only retry once (for ValueError), then fail on TypeError self.assertEqual(self.call_count, 2) mock_sleep.assert_called_once() # Only one retry attempt @@ -113,32 +116,38 @@ def test_function(): class TestRetryIntegration(unittest.TestCase): """Integration tests for the retry mechanism.""" - + def test_retry_with_network_simulation(self): """Test retry mechanism with simulated network failures.""" failures = [True, True, False] # Fail twice, succeed on third attempt - + @retry_with_backoff(max_retries=3, base_delay=0.1) def simulate_network_call(): if failures.pop(0): raise ConnectionError("Network timeout") return "Network response" - + result = simulate_network_call() self.assertEqual(result, "Network response") self.assertEqual(len(failures), 0) # All failures consumed - + def test_retry_with_different_exception_types(self): """Test retry mechanism with different types of exceptions.""" - exceptions = [ValueError("Bad value"), ConnectionError("Network error"), "Success"] - - @retry_with_backoff(max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError)) + exceptions = [ + ValueError("Bad value"), + ConnectionError("Network error"), + "Success", + ] + + @retry_with_backoff( + max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError) + ) def test_function(): exception = exceptions.pop(0) if isinstance(exception, Exception): raise exception return exception - + result = test_function() self.assertEqual(result, "Success") self.assertEqual(len(exceptions), 0) diff --git a/scripts/verification/verifier_etherscan.py b/scripts/verification/verifier_etherscan.py index 48a0907f..45b8db3c 100644 --- a/scripts/verification/verifier_etherscan.py +++ b/scripts/verification/verifier_etherscan.py @@ -2,60 +2,65 @@ """ Etherscan block explorer verifier implementation using forge verify-contract. """ -import sys import subprocess +import sys from .retry import retry_with_backoff # Block explorer configurations -CHAIN_ID = '1' # Mainnet only +CHAIN_ID = "1" # Mainnet only class VerifierEtherscan: """Etherscan block explorer verifier using forge verify-contract.""" - + def __init__(self, api_key: str, chain_id: str): self.api_key = api_key self.chain_id = chain_id - + def is_available(self) -> bool: """Check if Etherscan supports this chain.""" return self.chain_id == CHAIN_ID - + def get_verification_url(self, contract_address: str) -> str: """Get Etherscan URL for the verified contract.""" return f"https://etherscan.io/address/{contract_address}#code" - + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) def verify_contract( self, contract_name: str, contract_address: str, constructor_args: str, - library_address: str = "" + library_address: str = "", ) -> bool: """Verify contract on Etherscan using forge verify-contract.""" - print(f'\nVerifying {contract_name} at {contract_address} on Etherscan...') - + print(f"\nVerifying {contract_name} at {contract_address} on Etherscan...") + # Build forge verify-contract command cmd = [ - 'forge', 'verify-contract', + "forge", + "verify-contract", contract_address, - f'src/DssSpell.sol:{contract_name}', - '--verifier', 'etherscan', - '--etherscan-api-key', self.api_key, - '--flatten', - '--watch' + f"src/DssSpell.sol:{contract_name}", + "--verifier", + "etherscan", + "--etherscan-api-key", + self.api_key, + "--flatten", + "--watch", ] - + # Add constructor arguments if provided if constructor_args: - cmd.extend(['--constructor-args', constructor_args]) - + cmd.extend(["--constructor-args", constructor_args]) + # Add library linking if provided if library_address: - cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - + cmd.extend( + ["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"] + ) + try: subprocess.run( cmd, @@ -63,16 +68,21 @@ def verify_contract( text=True, check=True, ) - - print(f'✓ Contract verified successfully at {self.get_verification_url(contract_address)}') + + print( + f"✓ Contract verified successfully at {self.get_verification_url(contract_address)}" + ) return True - + except subprocess.CalledProcessError as e: # Check if it's already verified - if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): - print('✓ Contract is already verified on Etherscan') + if ( + "already verified" in e.stderr.lower() + or "already verified" in e.stdout.lower() + ): + print("✓ Contract is already verified on Etherscan") return True - + print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) return False except Exception as e: diff --git a/scripts/verification/verifier_sourcify.py b/scripts/verification/verifier_sourcify.py index eee66b1b..7e2f6684 100644 --- a/scripts/verification/verifier_sourcify.py +++ b/scripts/verification/verifier_sourcify.py @@ -2,57 +2,62 @@ """ Sourcify block explorer verifier implementation using forge verify-contract. """ -import sys import subprocess +import sys from .retry import retry_with_backoff # Block explorer configurations -CHAIN_ID = '1' # Mainnet only +CHAIN_ID = "1" # Mainnet only + class VerifierSourcify: """Sourcify block explorer verifier using forge verify-contract.""" - + def __init__(self, chain_id: str): self.chain_id = chain_id - + def is_available(self) -> bool: """Check if Sourcify supports this chain.""" return self.chain_id == CHAIN_ID - + def get_verification_url(self, contract_address: str) -> str: """Get Sourcify URL for the verified contract.""" return f"https://sourcify.dev/#/lookup/{contract_address}" - + @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) def verify_contract( self, contract_name: str, contract_address: str, constructor_args: str, - library_address: str = "" + library_address: str = "", ) -> bool: """Verify contract on Sourcify using forge verify-contract.""" - print(f'\nVerifying {contract_name} at {contract_address} on Sourcify...') - + print(f"\nVerifying {contract_name} at {contract_address} on Sourcify...") + # Build forge verify-contract command cmd = [ - 'forge', 'verify-contract', + "forge", + "verify-contract", contract_address, - f'src/DssSpell.sol:{contract_name}', - '--verifier', 'sourcify', - '--flatten', - '--watch' + f"src/DssSpell.sol:{contract_name}", + "--verifier", + "sourcify", + "--flatten", + "--watch", ] - + # Add constructor arguments if provided if constructor_args: - cmd.extend(['--constructor-args', constructor_args]) - + cmd.extend(["--constructor-args", constructor_args]) + # Add library linking if provided if library_address: - cmd.extend(['--libraries', f'src/DssExecLib.sol:DssExecLib:{library_address}']) - + cmd.extend( + ["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"] + ) + try: subprocess.run( cmd, @@ -60,16 +65,21 @@ def verify_contract( text=True, check=True, ) - - print(f'✓ Contract verified successfully at {self.get_verification_url(contract_address)}') + + print( + f"✓ Contract verified successfully at {self.get_verification_url(contract_address)}" + ) return True - + except subprocess.CalledProcessError as e: # Check if it's already verified - if 'already verified' in e.stderr.lower() or 'already verified' in e.stdout.lower(): - print('✓ Contract is already verified on Sourcify') + if ( + "already verified" in e.stderr.lower() + or "already verified" in e.stdout.lower() + ): + print("✓ Contract is already verified on Sourcify") return True - + print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) return False except Exception as e: From 64f0aab0bdc26a6c4a0b3b8ca40f3d54c536885e Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:09:42 -0300 Subject: [PATCH 12/16] refactor: :recycle: move verify.py into verification folder --- scripts/{ => verification}/verify.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) rename scripts/{ => verification}/verify.py (94%) mode change 100755 => 100644 diff --git a/scripts/verify.py b/scripts/verification/verify.py old mode 100755 new mode 100644 similarity index 94% rename from scripts/verify.py rename to scripts/verification/verify.py index 6293c747..22793829 --- a/scripts/verify.py +++ b/scripts/verification/verify.py @@ -10,19 +10,8 @@ from typing import Any, Tuple, List -def add_project_root_to_path(): - """Add the project root directory to Python's module search path.""" - project_root = Path(__file__).parent.parent.resolve() - if str(project_root) not in sys.path: - sys.path.append(str(project_root)) - - -# Add the project root to the Python path for imports -add_project_root_to_path() - # Import verifiers and contract data utilities from the verification package - -from scripts.verification import ( +from . import ( VerifierEtherscan, VerifierSourcify, get_chain_id, @@ -207,3 +196,5 @@ def main(): if __name__ == "__main__": main() + + From d1eeef354896ebb1a6c6eaf3feae876cfe9d5cc0 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:42:39 +0100 Subject: [PATCH 13/16] refactor: :fire: remove unnecessary verifier modules --- scripts/verification/verify.py | 212 +++++++++++++++++++-------------- 1 file changed, 123 insertions(+), 89 deletions(-) diff --git a/scripts/verification/verify.py b/scripts/verification/verify.py index 22793829..3eaf11b1 100644 --- a/scripts/verification/verify.py +++ b/scripts/verification/verify.py @@ -6,18 +6,10 @@ """ import os import sys -from pathlib import Path -from typing import Any, Tuple, List +import subprocess +from typing import Tuple, List - -# Import verifiers and contract data utilities from the verification package -from . import ( - VerifierEtherscan, - VerifierSourcify, - get_chain_id, - get_library_address, - get_action_address -) +from . import get_chain_id, get_library_address, get_action_address # Constants SOURCE_FILE_PATH = 'src/DssSpell.sol' @@ -53,83 +45,134 @@ def parse_command_line_args() -> Tuple[str, str, str]: return contract_name, contract_address, constructor_args -def setup_verifiers(chain_id: str) -> List[Any]: - """Setup available verifiers for the given chain.""" - verifiers = [] - - # Setup Etherscan verifier +def build_forge_cmd( + verifier: str, + address: str, + contract_name: str, + constructor_args: str, + library_address: str, + retries: int, + delay: int, + etherscan_api_key: str = "", +) -> List[str]: + cmd: List[str] = [ + "forge", + "verify-contract", + address, + f"{SOURCE_FILE_PATH}:{contract_name}", + "--verifier", + verifier, + "--flatten", + "--watch", + "--retries", + str(retries), + "--delay", + str(delay), + ] + + if constructor_args: + cmd.extend(["--constructor-args", constructor_args]) + + if library_address: + cmd.extend(["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"]) + + if verifier == "etherscan" and etherscan_api_key: + cmd.extend(["--etherscan-api-key", etherscan_api_key]) + + return cmd + + +def verify_once_on( + verifier: str, + address: str, + contract_name: str, + constructor_args: str, + library_address: str, + retries: int, + delay: int, + etherscan_api_key: str = "", +) -> bool: + cmd = build_forge_cmd( + verifier=verifier, + address=address, + contract_name=contract_name, + constructor_args=constructor_args, + library_address=library_address, + retries=retries, + delay=delay, + etherscan_api_key=etherscan_api_key, + ) + + print(f"\nVerifying {contract_name} at {address} on {verifier}...") try: - etherscan_api_key = get_env_var( - 'ETHERSCAN_API_KEY', - "Etherscan API key not found. Set ETHERSCAN_API_KEY environment variable." + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, ) - etherscan_verifier = VerifierEtherscan(etherscan_api_key, chain_id) - if etherscan_verifier.is_available(): - verifiers.append(etherscan_verifier) - print(f"✓ Etherscan verifier available for chain {chain_id}") - else: - print(f"✗ Etherscan verifier not available for chain {chain_id}") - except Exception as e: - print(f"✗ Failed to setup Etherscan verifier: {str(e)}", file=sys.stderr) - - # Setup Sourcify verifier - # Note: Blockscout automatically picks up any code verification from Sourcify - try: - sourcify_verifier = VerifierSourcify(chain_id) - if sourcify_verifier.is_available(): - verifiers.append(sourcify_verifier) - print(f"✓ Sourcify verifier available for chain {chain_id}") - else: - print(f"✗ Sourcify verifier not available for chain {chain_id}") - except Exception as e: - print(f"✗ Failed to setup Sourcify verifier: {str(e)}", file=sys.stderr) - - if not verifiers: - raise Exception("No verifiers available for the current chain") - - return verifiers + # forge prints useful info; surface stdout + if result.stdout: + print(result.stdout.strip()) + print(f"✓ {verifier} verification OK") + return True + except subprocess.CalledProcessError as e: + combined = (e.stdout or "") + "\n" + (e.stderr or "") + if "already verified" in combined.lower(): + print(f"✓ {verifier}: already verified") + return True + print(f"✗ {verifier} verification failed\n{combined}", file=sys.stderr) + return False def verify_contract_with_verifiers( contract_name: str, contract_address: str, constructor_args: str, - library_address: str, - verifiers: List[Any] + library_address: str ) -> bool: - """Verify contract using multiple verifiers with fallback.""" - print(f'\nVerifying {contract_name} at {contract_address}...') - - successful_verifications = 0 - total_verifiers = len(verifiers) - - for i, verifier in enumerate(verifiers): - print(f"\n--- Attempting verification with {verifier.__class__.__name__} ({i+1}/{total_verifiers}) ---") - - try: - success = verifier.verify_contract( - contract_name=contract_name, - contract_address=contract_address, - constructor_args=constructor_args, - library_address=library_address, - ) - - if success: - successful_verifications += 1 - - except Exception as e: - print(f"✗ Error during verification with {verifier.__class__.__name__}: {str(e)}", file=sys.stderr) - - # Report final results after trying all verifiers - if successful_verifications == 0: - print(f"\n❌ Failed to verify contract on all verifiers ({total_verifiers} attempted)") - return False - elif successful_verifications < total_verifiers: - print(f"\n⚠️ Contract verified successfully in only {successful_verifications}/{total_verifiers} verifiers!") - return True + """Verify contract by issuing forge commands per explorer.""" + # Configure retries/delay via env or defaults + retries = int(os.environ.get("VERIFY_RETRIES", "5")) + delay = int(os.environ.get("VERIFY_DELAY", "5")) + + chain_id = get_chain_id() + etherscan_api_key = os.environ.get("ETHERSCAN_API_KEY", "") + + successes = 0 + + # Sourcify (works without API key); blockscout pulls from it + if chain_id == "1": + if verify_once_on( + verifier="sourcify", + address=contract_address, + contract_name=contract_name, + constructor_args=constructor_args, + library_address=library_address, + retries=retries, + delay=delay, + ): + successes += 1 else: - print(f"\n🎉 Contract verified successfully in {total_verifiers} verifiers!") - return True + print(f"Sourcify not configured for CHAIN_ID {chain_id}, skipping.") + + # Etherscan (requires API key) + if chain_id == "1" and etherscan_api_key: + if verify_once_on( + verifier="etherscan", + address=contract_address, + contract_name=contract_name, + constructor_args=constructor_args, + library_address=library_address, + retries=retries, + delay=delay, + etherscan_api_key=etherscan_api_key, + ): + successes += 1 + elif chain_id == "1": + print("ETHERSCAN_API_KEY not set; skipping Etherscan.") + + return successes > 0 def main(): @@ -146,23 +189,15 @@ def main(): # Parse command line arguments spell_name, spell_address, constructor_args = parse_command_line_args() - # Get chain ID - chain_id = get_chain_id() - # Get library address library_address = get_library_address() - # Setup verifiers - print("Setting up verifiers...") - verifiers = setup_verifiers(chain_id) - # Verify spell contract spell_success = verify_contract_with_verifiers( contract_name=spell_name, contract_address=spell_address, constructor_args=constructor_args, - library_address=library_address, - verifiers=verifiers + library_address=library_address ) if not spell_success: @@ -179,8 +214,7 @@ def main(): contract_name="DssSpellAction", contract_address=action_address, constructor_args=constructor_args, - library_address=library_address, - verifiers=verifiers + library_address=library_address ) if not action_success: From 219a3335734112c367b259e2cf847880f8adcbb3 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:44:22 +0100 Subject: [PATCH 14/16] docs: :memo: update verification README.md --- scripts/verification/README.md | 204 ++++----------------------------- 1 file changed, 24 insertions(+), 180 deletions(-) diff --git a/scripts/verification/README.md b/scripts/verification/README.md index 9db5e635..a9445d18 100644 --- a/scripts/verification/README.md +++ b/scripts/verification/README.md @@ -1,195 +1,39 @@ -# Enhanced Contract Verification System +# Contract Verification -The `verify.py` script has been enhanced to provide robust retry mechanisms and support for multiple block explorers while maintaining the same simple interface. - -## Overview - -The verification system has been enhanced to address the following requirements: - -1. **Multiple Block Explorer Support**: Verify contracts on at least 2 well-known block explorers -2. **Robust Retry Mechanisms**: Implement proper retry logic with exponential backoff and jitter -3. **Fallback Options**: If one verifier fails, automatically try others -4. **Backward Compatibility**: Maintain compatibility with existing Makefile commands - -## Key Features - -### **Retry Logic with Exponential Backoff** -```python -@retry_with_backoff(max_retries=3, base_delay=2, max_delay=60) -def api_request(): - # Automatic retry with exponential backoff + jitter -``` - -### **Multi-Verifier Support** -- **Etherscan**: Primary verifier with full API support -- **Sourcify**: Secondary verifier (no API key required) -- **Automatic Fallback**: If one fails, tries the next - -### **Smart Error Handling** -- Graceful degradation when verifiers are unavailable -- Detailed logging of all attempts and failures -- Automatic retry for network issues and temporary failures +Minimal verification wrapper that shells out to `forge verify-contract` per explorer, using Foundry's built-in retries and delays. ## Usage -### **Standard Usage (Unchanged)** -```bash -make verify addr=0x1234567890123456789012345678901234567890 -``` - -### **Direct Script Usage** ```bash -./scripts/verify.py DssSpell 0x1234567890123456789012345678901234567890 -``` - -## Configuration - -### **Environment Variables** -- `ETHERSCAN_API_KEY`: Required for Etherscan verification -- `ETH_RPC_URL`: Required for chain operations +# from repo root +export ETH_RPC_URL="https://..." +# optional for Etherscan +export ETHERSCAN_API_KEY="..." -### **Retry Settings** -- **Max Retries**: 3 attempts -- **Base Delay**: 2 seconds -- **Max Delay**: 60 seconds -- **Backoff Factor**: 2 (exponential) -- **Jitter**: 10% random variation to prevent thundering herd problems +# optional overrides (defaults: 5) +export VERIFY_RETRIES=5 +export VERIFY_DELAY=5 -## Supported Block Explorers - -### Etherscan -- **API**: Etherscan API v2 -- **Requirements**: API key -- **Features**: Full verification with constructor arguments and libraries - -### Sourcify -- **API**: Sourcify API -- **Requirements**: No API key required -- **Features**: Open-source verification service - -## Retry Mechanisms - -### **Exponential Backoff with Jitter** - -The system implements exponential backoff with jitter to prevent thundering herd problems: - -```python -delay = min(base_delay * (backoff_factor ** attempt), max_delay) -jitter_amount = delay * jitter * random.uniform(-1, 1) -actual_delay = max(0, delay + jitter_amount) +python -m scripts.verification.verify DssSpell 0xYourSpellAddress [constructorArgs] ``` -### **Retry Scenarios** - -1. **API Request Failures**: Network timeouts, HTTP errors, JSON parsing errors -2. **Contract Not Found**: Retry when contract is not yet deployed -3. **Verification Pending**: Poll for verification completion -4. **Subprocess Failures**: Forge flatten, cast commands +This verifies: +- The Spell contract you pass (e.g., `DssSpell`) +- The associated `DssSpellAction` via `action()` lookup -### **Error Handling** +## Explorers +- Sourcify: used on mainnet; no API key needed +- Etherscan: used on mainnet when `ETHERSCAN_API_KEY` is set -- **Graceful Degradation**: If one verifier fails, others are still attempted -- **Detailed Logging**: All errors and retry attempts are logged -- **Fallback Strategy**: Try all available verifiers until one succeeds - -## Benefits - -1. **Reliability**: Multiple verifiers reduce single points of failure -2. **Resilience**: Retry mechanisms handle temporary network issues -3. **Transparency**: Detailed logging shows exactly what's happening -4. **Simplicity**: Single script with enhanced functionality -5. **Compatibility**: Existing workflows continue to work unchanged - -## Testing - -### **Running Tests** - -The test suite uses relative imports and must be run as a module: +## Notes +- Libraries: if `DssExecLib` is configured in `foundry.toml`, it is linked automatically via `--libraries`. +- Retries & delay: handled by `forge verify-contract` flags (`--retries`, `--delay`) per Foundry docs ([forge verify-contract](https://getfoundry.sh/forge/reference/verify-contract#forge-verify-contract)). +## Examples ```bash -# ✅ Correct way - run as module -python3 -m scripts.verification.retry_test - -# ❌ Incorrect way - direct execution fails -python3 scripts/verification/retry_test.py -``` +# Mainnet spell, with Etherscan +ETHERSCAN_API_KEY=... python -m scripts.verification.verify DssSpell 0xabc...def -**Why?** The test file uses relative imports (`from .retry import retry_with_backoff`) which only work when the file is run as part of a package, not as a standalone script. - -### **Test Coverage** - -The test suite covers: -- Successful retry scenarios -- Maximum retry limit handling -- Exponential backoff behavior -- Jitter variation -- Exception-specific retry logic -- Network simulation scenarios - -## Troubleshooting - -### **Common Issues** - -1. **"No verifiers available"**: Check chain ID support and API keys -2. **"Verification failed on all verifiers"**: Check contract deployment and source code -3. **"Etherscan API key not found"**: Set `ETHERSCAN_API_KEY` environment variable -4. **"ImportError: attempted relative import with no known parent package"**: Run tests as a module using `python3 -m scripts.verification.retry_test` - -### **Debug Mode** - -For detailed debugging, you can run the script directly and observe the output: - -```bash -./scripts/verify.py DssSpell 0x1234567890123456789012345678901234567890 +# Custom retries/delay +VERIFY_RETRIES=10 VERIFY_DELAY=8 python -m scripts.verification.verify DssSpell 0xabc...def ``` - -### **Log Files** - -Failed verifications create log files with the source code for debugging: - -- `verify-etherscan-{timestamp}.log` - -## Implementation Details - -### **Verifier Classes** - -The script includes two verifier classes: - -- **`EtherscanVerifier`**: Handles Etherscan API verification -- **`SourcifyVerifier`**: Handles Sourcify API verification - -Both classes implement the same interface and can be easily extended. - -### **Retry Decorator** - -The `@retry_with_backoff` decorator provides automatic retry functionality with: - -- Configurable retry counts and delays -- Exponential backoff with jitter -- Exception filtering -- Detailed logging - -### **Multi-Verifier Logic** - -The script automatically: - -1. Sets up all available verifiers for the current chain -2. Attempts verification with each verifier in sequence -3. Stops on first successful verification -4. Provides detailed feedback on all attempts - -## Future Enhancements - -1. **Configurable Retry**: Allow retry parameters via environment variables -2. **Parallel Verification**: Verify on multiple explorers simultaneously -3. **Verification Status**: Check if contract is already verified before attempting -4. **Custom Verifiers**: Allow custom verifier implementations - -## Contributing - -To add a new block explorer verifier: - -1. Create a new verifier class following the existing pattern -2. Implement the required methods (`verify_contract`, `is_available`, etc.) -3. Add the verifier to the `setup_verifiers` function -4. Test with different chains and scenarios From 456e91cafc0b4d860eb975d1d3c6396e38d1b0e6 Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:45:50 +0100 Subject: [PATCH 15/16] refactor: :fire: remove unnecessary retry logic from contract data script --- scripts/verification/__init__.py | 4 - scripts/verification/contract_data.py | 8 +- scripts/verification/retry.py | 77 ---------- scripts/verification/retry_test.py | 158 --------------------- scripts/verification/verifier_etherscan.py | 90 ------------ scripts/verification/verifier_sourcify.py | 87 ------------ 6 files changed, 2 insertions(+), 422 deletions(-) delete mode 100644 scripts/verification/retry.py delete mode 100644 scripts/verification/retry_test.py delete mode 100644 scripts/verification/verifier_etherscan.py delete mode 100644 scripts/verification/verifier_sourcify.py diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index 46ded63f..c9898f40 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -4,12 +4,8 @@ """ from .contract_data import get_action_address, get_chain_id, get_library_address -from .verifier_etherscan import VerifierEtherscan -from .verifier_sourcify import VerifierSourcify __all__ = [ - "VerifierEtherscan", - "VerifierSourcify", "get_chain_id", "get_library_address", "get_action_address", diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index f6d5a039..72a2ebdb 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -10,16 +10,13 @@ import sys from typing import Optional -from .retry import retry_with_backoff - # Constants SOURCE_FILE_PATH = "src/DssSpell.sol" LIBRARY_NAME = "DssExecLib" -@retry_with_backoff(max_retries=2, base_delay=1) def get_chain_id() -> str: - """Get the current chain ID with retry mechanism.""" + """Get the current chain ID.""" print("Obtaining chain ID... ") result = subprocess.run( ["cast", "chain-id"], capture_output=True, text=True, check=True @@ -56,9 +53,8 @@ def get_library_address() -> str: return "" -@retry_with_backoff(max_retries=2, base_delay=1) def get_action_address(spell_address: str) -> Optional[str]: - """Get the action contract address from the spell contract with retry mechanism.""" + """Get the action contract address from the spell contract.""" try: result = subprocess.run( ["cast", "call", spell_address, "action()(address)"], diff --git a/scripts/verification/retry.py b/scripts/verification/retry.py deleted file mode 100644 index d0e12e10..00000000 --- a/scripts/verification/retry.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -""" -Retry decorator with exponential backoff and jitter for robust API calls. -""" -import random -import sys -import time -from functools import wraps -from typing import Callable, Tuple - -# Default retry configuration -DEFAULT_MAX_RETRIES = 3 -DEFAULT_BASE_DELAY = 2 # seconds -DEFAULT_MAX_DELAY = 60 # seconds -DEFAULT_BACKOFF_FACTOR = 2 -DEFAULT_JITTER = 0.1 # 10% jitter - - -def retry_with_backoff( - max_retries: int = DEFAULT_MAX_RETRIES, - base_delay: float = DEFAULT_BASE_DELAY, - max_delay: float = DEFAULT_MAX_DELAY, - backoff_factor: float = DEFAULT_BACKOFF_FACTOR, - jitter: float = DEFAULT_JITTER, - exceptions: Tuple[Exception, ...] = (Exception,), -): - """ - Decorator that implements exponential backoff with jitter for retrying functions. - - Args: - max_retries: Maximum number of retry attempts - base_delay: Initial delay between retries in seconds - max_delay: Maximum delay between retries in seconds - backoff_factor: Multiplier for exponential backoff - jitter: Random variation factor (0.1 = 10% variation) - exceptions: Tuple of exceptions to catch and retry on - - Returns: - Decorated function with retry logic - """ - - def decorator(func: Callable) -> Callable: - @wraps(func) - def wrapper(*args, **kwargs): - last_exception = None - - for attempt in range(max_retries + 1): - try: - return func(*args, **kwargs) - except exceptions as e: - last_exception = e - - if attempt == max_retries: - print( - f"Failed after {max_retries + 1} attempts. Last error: {str(e)}", - file=sys.stderr, - ) - raise - - # Calculate delay with exponential backoff and jitter - delay = min(base_delay * (backoff_factor**attempt), max_delay) - jitter_amount = delay * jitter * random.uniform(-1, 1) - actual_delay = max(0, delay + jitter_amount) - - print(f"Attempt {attempt + 1} failed: {str(e)}", file=sys.stderr) - print( - f"Retrying in {actual_delay:.2f} seconds... (attempt {attempt + 2}/{max_retries + 1})", - file=sys.stderr, - ) - - time.sleep(actual_delay) - - raise last_exception - - return wrapper - - return decorator diff --git a/scripts/verification/retry_test.py b/scripts/verification/retry_test.py deleted file mode 100644 index fe90850e..00000000 --- a/scripts/verification/retry_test.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -""" -Test suite for the retry mechanism with exponential backoff and jitter. -""" -import time -import unittest -from unittest.mock import patch - -from .retry import retry_with_backoff - - -class TestRetryMechanism(unittest.TestCase): - """Test cases for the retry mechanism.""" - - def setUp(self): - """Set up test fixtures.""" - self.call_count = 0 - - def test_successful_retry(self): - """Test that retry mechanism works when function eventually succeeds.""" - - @retry_with_backoff(max_retries=3, base_delay=0.1) - def test_function(): - self.call_count += 1 - if self.call_count < 3: - raise Exception(f"Simulated failure #{self.call_count}") - return "Success!" - - result = test_function() - self.assertEqual(result, "Success!") - self.assertEqual(self.call_count, 3) - - def test_max_retries_exceeded(self): - """Test that retry mechanism fails after max retries.""" - - @retry_with_backoff(max_retries=2, base_delay=0.1) - def test_function(): - self.call_count += 1 - raise Exception(f"Persistent failure #{self.call_count}") - - with self.assertRaises(Exception) as context: - test_function() - - self.assertIn("Persistent failure #3", str(context.exception)) - self.assertEqual(self.call_count, 3) # 2 retries + 1 initial attempt - - def test_no_retry_on_success(self): - """Test that successful function doesn't retry.""" - - @retry_with_backoff(max_retries=3, base_delay=0.1) - def test_function(): - self.call_count += 1 - return "Success!" - - result = test_function() - self.assertEqual(result, "Success!") - self.assertEqual(self.call_count, 1) # Only one call, no retries - - def test_exponential_backoff(self): - """Test that delays increase exponentially.""" - delays = [] - - @retry_with_backoff(max_retries=3, base_delay=1, max_delay=10) - def test_function(): - delays.append(time.time()) - raise Exception("Test failure") - - with self.assertRaises(Exception): - test_function() - - # Check that delays are increasing (with some tolerance for jitter) - if len(delays) >= 3: - delay1 = delays[1] - delays[0] - delay2 = delays[2] - delays[1] - self.assertGreater(delay2, delay1 * 1.5) # Should be roughly 2x with jitter - - def test_jitter_variation(self): - """Test that jitter adds random variation to delays.""" - delays = [] - - @retry_with_backoff(max_retries=2, base_delay=1, jitter=0.2) - def test_function(): - delays.append(time.time()) - raise Exception("Test failure") - - with self.assertRaises(Exception): - test_function() - - # With jitter, delays should not be exactly the same - if len(delays) >= 2: - delay1 = delays[1] - delays[0] - delay2 = delays[2] - delays[1] - # Delays should be different due to jitter - self.assertNotEqual(delay1, delay2) - - @patch("time.sleep") - def test_specific_exception_handling(self, mock_sleep): - """Test that only specified exceptions trigger retries.""" - - @retry_with_backoff(max_retries=2, base_delay=0.1, exceptions=(ValueError,)) - def test_function(): - self.call_count += 1 - if self.call_count == 1: - raise ValueError("Value error") - elif self.call_count == 2: - raise TypeError("Type error") # Should not retry - return "Success!" - - with self.assertRaises(TypeError): - test_function() - - # Should only retry once (for ValueError), then fail on TypeError - self.assertEqual(self.call_count, 2) - mock_sleep.assert_called_once() # Only one retry attempt - - -class TestRetryIntegration(unittest.TestCase): - """Integration tests for the retry mechanism.""" - - def test_retry_with_network_simulation(self): - """Test retry mechanism with simulated network failures.""" - failures = [True, True, False] # Fail twice, succeed on third attempt - - @retry_with_backoff(max_retries=3, base_delay=0.1) - def simulate_network_call(): - if failures.pop(0): - raise ConnectionError("Network timeout") - return "Network response" - - result = simulate_network_call() - self.assertEqual(result, "Network response") - self.assertEqual(len(failures), 0) # All failures consumed - - def test_retry_with_different_exception_types(self): - """Test retry mechanism with different types of exceptions.""" - exceptions = [ - ValueError("Bad value"), - ConnectionError("Network error"), - "Success", - ] - - @retry_with_backoff( - max_retries=3, base_delay=0.1, exceptions=(ValueError, ConnectionError) - ) - def test_function(): - exception = exceptions.pop(0) - if isinstance(exception, Exception): - raise exception - return exception - - result = test_function() - self.assertEqual(result, "Success") - self.assertEqual(len(exceptions), 0) - - -if __name__ == "__main__": - # Run the tests - unittest.main(verbosity=2) diff --git a/scripts/verification/verifier_etherscan.py b/scripts/verification/verifier_etherscan.py deleted file mode 100644 index 45b8db3c..00000000 --- a/scripts/verification/verifier_etherscan.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -""" -Etherscan block explorer verifier implementation using forge verify-contract. -""" -import subprocess -import sys - -from .retry import retry_with_backoff - -# Block explorer configurations -CHAIN_ID = "1" # Mainnet only - - -class VerifierEtherscan: - """Etherscan block explorer verifier using forge verify-contract.""" - - def __init__(self, api_key: str, chain_id: str): - self.api_key = api_key - self.chain_id = chain_id - - def is_available(self) -> bool: - """Check if Etherscan supports this chain.""" - return self.chain_id == CHAIN_ID - - def get_verification_url(self, contract_address: str) -> str: - """Get Etherscan URL for the verified contract.""" - return f"https://etherscan.io/address/{contract_address}#code" - - @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def verify_contract( - self, - contract_name: str, - contract_address: str, - constructor_args: str, - library_address: str = "", - ) -> bool: - """Verify contract on Etherscan using forge verify-contract.""" - print(f"\nVerifying {contract_name} at {contract_address} on Etherscan...") - - # Build forge verify-contract command - cmd = [ - "forge", - "verify-contract", - contract_address, - f"src/DssSpell.sol:{contract_name}", - "--verifier", - "etherscan", - "--etherscan-api-key", - self.api_key, - "--flatten", - "--watch", - ] - - # Add constructor arguments if provided - if constructor_args: - cmd.extend(["--constructor-args", constructor_args]) - - # Add library linking if provided - if library_address: - cmd.extend( - ["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"] - ) - - try: - subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - ) - - print( - f"✓ Contract verified successfully at {self.get_verification_url(contract_address)}" - ) - return True - - except subprocess.CalledProcessError as e: - # Check if it's already verified - if ( - "already verified" in e.stderr.lower() - or "already verified" in e.stdout.lower() - ): - print("✓ Contract is already verified on Etherscan") - return True - - print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) - return False - except Exception as e: - print(f"✗ Unexpected error during verification: {str(e)}", file=sys.stderr) - return False diff --git a/scripts/verification/verifier_sourcify.py b/scripts/verification/verifier_sourcify.py deleted file mode 100644 index 7e2f6684..00000000 --- a/scripts/verification/verifier_sourcify.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -""" -Sourcify block explorer verifier implementation using forge verify-contract. -""" -import subprocess -import sys - -from .retry import retry_with_backoff - -# Block explorer configurations -CHAIN_ID = "1" # Mainnet only - - -class VerifierSourcify: - """Sourcify block explorer verifier using forge verify-contract.""" - - def __init__(self, chain_id: str): - self.chain_id = chain_id - - def is_available(self) -> bool: - """Check if Sourcify supports this chain.""" - return self.chain_id == CHAIN_ID - - def get_verification_url(self, contract_address: str) -> str: - """Get Sourcify URL for the verified contract.""" - return f"https://sourcify.dev/#/lookup/{contract_address}" - - @retry_with_backoff(max_retries=3, base_delay=2, max_delay=30) - def verify_contract( - self, - contract_name: str, - contract_address: str, - constructor_args: str, - library_address: str = "", - ) -> bool: - """Verify contract on Sourcify using forge verify-contract.""" - print(f"\nVerifying {contract_name} at {contract_address} on Sourcify...") - - # Build forge verify-contract command - cmd = [ - "forge", - "verify-contract", - contract_address, - f"src/DssSpell.sol:{contract_name}", - "--verifier", - "sourcify", - "--flatten", - "--watch", - ] - - # Add constructor arguments if provided - if constructor_args: - cmd.extend(["--constructor-args", constructor_args]) - - # Add library linking if provided - if library_address: - cmd.extend( - ["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"] - ) - - try: - subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - ) - - print( - f"✓ Contract verified successfully at {self.get_verification_url(contract_address)}" - ) - return True - - except subprocess.CalledProcessError as e: - # Check if it's already verified - if ( - "already verified" in e.stderr.lower() - or "already verified" in e.stdout.lower() - ): - print("✓ Contract is already verified on Sourcify") - return True - - print(f"✗ Verification failed: {e.stderr}", file=sys.stderr) - return False - except Exception as e: - print(f"✗ Unexpected error during verification: {str(e)}", file=sys.stderr) - return False From 86e5688e383eae563a681792947e4e225757df4f Mon Sep 17 00:00:00 2001 From: 0xLaz3r <219995259+0xLaz3r@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:33:51 +0100 Subject: [PATCH 16/16] refactor: :fire: remove unnecessary library address and constructor args --- scripts/verification/__init__.py | 3 +- scripts/verification/contract_data.py | 28 ----------------- scripts/verification/verify.py | 43 +++++---------------------- 3 files changed, 8 insertions(+), 66 deletions(-) diff --git a/scripts/verification/__init__.py b/scripts/verification/__init__.py index c9898f40..958caad7 100644 --- a/scripts/verification/__init__.py +++ b/scripts/verification/__init__.py @@ -3,10 +3,9 @@ Verification package for Sky Protocol spells. """ -from .contract_data import get_action_address, get_chain_id, get_library_address +from .contract_data import get_action_address, get_chain_id __all__ = [ "get_chain_id", - "get_library_address", "get_action_address", ] diff --git a/scripts/verification/contract_data.py b/scripts/verification/contract_data.py index 72a2ebdb..bb3f830c 100644 --- a/scripts/verification/contract_data.py +++ b/scripts/verification/contract_data.py @@ -25,34 +25,6 @@ def get_chain_id() -> str: print(f"CHAIN_ID: {chain_id}") return chain_id - -def get_library_address() -> str: - """Find the DssExecLib address from foundry.toml.""" - library_address = "" - - # Try to read from foundry.toml libraries - if os.path.exists("foundry.toml"): - try: - with open("foundry.toml", "r") as f: - config = f.read() - - result = re.search(r":DssExecLib:(0x[0-9a-fA-F]{40})", config) - if result: - library_address = result.group(1) - print(f"Using library {LIBRARY_NAME} at address {library_address}") - return library_address - else: - print("No DssExecLib configured in foundry.toml", file=sys.stderr) - except Exception as e: - print(f"Error reading foundry.toml: {str(e)}", file=sys.stderr) - else: - print("No foundry.toml found", file=sys.stderr) - - # If we get here, no library address was found - print("WARNING: Assuming this contract uses no libraries", file=sys.stderr) - return "" - - def get_action_address(spell_address: str) -> Optional[str]: """Get the action contract address from the spell contract.""" try: diff --git a/scripts/verification/verify.py b/scripts/verification/verify.py index 3eaf11b1..539ca513 100644 --- a/scripts/verification/verify.py +++ b/scripts/verification/verify.py @@ -9,7 +9,7 @@ import subprocess from typing import Tuple, List -from . import get_chain_id, get_library_address, get_action_address +from . import get_chain_id, get_action_address # Constants SOURCE_FILE_PATH = 'src/DssSpell.sol' @@ -24,11 +24,11 @@ def get_env_var(var_name: str, error_message: str) -> str: sys.exit(1) -def parse_command_line_args() -> Tuple[str, str, str]: +def parse_command_line_args() -> Tuple[str, str]: """Parse command line arguments.""" - if len(sys.argv) not in [3, 4]: + if len(sys.argv) != 3: print("""usage: -./verify.py
[constructorArgs] +./verify.py
""", file=sys.stderr) sys.exit(1) @@ -38,19 +38,13 @@ def parse_command_line_args() -> Tuple[str, str, str]: if len(contract_address) != 42: sys.exit('Malformed address') - constructor_args = '' - if len(sys.argv) == 4: - constructor_args = sys.argv[3] - - return contract_name, contract_address, constructor_args + return contract_name, contract_address def build_forge_cmd( verifier: str, address: str, contract_name: str, - constructor_args: str, - library_address: str, retries: int, delay: int, etherscan_api_key: str = "", @@ -70,12 +64,6 @@ def build_forge_cmd( str(delay), ] - if constructor_args: - cmd.extend(["--constructor-args", constructor_args]) - - if library_address: - cmd.extend(["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"]) - if verifier == "etherscan" and etherscan_api_key: cmd.extend(["--etherscan-api-key", etherscan_api_key]) @@ -86,8 +74,6 @@ def verify_once_on( verifier: str, address: str, contract_name: str, - constructor_args: str, - library_address: str, retries: int, delay: int, etherscan_api_key: str = "", @@ -96,8 +82,6 @@ def verify_once_on( verifier=verifier, address=address, contract_name=contract_name, - constructor_args=constructor_args, - library_address=library_address, retries=retries, delay=delay, etherscan_api_key=etherscan_api_key, @@ -128,8 +112,6 @@ def verify_once_on( def verify_contract_with_verifiers( contract_name: str, contract_address: str, - constructor_args: str, - library_address: str ) -> bool: """Verify contract by issuing forge commands per explorer.""" # Configure retries/delay via env or defaults @@ -147,8 +129,6 @@ def verify_contract_with_verifiers( verifier="sourcify", address=contract_address, contract_name=contract_name, - constructor_args=constructor_args, - library_address=library_address, retries=retries, delay=delay, ): @@ -162,8 +142,6 @@ def verify_contract_with_verifiers( verifier="etherscan", address=contract_address, contract_name=contract_name, - constructor_args=constructor_args, - library_address=library_address, retries=retries, delay=delay, etherscan_api_key=etherscan_api_key, @@ -187,17 +165,12 @@ def main(): ) # Parse command line arguments - spell_name, spell_address, constructor_args = parse_command_line_args() - - # Get library address - library_address = get_library_address() + spell_name, spell_address = parse_command_line_args() # Verify spell contract spell_success = verify_contract_with_verifiers( contract_name=spell_name, contract_address=spell_address, - constructor_args=constructor_args, - library_address=library_address ) if not spell_success: @@ -213,8 +186,6 @@ def main(): action_success = verify_contract_with_verifiers( contract_name="DssSpellAction", contract_address=action_address, - constructor_args=constructor_args, - library_address=library_address ) if not action_success: @@ -222,7 +193,7 @@ def main(): sys.exit(1) print('\n🎉 All verifications complete!') - + except Exception as e: print(f'\nError: {str(e)}', file=sys.stderr) sys.exit(1)