Changes:
- Add next-themes dependency for theme management
- Create ThemeProvider wrapper for app root layout
- Set dark mode as default theme
- Create ThemeToggle component with Sun/Moon icons
- Add theme toggle to home page navigation
- Add theme toggle to dashboard header
- App now starts in dark mode with ability to switch to light mode
Styling uses existing Tailwind dark mode variables configured in
tailwind.config.ts and globals.css. All existing components automatically
support dark theme.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
720 lines
24 KiB
Python
720 lines
24 KiB
Python
"""
|
|
PoA Validator Node - Blockchain Consensus & Management
|
|
|
|
This service implements a Proof-of-Authority validator that:
|
|
- Participates in PoA consensus (round-robin block creation)
|
|
- Maintains a distributed ledger of votes
|
|
- Communicates with other validators via P2P
|
|
- Exposes JSON-RPC interface for vote submission
|
|
- Registers with bootnode for peer discovery
|
|
|
|
Features:
|
|
- Block creation and validation
|
|
- Transaction pool management
|
|
- Peer synchronization
|
|
- JSON-RPC endpoints (eth_sendTransaction, eth_getTransactionReceipt, etc.)
|
|
- P2P networking with gossip
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import time
|
|
import logging
|
|
import asyncio
|
|
import hashlib
|
|
import uuid
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime, timezone
|
|
import aiohttp
|
|
from fastapi import FastAPI, HTTPException, status
|
|
from pydantic import BaseModel
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - [%(name)s] - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ============================================================================
|
|
# Data Models
|
|
# ============================================================================
|
|
|
|
class Transaction(BaseModel):
|
|
"""Vote transaction"""
|
|
voter_id: str
|
|
election_id: int
|
|
encrypted_vote: str
|
|
ballot_hash: str
|
|
proof: Optional[str] = None
|
|
timestamp: int
|
|
|
|
|
|
class Block:
|
|
"""Blockchain block"""
|
|
|
|
def __init__(
|
|
self,
|
|
index: int,
|
|
prev_hash: str,
|
|
timestamp: int,
|
|
transactions: List[Transaction],
|
|
validator: str,
|
|
block_hash: Optional[str] = None,
|
|
signature: Optional[str] = None
|
|
):
|
|
self.index = index
|
|
self.prev_hash = prev_hash
|
|
self.timestamp = timestamp
|
|
self.transactions = transactions
|
|
self.validator = validator
|
|
self.block_hash = block_hash
|
|
self.signature = signature
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert block to dictionary"""
|
|
return {
|
|
"index": self.index,
|
|
"prev_hash": self.prev_hash,
|
|
"timestamp": self.timestamp,
|
|
"transactions": [
|
|
{
|
|
"voter_id": t.voter_id,
|
|
"election_id": t.election_id,
|
|
"encrypted_vote": t.encrypted_vote,
|
|
"ballot_hash": t.ballot_hash,
|
|
"proof": t.proof,
|
|
"timestamp": t.timestamp
|
|
}
|
|
for t in self.transactions
|
|
],
|
|
"validator": self.validator,
|
|
"block_hash": self.block_hash,
|
|
"signature": self.signature
|
|
}
|
|
|
|
@staticmethod
|
|
def from_dict(data: Dict[str, Any]) -> 'Block':
|
|
"""Create block from dictionary"""
|
|
transactions = [
|
|
Transaction(**tx) for tx in data.get("transactions", [])
|
|
]
|
|
return Block(
|
|
index=data["index"],
|
|
prev_hash=data["prev_hash"],
|
|
timestamp=data["timestamp"],
|
|
transactions=transactions,
|
|
validator=data["validator"],
|
|
block_hash=data.get("block_hash"),
|
|
signature=data.get("signature")
|
|
)
|
|
|
|
|
|
class Blockchain:
|
|
"""Blockchain state management"""
|
|
|
|
# Genesis block configuration
|
|
GENESIS_INDEX = 0
|
|
GENESIS_PREV_HASH = "0" * 64
|
|
GENESIS_TIMESTAMP = 1699360000
|
|
AUTHORIZED_VALIDATORS = [
|
|
"validator-1",
|
|
"validator-2",
|
|
"validator-3"
|
|
]
|
|
|
|
def __init__(self):
|
|
self.chain: List[Block] = []
|
|
self._create_genesis_block()
|
|
|
|
def _create_genesis_block(self) -> None:
|
|
"""Create the genesis block"""
|
|
genesis_block = Block(
|
|
index=self.GENESIS_INDEX,
|
|
prev_hash=self.GENESIS_PREV_HASH,
|
|
timestamp=self.GENESIS_TIMESTAMP,
|
|
transactions=[],
|
|
validator="genesis",
|
|
block_hash="0x" + hashlib.sha256(
|
|
json.dumps({
|
|
"index": self.GENESIS_INDEX,
|
|
"prev_hash": self.GENESIS_PREV_HASH,
|
|
"timestamp": self.GENESIS_TIMESTAMP,
|
|
"transactions": []
|
|
}, sort_keys=True).encode()
|
|
).hexdigest(),
|
|
signature="genesis"
|
|
)
|
|
self.chain.append(genesis_block)
|
|
logger.info("Genesis block created")
|
|
|
|
def add_block(self, block: Block) -> bool:
|
|
"""Add a block to the chain"""
|
|
if not self.validate_block(block):
|
|
logger.error(f"Block validation failed: {block.index}")
|
|
return False
|
|
|
|
self.chain.append(block)
|
|
logger.info(
|
|
f"Block {block.index} added to chain "
|
|
f"(validator: {block.validator}, txs: {len(block.transactions)})"
|
|
)
|
|
return True
|
|
|
|
def validate_block(self, block: Block) -> bool:
|
|
"""Validate a block"""
|
|
# Check block index
|
|
if block.index != len(self.chain):
|
|
logger.warning(f"Invalid block index: {block.index}, expected {len(self.chain)}")
|
|
return False
|
|
|
|
# Check previous hash
|
|
prev_block = self.chain[-1]
|
|
if block.prev_hash != prev_block.block_hash:
|
|
logger.warning(f"Invalid prev_hash for block {block.index}")
|
|
return False
|
|
|
|
# Check validator is authorized
|
|
if block.validator not in self.AUTHORIZED_VALIDATORS and block.validator != "genesis":
|
|
logger.warning(f"Unauthorized validator: {block.validator}")
|
|
return False
|
|
|
|
# Check block hash
|
|
calculated_hash = self.calculate_block_hash(block)
|
|
if block.block_hash != calculated_hash:
|
|
logger.warning(f"Invalid block hash for block {block.index}")
|
|
return False
|
|
|
|
return True
|
|
|
|
@staticmethod
|
|
def calculate_block_hash(block: Block) -> str:
|
|
"""Calculate hash for a block"""
|
|
block_data = {
|
|
"index": block.index,
|
|
"prev_hash": block.prev_hash,
|
|
"timestamp": block.timestamp,
|
|
"transactions": [
|
|
{
|
|
"voter_id": t.voter_id,
|
|
"election_id": t.election_id,
|
|
"encrypted_vote": t.encrypted_vote,
|
|
"ballot_hash": t.ballot_hash,
|
|
"proof": t.proof,
|
|
"timestamp": t.timestamp
|
|
}
|
|
for t in block.transactions
|
|
],
|
|
"validator": block.validator
|
|
}
|
|
block_json = json.dumps(block_data, sort_keys=True)
|
|
return "0x" + hashlib.sha256(block_json.encode()).hexdigest()
|
|
|
|
def get_block(self, index: int) -> Optional[Block]:
|
|
"""Get block by index"""
|
|
if 0 <= index < len(self.chain):
|
|
return self.chain[index]
|
|
return None
|
|
|
|
def get_latest_block(self) -> Block:
|
|
"""Get the latest block"""
|
|
return self.chain[-1]
|
|
|
|
def get_chain_length(self) -> int:
|
|
"""Get blockchain length"""
|
|
return len(self.chain)
|
|
|
|
def verify_integrity(self) -> bool:
|
|
"""Verify blockchain integrity"""
|
|
for i in range(1, len(self.chain)):
|
|
current = self.chain[i]
|
|
previous = self.chain[i - 1]
|
|
|
|
if current.prev_hash != previous.block_hash:
|
|
logger.error(f"Chain integrity broken at block {i}")
|
|
return False
|
|
|
|
if current.block_hash != Blockchain.calculate_block_hash(current):
|
|
logger.error(f"Block hash mismatch at block {i}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# ============================================================================
|
|
# PoA Validator
|
|
# ============================================================================
|
|
|
|
class PoAValidator:
|
|
"""Proof-of-Authority Validator Node"""
|
|
|
|
def __init__(
|
|
self,
|
|
node_id: str,
|
|
private_key: str,
|
|
bootnode_url: str,
|
|
rpc_port: int = 8001,
|
|
p2p_port: int = 30303
|
|
):
|
|
self.node_id = node_id
|
|
self.private_key = private_key
|
|
self.bootnode_url = bootnode_url
|
|
self.rpc_port = rpc_port
|
|
self.p2p_port = p2p_port
|
|
self.public_key = f"0x{uuid.uuid4().hex[:40]}"
|
|
|
|
# State management
|
|
self.blockchain = Blockchain()
|
|
self.pending_transactions: List[Transaction] = []
|
|
self.peer_connections: Dict[str, str] = {} # node_id -> url
|
|
self.transaction_pool: Dict[str, Transaction] = {} # tx_id -> transaction
|
|
|
|
# Block creation state
|
|
self.last_block_time = time.time()
|
|
self.block_creation_interval = 5 # seconds
|
|
|
|
logger.info(f"PoAValidator initialized: {node_id}")
|
|
|
|
async def startup(self) -> None:
|
|
"""Initialize and connect to network"""
|
|
logger.info(f"Validator {self.node_id} starting up...")
|
|
|
|
try:
|
|
# 1. Register with bootnode
|
|
await self.register_with_bootnode()
|
|
|
|
# 2. Discover peers
|
|
await self.discover_peers()
|
|
|
|
# 3. Start block creation task
|
|
asyncio.create_task(self.block_creation_loop())
|
|
|
|
logger.info(f"Validator {self.node_id} is ready")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Startup error: {e}")
|
|
raise
|
|
|
|
async def register_with_bootnode(self) -> None:
|
|
"""Register this validator with the bootnode"""
|
|
try:
|
|
payload = {
|
|
"node_id": self.node_id,
|
|
"ip": self.node_id, # Docker service name
|
|
"p2p_port": self.p2p_port,
|
|
"rpc_port": self.rpc_port,
|
|
"public_key": self.public_key
|
|
}
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
f"{self.bootnode_url}/register_peer",
|
|
json=payload,
|
|
timeout=aiohttp.ClientTimeout(total=10)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
logger.info(f"Registered with bootnode: {self.node_id}")
|
|
else:
|
|
logger.error(f"Failed to register with bootnode: {resp.status}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error registering with bootnode: {e}")
|
|
raise
|
|
|
|
async def discover_peers(self) -> None:
|
|
"""Discover other validators from bootnode"""
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(
|
|
f"{self.bootnode_url}/discover?node_id={self.node_id}",
|
|
timeout=aiohttp.ClientTimeout(total=10)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
peers = data.get("peers", [])
|
|
|
|
for peer in peers:
|
|
self.peer_connections[peer["node_id"]] = (
|
|
f"http://{peer['ip']}:{peer['rpc_port']}"
|
|
)
|
|
|
|
logger.info(
|
|
f"Discovered {len(peers)} peers: "
|
|
f"{list(self.peer_connections.keys())}"
|
|
)
|
|
else:
|
|
logger.error(f"Failed to discover peers: {resp.status}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error discovering peers: {e}")
|
|
|
|
def should_create_block(self) -> bool:
|
|
"""Determine if this validator should create the next block"""
|
|
next_block_index = self.blockchain.get_chain_length()
|
|
authorized = Blockchain.AUTHORIZED_VALIDATORS
|
|
validator_index = authorized.index(self.node_id) if self.node_id in authorized else -1
|
|
|
|
if validator_index == -1:
|
|
return False
|
|
|
|
should_create = next_block_index % len(authorized) == validator_index
|
|
return should_create
|
|
|
|
async def block_creation_loop(self) -> None:
|
|
"""Main loop for creating blocks"""
|
|
while True:
|
|
try:
|
|
current_time = time.time()
|
|
time_since_last = current_time - self.last_block_time
|
|
|
|
if time_since_last >= self.block_creation_interval:
|
|
if self.should_create_block() and len(self.pending_transactions) > 0:
|
|
await self.create_and_broadcast_block()
|
|
self.last_block_time = current_time
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in block creation loop: {e}")
|
|
await asyncio.sleep(5)
|
|
|
|
async def create_and_broadcast_block(self) -> None:
|
|
"""Create a new block and broadcast to peers"""
|
|
try:
|
|
# Take up to 32 pending transactions
|
|
transactions = self.pending_transactions[:32]
|
|
self.pending_transactions = self.pending_transactions[32:]
|
|
|
|
# Create block
|
|
prev_block = self.blockchain.get_latest_block()
|
|
new_block = Block(
|
|
index=self.blockchain.get_chain_length(),
|
|
prev_hash=prev_block.block_hash,
|
|
timestamp=int(time.time()),
|
|
transactions=transactions,
|
|
validator=self.node_id
|
|
)
|
|
|
|
# Calculate hash
|
|
new_block.block_hash = Blockchain.calculate_block_hash(new_block)
|
|
|
|
# Sign (simplified - just use hash)
|
|
new_block.signature = new_block.block_hash[:16]
|
|
|
|
# Add to local chain
|
|
if self.blockchain.add_block(new_block):
|
|
logger.info(f"Block {new_block.index} created successfully")
|
|
# Broadcast to peers
|
|
await self.broadcast_block(new_block)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating block: {e}")
|
|
|
|
async def broadcast_block(self, block: Block) -> None:
|
|
"""Broadcast block to all peers"""
|
|
if not self.peer_connections:
|
|
logger.debug("No peers to broadcast to")
|
|
return
|
|
|
|
payload = json.dumps({
|
|
"type": "new_block",
|
|
"block": block.to_dict()
|
|
})
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
for node_id, peer_url in self.peer_connections.items():
|
|
try:
|
|
async with session.post(
|
|
f"{peer_url}/p2p/new_block",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
timeout=aiohttp.ClientTimeout(total=5)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
logger.debug(f"Block broadcast to {node_id}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to broadcast to {node_id}: {e}")
|
|
|
|
async def handle_new_block(self, block_data: Dict[str, Any]) -> None:
|
|
"""Handle a new block from peers"""
|
|
try:
|
|
block = Block.from_dict(block_data)
|
|
|
|
if self.blockchain.validate_block(block):
|
|
if self.blockchain.add_block(block):
|
|
# Broadcast to other peers
|
|
await self.broadcast_block(block)
|
|
logger.info(f"Block {block.index} accepted and propagated")
|
|
else:
|
|
logger.warning(f"Invalid block received: {block.index}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error handling new block: {e}")
|
|
|
|
def add_transaction(self, transaction: Transaction) -> str:
|
|
"""Add a transaction to the pending pool"""
|
|
tx_id = f"0x{uuid.uuid4().hex}"
|
|
self.transaction_pool[tx_id] = transaction
|
|
self.pending_transactions.append(transaction)
|
|
logger.info(f"Transaction added: {tx_id}")
|
|
return tx_id
|
|
|
|
def get_transaction_receipt(self, tx_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Get receipt for a transaction"""
|
|
# Look through blockchain for transaction
|
|
for block in self.blockchain.chain:
|
|
for tx in block.transactions:
|
|
if tx.voter_id == tx_id or tx.ballot_hash == tx_id:
|
|
return {
|
|
"transactionHash": tx_id,
|
|
"blockNumber": block.index,
|
|
"blockHash": block.block_hash,
|
|
"status": 1,
|
|
"timestamp": block.timestamp
|
|
}
|
|
|
|
return None
|
|
|
|
def get_blockchain_data(self) -> Dict[str, Any]:
|
|
"""Get full blockchain data"""
|
|
return {
|
|
"blocks": [block.to_dict() for block in self.blockchain.chain],
|
|
"verification": {
|
|
"chain_valid": self.blockchain.verify_integrity(),
|
|
"total_blocks": self.blockchain.get_chain_length(),
|
|
"total_votes": sum(
|
|
len(block.transactions)
|
|
for block in self.blockchain.chain
|
|
if block.index > 0 # Exclude genesis
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# FastAPI Application
|
|
# ============================================================================
|
|
|
|
app = FastAPI(
|
|
title="PoA Validator Node",
|
|
description="Proof-of-Authority blockchain validator",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# Global validator instance (set during startup)
|
|
validator: Optional[PoAValidator] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Startup/Shutdown
|
|
# ============================================================================
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
"""Initialize validator on startup"""
|
|
global validator
|
|
|
|
node_id = os.getenv("NODE_ID", "validator-1")
|
|
private_key = os.getenv("PRIVATE_KEY", f"0x{uuid.uuid4().hex}")
|
|
bootnode_url = os.getenv("BOOTNODE_URL", "http://bootnode:8546")
|
|
rpc_port = int(os.getenv("RPC_PORT", "8001"))
|
|
p2p_port = int(os.getenv("P2P_PORT", "30303"))
|
|
|
|
validator = PoAValidator(
|
|
node_id=node_id,
|
|
private_key=private_key,
|
|
bootnode_url=bootnode_url,
|
|
rpc_port=rpc_port,
|
|
p2p_port=p2p_port
|
|
)
|
|
|
|
await validator.startup()
|
|
|
|
|
|
# ============================================================================
|
|
# Health Check
|
|
# ============================================================================
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
return {
|
|
"status": "healthy",
|
|
"node_id": validator.node_id,
|
|
"chain_length": validator.blockchain.get_chain_length(),
|
|
"pending_transactions": len(validator.pending_transactions),
|
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# JSON-RPC Interface
|
|
# ============================================================================
|
|
|
|
@app.post("/rpc")
|
|
async def handle_json_rpc(request: dict):
|
|
"""Handle JSON-RPC requests"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
method = request.get("method")
|
|
params = request.get("params", [])
|
|
request_id = request.get("id")
|
|
|
|
try:
|
|
if method == "eth_sendTransaction":
|
|
# Submit a vote transaction
|
|
tx_data = params[0] if params else {}
|
|
data_hex = tx_data.get("data", "0x")
|
|
|
|
# Decode transaction data
|
|
if data_hex.startswith("0x"):
|
|
data_hex = data_hex[2:]
|
|
try:
|
|
data_json = bytes.fromhex(data_hex).decode()
|
|
tx_dict = json.loads(data_json)
|
|
except:
|
|
raise ValueError("Invalid transaction data")
|
|
else:
|
|
raise ValueError("Invalid data format")
|
|
|
|
# Create transaction
|
|
transaction = Transaction(**tx_dict)
|
|
tx_id = validator.add_transaction(transaction)
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"result": tx_id,
|
|
"id": request_id
|
|
}
|
|
|
|
elif method == "eth_getTransactionReceipt":
|
|
# Get transaction receipt
|
|
tx_id = params[0] if params else None
|
|
receipt = validator.get_transaction_receipt(tx_id)
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"result": receipt,
|
|
"id": request_id
|
|
}
|
|
|
|
elif method == "eth_blockNumber":
|
|
# Get current block number
|
|
block_number = validator.blockchain.get_chain_length() - 1
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"result": hex(block_number),
|
|
"id": request_id
|
|
}
|
|
|
|
elif method == "eth_getBlockByNumber":
|
|
# Get block by number
|
|
block_num = int(params[0], 0) if params else 0
|
|
block = validator.blockchain.get_block(block_num)
|
|
|
|
if block:
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"result": block.to_dict(),
|
|
"id": request_id
|
|
}
|
|
else:
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"result": None,
|
|
"id": request_id
|
|
}
|
|
|
|
else:
|
|
raise ValueError(f"Unknown method: {method}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"JSON-RPC error: {e}")
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"error": {"code": -32603, "message": str(e)},
|
|
"id": request_id
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# P2P Networking
|
|
# ============================================================================
|
|
|
|
@app.post("/p2p/new_block")
|
|
async def handle_new_block(block_data: dict):
|
|
"""Handle new block from peer"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
try:
|
|
await validator.handle_new_block(block_data)
|
|
return {"status": "ok"}
|
|
except Exception as e:
|
|
logger.error(f"Error handling P2P block: {e}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@app.post("/p2p/new_transaction")
|
|
async def handle_new_transaction(transaction: Transaction):
|
|
"""Handle new transaction from peer"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
try:
|
|
validator.add_transaction(transaction)
|
|
return {"status": "ok"}
|
|
except Exception as e:
|
|
logger.error(f"Error handling P2P transaction: {e}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# Admin Endpoints
|
|
# ============================================================================
|
|
|
|
@app.get("/blockchain")
|
|
async def get_blockchain():
|
|
"""Get full blockchain data"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
return validator.get_blockchain_data()
|
|
|
|
|
|
@app.get("/peers")
|
|
async def get_peers():
|
|
"""Get connected peers"""
|
|
if validator is None:
|
|
raise HTTPException(status_code=503, detail="Validator not initialized")
|
|
|
|
return {
|
|
"node_id": validator.node_id,
|
|
"peers": list(validator.peer_connections.keys()),
|
|
"peer_count": len(validator.peer_connections)
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
|
|
rpc_port = int(os.getenv("RPC_PORT", "8001"))
|
|
logger.info(f"Starting validator on RPC port {rpc_port}")
|
|
|
|
uvicorn.run(
|
|
app,
|
|
host="0.0.0.0",
|
|
port=rpc_port,
|
|
log_level="info"
|
|
)
|