- Change file mode from 644 to 755 for all project files - Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet" - Rename Miner.extra_meta_data to extra_metadata for consistency
499 lines
19 KiB
Python
Executable File
499 lines
19 KiB
Python
Executable File
"""
|
|
Chain manager for multi-chain operations
|
|
"""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any
|
|
from .config import MultiChainConfig, get_node_config
|
|
from .node_client import NodeClient
|
|
from ..models.chain import (
|
|
ChainConfig, ChainInfo, ChainType, ChainStatus,
|
|
GenesisBlock, ChainMigrationPlan, ChainMigrationResult,
|
|
ChainBackupResult, ChainRestoreResult
|
|
)
|
|
|
|
class ChainAlreadyExistsError(Exception):
|
|
"""Chain already exists error"""
|
|
pass
|
|
|
|
class ChainNotFoundError(Exception):
|
|
"""Chain not found error"""
|
|
pass
|
|
|
|
class NodeNotAvailableError(Exception):
|
|
"""Node not available error"""
|
|
pass
|
|
|
|
class ChainManager:
|
|
"""Multi-chain manager"""
|
|
|
|
def __init__(self, config: MultiChainConfig):
|
|
self.config = config
|
|
self._chain_cache: Dict[str, ChainInfo] = {}
|
|
self._node_clients: Dict[str, Any] = {}
|
|
|
|
async def list_chains(
|
|
self,
|
|
chain_type: Optional[ChainType] = None,
|
|
include_private: bool = False,
|
|
sort_by: str = "id"
|
|
) -> List[ChainInfo]:
|
|
"""List all available chains"""
|
|
chains = []
|
|
|
|
# Get chains from all available nodes
|
|
for node_id, node_config in self.config.nodes.items():
|
|
try:
|
|
node_chains = await self._get_node_chains(node_id)
|
|
for chain in node_chains:
|
|
# Filter private chains if not requested
|
|
if not include_private and chain.privacy.visibility == "private":
|
|
continue
|
|
|
|
# Filter by chain type if specified
|
|
if chain_type and chain.type != chain_type:
|
|
continue
|
|
|
|
chains.append(chain)
|
|
except Exception as e:
|
|
# Log error but continue with other nodes
|
|
print(f"Error getting chains from node {node_id}: {e}")
|
|
|
|
# Remove duplicates (same chain on multiple nodes)
|
|
unique_chains = {}
|
|
for chain in chains:
|
|
if chain.id not in unique_chains:
|
|
unique_chains[chain.id] = chain
|
|
|
|
chains = list(unique_chains.values())
|
|
|
|
# Sort chains
|
|
if sort_by == "id":
|
|
chains.sort(key=lambda x: x.id)
|
|
elif sort_by == "size":
|
|
chains.sort(key=lambda x: x.size_mb, reverse=True)
|
|
elif sort_by == "nodes":
|
|
chains.sort(key=lambda x: x.node_count, reverse=True)
|
|
elif sort_by == "created":
|
|
chains.sort(key=lambda x: x.created_at, reverse=True)
|
|
|
|
return chains
|
|
|
|
async def get_chain_info(self, chain_id: str, detailed: bool = False, metrics: bool = False) -> ChainInfo:
|
|
"""Get detailed information about a chain"""
|
|
# Check cache first
|
|
if chain_id in self._chain_cache:
|
|
chain_info = self._chain_cache[chain_id]
|
|
else:
|
|
# Get from node
|
|
chain_info = await self._find_chain_on_nodes(chain_id)
|
|
if not chain_info:
|
|
raise ChainNotFoundError(f"Chain {chain_id} not found")
|
|
|
|
# Cache the result
|
|
self._chain_cache[chain_id] = chain_info
|
|
|
|
# Add detailed information if requested
|
|
if detailed or metrics:
|
|
chain_info = await self._enrich_chain_info(chain_info)
|
|
|
|
return chain_info
|
|
|
|
async def create_chain(self, chain_config: ChainConfig, node_id: Optional[str] = None) -> str:
|
|
"""Create a new chain"""
|
|
# Generate chain ID
|
|
chain_id = self._generate_chain_id(chain_config)
|
|
|
|
# Check if chain already exists
|
|
try:
|
|
await self.get_chain_info(chain_id)
|
|
raise ChainAlreadyExistsError(f"Chain {chain_id} already exists")
|
|
except ChainNotFoundError:
|
|
pass # Chain doesn't exist, which is good
|
|
|
|
# Select node if not specified
|
|
if not node_id:
|
|
node_id = await self._select_best_node(chain_config)
|
|
|
|
# Validate node availability
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
# Create genesis block
|
|
genesis_block = await self._create_genesis_block(chain_config, chain_id)
|
|
|
|
# Create chain on node
|
|
await self._create_chain_on_node(node_id, genesis_block)
|
|
|
|
# Return chain ID
|
|
return chain_id
|
|
|
|
async def delete_chain(self, chain_id: str, force: bool = False) -> bool:
|
|
"""Delete a chain"""
|
|
chain_info = await self.get_chain_info(chain_id)
|
|
|
|
# Get all nodes hosting this chain
|
|
hosting_nodes = await self._get_chain_hosting_nodes(chain_id)
|
|
|
|
if not force and len(hosting_nodes) > 1:
|
|
raise ValueError(f"Chain {chain_id} is hosted on {len(hosting_nodes)} nodes. Use --force to delete.")
|
|
|
|
# Delete from all hosting nodes
|
|
success = True
|
|
for node_id in hosting_nodes:
|
|
try:
|
|
await self._delete_chain_from_node(node_id, chain_id)
|
|
except Exception as e:
|
|
print(f"Error deleting chain from node {node_id}: {e}")
|
|
success = False
|
|
|
|
# Remove from cache
|
|
if chain_id in self._chain_cache:
|
|
del self._chain_cache[chain_id]
|
|
|
|
return success
|
|
|
|
async def add_chain_to_node(self, chain_id: str, node_id: str) -> bool:
|
|
"""Add a chain to a node"""
|
|
# Validate node
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
# Get chain info
|
|
chain_info = await self.get_chain_info(chain_id)
|
|
|
|
# Add chain to node
|
|
try:
|
|
await self._add_chain_to_node(node_id, chain_info)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error adding chain to node: {e}")
|
|
return False
|
|
|
|
async def remove_chain_from_node(self, chain_id: str, node_id: str, migrate: bool = False) -> bool:
|
|
"""Remove a chain from a node"""
|
|
# Validate node
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
if migrate:
|
|
# Find alternative node
|
|
target_node = await self._find_alternative_node(chain_id, node_id)
|
|
if target_node:
|
|
# Migrate chain first
|
|
migration_result = await self.migrate_chain(chain_id, node_id, target_node)
|
|
if not migration_result.success:
|
|
return False
|
|
|
|
# Remove chain from node
|
|
try:
|
|
await self._remove_chain_from_node(node_id, chain_id)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error removing chain from node: {e}")
|
|
return False
|
|
|
|
async def migrate_chain(self, chain_id: str, from_node: str, to_node: str, dry_run: bool = False) -> ChainMigrationResult:
|
|
"""Migrate a chain between nodes"""
|
|
# Validate nodes
|
|
if from_node not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Source node {from_node} not configured")
|
|
if to_node not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Target node {to_node} not configured")
|
|
|
|
# Get chain info
|
|
chain_info = await self.get_chain_info(chain_id)
|
|
|
|
# Create migration plan
|
|
migration_plan = await self._create_migration_plan(chain_id, from_node, to_node, chain_info)
|
|
|
|
if dry_run:
|
|
return ChainMigrationResult(
|
|
chain_id=chain_id,
|
|
source_node=from_node,
|
|
target_node=to_node,
|
|
success=migration_plan.feasible,
|
|
blocks_transferred=0,
|
|
transfer_time_seconds=0,
|
|
verification_passed=False,
|
|
error=None if migration_plan.feasible else "Migration not feasible"
|
|
)
|
|
|
|
if not migration_plan.feasible:
|
|
return ChainMigrationResult(
|
|
chain_id=chain_id,
|
|
source_node=from_node,
|
|
target_node=to_node,
|
|
success=False,
|
|
blocks_transferred=0,
|
|
transfer_time_seconds=0,
|
|
verification_passed=False,
|
|
error="; ".join(migration_plan.issues)
|
|
)
|
|
|
|
# Execute migration
|
|
return await self._execute_migration(chain_id, from_node, to_node)
|
|
|
|
async def backup_chain(self, chain_id: str, backup_path: Optional[str] = None, compress: bool = False, verify: bool = False) -> ChainBackupResult:
|
|
"""Backup a chain"""
|
|
# Get chain info
|
|
chain_info = await self.get_chain_info(chain_id)
|
|
|
|
# Get hosting node
|
|
hosting_nodes = await self._get_chain_hosting_nodes(chain_id)
|
|
if not hosting_nodes:
|
|
raise ChainNotFoundError(f"Chain {chain_id} not found on any node")
|
|
|
|
node_id = hosting_nodes[0] # Use first available node
|
|
|
|
# Set backup path
|
|
if not backup_path:
|
|
backup_path = self.config.chains.backup_path / f"{chain_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz"
|
|
|
|
# Execute backup
|
|
return await self._execute_backup(chain_id, node_id, backup_path, compress, verify)
|
|
|
|
async def restore_chain(self, backup_file: str, node_id: Optional[str] = None, verify: bool = False) -> ChainRestoreResult:
|
|
"""Restore a chain from backup"""
|
|
backup_path = Path(backup_file)
|
|
if not backup_path.exists():
|
|
raise FileNotFoundError(f"Backup file {backup_file} not found")
|
|
|
|
# Select node if not specified
|
|
if not node_id:
|
|
node_id = await self._select_best_node_for_restore()
|
|
|
|
# Execute restore
|
|
return await self._execute_restore(backup_path, node_id, verify)
|
|
|
|
# Private methods
|
|
|
|
def _generate_chain_id(self, chain_config: ChainConfig) -> str:
|
|
"""Generate a unique chain ID"""
|
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
prefix = f"AITBC-{chain_config.type.value.upper()}-{chain_config.purpose.upper()}"
|
|
return f"{prefix}-{timestamp}"
|
|
|
|
async def _get_node_chains(self, node_id: str) -> List[ChainInfo]:
|
|
"""Get chains from a specific node"""
|
|
if node_id not in self.config.nodes:
|
|
return []
|
|
|
|
node_config = self.config.nodes[node_id]
|
|
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
return await client.get_hosted_chains()
|
|
except Exception as e:
|
|
print(f"Error getting chains from node {node_id}: {e}")
|
|
return []
|
|
|
|
async def _find_chain_on_nodes(self, chain_id: str) -> Optional[ChainInfo]:
|
|
"""Find a chain on available nodes"""
|
|
for node_id in self.config.nodes:
|
|
try:
|
|
chains = await self._get_node_chains(node_id)
|
|
for chain in chains:
|
|
if chain.id == chain_id:
|
|
return chain
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
async def _enrich_chain_info(self, chain_info: ChainInfo) -> ChainInfo:
|
|
"""Enrich chain info with detailed data"""
|
|
# This would get additional metrics and detailed information
|
|
# For now, return the same chain info
|
|
return chain_info
|
|
|
|
async def _select_best_node(self, chain_config: ChainConfig) -> str:
|
|
"""Select the best node for creating a chain"""
|
|
# Simple selection - in reality, this would consider load, resources, etc.
|
|
available_nodes = list(self.config.nodes.keys())
|
|
if not available_nodes:
|
|
raise NodeNotAvailableError("No nodes available")
|
|
return available_nodes[0]
|
|
|
|
async def _create_genesis_block(self, chain_config: ChainConfig, chain_id: str) -> GenesisBlock:
|
|
"""Create a genesis block for the chain"""
|
|
timestamp = datetime.now()
|
|
|
|
# Create state root (placeholder)
|
|
state_data = {
|
|
"chain_id": chain_id,
|
|
"config": chain_config.dict(),
|
|
"timestamp": timestamp.isoformat()
|
|
}
|
|
state_root = hashlib.sha256(json.dumps(state_data, sort_keys=True).encode()).hexdigest()
|
|
|
|
# Create genesis hash
|
|
genesis_data = {
|
|
"chain_id": chain_id,
|
|
"timestamp": timestamp.isoformat(),
|
|
"state_root": state_root
|
|
}
|
|
genesis_hash = hashlib.sha256(json.dumps(genesis_data, sort_keys=True).encode()).hexdigest()
|
|
|
|
return GenesisBlock(
|
|
chain_id=chain_id,
|
|
chain_type=chain_config.type,
|
|
purpose=chain_config.purpose,
|
|
name=chain_config.name,
|
|
description=chain_config.description,
|
|
timestamp=timestamp,
|
|
consensus=chain_config.consensus,
|
|
privacy=chain_config.privacy,
|
|
parameters=chain_config.parameters,
|
|
state_root=state_root,
|
|
hash=genesis_hash
|
|
)
|
|
|
|
async def _create_chain_on_node(self, node_id: str, genesis_block: GenesisBlock) -> None:
|
|
"""Create a chain on a specific node"""
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
node_config = self.config.nodes[node_id]
|
|
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
chain_id = await client.create_chain(genesis_block.dict())
|
|
print(f"Successfully created chain {chain_id} on node {node_id}")
|
|
except Exception as e:
|
|
print(f"Error creating chain on node {node_id}: {e}")
|
|
raise
|
|
|
|
async def _get_chain_hosting_nodes(self, chain_id: str) -> List[str]:
|
|
"""Get all nodes hosting a specific chain"""
|
|
hosting_nodes = []
|
|
for node_id in self.config.nodes:
|
|
try:
|
|
chains = await self._get_node_chains(node_id)
|
|
if any(chain.id == chain_id for chain in chains):
|
|
hosting_nodes.append(node_id)
|
|
except Exception:
|
|
continue
|
|
return hosting_nodes
|
|
|
|
async def _delete_chain_from_node(self, node_id: str, chain_id: str) -> None:
|
|
"""Delete a chain from a specific node"""
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
node_config = self.config.nodes[node_id]
|
|
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
success = await client.delete_chain(chain_id)
|
|
if success:
|
|
print(f"Successfully deleted chain {chain_id} from node {node_id}")
|
|
else:
|
|
raise Exception(f"Failed to delete chain {chain_id}")
|
|
except Exception as e:
|
|
print(f"Error deleting chain from node {node_id}: {e}")
|
|
raise
|
|
|
|
async def _add_chain_to_node(self, node_id: str, chain_info: ChainInfo) -> None:
|
|
"""Add a chain to a specific node"""
|
|
# This would actually add the chain to the node
|
|
print(f"Adding chain {chain_info.id} to node {node_id}")
|
|
|
|
async def _remove_chain_from_node(self, node_id: str, chain_id: str) -> None:
|
|
"""Remove a chain from a specific node"""
|
|
# This would actually remove the chain from the node
|
|
print(f"Removing chain {chain_id} from node {node_id}")
|
|
|
|
async def _find_alternative_node(self, chain_id: str, exclude_node: str) -> Optional[str]:
|
|
"""Find an alternative node for a chain"""
|
|
hosting_nodes = await self._get_chain_hosting_nodes(chain_id)
|
|
for node_id in hosting_nodes:
|
|
if node_id != exclude_node:
|
|
return node_id
|
|
return None
|
|
|
|
async def _create_migration_plan(self, chain_id: str, from_node: str, to_node: str, chain_info: ChainInfo) -> ChainMigrationPlan:
|
|
"""Create a migration plan"""
|
|
# This would analyze the migration and create a detailed plan
|
|
return ChainMigrationPlan(
|
|
chain_id=chain_id,
|
|
source_node=from_node,
|
|
target_node=to_node,
|
|
size_mb=chain_info.size_mb,
|
|
estimated_minutes=int(chain_info.size_mb / 100), # Rough estimate
|
|
required_space_mb=chain_info.size_mb * 1.5, # 50% extra space
|
|
available_space_mb=10000, # Placeholder
|
|
feasible=True,
|
|
issues=[]
|
|
)
|
|
|
|
async def _execute_migration(self, chain_id: str, from_node: str, to_node: str) -> ChainMigrationResult:
|
|
"""Execute the actual migration"""
|
|
# This would actually execute the migration
|
|
print(f"Migrating chain {chain_id} from {from_node} to {to_node}")
|
|
|
|
return ChainMigrationResult(
|
|
chain_id=chain_id,
|
|
source_node=from_node,
|
|
target_node=to_node,
|
|
success=True,
|
|
blocks_transferred=1000, # Placeholder
|
|
transfer_time_seconds=300, # Placeholder
|
|
verification_passed=True
|
|
)
|
|
|
|
async def _execute_backup(self, chain_id: str, node_id: str, backup_path: str, compress: bool, verify: bool) -> ChainBackupResult:
|
|
"""Execute the actual backup"""
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
node_config = self.config.nodes[node_id]
|
|
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
backup_info = await client.backup_chain(chain_id, backup_path)
|
|
|
|
return ChainBackupResult(
|
|
chain_id=chain_id,
|
|
backup_file=backup_info["backup_file"],
|
|
original_size_mb=backup_info["original_size_mb"],
|
|
backup_size_mb=backup_info["backup_size_mb"],
|
|
compression_ratio=backup_info["original_size_mb"] / backup_info["backup_size_mb"],
|
|
checksum=backup_info["checksum"],
|
|
verification_passed=verify
|
|
)
|
|
except Exception as e:
|
|
print(f"Error during backup: {e}")
|
|
raise
|
|
|
|
async def _execute_restore(self, backup_path: str, node_id: str, verify: bool) -> ChainRestoreResult:
|
|
"""Execute the actual restore"""
|
|
if node_id not in self.config.nodes:
|
|
raise NodeNotAvailableError(f"Node {node_id} not configured")
|
|
|
|
node_config = self.config.nodes[node_id]
|
|
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
restore_info = await client.restore_chain(backup_path)
|
|
|
|
return ChainRestoreResult(
|
|
chain_id=restore_info["chain_id"],
|
|
node_id=node_id,
|
|
blocks_restored=restore_info["blocks_restored"],
|
|
verification_passed=restore_info["verification_passed"]
|
|
)
|
|
except Exception as e:
|
|
print(f"Error during restore: {e}")
|
|
raise
|
|
|
|
async def _select_best_node_for_restore(self) -> str:
|
|
"""Select the best node for restoring a chain"""
|
|
available_nodes = list(self.config.nodes.keys())
|
|
if not available_nodes:
|
|
raise NodeNotAvailableError("No nodes available")
|
|
return available_nodes[0]
|