Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,22 @@ CSRF_TRUSTED_ORIGINS=https://your-frontend-domain.com,https://another-domain.com
# Ethereum Authentication
SIWE_DOMAIN=localhost

# Blockchain Settings
FACTORY_CONTRACT_ADDRESS=0x19f030293B97281fb742D9f3699DC9bA439706dD # ValidatorWalletFactory.sol
VALIDATOR_CONTRACT_ADDRESS=0x19f030293B97281fb742D9f3699DC9bA439706dD # Staking.sol
# Blockchain Settings - Shared
VALIDATOR_RPC_URL=https://zksync-os-testnet-genlayer.zksync.dev

# Asimov Testnet (legacy VALIDATOR_CONTRACT_ADDRESS/FACTORY_CONTRACT_ADDRESS also supported)
ASIMOV_STAKING_CONTRACT_ADDRESS=0x19f030293B97281fb742D9f3699DC9bA439706dD
ASIMOV_FACTORY_CONTRACT_ADDRESS=0x19f030293B97281fb742D9f3699DC9bA439706dD
ASIMOV_EXPLORER_URL=https://explorer-asimov.genlayer.com

# Bradbury Testnet (leave empty until Bradbury launches)
BRADBURY_STAKING_CONTRACT_ADDRESS=
BRADBURY_FACTORY_CONTRACT_ADDRESS=
BRADBURY_EXPLORER_URL=

# Uptime lookback window - days to check for active validator status
UPTIME_LOOKBACK_DAYS=7

# GenLayer Integration
# The VALIDATOR_RPC_URL is also used for GenLayer Studio deployment checking
# If you need a different RPC URL for GenLayer operations, you can optionally add:
Expand Down
260 changes: 124 additions & 136 deletions backend/contributions/management/commands/add_daily_uptime.py

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions backend/deploy-apprunner-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
"CSRF_TRUSTED_ORIGINS": "$SSM_PREFIX/$SSM_ENV/csrf_trusted_origins",
"SIWE_DOMAIN": "$SSM_PREFIX/$SSM_ENV/siwe_domain",
"VALIDATOR_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/validator_contract_address",
"ASIMOV_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/asimov_staking_contract_address",
"ASIMOV_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/asimov_factory_contract_address",
"ASIMOV_EXPLORER_URL": "$SSM_PREFIX/$SSM_ENV/asimov_explorer_url",
"BRADBURY_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/bradbury_staking_contract_address",
"BRADBURY_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/bradbury_factory_contract_address",
"BRADBURY_EXPLORER_URL": "$SSM_PREFIX/$SSM_ENV/bradbury_explorer_url",
"UPTIME_LOOKBACK_DAYS": "$SSM_PREFIX/$SSM_ENV/uptime_lookback_days",
"VALIDATOR_RPC_URL": "$SSM_PREFIX/$SSM_ENV/validator_rpc_url",
"ALLOWED_CIDR_NETS": "$SSM_PREFIX/$SSM_ENV/allowed_cidr_nets",
"CLOUDINARY_CLOUD_NAME": "$SSM_PREFIX/$SSM_ENV/cloudinary_cloud_name",
Expand Down Expand Up @@ -215,6 +222,13 @@ EOF
"CSRF_TRUSTED_ORIGINS": "$SSM_PREFIX/$SSM_ENV/csrf_trusted_origins",
"SIWE_DOMAIN": "$SSM_PREFIX/$SSM_ENV/siwe_domain",
"VALIDATOR_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/validator_contract_address",
"ASIMOV_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/asimov_staking_contract_address",
"ASIMOV_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/asimov_factory_contract_address",
"ASIMOV_EXPLORER_URL": "$SSM_PREFIX/$SSM_ENV/asimov_explorer_url",
"BRADBURY_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/bradbury_staking_contract_address",
"BRADBURY_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/$SSM_ENV/bradbury_factory_contract_address",
"BRADBURY_EXPLORER_URL": "$SSM_PREFIX/$SSM_ENV/bradbury_explorer_url",
"UPTIME_LOOKBACK_DAYS": "$SSM_PREFIX/$SSM_ENV/uptime_lookback_days",
"VALIDATOR_RPC_URL": "$SSM_PREFIX/$SSM_ENV/validator_rpc_url",
"ALLOWED_CIDR_NETS": "$SSM_PREFIX/$SSM_ENV/allowed_cidr_nets",
"CLOUDINARY_CLOUD_NAME": "$SSM_PREFIX/$SSM_ENV/cloudinary_cloud_name",
Expand Down
14 changes: 14 additions & 0 deletions backend/deploy-apprunner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
"CSRF_TRUSTED_ORIGINS": "$SSM_PREFIX/prod/csrf_trusted_origins",
"SIWE_DOMAIN": "$SSM_PREFIX/prod/siwe_domain",
"VALIDATOR_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/validator_contract_address",
"ASIMOV_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/asimov_staking_contract_address",
"ASIMOV_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/asimov_factory_contract_address",
"ASIMOV_EXPLORER_URL": "$SSM_PREFIX/prod/asimov_explorer_url",
"BRADBURY_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/bradbury_staking_contract_address",
"BRADBURY_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/bradbury_factory_contract_address",
"BRADBURY_EXPLORER_URL": "$SSM_PREFIX/prod/bradbury_explorer_url",
"UPTIME_LOOKBACK_DAYS": "$SSM_PREFIX/prod/uptime_lookback_days",
"VALIDATOR_RPC_URL": "$SSM_PREFIX/prod/validator_rpc_url",
"ALLOWED_CIDR_NETS": "$SSM_PREFIX/prod/allowed_cidr_nets",
"CLOUDINARY_CLOUD_NAME": "$SSM_PREFIX/prod/cloudinary_cloud_name",
Expand Down Expand Up @@ -280,6 +287,13 @@ else
"CSRF_TRUSTED_ORIGINS": "$SSM_PREFIX/prod/csrf_trusted_origins",
"SIWE_DOMAIN": "$SSM_PREFIX/prod/siwe_domain",
"VALIDATOR_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/validator_contract_address",
"ASIMOV_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/asimov_staking_contract_address",
"ASIMOV_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/asimov_factory_contract_address",
"ASIMOV_EXPLORER_URL": "$SSM_PREFIX/prod/asimov_explorer_url",
"BRADBURY_STAKING_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/bradbury_staking_contract_address",
"BRADBURY_FACTORY_CONTRACT_ADDRESS": "$SSM_PREFIX/prod/bradbury_factory_contract_address",
"BRADBURY_EXPLORER_URL": "$SSM_PREFIX/prod/bradbury_explorer_url",
"UPTIME_LOOKBACK_DAYS": "$SSM_PREFIX/prod/uptime_lookback_days",
"VALIDATOR_RPC_URL": "$SSM_PREFIX/prod/validator_rpc_url",
"ALLOWED_CIDR_NETS": "$SSM_PREFIX/prod/allowed_cidr_nets",
"CLOUDINARY_CLOUD_NAME": "$SSM_PREFIX/prod/cloudinary_cloud_name",
Expand Down
38 changes: 36 additions & 2 deletions backend/tally/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,43 @@ def get_port_from_argv():
AUTH_USER_MODEL = 'users.User'

# Blockchain settings
VALIDATOR_CONTRACT_ADDRESS = get_required_env('VALIDATOR_CONTRACT_ADDRESS')
# Shared RPC URL for all networks
VALIDATOR_RPC_URL = get_required_env('VALIDATOR_RPC_URL')
FACTORY_CONTRACT_ADDRESS = os.environ.get('FACTORY_CONTRACT_ADDRESS')

# Legacy settings (backward compatibility - deprecated, use TESTNET_NETWORKS instead)
VALIDATOR_CONTRACT_ADDRESS = os.environ.get(
'ASIMOV_STAKING_CONTRACT_ADDRESS',
os.environ.get('VALIDATOR_CONTRACT_ADDRESS', '')
)
FACTORY_CONTRACT_ADDRESS = os.environ.get(
'ASIMOV_FACTORY_CONTRACT_ADDRESS',
os.environ.get('FACTORY_CONTRACT_ADDRESS', '')
)

# Multi-network configuration
TESTNET_NETWORKS = {
'asimov': {
'name': 'Asimov',
'staking_contract_address': os.environ.get(
'ASIMOV_STAKING_CONTRACT_ADDRESS',
os.environ.get('VALIDATOR_CONTRACT_ADDRESS', '')
),
'factory_contract_address': os.environ.get(
'ASIMOV_FACTORY_CONTRACT_ADDRESS',
os.environ.get('FACTORY_CONTRACT_ADDRESS', '')
),
'explorer_url': os.environ.get('ASIMOV_EXPLORER_URL', 'https://explorer-asimov.genlayer.com'),
},
'bradbury': {
'name': 'Bradbury',
'staking_contract_address': os.environ.get('BRADBURY_STAKING_CONTRACT_ADDRESS', ''),
'factory_contract_address': os.environ.get('BRADBURY_FACTORY_CONTRACT_ADDRESS', ''),
'explorer_url': os.environ.get('BRADBURY_EXPLORER_URL', ''),
},
}

# Uptime lookback window (days) - how many days back to check for active status
UPTIME_LOOKBACK_DAYS = int(os.environ.get('UPTIME_LOOKBACK_DAYS', '7') or '7')

# AWS Health Check IPs - Allow these IPs to bypass ALLOWED_HOSTS
# Required environment variable with AWS internal/metadata service IPs
Expand Down
4 changes: 2 additions & 2 deletions backend/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ def _get_web3_contract(self):
# Connect to the blockchain using environment variables
w3 = Web3(Web3.HTTPProvider(settings.VALIDATOR_RPC_URL))

# Contract address from environment variables
contract_address = settings.VALIDATOR_CONTRACT_ADDRESS
# Contract address from network config (Asimov - this endpoint is Asimov-only)
contract_address = settings.TESTNET_NETWORKS['asimov']['staking_contract_address']

# Minimal ABI for the validators functions
abi = [
Expand Down
29 changes: 19 additions & 10 deletions backend/validators/admin.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from django.contrib import admin
from .models import Validator, ValidatorWallet
from .models import Validator, ValidatorWallet, ValidatorWalletStatusSnapshot


class ValidatorInline(admin.StackedInline):
"""Inline admin for Validator model to be used in UserAdmin"""
model = Validator
extra = 0 # Don't show empty rows
max_num = 1 # Only one validator per user
extra = 0
max_num = 1
fields = ('node_version',)
verbose_name = "Validator Information"
verbose_name_plural = "Validator Information"
can_delete = True # Allow deletion through inline
can_delete = True


@admin.register(Validator)
Expand All @@ -19,7 +18,7 @@ class ValidatorAdmin(admin.ModelAdmin):
search_fields = ('user__email', 'user__name', 'node_version')
list_filter = ('created_at', 'updated_at')
ordering = ('-created_at',)

fieldsets = (
(None, {
'fields': ('user', 'node_version')
Expand All @@ -34,15 +33,15 @@ class ValidatorAdmin(admin.ModelAdmin):

@admin.register(ValidatorWallet)
class ValidatorWalletAdmin(admin.ModelAdmin):
list_display = ('address', 'status', 'operator_address', 'operator', 'moniker', 'created_at')
list_filter = ('status', 'created_at')
list_display = ('address', 'network', 'status', 'operator_address', 'operator', 'moniker', 'created_at')
list_filter = ('network', 'status', 'created_at')
search_fields = ('address', 'operator_address', 'moniker')
ordering = ('-created_at',)
readonly_fields = ('created_at', 'updated_at')

fieldsets = (
(None, {
'fields': ('address', 'status', 'operator_address', 'operator')
'fields': ('address', 'network', 'status', 'operator_address', 'operator')
}),
('Metadata', {
'fields': ('moniker', 'logo_uri', 'website', 'description'),
Expand All @@ -59,4 +58,14 @@ class ValidatorWalletAdmin(admin.ModelAdmin):
)


# Note: ValidatorInline is imported and added to UserAdmin in users/admin.py
@admin.register(ValidatorWalletStatusSnapshot)
class ValidatorWalletStatusSnapshotAdmin(admin.ModelAdmin):
list_display = ('wallet', 'date', 'status')
list_filter = ('status', 'date', 'wallet__network')
search_fields = ('wallet__address',)
ordering = ('-date',)
readonly_fields = ('created_at', 'updated_at')
raw_id_fields = ('wallet',)


# Note: ValidatorInline is imported and added to UserAdmin in users/admin.py
76 changes: 65 additions & 11 deletions backend/validators/genlayer_validators_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from typing import Dict, List, Optional, Any
from django.conf import settings
from django.utils import timezone
from web3 import Web3

from tally.middleware.logging_utils import get_app_logger
Expand Down Expand Up @@ -125,8 +126,10 @@ class GenLayerValidatorsService:
Service class for syncing validator wallet data from GenLayer blockchain.
"""

def __init__(self):
"""Initialize Web3 connection and contract instances."""
def __init__(self, network_key='asimov'):
"""Initialize Web3 connection and contract instances for a specific network."""
self.network_key = network_key
self.network_config = settings.TESTNET_NETWORKS.get(network_key, {})
self.w3 = None
self.staking_contract = None
self.factory_contract = None
Expand All @@ -137,25 +140,28 @@ def _initialize_client(self):
try:
self.w3 = Web3(Web3.HTTPProvider(settings.VALIDATOR_RPC_URL))

# Staking contract (uses existing VALIDATOR_CONTRACT_ADDRESS)
staking_address = self.network_config.get('staking_contract_address')
if not staking_address:
logger.warning(f"No staking contract address configured for network '{self.network_key}'")
return False

self.staking_contract = self.w3.eth.contract(
address=settings.VALIDATOR_CONTRACT_ADDRESS,
address=staking_address,
abi=STAKING_ABI
)

# Factory contract (uses new FACTORY_CONTRACT_ADDRESS if available)
factory_address = getattr(settings, 'FACTORY_CONTRACT_ADDRESS', None)
factory_address = self.network_config.get('factory_contract_address')
if factory_address:
self.factory_contract = self.w3.eth.contract(
address=factory_address,
abi=FACTORY_ABI
)
else:
logger.warning("FACTORY_CONTRACT_ADDRESS not configured")
logger.warning(f"FACTORY_CONTRACT_ADDRESS not configured for network '{self.network_key}'")

return True
except Exception as e:
logger.error(f"Failed to initialize GenLayerValidatorsService: {str(e)}")
logger.error(f"Failed to initialize GenLayerValidatorsService for '{self.network_key}': {str(e)}")
return False

def _get_validator_wallet_contract(self, wallet_address: str):
Expand Down Expand Up @@ -347,7 +353,7 @@ def sync_all_validators(self) -> Dict[str, Any]:

# Also include existing validators from DB to catch "zombie" validators
# that disappeared from both active and banned lists
existing_addresses = ValidatorWallet.objects.values_list('address', flat=True)
existing_addresses = ValidatorWallet.objects.filter(network=self.network_key).values_list('address', flat=True)
for addr in existing_addresses:
all_addresses.add(addr.lower())

Expand All @@ -367,6 +373,9 @@ def sync_all_validators(self) -> Dict[str, Any]:
logger.error(f"Error processing validator: {str(e)}")
stats['errors'] += 1

# Record status snapshots for today
self._record_status_snapshots()

except Exception as e:
logger.error(f"Error during sync: {str(e)}")
stats['errors'] += 1
Expand Down Expand Up @@ -396,10 +405,10 @@ def _process_validator(

# Check if exists
try:
wallet = ValidatorWallet.objects.get(address__iexact=address_lower)
wallet = ValidatorWallet.objects.get(address__iexact=address_lower, network=self.network_key)
is_new = False
except ValidatorWallet.DoesNotExist:
wallet = ValidatorWallet(address=address_lower)
wallet = ValidatorWallet(address=address_lower, network=self.network_key)
is_new = True

# Track if anything changed
Expand Down Expand Up @@ -494,6 +503,7 @@ def get_validators_for_operator(self, operator_address: str) -> List[Dict[str, A
return [
{
'address': w.address,
'network': w.network,
'status': w.status,
'v_stake': w.v_stake,
'd_stake': w.d_stake,
Expand All @@ -502,3 +512,47 @@ def get_validators_for_operator(self, operator_address: str) -> List[Dict[str, A
}
for w in wallets
]

def _record_status_snapshots(self):
"""Record status snapshots for all wallets on this network for today."""
from .models import ValidatorWallet, ValidatorWalletStatusSnapshot

today = timezone.now().date()
wallets = ValidatorWallet.objects.filter(network=self.network_key)

snapshots = [
ValidatorWalletStatusSnapshot(wallet=wallet, date=today, status=wallet.status)
for wallet in wallets
]
if snapshots:
ValidatorWalletStatusSnapshot.objects.bulk_create(
snapshots,
update_conflicts=True,
unique_fields=['wallet', 'date'],
update_fields=['status']
)

@classmethod
def sync_all_networks(cls):
"""
Sync validators for all configured networks.
Skips networks without staking contract addresses configured.
Returns dict of per-network stats.
"""
all_stats = {}
for network_key, config in settings.TESTNET_NETWORKS.items():
if not config.get('staking_contract_address'):
logger.info(f"Skipping network '{network_key}' - no staking contract configured")
continue

logger.info(f"Syncing validators for network '{network_key}'...")
try:
service = cls(network_key=network_key)
stats = service.sync_all_validators()
all_stats[network_key] = stats
logger.info(f"Network '{network_key}' sync complete: {stats}")
except Exception as e:
logger.error(f"Error syncing network '{network_key}': {str(e)}")
all_stats[network_key] = {'error': str(e)}

return all_stats
Loading