-
Notifications
You must be signed in to change notification settings - Fork 59
Retry mechanism for verification in multiple explorers #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
493e14e
850b03f
7c3ec30
547d643
fd22657
4e1d4c4
408a1e6
b4a1ac2
eea1b68
3fc520f
6a49e3a
458e249
64f0aab
d1eeef3
219a333
456e91c
86e5688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # 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 | ||
|
|
||
| ## 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** | ||
|
|
||
| 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** | ||
|
|
||
| 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. **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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Verification package for Sky Protocol spells. | ||
| """ | ||
|
|
||
| from .etherscan_verifier import EtherscanVerifier | ||
| from .sourcify_verifier import SourcifyVerifier | ||
| from .contract_data import ( | ||
| get_chain_id, | ||
| get_library_address, | ||
| get_contract_metadata, | ||
| get_action_address | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| 'EtherscanVerifier', | ||
| 'SourcifyVerifier', | ||
| 'get_chain_id', | ||
| 'get_library_address', | ||
| 'get_contract_metadata', | ||
| 'get_action_address' | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #!/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 | ||
| 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) | ||
amusingaxl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # If we get here, no library address was found | ||
| print('WARNING: Assuming this contract uses no libraries', file=sys.stderr) | ||
| 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.""" | ||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test name needs updating here.