diff --git a/.gitignore b/.gitignore index 8b65b0a5..49d1dfc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # AITBC Monorepo ignore rules +# =================== # Python +# =================== __pycache__/ *.pyc *.pyo @@ -9,30 +11,134 @@ __pycache__/ .venv/ */.venv/ venv/ +env/ +*.egg-info/ +*.egg +.eggs/ +pip-wheel-metadata/ +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.mypy_cache/ +.ruff_cache/ + +# Environment files *.env *.env.* +.env.local +.env.*.local -# Databases & Alembic artifacts +# =================== +# Databases +# =================== *.db +*.sqlite +*.sqlite3 */data/*.db +data/ + +# Alembic alembic.ini migrations/versions/__pycache__/ -# Node / JS +# =================== +# Node / JavaScript +# =================== node_modules/ dist/ build/ .npm/ +.pnpm/ yarn.lock package-lock.json pnpm-lock.yaml +.next/ +.nuxt/ +.cache/ -# Editor +# =================== +# Logs & Runtime +# =================== +logs/ +*.log +*.log.* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pids/ +*.pid +*.seed + +# =================== +# Editor & IDE +# =================== .idea/ .vscode/ *.swp *.swo +*~ +.project +.classpath +.settings/ -# OS +# =================== +# OS Files +# =================== .DS_Store +.DS_Store? +._* Thumbs.db +ehthumbs.db +Desktop.ini + +# =================== +# Build & Compiled +# =================== +*.o +*.a +*.lib +*.dll +*.dylib +target/ +out/ + +# =================== +# Secrets & Credentials +# =================== +*.pem +*.key +*.crt +*.p12 +secrets/ +credentials/ +.secrets + +# =================== +# Temporary Files +# =================== +tmp/ +temp/ +*.tmp +*.temp +*.bak +*.backup + +# =================== +# Project Specific +# =================== +# Coordinator database +apps/coordinator-api/src/*.db + +# Explorer build artifacts +apps/explorer-web/dist/ + +# Local test data +tests/fixtures/generated/ + +# GPU miner local configs +scripts/gpu/*.local.py + +# Deployment secrets +scripts/deploy/*.secret.* +infra/nginx/*.local.conf diff --git a/.windsurf/skills/blockchain-operations/SKILL.md b/.windsurf/skills/blockchain-operations/SKILL.md new file mode 100644 index 00000000..6a469012 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/SKILL.md @@ -0,0 +1,97 @@ +--- +name: blockchain-operations +description: Comprehensive blockchain node management and operations for AITBC +version: 1.0.0 +author: Cascade +tags: [blockchain, node, mining, transactions, aitbc, operations] +--- + +# Blockchain Operations Skill + +This skill provides standardized procedures for managing AITBC blockchain nodes, verifying transactions, and optimizing mining operations. + +## Overview + +The blockchain operations skill ensures reliable management of all blockchain-related components including node synchronization, transaction processing, mining operations, and network health monitoring. + +## Capabilities + +### Node Management +- Node deployment and configuration +- Sync status monitoring +- Peer management +- Network diagnostics + +### Transaction Operations +- Transaction verification and debugging +- Gas optimization +- Batch processing +- Mempool management + +### Mining Operations +- Mining performance optimization +- Pool management +- Reward tracking +- Hash rate optimization + +### Network Health +- Network connectivity checks +- Block propagation monitoring +- Fork detection and resolution +- Consensus validation + +## Common Workflows + +### 1. Node Health Check +- Verify node synchronization +- Check peer connections +- Validate consensus rules +- Monitor resource usage + +### 2. Transaction Debugging +- Trace transaction lifecycle +- Verify gas usage +- Check receipt status +- Debug failed transactions + +### 3. Mining Optimization +- Analyze mining performance +- Optimize GPU settings +- Configure mining pools +- Monitor profitability + +### 4. Network Diagnostics +- Test connectivity to peers +- Analyze block propagation +- Detect network partitions +- Validate consensus state + +## Supporting Files + +- `node-health.sh` - Comprehensive node health monitoring +- `tx-tracer.py` - Transaction tracing and debugging tool +- `mining-optimize.sh` - GPU mining optimization script +- `network-diag.py` - Network diagnostics and analysis +- `sync-monitor.py` - Real-time sync status monitor + +## Usage + +This skill is automatically invoked when you request blockchain-related operations such as: +- "check node status" +- "debug transaction" +- "optimize mining" +- "network diagnostics" + +## Safety Features + +- Automatic backup of node data before operations +- Validation of all transactions before processing +- Safe mining parameter adjustments +- Rollback capability for configuration changes + +## Prerequisites + +- AITBC node installed and configured +- GPU drivers installed (for mining operations) +- Proper network connectivity +- Sufficient disk space for blockchain data diff --git a/.windsurf/skills/blockchain-operations/mining-optimize.sh b/.windsurf/skills/blockchain-operations/mining-optimize.sh new file mode 100755 index 00000000..2e0b49d0 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/mining-optimize.sh @@ -0,0 +1,296 @@ +#!/bin/bash + +# AITBC GPU Mining Optimization Script +# Optimizes GPU settings for maximum mining efficiency + +set -e + +# Configuration +LOG_FILE="/var/log/aitbc/mining-optimize.log" +CONFIG_FILE="/etc/aitbc/mining.conf" +GPU_VENDOR="" # Will be auto-detected + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE +} + +# Detect GPU vendor +detect_gpu() { + echo -e "${BLUE}=== Detecting GPU ===${NC}" + + if command -v nvidia-smi &> /dev/null; then + GPU_VENDOR="nvidia" + echo -e "${GREEN}✓${NC} NVIDIA GPU detected" + log "GPU vendor: NVIDIA" + elif command -v rocm-smi &> /dev/null; then + GPU_VENDOR="amd" + echo -e "${GREEN}✓${NC} AMD GPU detected" + log "GPU vendor: AMD" + elif lspci | grep -i vga &> /dev/null; then + echo -e "${YELLOW}⚠${NC} GPU detected but vendor-specific tools not found" + log "GPU detected but vendor unknown" + GPU_VENDOR="unknown" + else + echo -e "${RED}✗${NC} No GPU detected" + log "No GPU detected - cannot optimize mining" + exit 1 + fi +} + +# Get GPU information +get_gpu_info() { + echo -e "\n${BLUE}=== GPU Information ===${NC}" + + case $GPU_VENDOR in + "nvidia") + nvidia-smi --query-gpu=name,memory.total,temperature.gpu,utilization.gpu,power.draw --format=csv,noheader,nounits + ;; + "amd") + rocm-smi --showproductname + rocm-smi --showmeminfo vram + rocm-smi --showtemp + ;; + *) + echo "GPU info not available for vendor: $GPU_VENDOR" + ;; + esac +} + +# Optimize NVIDIA GPU +optimize_nvidia() { + echo -e "\n${BLUE}=== Optimizing NVIDIA GPU ===${NC}" + + # Get current power limit + CURRENT_POWER=$(nvidia-smi --query-gpu=power.limit --format=csv,noheader,nounits | head -n1) + echo "Current power limit: ${CURRENT_POWER}W" + + # Set optimal power limit (80% of max for efficiency) + MAX_POWER=$(nvidia-smi --query-gpu=power.max_limit --format=csv,noheader,nounits | head -n1) + OPTIMAL_POWER=$((MAX_POWER * 80 / 100)) + + echo "Setting power limit to ${OPTIMAL_POWER}W (80% of max)" + sudo nvidia-smi -pl $OPTIMAL_POWER + log "NVIDIA power limit set to ${OPTIMAL_POWER}W" + + # Set performance mode + echo "Setting performance mode to maximum" + sudo nvidia-smi -ac 877,1215 + log "NVIDIA performance mode set to maximum" + + # Configure memory clock + echo "Optimizing memory clock" + sudo nvidia-smi -pm 1 + log "NVIDIA persistence mode enabled" + + # Create optimized mining config + cat > $CONFIG_FILE << EOF +[nvidia] +power_limit = $OPTIMAL_POWER +performance_mode = maximum +memory_clock = max +fan_speed = auto +temperature_limit = 85 +EOF + + echo -e "${GREEN}✓${NC} NVIDIA GPU optimized" +} + +# Optimize AMD GPU +optimize_amd() { + echo -e "\n${BLUE}=== Optimizing AMD GPU ===${NC}" + + # Set performance level + echo "Setting performance level to high" + sudo rocm-smi --setperflevel high + log "AMD performance level set to high" + + # Set memory clock + echo "Optimizing memory clock" + sudo rocm-smi --setmclk 1 + log "AMD memory clock optimized" + + # Create optimized mining config + cat > $CONFIG_FILE << EOF +[amd] +performance_level = high +memory_clock = high +fan_speed = auto +temperature_limit = 85 +EOF + + echo -e "${GREEN}✓${NC} AMD GPU optimized" +} + +# Monitor mining performance +monitor_mining() { + echo -e "\n${BLUE}=== Mining Performance Monitor ===${NC}" + + # Check if miner is running + if ! pgrep -f "aitbc-miner" > /dev/null; then + echo -e "${YELLOW}⚠${NC} Miner is not running" + return 1 + fi + + # Monitor for 30 seconds + echo "Monitoring mining performance for 30 seconds..." + + for i in {1..6}; do + echo -e "\n--- Check $i/6 ---" + + case $GPU_VENDOR in + "nvidia") + nvidia-smi --query-gpu=temperature.gpu,utilization.gpu,power.draw,fan.speed --format=csv,noheader,nounits + ;; + "amd") + rocm-smi --showtemp --showutilization + ;; + esac + + # Get hash rate from miner API + if curl -s http://localhost:8081/api/status > /dev/null; then + HASHRATE=$(curl -s http://localhost:8081/api/status | jq -r '.hashrate') + echo "Hash rate: ${HASHRATE} H/s" + fi + + sleep 5 + done +} + +# Tune fan curves +tune_fans() { + echo -e "\n${BLUE}=== Tuning Fan Curves ===${NC}" + + case $GPU_VENDOR in + "nvidia") + # Set custom fan curve + echo "Setting custom fan curve for NVIDIA" + # This would use nvidia-settings or similar + echo "Target: 30% fan at 50°C, 60% at 70°C, 100% at 85°C" + log "NVIDIA fan curve configured" + ;; + "amd") + echo "Setting fan control to auto for AMD" + # AMD cards usually handle this automatically + log "AMD fan control set to auto" + ;; + esac +} + +# Check mining profitability +check_profitability() { + echo -e "\n${BLUE}=== Profitability Analysis ===${NC}" + + # Get current hash rate + if curl -s http://localhost:8081/api/status > /dev/null; then + HASHRATE=$(curl -s http://localhost:8081/api/status | jq -r '.hashrate') + POWER_USAGE=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits | head -n1) + + echo "Current hash rate: ${HASHRATE} H/s" + echo "Power usage: ${POWER_USAGE}W" + + # Calculate efficiency + if [ "$HASHRATE" != "null" ] && [ -n "$POWER_USAGE" ]; then + EFFICIENCY=$(echo "scale=2; $HASHRATE / $POWER_USAGE" | bc) + echo "Efficiency: ${EFFICIENCY} H/W" + + # Efficiency rating + if (( $(echo "$EFFICIENCY > 10" | bc -l) )); then + echo -e "${GREEN}✓${NC} Excellent efficiency" + elif (( $(echo "$EFFICIENCY > 5" | bc -l) )); then + echo -e "${YELLOW}⚠${NC} Good efficiency" + else + echo -e "${RED}✗${NC} Poor efficiency - consider optimization" + fi + fi + else + echo "Miner API not accessible" + fi +} + +# Generate optimization report +generate_report() { + echo -e "\n${BLUE}=== Optimization Report ===${NC}" + + echo "GPU Vendor: $GPU_VENDOR" + echo "Configuration: $CONFIG_FILE" + echo "Optimization completed: $(date)" + + # Current settings + echo -e "\nCurrent Settings:" + case $GPU_VENDOR in + "nvidia") + nvidia-smi --query-gpu=power.limit,temperature.gpu,utilization.gpu --format=csv,noheader,nounits + ;; + "amd") + rocm-smi --showtemp --showutilization + ;; + esac + + log "Optimization report generated" +} + +# Main execution +main() { + log "Starting mining optimization" + echo -e "${BLUE}AITBC GPU Mining Optimizer${NC}" + echo "===============================" + + # Check root privileges + if [ "$EUID" -ne 0 ]; then + echo -e "${YELLOW}⚠${NC} Some optimizations require sudo privileges" + fi + + detect_gpu + get_gpu_info + + # Perform optimization based on vendor + case $GPU_VENDOR in + "nvidia") + optimize_nvidia + ;; + "amd") + optimize_amd + ;; + *) + echo -e "${YELLOW}⚠${NC} Cannot optimize unknown GPU vendor" + ;; + esac + + tune_fans + monitor_mining + check_profitability + generate_report + + echo -e "\n${GREEN}Mining optimization completed!${NC}" + echo "Configuration saved to: $CONFIG_FILE" + echo "Log saved to: $LOG_FILE" + + log "Mining optimization completed successfully" +} + +# Parse command line arguments +case "${1:-optimize}" in + "optimize") + main + ;; + "monitor") + detect_gpu + monitor_mining + ;; + "report") + detect_gpu + generate_report + ;; + *) + echo "Usage: $0 [optimize|monitor|report]" + exit 1 + ;; +esac diff --git a/.windsurf/skills/blockchain-operations/network-diag.py b/.windsurf/skills/blockchain-operations/network-diag.py new file mode 100755 index 00000000..784ceb15 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/network-diag.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 +""" +AITBC Network Diagnostics Tool +Analyzes network connectivity, peer health, and block propagation +""" + +import json +import sys +import time +import socket +import subprocess +from datetime import datetime +from typing import Dict, List, Tuple, Optional +import requests + +class NetworkDiagnostics: + def __init__(self, node_url: str = "http://localhost:8545"): + """Initialize network diagnostics""" + self.node_url = node_url + self.results = {} + + def rpc_call(self, method: str, params: List = None) -> Optional[Dict]: + """Make JSON-RPC call to node""" + try: + response = requests.post( + self.node_url, + json={ + "jsonrpc": "2.0", + "method": method, + "params": params or [], + "id": 1 + }, + timeout=10 + ) + return response.json().get('result') + except Exception as e: + return None + + def check_connectivity(self) -> Dict[str, any]: + """Check basic network connectivity""" + print("Checking network connectivity...") + + results = { + 'node_reachable': False, + 'dns_resolution': {}, + 'port_checks': {}, + 'internet_connectivity': False + } + + # Check if node is reachable + try: + response = requests.get(self.node_url, timeout=5) + results['node_reachable'] = response.status_code == 200 + except: + pass + + # DNS resolution checks + domains = ['aitbc.io', 'api.aitbc.io', 'mainnet.aitbc.io'] + for domain in domains: + try: + ip = socket.gethostbyname(domain) + results['dns_resolution'][domain] = { + 'resolvable': True, + 'ip': ip + } + except: + results['dns_resolution'][domain] = { + 'resolvable': False, + 'ip': None + } + + # Port checks + ports = [ + ('localhost', 8545, 'RPC'), + ('localhost', 8546, 'WS'), + ('localhost', 30303, 'P2P TCP'), + ('localhost', 30303, 'P2P UDP') + ] + + for host, port, service in ports: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(3) + result = sock.connect_ex((host, port)) + results['port_checks'][f'{host}:{port} ({service})'] = result == 0 + sock.close() + + # Internet connectivity + try: + response = requests.get('https://google.com', timeout=5) + results['internet_connectivity'] = response.status_code == 200 + except: + pass + + self.results['connectivity'] = results + return results + + def analyze_peers(self) -> Dict[str, any]: + """Analyze peer connections""" + print("Analyzing peer connections...") + + results = { + 'peer_count': 0, + 'peer_details': [], + 'peer_distribution': {}, + 'connection_types': {}, + 'latency_stats': {} + } + + # Get peer list + peers = self.rpc_call("admin_peers") or [] + results['peer_count'] = len(peers) + + # Analyze each peer + for peer in peers: + peer_info = { + 'id': (peer.get('id', '')[:10] + '...') if peer.get('id') else '', + 'address': peer.get('network', {}).get('remoteAddress', 'Unknown'), + 'local_address': peer.get('network', {}).get('localAddress', 'Unknown'), + 'caps': list(peer.get('protocols', {}).keys()), + 'connected_duration': peer.get('network', {}).get('connectedDuration', 0) + } + + # Extract IP for geolocation + if ':' in peer_info['address']: + ip = peer_info['address'].split(':')[0] + peer_info['ip'] = ip + + # Get country (would use geoip library in production) + try: + # Simple ping test for latency + start = time.time() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + result = sock.connect_ex((ip, 30303)) + latency = (time.time() - start) * 1000 if result == 0 else None + sock.close() + peer_info['latency_ms'] = latency + except: + peer_info['latency_ms'] = None + + results['peer_details'].append(peer_info) + + # Calculate distribution + countries = {} + for peer in results['peer_details']: + country = peer.get('country', 'Unknown') + countries[country] = countries.get(country, 0) + 1 + results['peer_distribution'] = countries + + # Calculate latency stats + latencies = [p['latency_ms'] for p in results['peer_details'] if p['latency_ms'] is not None] + if latencies: + results['latency_stats'] = { + 'min': min(latencies), + 'max': max(latencies), + 'avg': sum(latencies) / len(latencies) + } + + self.results['peers'] = results + return results + + def test_block_propagation(self) -> Dict[str, any]: + """Test block propagation speed""" + print("Testing block propagation...") + + results = { + 'latest_block': 0, + 'block_age': 0, + 'propagation_delay': None, + 'uncle_rate': 0, + 'network_hashrate': 0 + } + + # Get latest block + latest_block = self.rpc_call("eth_getBlockByNumber", ["latest", False]) + if latest_block: + results['latest_block'] = int(latest_block['number'], 16) + block_timestamp = int(latest_block['timestamp'], 16) + results['block_age'] = int(time.time()) - block_timestamp + + # Get uncle rate (check last 100 blocks) + try: + uncle_count = 0 + for i in range(100): + block = self.rpc_call("eth_getBlockByNumber", [hex(results['latest_block'] - i), False]) + if block and block.get('uncles'): + uncle_count += len(block['uncles']) + results['uncle_rate'] = (uncle_count / 100) * 100 + except: + pass + + # Get network hashrate + try: + latest = self.rpc_call("eth_getBlockByNumber", ["latest", False]) + if latest: + difficulty = int(latest['difficulty'], 16) + block_time = 13 # Average block time for ETH-like chains + results['network_hashrate'] = difficulty / block_time + except: + pass + + self.results['block_propagation'] = results + return results + + def check_fork_status(self) -> Dict[str, any]: + """Check for network forks""" + print("Checking for network forks...") + + results = { + 'current_fork': None, + 'fork_blocks': [], + 'reorg_detected': False, + 'chain_head': {} + } + + # Get current fork block + try: + fork_block = self.rpc_call("eth_forkBlock") + if fork_block: + results['current_fork'] = int(fork_block, 16) + except: + pass + + # Check for recent reorganizations + try: + # Get last 10 blocks and check for inconsistencies + for i in range(10): + block_num = hex(int(self.rpc_call("eth_blockNumber"), 16) - i) + block = self.rpc_call("eth_getBlockByNumber", [block_num, False]) + if block: + results['chain_head'][block_num] = { + 'hash': block['hash'], + 'parent': block.get('parentHash'), + 'number': block['number'] + } + except: + pass + + self.results['fork_status'] = results + return results + + def analyze_network_performance(self) -> Dict[str, any]: + """Analyze overall network performance""" + print("Analyzing network performance...") + + results = { + 'rpc_response_time': 0, + 'ws_connection': False, + 'bandwidth_estimate': 0, + 'packet_loss': 0 + } + + # Test RPC response time + start = time.time() + self.rpc_call("eth_blockNumber") + results['rpc_response_time'] = (time.time() - start) * 1000 + + # Test WebSocket connection + try: + import websocket + # Would implement actual WS connection test + results['ws_connection'] = True + except: + results['ws_connection'] = False + + self.results['performance'] = results + return results + + def generate_recommendations(self) -> List[str]: + """Generate network improvement recommendations""" + recommendations = [] + + # Connectivity recommendations + if not self.results.get('connectivity', {}).get('node_reachable'): + recommendations.append("Node is not reachable - check if the node is running") + + if not self.results.get('connectivity', {}).get('internet_connectivity'): + recommendations.append("No internet connectivity - check network connection") + + # Peer recommendations + peer_count = self.results.get('peers', {}).get('peer_count', 0) + if peer_count < 5: + recommendations.append(f"Low peer count ({peer_count}) - check firewall and port forwarding") + + # Performance recommendations + rpc_time = self.results.get('performance', {}).get('rpc_response_time', 0) + if rpc_time > 1000: + recommendations.append("High RPC response time - consider optimizing node or upgrading hardware") + + # Block propagation recommendations + block_age = self.results.get('block_propagation', {}).get('block_age', 0) + if block_age > 60: + recommendations.append("Stale blocks detected - possible sync issues") + + return recommendations + + def print_report(self): + """Print comprehensive diagnostic report""" + print("\n" + "="*60) + print("AITBC Network Diagnostics Report") + print("="*60) + print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"Node URL: {self.node_url}") + + # Connectivity section + print("\n[Connectivity]") + conn = self.results.get('connectivity', {}) + print(f" Node Reachable: {'✓' if conn.get('node_reachable') else '✗'}") + print(f" Internet Access: {'✓' if conn.get('internet_connectivity') else '✗'}") + + for domain, info in conn.get('dns_resolution', {}).items(): + status = '✓' if info['resolvable'] else '✗' + print(f" DNS {domain}: {status}") + + # Peers section + print("\n[Peer Analysis]") + peers = self.results.get('peers', {}) + print(f" Connected Peers: {peers.get('peer_count', 0)}") + + if peers.get('peer_distribution'): + print(" Geographic Distribution:") + for country, count in list(peers['peer_distribution'].items())[:5]: + print(f" {country}: {count} peers") + + if peers.get('latency_stats'): + stats = peers['latency_stats'] + print(f" Latency: {stats['avg']:.0f}ms avg (min: {stats['min']:.0f}ms, max: {stats['max']:.0f}ms)") + + # Block propagation section + print("\n[Block Propagation]") + prop = self.results.get('block_propagation', {}) + print(f" Latest Block: {prop.get('latest_block', 0):,}") + print(f" Block Age: {prop.get('block_age', 0)} seconds") + print(f" Uncle Rate: {prop.get('uncle_rate', 0):.2f}%") + + # Performance section + print("\n[Performance]") + perf = self.results.get('performance', {}) + print(f" RPC Response Time: {perf.get('rpc_response_time', 0):.0f}ms") + print(f" WebSocket: {'✓' if perf.get('ws_connection') else '✗'}") + + # Recommendations + recommendations = self.generate_recommendations() + if recommendations: + print("\n[Recommendations]") + for i, rec in enumerate(recommendations, 1): + print(f" {i}. {rec}") + + print("\n" + "="*60) + + def save_report(self, filename: str): + """Save detailed report to file""" + report = { + 'timestamp': datetime.now().isoformat(), + 'node_url': self.node_url, + 'results': self.results, + 'recommendations': self.generate_recommendations() + } + + with open(filename, 'w') as f: + json.dump(report, f, indent=2) + + print(f"\nDetailed report saved to: {filename}") + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='AITBC Network Diagnostics') + parser.add_argument('--node', default='http://localhost:8545', help='Node URL') + parser.add_argument('--output', help='Save report to file') + parser.add_argument('--quick', action='store_true', help='Quick diagnostics') + + args = parser.parse_args() + + # Run diagnostics + diag = NetworkDiagnostics(args.node) + + print("Running AITBC network diagnostics...") + print("-" * 40) + + # Run all tests + diag.check_connectivity() + + if not args.quick: + diag.analyze_peers() + diag.test_block_propagation() + diag.check_fork_status() + diag.analyze_network_performance() + + # Print report + diag.print_report() + + # Save if requested + if args.output: + diag.save_report(args.output) + +if __name__ == "__main__": + main() diff --git a/.windsurf/skills/blockchain-operations/node-health.sh b/.windsurf/skills/blockchain-operations/node-health.sh new file mode 100755 index 00000000..40f02ff8 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/node-health.sh @@ -0,0 +1,248 @@ +#!/bin/bash + +# AITBC Node Health Check Script +# Monitors and reports on blockchain node health + +set -e + +# Configuration +NODE_URL="http://localhost:8545" +LOG_FILE="/var/log/aitbc/node-health.log" +ALERT_THRESHOLD=90 # Sync threshold percentage + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE +} + +# JSON RPC call function +rpc_call() { + local method=$1 + local params=$2 + curl -s -X POST $NODE_URL \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" \ + | jq -r '.result' +} + +# Check if node is running +check_node_running() { + echo -e "\n${BLUE}=== Checking Node Status ===${NC}" + + if pgrep -f "aitbc-node" > /dev/null; then + echo -e "${GREEN}✓${NC} AITBC node process is running" + log "Node process: RUNNING" + else + echo -e "${RED}✗${NC} AITBC node is not running" + log "Node process: NOT RUNNING" + return 1 + fi +} + +# Check sync status +check_sync_status() { + echo -e "\n${BLUE}=== Checking Sync Status ===${NC}" + + local sync_result=$(rpc_call "eth_syncing" "[]") + + if [ "$sync_result" = "false" ]; then + echo -e "${GREEN}✓${NC} Node is fully synchronized" + log "Sync status: FULLY SYNCED" + else + local current_block=$(echo $sync_result | jq -r '.currentBlock') + local highest_block=$(echo $sync_result | jq -r '.highestBlock') + local sync_percent=$(echo "scale=2; $current_block * 100 / $highest_block" | bc) + + if (( $(echo "$sync_percent > $ALERT_THRESHOLD" | bc -l) )); then + echo -e "${YELLOW}⚠${NC} Node syncing: ${sync_percent}% (Block $current_block / $highest_block)" + log "Sync status: SYNCING at ${sync_percent}%" + else + echo -e "${RED}✗${NC} Node far behind: ${sync_percent}% (Block $current_block / $highest_block)" + log "Sync status: FAR BEHIND at ${sync_percent}%" + fi + fi +} + +# Check peer connections +check_peers() { + echo -e "\n${BLUE}=== Checking Peer Connections ===${NC}" + + local peer_count=$(rpc_call "net_peerCount" "[]") + local peer_count_dec=$((peer_count)) + + if [ $peer_count_dec -gt 0 ]; then + echo -e "${GREEN}✓${NC} Connected to $peer_count_dec peers" + log "Peer count: $peer_count_dec" + + # Get detailed peer info + local peers=$(rpc_call "admin_peers" "[]") + local active_peers=$(echo $peers | jq '. | length') + echo -e " Active peers: $active_peers" + + # Show peer countries + echo -e "\n Peer Distribution:" + echo $peers | jq -r '.[].network.remoteAddress' | cut -d: -f1 | sort | uniq -c | sort -nr | head -5 | while read count ip; do + country=$(geoiplookup $ip 2>/dev/null | awk -F': ' '{print $2}' | awk -F',' '{print $1}' || echo "Unknown") + echo " $country: $count peers" + done + else + echo -e "${RED}✗${NC} No peer connections" + log "Peer count: 0 - CRITICAL" + fi +} + +# Check block propagation +check_block_propagation() { + echo -e "\n${BLUE}=== Checking Block Propagation ===${NC}" + + local latest_block=$(rpc_call "eth_getBlockByNumber" '["latest", false]') + local block_number=$(echo $latest_block | jq -r '.number') + local block_timestamp=$(echo $latest_block | jq -r '.timestamp') + local current_time=$(date +%s) + local block_age=$((current_time - block_timestamp)) + + if [ $block_age -lt 30 ]; then + echo -e "${GREEN}✓${NC} Latest block received ${block_age} seconds ago" + log "Block propagation: ${block_age}s ago - GOOD" + elif [ $block_age -lt 120 ]; then + echo -e "${YELLOW}⚠${NC} Latest block received ${block_age} seconds ago" + log "Block propagation: ${block_age}s ago - SLOW" + else + echo -e "${RED}✗${NC} Stale block (${block_age} seconds old)" + log "Block propagation: ${block_age}s ago - CRITICAL" + fi + + # Show block details + local gas_limit=$(echo $latest_block | jq -r '.gasLimit') + local gas_used=$(echo $latest_block | jq -r '.gasUsed') + local utilization=$(echo "scale=2; $gas_used * 100 / $gas_limit" | bc) + echo -e " Block #$(($block_number)) - Gas utilization: ${utilization}%" +} + +# Check resource usage +check_resources() { + echo -e "\n${BLUE}=== Checking Resource Usage ===${NC}" + + # Memory usage + local node_pid=$(pgrep -f "aitbc-node") + if [ -n "$node_pid" ]; then + local memory=$(ps -p $node_pid -o rss= | awk '{print $1/1024 " MB"}') + local cpu=$(ps -p $node_pid -o %cpu= | awk '{print $1 "%"}') + + echo -e " Memory usage: $memory" + echo -e " CPU usage: $cpu" + log "Resource usage - Memory: $memory, CPU: $cpu" + + # Check if memory usage is high + local memory_mb=$(ps -p $node_pid -o rss= | awk '{print $1}') + if [ $memory_mb -gt 8388608 ]; then # 8GB + echo -e "${YELLOW}⚠${NC} High memory usage detected" + fi + fi + + # Disk usage for blockchain data + local blockchain_dir="/var/lib/aitbc/blockchain" + if [ -d "$blockchain_dir" ]; then + local disk_usage=$(du -sh $blockchain_dir | awk '{print $1}') + echo -e " Blockchain data size: $disk_usage" + fi +} + +# Check consensus status +check_consensus() { + echo -e "\n${BLUE}=== Checking Consensus Status ===${NC}" + + # Get latest block and verify consensus + local latest_block=$(rpc_call "eth_getBlockByNumber" '["latest", false]') + local block_hash=$(echo $latest_block | jq -r '.hash') + local difficulty=$(echo $latest_block | jq -r '.difficulty') + + echo -e " Latest block hash: ${block_hash:0:10}..." + echo -e " Difficulty: $difficulty" + + # Check for consensus alerts + local chain_id=$(rpc_call "eth_chainId" "[]") + echo -e " Chain ID: $chain_id" + + log "Consensus check - Block: ${block_hash:0:10}..., Chain: $chain_id" +} + +# Generate health report +generate_report() { + echo -e "\n${BLUE}=== Health Report Summary ===${NC}" + + # Overall status + local score=0 + local total=5 + + # Node running + if pgrep -f "aitbc-node" > /dev/null; then + ((score++)) + fi + + # Sync status + local sync_result=$(rpc_call "eth_syncing" "[]") + if [ "$sync_result" = "false" ]; then + ((score++)) + fi + + # Peers + local peer_count=$(rpc_call "net_peerCount" "[]") + if [ $((peer_count)) -gt 0 ]; then + ((score++)) + fi + + # Block propagation + local latest_block=$(rpc_call "eth_getBlockByNumber" '["latest", false]') + local block_timestamp=$(echo $latest_block | jq -r '.timestamp') + local current_time=$(date +%s) + local block_age=$((current_time - block_timestamp)) + if [ $block_age -lt 30 ]; then + ((score++)) + fi + + # Resources + local node_pid=$(pgrep -f "aitbc-node") + if [ -n "$node_pid" ]; then + ((score++)) + fi + + local health_percent=$((score * 100 / total)) + + if [ $health_percent -eq 100 ]; then + echo -e "${GREEN}Overall Health: EXCELLENT (${health_percent}%)${NC}" + elif [ $health_percent -ge 80 ]; then + echo -e "${YELLOW}Overall Health: GOOD (${health_percent}%)${NC}" + else + echo -e "${RED}Overall Health: POOR (${health_percent}%)${NC}" + fi + + log "Health check completed - Score: ${score}/${total} (${health_percent}%)" +} + +# Main execution +main() { + log "Starting node health check" + echo -e "${BLUE}AITBC Node Health Check${NC}" + echo "============================" + + check_node_running + check_sync_status + check_peers + check_block_propagation + check_resources + check_consensus + generate_report + + echo -e "\n${BLUE}Health check completed. Log saved to: $LOG_FILE${NC}" +} + +# Run main function +main "$@" diff --git a/.windsurf/skills/blockchain-operations/ollama-test-scenario.md b/.windsurf/skills/blockchain-operations/ollama-test-scenario.md new file mode 100644 index 00000000..b894d00a --- /dev/null +++ b/.windsurf/skills/blockchain-operations/ollama-test-scenario.md @@ -0,0 +1,329 @@ +# Ollama GPU Inference Testing Scenario + +## Overview + +This document describes the complete end-to-end testing workflow for Ollama GPU inference jobs on the AITBC platform, from job submission to receipt generation. + +## Test Architecture + +``` +Client (CLI) → Coordinator API → GPU Miner (Host) → Ollama → Receipt → Blockchain + ↓ ↓ ↓ ↓ ↓ ↓ + Submit Job Queue Job Process Job Run Model Generate Record Tx + Check Status Assign Miner Submit Result Metrics Receipt with Payment +``` + +## Prerequisites + +### System Setup +```bash +# Repository location +cd /home/oib/windsurf/aitbc + +# Virtual environment +source .venv/bin/activate + +# Ensure services are running +./scripts/aitbc-cli.sh health +``` + +### Required Services +- Coordinator API: http://127.0.0.1:18000 +- Ollama API: http://localhost:11434 +- GPU Miner Service: systemd service +- Blockchain Node: http://127.0.0.1:19000 + +## Test Scenarios + +### Scenario 1: Basic Inference Job + +#### Step 1: Submit Job +```bash +./scripts/aitbc-cli.sh submit inference \ + --prompt "What is artificial intelligence?" \ + --model llama3.2:latest \ + --ttl 900 + +# Expected output: +# ✅ Job submitted successfully! +# Job ID: abc123def456... +``` + +#### Step 2: Monitor Job Status +```bash +# Check status immediately +./scripts/aitbc-cli.sh status abc123def456 + +# Expected: State = RUNNING + +# Monitor until completion +watch -n 2 "./scripts/aitbc-cli.sh status abc123def456" +``` + +#### Step 3: Verify Completion +```bash +# Once completed, check receipt +./scripts/aitbc-cli.sh receipts --job-id abc123def456 + +# Expected: Receipt with price > 0 +``` + +#### Step 4: Blockchain Verification +```bash +# View on blockchain explorer +./scripts/aitbc-cli.sh browser --receipt-limit 1 + +# Expected: Transaction showing payment amount +``` + +### Scenario 2: Concurrent Jobs Test + +#### Submit Multiple Jobs +```bash +# Submit 5 jobs concurrently +for i in {1..5}; do + ./scripts/aitbc-cli.sh submit inference \ + --prompt "Explain topic $i in detail" \ + --model mistral:latest & +done + +# Wait for all to submit +wait +``` + +#### Monitor All Jobs +```bash +# Check all active jobs +./scripts/aitbc-cli.sh admin-jobs + +# Expected: Multiple RUNNING jobs, then COMPLETED +``` + +#### Verify All Receipts +```bash +# List recent receipts +./scripts/aitbc-cli.sh receipts --limit 5 + +# Expected: 5 receipts with different payment amounts +``` + +### Scenario 3: Model Performance Test + +#### Test Different Models +```bash +# Test with various models +models=("llama3.2:latest" "mistral:latest" "deepseek-coder:6.7b-base" "qwen2.5:1.5b") + +for model in "${models[@]}"; do + echo "Testing model: $model" + ./scripts/aitbc-cli.sh submit inference \ + --prompt "Write a Python hello world" \ + --model "$model" \ + --ttl 900 +done +``` + +#### Compare Performance +```bash +# Check receipts for performance metrics +./scripts/aitbc-cli.sh receipts --limit 10 + +# Note: Different models have different processing times and costs +``` + +### Scenario 4: Error Handling Test + +#### Test Job Expiration +```bash +# Submit job with very short TTL +./scripts/aitbc-cli.sh submit inference \ + --prompt "This should expire" \ + --model llama3.2:latest \ + --ttl 5 + +# Wait for expiration +sleep 10 + +# Check status +./scripts/aitbc-cli.sh status + +# Expected: State = EXPIRED +``` + +#### Test Job Cancellation +```bash +# Submit job +job_id=$(./scripts/aitbc-cli.sh submit inference \ + --prompt "Cancel me" \ + --model llama3.2:latest | grep "Job ID" | awk '{print $3}') + +# Cancel immediately +./scripts/aitbc-cli.sh cancel "$job_id" + +# Verify cancellation +./scripts/aitbc-cli.sh status "$job_id" + +# Expected: State = CANCELED +``` + +## Monitoring and Debugging + +### Check Miner Health +```bash +# Systemd service status +sudo systemctl status aitbc-host-gpu-miner.service + +# Real-time logs +sudo journalctl -u aitbc-host-gpu-miner.service -f + +# Manual run for debugging +python3 scripts/gpu/gpu_miner_host.py +``` + +### Verify Ollama Integration +```bash +# Check Ollama status +curl http://localhost:11434/api/tags + +# Test Ollama directly +curl -X POST http://localhost:11434/api/generate \ + -H "Content-Type: application/json" \ + -d '{"model": "llama3.2:latest", "prompt": "Hello", "stream": false}' +``` + +### Check Coordinator API +```bash +# Health check +curl http://127.0.0.1:18000/v1/health + +# List registered miners +curl -H "X-Api-Key: REDACTED_ADMIN_KEY" \ + http://127.0.0.1:18000/v1/admin/miners + +# List all jobs +curl -H "X-Api-Key: REDACTED_ADMIN_KEY" \ + http://127.0.0.1:18000/v1/admin/jobs +``` + +## Expected Results + +### Successful Job Flow +1. **Submission**: Job ID returned, state = QUEUED +2. **Acquisition**: Miner picks up job, state = RUNNING +3. **Processing**: Ollama runs inference (visible in logs) +4. **Completion**: Miner submits result, state = COMPLETED +5. **Receipt**: Generated with: + - units: Processing time in seconds + - unit_price: 0.02 AITBC/second (default) + - price: Total payment (units × unit_price) +6. **Blockchain**: Transaction recorded with payment + +### Sample Receipt +```json +{ + "receipt_id": "abc123...", + "job_id": "def456...", + "provider": "REDACTED_MINER_KEY", + "client": "REDACTED_CLIENT_KEY", + "status": "completed", + "units": 2.5, + "unit_type": "gpu_seconds", + "unit_price": 0.02, + "price": 0.05, + "signature": "0x..." +} +``` + +## Common Issues and Solutions + +### Jobs Stay RUNNING +- **Cause**: Miner not running or not polling +- **Solution**: Check miner service status and logs +- **Command**: `sudo systemctl restart aitbc-host-gpu-miner.service` + +### No Payment in Receipt +- **Cause**: Missing metrics in job result +- **Solution**: Ensure miner submits duration/units +- **Check**: `./scripts/aitbc-cli.sh receipts --job-id ` + +### Ollama Connection Failed +- **Cause**: Ollama not running or wrong port +- **Solution**: Start Ollama service +- **Command**: `ollama serve` + +### GPU Not Detected +- **Cause**: NVIDIA drivers not installed +- **Solution**: Install drivers and verify +- **Command**: `nvidia-smi` + +## Performance Metrics + +### Expected Processing Times +- llama3.2:latest: ~1-3 seconds per response +- mistral:latest: ~1-2 seconds per response +- deepseek-coder:6.7b-base: ~2-4 seconds per response +- qwen2.5:1.5b: ~0.5-1 second per response + +### Expected Costs +- Default rate: 0.02 AITBC/second +- Typical job cost: 0.02-0.1 AITBC +- Minimum charge: 0.01 AITBC + +## Automation Script + +### End-to-End Test Script +```bash +#!/bin/bash +# e2e-ollama-test.sh + +set -e + +echo "Starting Ollama E2E Test..." + +# Check prerequisites +echo "Checking services..." +./scripts/aitbc-cli.sh health + +# Start miner if needed +if ! systemctl is-active --quiet aitbc-host-gpu-miner.service; then + echo "Starting miner service..." + sudo systemctl start aitbc-host-gpu-miner.service +fi + +# Submit test job +echo "Submitting test job..." +job_id=$(./scripts/aitbc-cli.sh submit inference \ + --prompt "E2E test: What is 2+2?" \ + --model llama3.2:latest | grep "Job ID" | awk '{print $3}') + +echo "Job submitted: $job_id" + +# Monitor job +echo "Monitoring job..." +while true; do + status=$(./scripts/aitbc-cli.sh status "$job_id" | grep "State" | awk '{print $2}') + echo "Status: $status" + + if [ "$status" = "COMPLETED" ]; then + echo "Job completed!" + break + elif [ "$status" = "FAILED" ] || [ "$status" = "CANCELED" ] || [ "$status" = "EXPIRED" ]; then + echo "Job failed with status: $status" + exit 1 + fi + + sleep 2 +done + +# Verify receipt +echo "Checking receipt..." +./scripts/aitbc-cli.sh receipts --job-id "$job_id" + +echo "E2E test completed successfully!" +``` + +Run with: +```bash +chmod +x e2e-ollama-test.sh +./e2e-ollama-test.sh +``` diff --git a/.windsurf/skills/blockchain-operations/skill.md b/.windsurf/skills/blockchain-operations/skill.md new file mode 100644 index 00000000..5a279ff0 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/skill.md @@ -0,0 +1,268 @@ +# Blockchain Operations Skill + +This skill provides standardized procedures for managing AITBC blockchain nodes, verifying transactions, and optimizing mining operations, including end-to-end Ollama GPU inference testing. + +## Overview + +The blockchain operations skill ensures reliable management of all blockchain-related components including node synchronization, transaction processing, mining operations, and network health monitoring. It also includes comprehensive testing scenarios for Ollama-based GPU inference workflows. + +## Capabilities + +### Node Management +- Node deployment and configuration +- Sync status monitoring +- Peer management +- Network diagnostics + +### Transaction Operations +- Transaction verification and debugging +- Gas optimization +- Batch processing +- Mempool management +- Receipt generation and verification + +### Mining Operations +- Mining performance optimization +- Pool management +- Reward tracking +- Hash rate optimization +- GPU miner service management + +### Ollama GPU Inference Testing +- End-to-end job submission and processing +- Miner registration and heartbeat monitoring +- Job lifecycle management (submit → running → completed) +- Receipt generation with payment amounts +- Blockchain explorer verification + +### Network Health +- Network connectivity checks +- Block propagation monitoring +- Fork detection and resolution +- Consensus validation + +## Common Workflows + +### 1. Node Health Check +- Verify node synchronization +- Check peer connections +- Validate consensus rules +- Monitor resource usage + +### 2. Transaction Debugging +- Trace transaction lifecycle +- Verify gas usage +- Check receipt status +- Debug failed transactions + +### 3. Mining Optimization +- Analyze mining performance +- Optimize GPU settings +- Configure mining pools +- Monitor profitability + +### 4. Network Diagnostics +- Test connectivity to peers +- Analyze block propagation +- Detect network partitions +- Validate consensus state + +### 5. Ollama End-to-End Testing +```bash +# Setup environment +cd /home/oib/windsurf/aitbc +source .venv/bin/activate + +# Check all services +./scripts/aitbc-cli.sh health + +# Start GPU miner service +sudo systemctl restart aitbc-host-gpu-miner.service +sudo journalctl -u aitbc-host-gpu-miner.service -f + +# Submit inference job +./scripts/aitbc-cli.sh submit inference \ + --prompt "Explain quantum computing" \ + --model llama3.2:latest \ + --ttl 900 + +# Monitor job progress +./scripts/aitbc-cli.sh status + +# View blockchain receipt +./scripts/aitbc-cli.sh browser --receipt-limit 5 + +# Verify payment in receipt +./scripts/aitbc-cli.sh receipts --job-id +``` + +### 6. Job Lifecycle Testing +1. **Submission**: Client submits job via CLI +2. **Queued**: Job enters queue, waits for miner +3. **Acquisition**: Miner polls and acquires job +4. **Processing**: Miner runs Ollama inference +5. **Completion**: Miner submits result with metrics +6. **Receipt**: System generates signed receipt with payment +7. **Blockchain**: Transaction recorded on blockchain + +### 7. Miner Service Management +```bash +# Check miner status +sudo systemctl status aitbc-host-gpu-miner.service + +# View miner logs +sudo journalctl -u aitbc-host-gpu-miner.service -n 100 + +# Restart miner service +sudo systemctl restart aitbc-host-gpu-miner.service + +# Run miner manually for debugging +python3 scripts/gpu/gpu_miner_host.py + +# Check registered miners +./scripts/aitbc-cli.sh admin-miners + +# View active jobs +./scripts/aitbc-cli.sh admin-jobs +``` + +## Testing Scenarios + +### Basic Inference Test +```bash +# Submit simple inference +./scripts/aitbc-cli.sh submit inference \ + --prompt "Hello AITBC" \ + --model llama3.2:latest + +# Expected flow: +# 1. Job submitted → RUNNING +# 2. Miner picks up job +# 3. Ollama processes inference +# 4. Job status → COMPLETED +# 5. Receipt generated with payment amount +``` + +### Stress Testing Multiple Jobs +```bash +# Submit multiple jobs concurrently +for i in {1..5}; do + ./scripts/aitbc-cli.sh submit inference \ + --prompt "Test job $i: Explain AI" \ + --model mistral:latest & +done + +# Monitor all jobs +./scripts/aitbc-cli.sh admin-jobs +``` + +### Payment Verification Test +```bash +# Submit job with specific model +./scripts/aitbc-cli.sh submit inference \ + --prompt "Detailed analysis" \ + --model deepseek-r1:14b + +# After completion, check receipt +./scripts/aitbc-cli.sh receipts --limit 1 + +# Verify transaction on blockchain +./scripts/aitbc-cli.sh browser --receipt-limit 1 + +# Expected: Receipt shows units, unit_price, and total price +``` + +## Supporting Files + +- `node-health.sh` - Comprehensive node health monitoring +- `tx-tracer.py` - Transaction tracing and debugging tool +- `mining-optimize.sh` - GPU mining optimization script +- `network-diag.py` - Network diagnostics and analysis +- `sync-monitor.py` - Real-time sync status monitor +- `scripts/gpu/gpu_miner_host.py` - Host GPU miner client with Ollama integration +- `aitbc-cli.sh` - Bash CLI wrapper for all operations +- `ollama-test-scenario.md` - Detailed Ollama testing documentation + +## Usage + +This skill is automatically invoked when you request blockchain-related operations such as: +- "check node status" +- "debug transaction" +- "optimize mining" +- "network diagnostics" +- "test ollama inference" +- "submit gpu job" +- "verify payment receipt" + +## Safety Features + +- Automatic backup of node data before operations +- Validation of all transactions before processing +- Safe mining parameter adjustments +- Rollback capability for configuration changes +- Job expiration handling (15 minutes TTL) +- Graceful miner shutdown and restart + +## Prerequisites + +- AITBC node installed and configured +- GPU drivers installed (for mining operations) +- Ollama installed and running with models +- Proper network connectivity +- Sufficient disk space for blockchain data +- Virtual environment with dependencies installed +- systemd service for GPU miner + +## Troubleshooting + +### Jobs Stuck in RUNNING +1. Check if miner is running: `sudo systemctl status aitbc-host-gpu-miner.service` +2. View miner logs: `sudo journalctl -u aitbc-host-gpu-miner.service -f` +3. Verify coordinator API: `./scripts/aitbc-cli.sh health` +4. Cancel stuck jobs: `./scripts/aitbc-cli.sh cancel ` + +### No Payment in Receipt +1. Check job completed successfully +2. Verify metrics include duration or units +3. Check receipt service logs +4. Ensure miner submitted result with metrics + +### Miner Not Processing Jobs +1. Restart miner service +2. Check Ollama is running: `curl http://localhost:11434/api/tags` +3. Verify GPU availability: `nvidia-smi` +4. Check miner registration: `./scripts/aitbc-cli.sh admin-miners` + +## Key Components + +### Coordinator API Endpoints +- POST /v1/jobs/create - Submit new job +- GET /v1/jobs/{id}/status - Check job status +- POST /v1/miners/register - Register miner +- POST /v1/miners/poll - Poll for jobs +- POST /v1/miners/{id}/result - Submit job result + +### CLI Commands +- `submit` - Submit inference job +- `status` - Check job status +- `browser` - View blockchain state +- `receipts` - List payment receipts +- `admin-miners` - List registered miners +- `admin-jobs` - List all jobs +- `cancel` - Cancel stuck job + +### Receipt Structure +```json +{ + "receipt_id": "...", + "job_id": "...", + "provider": "REDACTED_MINER_KEY", + "client": "REDACTED_CLIENT_KEY", + "status": "completed", + "units": 1.234, + "unit_type": "gpu_seconds", + "unit_price": 0.02, + "price": 0.02468, + "signature": "..." +} +``` diff --git a/.windsurf/skills/blockchain-operations/sync-monitor.py b/.windsurf/skills/blockchain-operations/sync-monitor.py new file mode 100755 index 00000000..7c1be995 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/sync-monitor.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +""" +AITBC Blockchain Sync Monitor +Real-time monitoring of blockchain synchronization status +""" + +import time +import json +import sys +import requests +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple +import threading +import signal + +class SyncMonitor: + def __init__(self, node_url: str = "http://localhost:8545"): + """Initialize the sync monitor""" + self.node_url = node_url + self.running = False + self.start_time = None + self.last_block = 0 + self.sync_history = [] + self.max_history = 100 + + # ANSI colors for terminal output + self.colors = { + 'red': '\033[91m', + 'green': '\033[92m', + 'yellow': '\033[93m', + 'blue': '\033[94m', + 'magenta': '\033[95m', + 'cyan': '\033[96m', + 'white': '\033[97m', + 'end': '\033[0m' + } + + def rpc_call(self, method: str, params: List = None) -> Optional[Dict]: + """Make JSON-RPC call to node""" + try: + response = requests.post( + self.node_url, + json={ + "jsonrpc": "2.0", + "method": method, + "params": params or [], + "id": 1 + }, + timeout=5 + ) + return response.json().get('result') + except Exception as e: + return None + + def get_sync_status(self) -> Dict: + """Get current sync status""" + sync_result = self.rpc_call("eth_syncing") + + if sync_result is False: + # Fully synced + latest_block = self.rpc_call("eth_blockNumber") + return { + 'syncing': False, + 'current_block': int(latest_block, 16) if latest_block else 0, + 'highest_block': int(latest_block, 16) if latest_block else 0, + 'sync_percent': 100.0 + } + else: + # Still syncing + current = int(sync_result.get('currentBlock', '0x0'), 16) + highest = int(sync_result.get('highestBlock', '0x0'), 16) + percent = (current / highest * 100) if highest > 0 else 0 + + return { + 'syncing': True, + 'current_block': current, + 'highest_block': highest, + 'sync_percent': percent, + 'starting_block': int(sync_result.get('startingBlock', '0x0'), 16), + 'pulled_states': sync_result.get('pulledStates', '0x0'), + 'known_states': sync_result.get('knownStates', '0x0') + } + + def get_peer_count(self) -> int: + """Get number of connected peers""" + result = self.rpc_call("net_peerCount") + return int(result, 16) if result else 0 + + def get_block_time(self, block_number: int) -> Optional[datetime]: + """Get block timestamp""" + block = self.rpc_call("eth_getBlockByNumber", [hex(block_number), False]) + if block and 'timestamp' in block: + return datetime.fromtimestamp(int(block['timestamp'], 16)) + return None + + def calculate_sync_speed(self) -> Optional[float]: + """Calculate current sync speed (blocks/second)""" + if len(self.sync_history) < 2: + return None + + # Get last two data points + recent = self.sync_history[-2:] + blocks_diff = recent[1]['current_block'] - recent[0]['current_block'] + time_diff = (recent[1]['timestamp'] - recent[0]['timestamp']).total_seconds() + + if time_diff > 0: + return blocks_diff / time_diff + return None + + def estimate_time_remaining(self, current: int, target: int, speed: float) -> str: + """Estimate time remaining to sync""" + if speed <= 0: + return "Unknown" + + blocks_remaining = target - current + seconds_remaining = blocks_remaining / speed + + if seconds_remaining < 60: + return f"{int(seconds_remaining)} seconds" + elif seconds_remaining < 3600: + return f"{int(seconds_remaining / 60)} minutes" + elif seconds_remaining < 86400: + return f"{int(seconds_remaining / 3600)} hours" + else: + return f"{int(seconds_remaining / 86400)} days" + + def print_status_bar(self, status: Dict): + """Print a visual sync status bar""" + width = 50 + filled = int(width * status['sync_percent'] / 100) + bar = '█' * filled + '░' * (width - filled) + + color = self.colors['green'] if status['sync_percent'] > 90 else \ + self.colors['yellow'] if status['sync_percent'] > 50 else \ + self.colors['red'] + + print(f"\r{color}[{bar}]{self.colors['end']} {status['sync_percent']:.2f}%", end='', flush=True) + + def print_detailed_status(self, status: Dict, speed: float, peers: int): + """Print detailed sync information""" + print(f"\n{'='*60}") + print(f"{self.colors['cyan']}AITBC Blockchain Sync Monitor{self.colors['end']}") + print(f"{'='*60}") + + # Sync status + if status['syncing']: + print(f"\n{self.colors['yellow']}Syncing...{self.colors['end']}") + else: + print(f"\n{self.colors['green']}Fully Synchronized!{self.colors['end']}") + + # Block information + print(f"\n{self.colors['blue']}Block Information:{self.colors['end']}") + print(f" Current: {status['current_block']:,}") + print(f" Highest: {status['highest_block']:,}") + print(f" Progress: {status['sync_percent']:.2f}%") + + if status['syncing'] and speed: + eta = self.estimate_time_remaining( + status['current_block'], + status['highest_block'], + speed + ) + print(f" ETA: {eta}") + + # Sync speed + if speed: + print(f"\n{self.colors['blue']}Sync Speed:{self.colors['end']}") + print(f" {speed:.2f} blocks/second") + + # Calculate blocks per minute/hour + print(f" {speed * 60:.0f} blocks/minute") + print(f" {speed * 3600:.0f} blocks/hour") + + # Network information + print(f"\n{self.colors['blue']}Network:{self.colors['end']}") + print(f" Peers connected: {peers}") + + # State sync (if available) + if status.get('pulled_states') and status.get('known_states'): + pulled = int(status['pulled_states'], 16) + known = int(status['known_states'], 16) + if known > 0: + state_percent = (pulled / known) * 100 + print(f" State sync: {state_percent:.2f}%") + + # Time information + if self.start_time: + elapsed = datetime.now() - self.start_time + print(f"\n{self.colors['blue']}Time:{self.colors['end']}") + print(f" Started: {self.start_time.strftime('%H:%M:%S')}") + print(f" Elapsed: {str(elapsed).split('.')[0]}") + + def monitor_loop(self, interval: int = 5, detailed: bool = False): + """Main monitoring loop""" + self.running = True + self.start_time = datetime.now() + + print(f"Starting sync monitor (interval: {interval}s)") + print("Press Ctrl+C to stop\n") + + try: + while self.running: + # Get current status + status = self.get_sync_status() + peers = self.get_peer_count() + + # Add to history + status['timestamp'] = datetime.now() + self.sync_history.append(status) + if len(self.sync_history) > self.max_history: + self.sync_history.pop(0) + + # Calculate sync speed + speed = self.calculate_sync_speed() + + # Display + if detailed: + self.print_detailed_status(status, speed, peers) + else: + self.print_status_bar(status) + + # Check if fully synced + if not status['syncing']: + if not detailed: + print() # New line after status bar + print(f"\n{self.colors['green']}✓ Sync completed!{self.colors['end']}") + break + + # Wait for next interval + time.sleep(interval) + + except KeyboardInterrupt: + self.running = False + print(f"\n\n{self.colors['yellow']}Sync monitor stopped by user{self.colors['end']}") + + # Print final summary + self.print_summary() + + def print_summary(self): + """Print sync summary""" + if not self.sync_history: + return + + print(f"\n{self.colors['cyan']}Sync Summary{self.colors['end']}") + print("-" * 40) + + if self.start_time: + total_time = datetime.now() - self.start_time + print(f"Total time: {str(total_time).split('.')[0]}") + + if len(self.sync_history) >= 2: + blocks_synced = self.sync_history[-1]['current_block'] - self.sync_history[0]['current_block'] + print(f"Blocks synced: {blocks_synced:,}") + + if total_time.total_seconds() > 0: + avg_speed = blocks_synced / total_time.total_seconds() + print(f"Average speed: {avg_speed:.2f} blocks/second") + + def save_report(self, filename: str): + """Save sync report to file""" + report = { + 'start_time': self.start_time.isoformat() if self.start_time else None, + 'end_time': datetime.now().isoformat(), + 'sync_history': [ + { + 'timestamp': entry['timestamp'].isoformat(), + 'current_block': entry['current_block'], + 'highest_block': entry['highest_block'], + 'sync_percent': entry['sync_percent'] + } + for entry in self.sync_history + ] + } + + with open(filename, 'w') as f: + json.dump(report, f, indent=2) + + print(f"Report saved to: {filename}") + +def signal_handler(signum, frame): + """Handle Ctrl+C""" + print("\n\nStopping sync monitor...") + sys.exit(0) + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='AITBC Blockchain Sync Monitor') + parser.add_argument('--node', default='http://localhost:8545', help='Node URL') + parser.add_argument('--interval', type=int, default=5, help='Update interval (seconds)') + parser.add_argument('--detailed', action='store_true', help='Show detailed output') + parser.add_argument('--report', help='Save report to file') + + args = parser.parse_args() + + # Set up signal handler + signal.signal(signal.SIGINT, signal_handler) + + # Create and run monitor + monitor = SyncMonitor(args.node) + + try: + monitor.monitor_loop(interval=args.interval, detailed=args.detailed) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + # Save report if requested + if args.report: + monitor.save_report(args.report) + +if __name__ == "__main__": + main() diff --git a/.windsurf/skills/blockchain-operations/tx-tracer.py b/.windsurf/skills/blockchain-operations/tx-tracer.py new file mode 100644 index 00000000..724be728 --- /dev/null +++ b/.windsurf/skills/blockchain-operations/tx-tracer.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +""" +AITBC Transaction Tracer +Comprehensive transaction debugging and analysis tool +""" + +import web3 +import json +import sys +import argparse +from datetime import datetime +from typing import Dict, List, Optional, Any + +class TransactionTracer: + def __init__(self, node_url: str = "http://localhost:8545"): + """Initialize the transaction tracer""" + self.w3 = web3.Web3(web3.HTTPProvider(node_url)) + if not self.w3.is_connected(): + raise Exception("Failed to connect to AITBC node") + + def trace_transaction(self, tx_hash: str) -> Dict[str, Any]: + """Trace a transaction and return comprehensive information""" + try: + # Get transaction details + tx = self.w3.eth.get_transaction(tx_hash) + receipt = self.w3.eth.get_transaction_receipt(tx_hash) + + # Build trace result + trace = { + 'hash': tx_hash, + 'status': 'success' if receipt.status == 1 else 'failed', + 'block_number': tx.blockNumber, + 'block_hash': tx.blockHash.hex(), + 'transaction_index': receipt.transactionIndex, + 'from_address': tx['from'], + 'to_address': tx.get('to'), + 'value': self.w3.from_wei(tx.value, 'ether'), + 'gas_limit': tx.gas, + 'gas_used': receipt.gasUsed, + 'gas_price': self.w3.from_wei(tx.gasPrice, 'gwei'), + 'effective_gas_price': self.w3.from_wei(receipt.effectiveGasPrice, 'gwei'), + 'nonce': tx.nonce, + 'max_fee_per_gas': None, + 'max_priority_fee_per_gas': None, + 'type': tx.get('type', 0) + } + + # EIP-1559 transaction fields + if tx.get('type') == 2: + trace['max_fee_per_gas'] = self.w3.from_wei(tx.maxFeePerGas, 'gwei') + trace['max_priority_fee_per_gas'] = self.w3.from_wei(tx.maxPriorityFeePerGas, 'gwei') + + # Calculate gas efficiency + trace['gas_efficiency'] = f"{(receipt.gasUsed / tx.gas * 100):.2f}%" + + # Get logs + trace['logs'] = self._parse_logs(receipt.logs) + + # Get contract creation info if applicable + if tx.get('to') is None: + trace['contract_created'] = receipt.contractAddress + trace['contract_code'] = self.w3.eth.get_code(receipt.contractAddress).hex() + + # Get internal transfers (if tracing is available) + trace['internal_transfers'] = self._get_internal_transfers(tx_hash) + + return trace + + except Exception as e: + return {'error': str(e), 'hash': tx_hash} + + def _parse_logs(self, logs: List) -> List[Dict]: + """Parse transaction logs""" + parsed_logs = [] + for log in logs: + parsed_logs.append({ + 'address': log.address, + 'topics': [topic.hex() for topic in log.topics], + 'data': log.data.hex(), + 'log_index': log.logIndex, + 'decoded': self._decode_log(log) + }) + return parsed_logs + + def _decode_log(self, log) -> Optional[Dict]: + """Attempt to decode log events""" + # This would contain ABI decoding logic + # For now, return basic info + return { + 'signature': log.topics[0].hex() if log.topics else None, + 'event_name': 'Unknown' # Would be decoded from ABI + } + + def _get_internal_transfers(self, tx_hash: str) -> List[Dict]: + """Get internal ETH transfers (requires tracing)""" + try: + # Try debug_traceTransaction if available + trace = self.w3.provider.make_request('debug_traceTransaction', [tx_hash, {}]) + transfers = [] + + # Parse trace for transfers + if trace and 'result' in trace: + # Implementation would parse the trace for CALL/DELEGATECALL with value + pass + + return transfers + except: + return [] + + def analyze_gas_usage(self, tx_hash: str) -> Dict[str, Any]: + """Analyze gas usage and provide optimization tips""" + trace = self.trace_transaction(tx_hash) + + if 'error' in trace: + return trace + + analysis = { + 'gas_used': trace['gas_used'], + 'gas_limit': trace['gas_limit'], + 'efficiency': trace['gas_efficiency'], + 'recommendations': [] + } + + # Gas efficiency recommendations + if trace['gas_used'] < trace['gas_limit'] * 0.5: + analysis['recommendations'].append( + f"Gas limit too high. Consider reducing to ~{int(trace['gas_used'] * 1.2)}" + ) + + # Gas price analysis + if trace['gas_price'] > 100: # High gas price threshold + analysis['recommendations'].append( + "High gas price detected. Consider using EIP-1559 or waiting for lower gas" + ) + + return analysis + + def debug_failed_transaction(self, tx_hash: str) -> Dict[str, Any]: + """Debug why a transaction failed""" + trace = self.trace_transaction(tx_hash) + + if trace.get('status') == 'success': + return {'error': 'Transaction was successful', 'hash': tx_hash} + + debug_info = { + 'hash': tx_hash, + 'failure_reason': 'Unknown', + 'possible_causes': [], + 'debug_steps': [] + } + + # Check for common failure reasons + debug_info['debug_steps'].append("1. Checking if transaction ran out of gas...") + if trace['gas_used'] == trace['gas_limit']: + debug_info['failure_reason'] = 'Out of gas' + debug_info['possible_causes'].append('Transaction required more gas than provided') + debug_info['debug_steps'].append(" ✓ Transaction ran out of gas") + + debug_info['debug_steps'].append("2. Checking for revert reasons...") + # Would implement revert reason decoding here + debug_info['debug_steps'].append(" ✗ Could not decode revert reason") + + debug_info['debug_steps'].append("3. Checking nonce issues...") + # Would check for nonce problems + debug_info['debug_steps'].append(" ✓ Nonce appears correct") + + return debug_info + + def monitor_mempool(self, address: str = None) -> Dict[str, Any]: + """Monitor transaction mempool""" + try: + # Get pending transactions + pending_block = self.w3.eth.get_block('pending', full_transactions=True) + pending_txs = pending_block.transactions + + mempool_info = { + 'pending_count': len(pending_txs), + 'pending_by_address': {}, + 'high_priority_txs': [], + 'stuck_txs': [] + } + + # Analyze pending transactions + for tx in pending_txs: + from_addr = str(tx['from']) + if from_addr not in mempool_info['pending_by_address']: + mempool_info['pending_by_address'][from_addr] = 0 + mempool_info['pending_by_address'][from_addr] += 1 + + # High priority transactions (high gas price) + if tx.gasPrice > web3.Web3.to_wei(50, 'gwei'): + mempool_info['high_priority_txs'].append({ + 'hash': tx.hash.hex(), + 'gas_price': web3.Web3.from_wei(tx.gasPrice, 'gwei'), + 'from': from_addr + }) + + return mempool_info + + except Exception as e: + return {'error': str(e)} + + def print_trace(self, trace: Dict[str, Any]): + """Print formatted transaction trace""" + if 'error' in trace: + print(f"Error: {trace['error']}") + return + + print(f"\n{'='*60}") + print(f"Transaction Trace: {trace['hash']}") + print(f"{'='*60}") + print(f"Status: {trace['status'].upper()}") + print(f"Block: #{trace['block_number']} ({trace['block_hash'][:10]}...)") + print(f"From: {trace['from_address']}") + print(f"To: {trace['to_address'] or 'Contract Creation'}") + print(f"Value: {trace['value']} ETH") + print(f"Gas Used: {trace['gas_used']:,} / {trace['gas_limit']:,} ({trace['gas_efficiency']})") + print(f"Gas Price: {trace['gas_price']} gwei") + if trace['max_fee_per_gas']: + print(f"Max Fee: {trace['max_fee_per_gas']} gwei") + print(f"Priority Fee: {trace['max_priority_fee_per_gas']} gwei") + + if trace.get('contract_created'): + print(f"\nContract Created: {trace['contract_created']}") + + if trace['logs']: + print(f"\nLogs ({len(trace['logs'])}):") + for log in trace['logs'][:5]: # Show first 5 logs + print(f" - {log['address']}: {log['decoded']['event_name'] or 'Unknown Event'}") + + if trace['internal_transfers']: + print(f"\nInternal Transfers:") + for transfer in trace['internal_transfers']: + print(f" {transfer['from']} -> {transfer['to']}: {transfer['value']} ETH") + +def main(): + parser = argparse.ArgumentParser(description='AITBC Transaction Tracer') + parser.add_argument('command', choices=['trace', 'analyze', 'debug', 'mempool']) + parser.add_argument('--tx', help='Transaction hash') + parser.add_argument('--address', help='Address for mempool monitoring') + parser.add_argument('--node', default='http://localhost:8545', help='Node URL') + + args = parser.parse_args() + + tracer = TransactionTracer(args.node) + + if args.command == 'trace': + if not args.tx: + print("Error: Transaction hash required for trace command") + sys.exit(1) + trace = tracer.trace_transaction(args.tx) + tracer.print_trace(trace) + + elif args.command == 'analyze': + if not args.tx: + print("Error: Transaction hash required for analyze command") + sys.exit(1) + analysis = tracer.analyze_gas_usage(args.tx) + print(json.dumps(analysis, indent=2)) + + elif args.command == 'debug': + if not args.tx: + print("Error: Transaction hash required for debug command") + sys.exit(1) + debug = tracer.debug_failed_transaction(args.tx) + print(json.dumps(debug, indent=2)) + + elif args.command == 'mempool': + mempool = tracer.monitor_mempool(args.address) + print(json.dumps(mempool, indent=2)) + +if __name__ == "__main__": + main() diff --git a/.windsurf/skills/deploy-production/SKILL.md b/.windsurf/skills/deploy-production/SKILL.md new file mode 100644 index 00000000..49269411 --- /dev/null +++ b/.windsurf/skills/deploy-production/SKILL.md @@ -0,0 +1,76 @@ +--- +name: deploy-production +description: Automated production deployment workflow for AITBC blockchain components +version: 1.0.0 +author: Cascade +tags: [deployment, production, blockchain, aitbc] +--- + +# Production Deployment Skill + +This skill provides a standardized workflow for deploying AITBC components to production environments. + +## Overview + +The production deployment skill ensures safe, consistent, and verifiable deployments of all AITBC stack components including: +- Coordinator services +- Blockchain node +- Miner daemon +- Web applications +- Infrastructure components + +## Prerequisites + +- Production server access configured +- SSL certificates installed +- Environment variables set +- Backup procedures in place +- Monitoring systems active + +## Deployment Steps + +### 1. Pre-deployment Checks +- Run health checks on all services +- Verify backup integrity +- Check disk space and resources +- Validate configuration files +- Review recent changes + +### 2. Environment Preparation +- Update dependencies +- Build new artifacts +- Run smoke tests +- Prepare rollback plan + +### 3. Deployment Execution +- Stop services gracefully +- Deploy new code +- Update configurations +- Restart services +- Verify health status + +### 4. Post-deployment Verification +- Run integration tests +- Check API endpoints +- Verify blockchain sync +- Monitor system metrics +- Validate user access + +## Supporting Files + +- `pre-deploy-checks.sh` - Automated pre-deployment validation +- `environment-template.env` - Production environment template +- `rollback-steps.md` - Emergency rollback procedures +- `health-check.py` - Service health verification script + +## Usage + +This skill is automatically invoked when you request production deployment. You can also manually invoke it by mentioning "deploy production" or "production deployment". + +## Safety Features + +- Automatic rollback on failure +- Service health monitoring +- Configuration validation +- Backup verification +- Rollback checkpoint creation diff --git a/.windsurf/skills/deploy-production/health-check.py b/.windsurf/skills/deploy-production/health-check.py new file mode 100755 index 00000000..023f8116 --- /dev/null +++ b/.windsurf/skills/deploy-production/health-check.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +AITBC Production Health Check Script +Verifies the health of all AITBC services after deployment +""" + +import requests +import json +import sys +import time +from datetime import datetime +from typing import Dict, List, Tuple + +# Configuration +SERVICES = { + "coordinator": { + "url": "http://localhost:8080/health", + "expected_status": 200, + "timeout": 10 + }, + "blockchain-node": { + "url": "http://localhost:8545", + "method": "POST", + "payload": { + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 + }, + "expected_status": 200, + "timeout": 10 + }, + "dashboard": { + "url": "https://aitbc.io/health", + "expected_status": 200, + "timeout": 10 + }, + "api": { + "url": "https://api.aitbc.io/v1/status", + "expected_status": 200, + "timeout": 10 + }, + "miner": { + "url": "http://localhost:8081/api/status", + "expected_status": 200, + "timeout": 10 + } +} + +# Colors for output +class Colors: + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + ENDC = '\033[0m' + +def print_status(message: str, status: str = "INFO"): + """Print colored status message""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if status == "SUCCESS": + print(f"{Colors.GREEN}[✓]{Colors.ENDC} {timestamp} - {message}") + elif status == "ERROR": + print(f"{Colors.RED}[✗]{Colors.ENDC} {timestamp} - {message}") + elif status == "WARNING": + print(f"{Colors.YELLOW}[⚠]{Colors.ENDC} {timestamp} - {message}") + else: + print(f"{Colors.BLUE}[ℹ]{Colors.ENDC} {timestamp} - {message}") + +def check_service(name: str, config: Dict) -> Tuple[bool, str]: + """Check individual service health""" + try: + method = config.get('method', 'GET') + timeout = config.get('timeout', 10) + expected_status = config.get('expected_status', 200) + + if method == 'POST': + response = requests.post( + config['url'], + json=config.get('payload', {}), + timeout=timeout, + headers={'Content-Type': 'application/json'} + ) + else: + response = requests.get(config['url'], timeout=timeout) + + if response.status_code == expected_status: + # Additional checks for specific services + if name == "blockchain-node": + data = response.json() + if 'result' in data: + block_number = int(data['result'], 16) + return True, f"Block number: {block_number}" + return False, "Invalid response format" + + elif name == "coordinator": + data = response.json() + if data.get('status') == 'healthy': + return True, f"Version: {data.get('version', 'unknown')}" + return False, f"Status: {data.get('status')}" + + return True, f"Status: {response.status_code}" + else: + return False, f"HTTP {response.status_code}" + + except requests.exceptions.Timeout: + return False, "Timeout" + except requests.exceptions.ConnectionError: + return False, "Connection refused" + except Exception as e: + return False, str(e) + +def check_database() -> Tuple[bool, str]: + """Check database connectivity""" + try: + # This would use your actual database connection + import psycopg2 + conn = psycopg2.connect( + host="localhost", + database="aitbc_prod", + user="postgres", + password="your_password" + ) + cursor = conn.cursor() + cursor.execute("SELECT 1") + cursor.close() + conn.close() + return True, "Database connected" + except Exception as e: + return False, str(e) + +def check_redis() -> Tuple[bool, str]: + """Check Redis connectivity""" + try: + import redis + r = redis.Redis(host='localhost', port=6379, db=0) + r.ping() + return True, "Redis connected" + except Exception as e: + return False, str(e) + +def check_disk_space() -> Tuple[bool, str]: + """Check disk space usage""" + import shutil + total, used, free = shutil.disk_usage("/") + percent_used = (used / total) * 100 + if percent_used < 80: + return True, f"Disk usage: {percent_used:.1f}%" + else: + return False, f"Disk usage critical: {percent_used:.1f}%" + +def check_ssl_certificates() -> Tuple[bool, str]: + """Check SSL certificate validity""" + import ssl + import socket + from datetime import datetime + + try: + context = ssl.create_default_context() + with socket.create_connection(("aitbc.io", 443)) as sock: + with context.wrap_socket(sock, server_hostname="aitbc.io") as ssock: + cert = ssock.getpeercert() + expiry_date = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z') + days_until_expiry = (expiry_date - datetime.now()).days + + if days_until_expiry > 7: + return True, f"SSL valid for {days_until_expiry} days" + else: + return False, f"SSL expires in {days_until_expiry} days" + except Exception as e: + return False, str(e) + +def main(): + """Main health check function""" + print_status("Starting AITBC Production Health Check", "INFO") + print("=" * 60) + + all_passed = True + failed_services = [] + + # Check all services + print_status("\n=== Service Health Checks ===") + for name, config in SERVICES.items(): + success, message = check_service(name, config) + if success: + print_status(f"{name}: {message}", "SUCCESS") + else: + print_status(f"{name}: {message}", "ERROR") + all_passed = False + failed_services.append(name) + + # Check infrastructure components + print_status("\n=== Infrastructure Checks ===") + + # Database + db_success, db_message = check_database() + if db_success: + print_status(f"Database: {db_message}", "SUCCESS") + else: + print_status(f"Database: {db_message}", "ERROR") + all_passed = False + + # Redis + redis_success, redis_message = check_redis() + if redis_success: + print_status(f"Redis: {redis_message}", "SUCCESS") + else: + print_status(f"Redis: {redis_message}", "ERROR") + all_passed = False + + # Disk space + disk_success, disk_message = check_disk_space() + if disk_success: + print_status(f"Disk: {disk_message}", "SUCCESS") + else: + print_status(f"Disk: {disk_message}", "ERROR") + all_passed = False + + # SSL certificates + ssl_success, ssl_message = check_ssl_certificates() + if ssl_success: + print_status(f"SSL: {ssl_message}", "SUCCESS") + else: + print_status(f"SSL: {ssl_message}", "ERROR") + all_passed = False + + # Summary + print("\n" + "=" * 60) + if all_passed: + print_status("All checks passed! System is healthy.", "SUCCESS") + sys.exit(0) + else: + print_status(f"Health check failed! Failed services: {', '.join(failed_services)}", "ERROR") + print_status("Please check the logs and investigate the issues.", "WARNING") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/.windsurf/skills/deploy-production/pre-deploy-checks.sh b/.windsurf/skills/deploy-production/pre-deploy-checks.sh new file mode 100755 index 00000000..603c085e --- /dev/null +++ b/.windsurf/skills/deploy-production/pre-deploy-checks.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Pre-deployment checks for AITBC production deployment +# This script validates system readiness before deployment + +set -e + +echo "=== AITBC Production Pre-deployment Checks ===" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print status +check_status() { + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓${NC} $1" + else + echo -e "${RED}✗${NC} $1" + exit 1 + fi +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# 1. Check disk space +echo -e "\n1. Checking disk space..." +DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') +if [ $DISK_USAGE -lt 80 ]; then + check_status "Disk space usage: ${DISK_USAGE}%" +else + warning "Disk space usage is high: ${DISK_USAGE}%" +fi + +# 2. Check memory usage +echo -e "\n2. Checking memory usage..." +MEM_AVAILABLE=$(free -m | awk 'NR==2{printf "%.0f", $7}') +if [ $MEM_AVAILABLE -gt 1024 ]; then + check_status "Available memory: ${MEM_AVAILABLE}MB" +else + warning "Low memory available: ${MEM_AVAILABLE}MB" +fi + +# 3. Check service status +echo -e "\n3. Checking critical services..." +services=("nginx" "docker" "postgresql") +for service in "${services[@]}"; do + if systemctl is-active --quiet $service; then + check_status "$service is running" + else + echo -e "${RED}✗${NC} $service is not running" + fi +done + +# 4. Check SSL certificates +echo -e "\n4. Checking SSL certificates..." +if [ -f "/etc/letsencrypt/live/$(hostname)/fullchain.pem" ]; then + EXPIRY=$(openssl x509 -in /etc/letsencrypt/live/$(hostname)/fullchain.pem -noout -enddate | cut -d= -f2) + check_status "SSL certificate valid until: $EXPIRY" +else + warning "SSL certificate not found" +fi + +# 5. Check backup +echo -e "\n5. Checking recent backup..." +BACKUP_DIR="/var/backups/aitbc" +if [ -d "$BACKUP_DIR" ]; then + LATEST_BACKUP=$(ls -lt $BACKUP_DIR | head -n 2 | tail -n 1 | awk '{print $9}') + if [ -n "$LATEST_BACKUP" ]; then + check_status "Latest backup: $LATEST_BACKUP" + else + warning "No recent backup found" + fi +else + warning "Backup directory not found" +fi + +# 6. Check environment variables +echo -e "\n6. Checking environment configuration..." +if [ -f "/etc/environment" ] && grep -q "AITBC_ENV=production" /etc/environment; then + check_status "Production environment configured" +else + warning "Production environment not set" +fi + +# 7. Check ports +echo -e "\n7. Checking required ports..." +ports=("80" "443" "8080" "8545") +for port in "${ports[@]}"; do + if netstat -tuln | grep -q ":$port "; then + check_status "Port $port is listening" + else + warning "Port $port is not listening" + fi +done + +echo -e "\n=== Pre-deployment checks completed ===" +echo -e "${GREEN}Ready for deployment!${NC}" diff --git a/.windsurf/skills/deploy-production/rollback-steps.md b/.windsurf/skills/deploy-production/rollback-steps.md new file mode 100644 index 00000000..30974bcf --- /dev/null +++ b/.windsurf/skills/deploy-production/rollback-steps.md @@ -0,0 +1,187 @@ +# Production Rollback Procedures + +## Emergency Rollback Guide + +Use these procedures when a deployment causes critical issues in production. + +### Immediate Actions (First 5 minutes) + +1. **Assess the Impact** + - Check monitoring dashboards + - Review error logs + - Identify affected services + - Determine if rollback is necessary + +2. **Communicate** + - Notify team in #production-alerts + - Post status on status page if needed + - Document start time of incident + +### Automated Rollback (if available) + +```bash +# Quick rollback to previous version +./scripts/rollback-to-previous.sh + +# Rollback to specific version +./scripts/rollback-to-version.sh v1.2.3 +``` + +### Manual Rollback Steps + +#### 1. Stop Current Services +```bash +# Stop all AITBC services +sudo systemctl stop aitbc-coordinator +sudo systemctl stop aitbc-node +sudo systemctl stop aitbc-miner +sudo systemctl stop aitbc-dashboard +sudo docker-compose down +``` + +#### 2. Restore Previous Code +```bash +# Get previous deployment tag +git tag --sort=-version:refname | head -n 5 + +# Checkout previous stable version +git checkout v1.2.3 + +# Rebuild if necessary +docker-compose build --no-cache +``` + +#### 3. Restore Database (if needed) +```bash +# List available backups +aws s3 ls s3://aitbc-backups/database/ + +# Restore latest backup +pg_restore -h localhost -U postgres -d aitbc_prod latest_backup.dump +``` + +#### 4. Restore Configuration +```bash +# Restore from backup +cp /etc/aitbc/backup/config.yaml /etc/aitbc/config.yaml +cp /etc/aitbc/backup/.env /etc/aitbc/.env +``` + +#### 5. Restart Services +```bash +# Start services in correct order +sudo systemctl start aitbc-coordinator +sleep 10 +sudo systemctl start aitbc-node +sleep 10 +sudo systemctl start aitbc-miner +sleep 10 +sudo systemctl start aitbc-dashboard +``` + +#### 6. Verify Rollback +```bash +# Check service status +./scripts/health-check.sh + +# Run smoke tests +./scripts/smoke-test.sh + +# Verify blockchain sync +curl -X POST http://localhost:8545 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' +``` + +### Database-Specific Rollbacks + +#### Partial Data Rollback +```bash +# Create backup before changes +pg_dump -h localhost -U postgres aitbc_prod > pre-rollback-backup.sql + +# Rollback specific tables +psql -h localhost -U postgres -d aitbc_prod < rollback-tables.sql +``` + +#### Migration Rollback +```bash +# Check migration status +./scripts/migration-status.sh + +# Rollback last migration +./scripts/rollback-migration.sh +``` + +### Service-Specific Rollbacks + +#### Coordinator Service +```bash +# Restore coordinator state +sudo systemctl stop aitbc-coordinator +cp /var/lib/aitbc/coordinator/backup/state.db /var/lib/aitbc/coordinator/ +sudo systemctl start aitbc-coordinator +``` + +#### Blockchain Node +```bash +# Reset to last stable block +sudo systemctl stop aitbc-node +aitbc-node --reset-to-block 123456 +sudo systemctl start aitbc-node +``` + +#### Mining Operations +```bash +# Stop mining immediately +curl -X POST http://localhost:8080/api/mining/stop + +# Reset mining state +redis-cli FLUSHDB +``` + +### Verification Checklist + +- [ ] All services running +- [ ] Database connectivity +- [ ] API endpoints responding +- [ ] Blockchain syncing +- [ ] Mining operations (if applicable) +- [ ] Dashboard accessible +- [ ] SSL certificates valid +- [ ] Monitoring alerts cleared + +### Post-Rollback Actions + +1. **Root Cause Analysis** + - Document what went wrong + - Identify failure point + - Create prevention plan + +2. **Team Communication** + - Update incident ticket + - Share lessons learned + - Update runbooks + +3. **Preventive Measures** + - Add additional tests + - Improve monitoring + - Update deployment checklist + +### Contact Information + +- **On-call Engineer**: [Phone/Slack] +- **Engineering Lead**: [Phone/Slack] +- **DevOps Team**: #devops-alerts +- **Management**: #management-alerts + +### Escalation + +1. **Level 1**: On-call engineer (first 15 minutes) +2. **Level 2**: Engineering lead (after 15 minutes) +3. **Level 3**: CTO (after 30 minutes) + +### Notes + +- Always create a backup before rollback +- Document every step during rollback +- Test in staging before production if possible +- Keep stakeholders informed throughout process diff --git a/.windsurf/skills/ollama-gpu-provider/SKILL.md b/.windsurf/skills/ollama-gpu-provider/SKILL.md new file mode 100644 index 00000000..4ba91c45 --- /dev/null +++ b/.windsurf/skills/ollama-gpu-provider/SKILL.md @@ -0,0 +1,39 @@ +--- +name: ollama-gpu-provider +description: End-to-end Ollama prompt payment test against the GPU miner provider +version: 1.0.0 +author: Cascade +tags: [gpu, miner, ollama, payments, receipts, test] +--- + +# Ollama GPU Provider Test Skill + +This skill runs an end-to-end client → coordinator → GPU miner → receipt flow using an Ollama prompt. + +## Overview + +The test submits a prompt (default: "hello") to the coordinator via the host proxy, waits for completion, and verifies that the job result and signed receipt are returned. + +## Prerequisites + +- Host GPU miner running and registered (RTX 4060 Ti + Ollama) +- Incus proxy forwarding `127.0.0.1:18000` → container `127.0.0.1:8000` +- Coordinator running in container (`coordinator-api.service`) +- Receipt signing key configured in `/opt/coordinator-api/src/.env` + +## Test Command + +```bash +python3 cli/test_ollama_gpu_provider.py --url http://127.0.0.1:18000 --prompt "hello" +``` + +## Expected Outcome + +- Job reaches `COMPLETED` +- Output returned from Ollama +- Receipt present with a `receipt_id` + +## Notes + +- Use `--timeout` to allow longer runs for large models. +- If the receipt is missing, verify `receipt_signing_key_hex` is set and restart the coordinator. diff --git a/api/exchange_mock_api.py b/api/exchange_mock_api.py new file mode 100644 index 00000000..449fdecb --- /dev/null +++ b/api/exchange_mock_api.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +import json +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse + + +class Handler(BaseHTTPRequestHandler): + def _json(self, payload, status=200): + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type, X-Api-Key") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_OPTIONS(self): + self.send_response(204) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type, X-Api-Key") + self.end_headers() + + def do_GET(self): + path = urlparse(self.path).path + + if path == "/api/trades/recent": + trades = [ + {"id": 1, "price": 0.00001, "amount": 1500, "created_at": "2026-01-21T17:00:00Z"}, + {"id": 2, "price": 0.0000095, "amount": 500, "created_at": "2026-01-21T16:55:00Z"}, + ] + return self._json(trades) + + if path == "/api/orders/orderbook": + orderbook = { + "sells": [{"price": 0.00001, "remaining": 1500, "amount": 1500}], + "buys": [{"price": 0.000009, "remaining": 1000, "amount": 1000}], + } + return self._json(orderbook) + + if path == "/api/wallet/balance": + return self._json({"balance": 1000, "currency": "AITBC"}) + + if path == "/api/treasury-balance": + return self._json({ + "balance": 50000, + "currency": "AITBC", + "usd_value": 5000.00, + "last_updated": "2026-01-21T18:00:00Z" + }) + + if path == "/api/exchange/wallet/info": + return self._json({ + "address": "aitbc1exchange123456789", + "balance": 1000, + "currency": "AITBC", + "total_transactions": 150, + "status": "active", + "transactions": [ + { + "id": "txn_001", + "type": "deposit", + "amount": 500, + "timestamp": "2026-01-21T17:00:00Z", + "status": "completed" + }, + { + "id": "txn_002", + "type": "withdrawal", + "amount": 200, + "timestamp": "2026-01-21T16:30:00Z", + "status": "completed" + }, + { + "id": "txn_003", + "type": "trade", + "amount": 100, + "timestamp": "2026-01-21T16:00:00Z", + "status": "completed" + } + ] + }) + + return self._json({"detail": "Not Found"}, status=404) + + def do_POST(self): + path = urlparse(self.path).path + + if path == "/api/wallet/connect": + resp = { + "success": True, + "address": "aitbc1wallet123456789", + "message": "Wallet connected successfully", + } + return self._json(resp) + + return self._json({"detail": "Not Found"}, status=404) + + +def main(): + HTTPServer(("127.0.0.1", 8085), Handler).serve_forever() + + +if __name__ == "__main__": + main() diff --git a/apps/blockchain-node/data/chain.db-journal b/apps/blockchain-node/data/chain.db-journal deleted file mode 100644 index 70fbcfce..00000000 Binary files a/apps/blockchain-node/data/chain.db-journal and /dev/null differ diff --git a/apps/blockchain-node/data/devnet/genesis.json b/apps/blockchain-node/data/devnet/genesis.json index 0ab64e7e..89481c73 100644 --- a/apps/blockchain-node/data/devnet/genesis.json +++ b/apps/blockchain-node/data/devnet/genesis.json @@ -19,5 +19,5 @@ "fee_per_byte": 1, "mint_per_unit": 1000 }, - "timestamp": 1767000206 + "timestamp": 1768834652 } diff --git a/apps/blockchain-node/scripts/create_bootstrap_genesis.py b/apps/blockchain-node/scripts/create_bootstrap_genesis.py new file mode 100644 index 00000000..65777225 --- /dev/null +++ b/apps/blockchain-node/scripts/create_bootstrap_genesis.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Generate a genesis file with initial distribution for the exchange economy.""" + +import json +import time +from pathlib import Path + +# Genesis configuration with initial token distribution +GENESIS_CONFIG = { + "chain_id": "ait-mainnet", + "timestamp": None, # populated at runtime + "params": { + "mint_per_unit": 1000, + "coordinator_ratio": 0.05, + "base_fee": 10, + "fee_per_byte": 1, + }, + "accounts": [ + # Exchange Treasury - 10 million AITBC for liquidity + { + "address": "aitbcexchange00000000000000000000000000000000", + "balance": 10_000_000_000_000, # 10 million AITBC (in smallest units) + "nonce": 0, + }, + # Community Faucet - 1 million AITBC for airdrop + { + "address": "aitbcfaucet0000000000000000000000000000000000", + "balance": 1_000_000_000_000, # 1 million AITBC + "nonce": 0, + }, + # Team/Dev Fund - 2 million AITBC + { + "address": "aitbcteamfund00000000000000000000000000000000", + "balance": 2_000_000_000_000, # 2 million AITBC + "nonce": 0, + }, + # Early Investor Fund - 5 million AITBC + { + "address": "aitbcearlyinvest000000000000000000000000000000", + "balance": 5_000_000_000_000, # 5 million AITBC + "nonce": 0, + }, + # Ecosystem Fund - 3 million AITBC + { + "address": "aitbecosystem000000000000000000000000000000000", + "balance": 3_000_000_000_000, # 3 million AITBC + "nonce": 0, + } + ], + "authorities": [ + { + "address": "aitbcvalidator00000000000000000000000000000000", + "weight": 1, + } + ], +} + +def create_genesis_with_bootstrap(): + """Create genesis file with initial token distribution""" + + # Set timestamp + GENESIS_CONFIG["timestamp"] = int(time.time()) + + # Calculate total initial distribution + total_supply = sum(account["balance"] for account in GENESIS_CONFIG["accounts"]) + + print("=" * 60) + print("AITBC GENESIS BOOTSTRAP DISTRIBUTION") + print("=" * 60) + print(f"Total Initial Supply: {total_supply / 1_000_000:,.0f} AITBC") + print("\nInitial Distribution:") + + for account in GENESIS_CONFIG["accounts"]: + balance_aitbc = account["balance"] / 1_000_000 + percent = (balance_aitbc / 21_000_000) * 100 + print(f" {account['address']}: {balance_aitbc:,.0f} AITBC ({percent:.1f}%)") + + print("\nPurpose of Funds:") + print(" - Exchange Treasury: Provides liquidity for trading") + print(" - Community Faucet: Airdrop to early users") + print(" - Team Fund: Development incentives") + print(" - Early Investors: Initial backers") + print(" - Ecosystem Fund: Partnerships and growth") + print("=" * 60) + + return GENESIS_CONFIG + +def write_genesis_file(genesis_data, output_path="data/genesis_with_bootstrap.json"): + """Write genesis to file""" + path = Path(output_path) + path.parent.mkdir(parents=True, exist_ok=True) + + with open(path, 'w') as f: + json.dump(genesis_data, f, indent=2, sort_keys=True) + + print(f"\nGenesis file written to: {path}") + return path + +if __name__ == "__main__": + # Create genesis with bootstrap distribution + genesis = create_genesis_with_bootstrap() + + # Write to file + genesis_path = write_genesis_file(genesis) + + print("\nTo apply this genesis:") + print("1. Stop the blockchain node") + print("2. Replace the genesis.json file") + print("3. Reset the blockchain database") + print("4. Restart the node") diff --git a/apps/blockchain-node/scripts/load_genesis.py b/apps/blockchain-node/scripts/load_genesis.py new file mode 100644 index 00000000..1fcba40d --- /dev/null +++ b/apps/blockchain-node/scripts/load_genesis.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Load genesis accounts into the blockchain database""" + +import json +import sys +from pathlib import Path + +# Add the src directory to the path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from aitbc_chain.database import session_scope +from aitbc_chain.models import Account + +def load_genesis_accounts(genesis_path: str = "data/devnet/genesis.json"): + """Load accounts from genesis file into database""" + + # Read genesis file + genesis_file = Path(genesis_path) + if not genesis_file.exists(): + print(f"Error: Genesis file not found at {genesis_path}") + return False + + with open(genesis_file) as f: + genesis = json.load(f) + + # Load accounts + with session_scope() as session: + for account_data in genesis.get("accounts", []): + address = account_data["address"] + balance = account_data["balance"] + nonce = account_data.get("nonce", 0) + + # Check if account already exists + existing = session.query(Account).filter_by(address=address).first() + if existing: + existing.balance = balance + existing.nonce = nonce + print(f"Updated account {address}: balance={balance}") + else: + account = Account(address=address, balance=balance, nonce=nonce) + session.add(account) + print(f"Created account {address}: balance={balance}") + + session.commit() + + print("\nGenesis accounts loaded successfully!") + return True + +if __name__ == "__main__": + if len(sys.argv) > 1: + genesis_path = sys.argv[1] + else: + genesis_path = "data/devnet/genesis.json" + + success = load_genesis_accounts(genesis_path) + sys.exit(0 if success else 1) diff --git a/apps/coordinator-api/scripts/migrate_complete.py b/apps/coordinator-api/scripts/migrate_complete.py new file mode 100644 index 00000000..1ad80b2d --- /dev/null +++ b/apps/coordinator-api/scripts/migrate_complete.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Complete migration script for Coordinator API""" + +import sqlite3 +import psycopg2 +from psycopg2.extras import RealDictCursor +import json +from decimal import Decimal + +# Database configurations +SQLITE_DB = "coordinator.db" +PG_CONFIG = { + "host": "localhost", + "database": "aitbc_coordinator", + "user": "aitbc_user", + "password": "aitbc_password", + "port": 5432 +} + +def migrate_all_data(): + """Migrate all data from SQLite to PostgreSQL""" + + print("\nStarting complete data migration...") + + # Connect to SQLite + sqlite_conn = sqlite3.connect(SQLITE_DB) + sqlite_conn.row_factory = sqlite3.Row + sqlite_cursor = sqlite_conn.cursor() + + # Connect to PostgreSQL + pg_conn = psycopg2.connect(**PG_CONFIG) + pg_cursor = pg_conn.cursor() + + # Get all tables + sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in sqlite_cursor.fetchall()] + + for table_name in tables: + if table_name == 'sqlite_sequence': + continue + + print(f"\nMigrating {table_name}...") + + # Get table schema + sqlite_cursor.execute(f"PRAGMA table_info({table_name})") + columns = sqlite_cursor.fetchall() + column_names = [col[1] for col in columns] + + # Get data + sqlite_cursor.execute(f"SELECT * FROM {table_name}") + rows = sqlite_cursor.fetchall() + + if not rows: + print(f" No data in {table_name}") + continue + + # Build insert query + if table_name == 'user': + insert_sql = f''' + INSERT INTO "{table_name}" ({', '.join(column_names)}) + VALUES ({', '.join(['%s'] * len(column_names))}) + ''' + else: + insert_sql = f''' + INSERT INTO {table_name} ({', '.join(column_names)}) + VALUES ({', '.join(['%s'] * len(column_names))}) + ''' + + # Insert data + count = 0 + for row in rows: + values = [] + for i, value in enumerate(row): + col_name = column_names[i] + # Handle special cases + if col_name in ['payload', 'constraints', 'result', 'receipt', 'capabilities', + 'extra_metadata', 'sla', 'attributes', 'metadata'] and value: + if isinstance(value, str): + try: + value = json.loads(value) + except: + pass + elif col_name in ['balance', 'price', 'average_job_duration_ms'] and value is not None: + value = Decimal(str(value)) + values.append(value) + + try: + pg_cursor.execute(insert_sql, values) + count += 1 + except Exception as e: + print(f" Error inserting row: {e}") + print(f" Values: {values}") + + print(f" Migrated {count} rows from {table_name}") + + pg_conn.commit() + sqlite_conn.close() + pg_conn.close() + + print("\n✅ Complete migration finished!") + +if __name__ == "__main__": + migrate_all_data() diff --git a/apps/coordinator-api/scripts/migrate_to_postgresql.py b/apps/coordinator-api/scripts/migrate_to_postgresql.py new file mode 100644 index 00000000..85d59095 --- /dev/null +++ b/apps/coordinator-api/scripts/migrate_to_postgresql.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Migration script for Coordinator API from SQLite to PostgreSQL""" + +import os +import sys +from pathlib import Path + +# Add the src directory to the path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +import sqlite3 +import psycopg2 +from psycopg2.extras import RealDictCursor +from datetime import datetime +from decimal import Decimal +import json + +# Database configurations +SQLITE_DB = "coordinator.db" +PG_CONFIG = { + "host": "localhost", + "database": "aitbc_coordinator", + "user": "aitbc_user", + "password": "aitbc_password", + "port": 5432 +} + +def create_pg_schema(): + """Create PostgreSQL schema with optimized types""" + + conn = psycopg2.connect(**PG_CONFIG) + cursor = conn.cursor() + + print("Creating PostgreSQL schema...") + + # Drop existing tables + cursor.execute("DROP TABLE IF EXISTS jobreceipt CASCADE") + cursor.execute("DROP TABLE IF EXISTS marketplacebid CASCADE") + cursor.execute("DROP TABLE IF EXISTS marketplaceoffer CASCADE") + cursor.execute("DROP TABLE IF EXISTS job CASCADE") + cursor.execute("DROP TABLE IF EXISTS usersession CASCADE") + cursor.execute("DROP TABLE IF EXISTS wallet CASCADE") + cursor.execute("DROP TABLE IF EXISTS miner CASCADE") + cursor.execute("DROP TABLE IF EXISTS transaction CASCADE") + cursor.execute("DROP TABLE IF EXISTS user CASCADE") + + # Create user table + cursor.execute(""" + CREATE TABLE user ( + id VARCHAR(255) PRIMARY KEY, + email VARCHAR(255), + username VARCHAR(255), + status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'suspended')), + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + last_login TIMESTAMP WITH TIME ZONE + ) + """) + + # Create wallet table + cursor.execute(""" + CREATE TABLE wallet ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(255) REFERENCES user(id), + address VARCHAR(255) UNIQUE, + balance NUMERIC(20, 8) DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + """) + + # Create usersession table + cursor.execute(""" + CREATE TABLE usersession ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(255) REFERENCES user(id), + token VARCHAR(255) UNIQUE, + expires_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_used TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + """) + + # Create miner table + cursor.execute(""" + CREATE TABLE miner ( + id VARCHAR(255) PRIMARY KEY, + region VARCHAR(100), + capabilities JSONB, + concurrency INTEGER DEFAULT 1, + status VARCHAR(20) DEFAULT 'active', + inflight INTEGER DEFAULT 0, + extra_metadata JSONB, + last_heartbeat TIMESTAMP WITH TIME ZONE, + session_token VARCHAR(255), + last_job_at TIMESTAMP WITH TIME ZONE, + jobs_completed INTEGER DEFAULT 0, + jobs_failed INTEGER DEFAULT 0, + total_job_duration_ms BIGINT DEFAULT 0, + average_job_duration_ms NUMERIC(10, 2) DEFAULT 0, + last_receipt_id VARCHAR(255) + ) + """) + + # Create job table + cursor.execute(""" + CREATE TABLE job ( + id VARCHAR(255) PRIMARY KEY, + client_id VARCHAR(255), + state VARCHAR(20) CHECK (state IN ('pending', 'assigned', 'running', 'completed', 'failed', 'expired')), + payload JSONB, + constraints JSONB, + ttl_seconds INTEGER, + requested_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + expires_at TIMESTAMP WITH TIME ZONE, + assigned_miner_id VARCHAR(255) REFERENCES miner(id), + result JSONB, + receipt JSONB, + receipt_id VARCHAR(255), + error TEXT + ) + """) + + # Create marketplaceoffer table + cursor.execute(""" + CREATE TABLE marketplaceoffer ( + id VARCHAR(255) PRIMARY KEY, + provider VARCHAR(255), + capacity INTEGER, + price NUMERIC(20, 8), + sla JSONB, + status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'filled', 'expired')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + attributes JSONB + ) + """) + + # Create marketplacebid table + cursor.execute(""" + CREATE TABLE marketplacebid ( + id VARCHAR(255) PRIMARY KEY, + provider VARCHAR(255), + capacity INTEGER, + price NUMERIC(20, 8), + notes TEXT, + status VARCHAR(20) DEFAULT 'pending', + submitted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + """) + + # Create jobreceipt table + cursor.execute(""" + CREATE TABLE jobreceipt ( + id VARCHAR(255) PRIMARY KEY, + job_id VARCHAR(255) REFERENCES job(id), + receipt_id VARCHAR(255), + payload JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + """) + + # Create transaction table + cursor.execute(""" + CREATE TABLE transaction ( + id VARCHAR(255) PRIMARY KEY, + user_id VARCHAR(255), + type VARCHAR(50), + amount NUMERIC(20, 8), + status VARCHAR(20), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + metadata JSONB + ) + """) + + # Create indexes for performance + print("Creating indexes...") + cursor.execute("CREATE INDEX idx_job_state ON job(state)") + cursor.execute("CREATE INDEX idx_job_client ON job(client_id)") + cursor.execute("CREATE INDEX idx_job_expires ON job(expires_at)") + cursor.execute("CREATE INDEX idx_miner_status ON miner(status)") + cursor.execute("CREATE INDEX idx_miner_heartbeat ON miner(last_heartbeat)") + cursor.execute("CREATE INDEX idx_wallet_user ON wallet(user_id)") + cursor.execute("CREATE INDEX idx_usersession_token ON usersession(token)") + cursor.execute("CREATE INDEX idx_usersession_expires ON usersession(expires_at)") + cursor.execute("CREATE INDEX idx_marketplaceoffer_status ON marketplaceoffer(status)") + cursor.execute("CREATE INDEX idx_marketplaceoffer_provider ON marketplaceoffer(provider)") + cursor.execute("CREATE INDEX idx_marketplacebid_provider ON marketplacebid(provider)") + + conn.commit() + conn.close() + print("✅ PostgreSQL schema created successfully!") + +def migrate_data(): + """Migrate data from SQLite to PostgreSQL""" + + print("\nStarting data migration...") + + # Connect to SQLite + sqlite_conn = sqlite3.connect(SQLITE_DB) + sqlite_conn.row_factory = sqlite3.Row + sqlite_cursor = sqlite_conn.cursor() + + # Connect to PostgreSQL + pg_conn = psycopg2.connect(**PG_CONFIG) + pg_cursor = pg_conn.cursor() + + # Migration order respecting foreign keys + migrations = [ + ('user', ''' + INSERT INTO "user" (id, email, username, status, created_at, updated_at, last_login) + VALUES (%s, %s, %s, %s, %s, %s, %s) + '''), + ('wallet', ''' + INSERT INTO wallet (id, user_id, address, balance, created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s) + '''), + ('miner', ''' + INSERT INTO miner (id, region, capabilities, concurrency, status, inflight, + extra_metadata, last_heartbeat, session_token, last_job_at, + jobs_completed, jobs_failed, total_job_duration_ms, + average_job_duration_ms, last_receipt_id) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + '''), + ('job', ''' + INSERT INTO job (id, client_id, state, payload, constraints, ttl_seconds, + requested_at, expires_at, assigned_miner_id, result, receipt, + receipt_id, error) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + '''), + ('marketplaceoffer', ''' + INSERT INTO marketplaceoffer (id, provider, capacity, price, sla, status, + created_at, attributes) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + '''), + ('marketplacebid', ''' + INSERT INTO marketplacebid (id, provider, capacity, price, notes, status, + submitted_at) + VALUES (%s, %s, %s, %s, %s, %s, %s) + '''), + ('jobreceipt', ''' + INSERT INTO jobreceipt (id, job_id, receipt_id, payload, created_at) + VALUES (%s, %s, %s, %s, %s) + '''), + ('usersession', ''' + INSERT INTO usersession (id, user_id, token, expires_at, created_at, last_used) + VALUES (%s, %s, %s, %s, %s, %s) + '''), + ('transaction', ''' + INSERT INTO transaction (id, user_id, type, amount, status, created_at, metadata) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ''') + ] + + for table_name, insert_sql in migrations: + print(f"Migrating {table_name}...") + sqlite_cursor.execute(f"SELECT * FROM {table_name}") + rows = sqlite_cursor.fetchall() + + count = 0 + for row in rows: + # Convert row to dict and handle JSON fields + values = [] + for key in row.keys(): + value = row[key] + if key in ['payload', 'constraints', 'result', 'receipt', 'capabilities', + 'extra_metadata', 'sla', 'attributes', 'metadata']: + # Handle JSON fields + if isinstance(value, str): + try: + value = json.loads(value) + except: + pass + elif key in ['balance', 'price', 'average_job_duration_ms']: + # Handle numeric fields + if value is not None: + value = Decimal(str(value)) + values.append(value) + + pg_cursor.execute(insert_sql, values) + count += 1 + + print(f" - Migrated {count} {table_name} records") + + pg_conn.commit() + + print(f"\n✅ Migration complete!") + sqlite_conn.close() + pg_conn.close() + +def main(): + """Main migration process""" + + print("=" * 60) + print("AITBC Coordinator API SQLite to PostgreSQL Migration") + print("=" * 60) + + # Check if SQLite DB exists + if not Path(SQLITE_DB).exists(): + print(f"❌ SQLite database '{SQLITE_DB}' not found!") + return + + # Create PostgreSQL schema + create_pg_schema() + + # Migrate data + migrate_data() + + print("\n" + "=" * 60) + print("Migration completed successfully!") + print("=" * 60) + print("\nNext steps:") + print("1. Update coordinator-api configuration") + print("2. Install PostgreSQL dependencies") + print("3. Restart the coordinator service") + print("4. Verify data integrity") + +if __name__ == "__main__": + main() diff --git a/apps/coordinator-api/scripts/setup_postgresql.sh b/apps/coordinator-api/scripts/setup_postgresql.sh new file mode 100644 index 00000000..fb68478e --- /dev/null +++ b/apps/coordinator-api/scripts/setup_postgresql.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "=== PostgreSQL Setup for AITBC Coordinator API ===" +echo "" + +# Create database and user +echo "Creating coordinator database..." +sudo -u postgres psql -c "CREATE DATABASE aitbc_coordinator;" +sudo -u postgres psql -c "CREATE USER aitbc_user WITH PASSWORD 'aitbc_password';" +sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE aitbc_coordinator TO aitbc_user;" + +# Grant schema permissions +sudo -u postgres psql -d aitbc_coordinator -c 'ALTER SCHEMA public OWNER TO aitbc_user;' +sudo -u postgres psql -d aitbc_coordinator -c 'GRANT CREATE ON SCHEMA public TO aitbc_user;' + +# Test connection +echo "Testing connection..." +sudo -u postgres psql -c "\l" | grep aitbc_coordinator + +echo "" +echo "✅ PostgreSQL setup complete for Coordinator API!" +echo "" +echo "Connection details:" +echo " Database: aitbc_coordinator" +echo " User: aitbc_user" +echo " Host: localhost" +echo " Port: 5432" +echo "" +echo "You can now run the migration script." diff --git a/apps/coordinator-api/src/app/config_pg.py b/apps/coordinator-api/src/app/config_pg.py new file mode 100644 index 00000000..d3e9b454 --- /dev/null +++ b/apps/coordinator-api/src/app/config_pg.py @@ -0,0 +1,57 @@ +"""Coordinator API configuration with PostgreSQL support""" + +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + """Application settings""" + + # API Configuration + api_host: str = "0.0.0.0" + api_port: int = 8000 + api_prefix: str = "/v1" + debug: bool = False + + # Database Configuration + database_url: str = "postgresql://aitbc_user:aitbc_password@localhost:5432/aitbc_coordinator" + + # JWT Configuration + jwt_secret: str = "your-secret-key-change-in-production" + jwt_algorithm: str = "HS256" + jwt_expiration_hours: int = 24 + + # Job Configuration + default_job_ttl_seconds: int = 3600 # 1 hour + max_job_ttl_seconds: int = 86400 # 24 hours + job_cleanup_interval_seconds: int = 300 # 5 minutes + + # Miner Configuration + miner_heartbeat_timeout_seconds: int = 120 # 2 minutes + miner_max_inflight: int = 10 + + # Marketplace Configuration + marketplace_offer_ttl_seconds: int = 3600 # 1 hour + + # Wallet Configuration + wallet_rpc_url: str = "http://localhost:9080" + + # CORS Configuration + cors_origins: list[str] = [ + "http://localhost:3000", + "http://localhost:8080", + "https://aitbc.bubuit.net", + "https://aitbc.bubuit.net:8080" + ] + + # Logging Configuration + log_level: str = "INFO" + log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +# Create global settings instance +settings = Settings() diff --git a/apps/coordinator-api/src/app/main.py b/apps/coordinator-api/src/app/main.py index 2a117382..1686cea2 100644 --- a/apps/coordinator-api/src/app/main.py +++ b/apps/coordinator-api/src/app/main.py @@ -15,6 +15,7 @@ from .routers import ( services, marketplace_offers, zk_applications, + explorer, ) from .routers import zk_applications from .routers.governance import router as governance @@ -51,6 +52,7 @@ def create_app() -> FastAPI: app.include_router(zk_applications.router, prefix="/v1") app.include_router(governance, prefix="/v1") app.include_router(partners, prefix="/v1") + app.include_router(explorer, prefix="/v1") # Add Prometheus metrics endpoint metrics_app = make_asgi_app() diff --git a/apps/coordinator-api/src/app/routers/admin.py b/apps/coordinator-api/src/app/routers/admin.py index de0fd8d6..671c809f 100644 --- a/apps/coordinator-api/src/app/routers/admin.py +++ b/apps/coordinator-api/src/app/routers/admin.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import select from ..deps import require_admin_key from ..services import JobService, MinerService @@ -53,7 +54,7 @@ async def list_miners(session: SessionDep, admin_key: str = Depends(require_admi miner_service = MinerService(session) miners = [ { - "miner_id": record.miner_id, + "miner_id": record.id, "status": record.status, "inflight": record.inflight, "concurrency": record.concurrency, diff --git a/apps/coordinator-api/src/app/routers/client.py b/apps/coordinator-api/src/app/routers/client.py index b7015f33..fac4de84 100644 --- a/apps/coordinator-api/src/app/routers/client.py +++ b/apps/coordinator-api/src/app/routers/client.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from ..deps import require_client_key from ..schemas import JobCreate, JobView, JobResult +from ..types import JobState from ..services import JobService from ..storage import SessionDep diff --git a/apps/coordinator-api/src/app/routers/miner.py b/apps/coordinator-api/src/app/routers/miner.py index 663267b2..a74ae4d7 100644 --- a/apps/coordinator-api/src/app/routers/miner.py +++ b/apps/coordinator-api/src/app/routers/miner.py @@ -73,7 +73,7 @@ async def submit_result( duration_ms = int((datetime.utcnow() - job.requested_at).total_seconds() * 1000) metrics["duration_ms"] = duration_ms - receipt = receipt_service.create_receipt(job, miner_id, req.result, metrics) + receipt = await receipt_service.create_receipt(job, miner_id, req.result, metrics) job.receipt = receipt job.receipt_id = receipt["receipt_id"] if receipt else None session.add(job) diff --git a/apps/coordinator-api/src/app/routers/partners.py b/apps/coordinator-api/src/app/routers/partners.py index d75ca044..5b26c909 100644 --- a/apps/coordinator-api/src/app/routers/partners.py +++ b/apps/coordinator-api/src/app/routers/partners.py @@ -20,9 +20,9 @@ class PartnerRegister(BaseModel): """Register a new partner application""" name: str = Field(..., min_length=3, max_length=100) description: str = Field(..., min_length=10, max_length=500) - website: str = Field(..., regex=r'^https?://') - contact: str = Field(..., regex=r'^[^@]+@[^@]+\.[^@]+$') - integration_type: str = Field(..., regex="^(explorer|analytics|wallet|exchange|other)$") + website: str = Field(..., pattern=r'^https?://') + contact: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$') + integration_type: str = Field(..., pattern="^(explorer|analytics|wallet|exchange|other)$") class PartnerResponse(BaseModel): @@ -36,7 +36,7 @@ class PartnerResponse(BaseModel): class WebhookCreate(BaseModel): """Create a webhook subscription""" - url: str = Field(..., regex=r'^https?://') + url: str = Field(..., pattern=r'^https?://') events: List[str] = Field(..., min_items=1) secret: Optional[str] = Field(max_length=100) diff --git a/apps/coordinator-api/src/app/schemas.py b/apps/coordinator-api/src/app/schemas.py index e5dc8d2e..ded919b2 100644 --- a/apps/coordinator-api/src/app/schemas.py +++ b/apps/coordinator-api/src/app/schemas.py @@ -195,6 +195,7 @@ class ReceiptSummary(BaseModel): model_config = ConfigDict(populate_by_name=True) receiptId: str + jobId: Optional[str] = None miner: str coordinator: str issuedAt: datetime diff --git a/apps/coordinator-api/src/app/services/explorer.py b/apps/coordinator-api/src/app/services/explorer.py index 859ce07b..0c4a7b07 100644 --- a/apps/coordinator-api/src/app/services/explorer.py +++ b/apps/coordinator-api/src/app/services/explorer.py @@ -50,7 +50,7 @@ class ExplorerService: height=height, hash=job.id, timestamp=job.requested_at, - tx_count=1, + txCount=1, proposer=proposer, ) ) @@ -71,13 +71,22 @@ class ExplorerService: for index, job in enumerate(jobs): height = _DEFAULT_HEIGHT_BASE + offset + index status_label = _STATUS_LABELS.get(job.state, job.state.value.title()) - value = job.payload.get("value") if isinstance(job.payload, dict) else None - if value is None: - value_str = "0" - elif isinstance(value, (int, float)): - value_str = f"{value}" - else: - value_str = str(value) + + # Try to get payment amount from receipt + value_str = "0" + if job.receipt and isinstance(job.receipt, dict): + price = job.receipt.get("price") + if price is not None: + value_str = f"{price}" + + # Fallback to payload value if no receipt + if value_str == "0": + value = job.payload.get("value") if isinstance(job.payload, dict) else None + if value is not None: + if isinstance(value, (int, float)): + value_str = f"{value}" + else: + value_str = str(value) items.append( TransactionSummary( @@ -100,14 +109,16 @@ class ExplorerService: address_map: dict[str, dict[str, object]] = defaultdict( lambda: { "address": "", - "balance": "0", + "balance": 0.0, "tx_count": 0, "last_active": datetime.min, "recent_transactions": deque(maxlen=5), + "earned": 0.0, + "spent": 0.0, } ) - def touch(address: Optional[str], tx_id: str, when: datetime, value_hint: Optional[str] = None) -> None: + def touch(address: Optional[str], tx_id: str, when: datetime, earned: float = 0.0, spent: float = 0.0) -> None: if not address: return entry = address_map[address] @@ -115,18 +126,27 @@ class ExplorerService: entry["tx_count"] = int(entry["tx_count"]) + 1 if when > entry["last_active"]: entry["last_active"] = when - if value_hint: - entry["balance"] = value_hint + # Track earnings and spending + entry["earned"] = float(entry["earned"]) + earned + entry["spent"] = float(entry["spent"]) + spent + entry["balance"] = float(entry["earned"]) - float(entry["spent"]) recent: deque[str] = entry["recent_transactions"] # type: ignore[assignment] recent.appendleft(tx_id) for job in jobs: - value = job.payload.get("value") if isinstance(job.payload, dict) else None - value_hint: Optional[str] = None - if value is not None: - value_hint = str(value) - touch(job.client_id, job.id, job.requested_at, value_hint=value_hint) - touch(job.assigned_miner_id, job.id, job.requested_at) + # Get payment amount from receipt if available + price = 0.0 + if job.receipt and isinstance(job.receipt, dict): + receipt_price = job.receipt.get("price") + if receipt_price is not None: + try: + price = float(receipt_price) + except (TypeError, ValueError): + pass + + # Miner earns, client spends + touch(job.assigned_miner_id, job.id, job.requested_at, earned=price) + touch(job.client_id, job.id, job.requested_at, spent=price) sorted_addresses = sorted( address_map.values(), @@ -138,7 +158,7 @@ class ExplorerService: items = [ AddressSummary( address=entry["address"], - balance=str(entry["balance"]), + balance=f"{float(entry['balance']):.6f}", txCount=int(entry["tx_count"]), lastActive=entry["last_active"], recentTransactions=list(entry["recent_transactions"]), @@ -164,19 +184,24 @@ class ExplorerService: items: list[ReceiptSummary] = [] for row in rows: payload = row.payload or {} - miner = payload.get("miner") or payload.get("miner_id") or "unknown" - coordinator = payload.get("coordinator") or payload.get("coordinator_id") or "unknown" + # Extract miner from provider field (receipt format) or fallback + miner = payload.get("provider") or payload.get("miner") or payload.get("miner_id") or "unknown" + # Extract client as coordinator (receipt format) or fallback + coordinator = payload.get("client") or payload.get("coordinator") or payload.get("coordinator_id") or "unknown" status = payload.get("status") or payload.get("state") or "Unknown" + # Get job_id from payload + job_id_from_payload = payload.get("job_id") or row.job_id items.append( ReceiptSummary( - receipt_id=row.receipt_id, + receiptId=row.receipt_id, miner=miner, coordinator=coordinator, - issued_at=row.created_at, + issuedAt=row.created_at, status=status, payload=payload, + jobId=job_id_from_payload, ) ) resolved_job_id = job_id or "all" - return ReceiptListResponse(job_id=resolved_job_id, items=items) + return ReceiptListResponse(jobId=resolved_job_id, items=items) diff --git a/apps/coordinator-api/src/app/services/jobs.py b/apps/coordinator-api/src/app/services/jobs.py index 7a708053..12ea15bc 100644 --- a/apps/coordinator-api/src/app/services/jobs.py +++ b/apps/coordinator-api/src/app/services/jobs.py @@ -101,7 +101,7 @@ class JobService: return None def _ensure_not_expired(self, job: Job) -> Job: - if job.state == JobState.queued and job.expires_at <= datetime.utcnow(): + if job.state in {JobState.queued, JobState.running} and job.expires_at <= datetime.utcnow(): job.state = JobState.expired job.error = "job expired" self.session.add(job) diff --git a/apps/coordinator-api/src/app/services/miners.py b/apps/coordinator-api/src/app/services/miners.py index 50bb70e5..7844f9dc 100644 --- a/apps/coordinator-api/src/app/services/miners.py +++ b/apps/coordinator-api/src/app/services/miners.py @@ -32,6 +32,7 @@ class MinerService: miner.concurrency = payload.concurrency miner.region = payload.region miner.session_token = session_token + miner.inflight = 0 miner.last_heartbeat = datetime.utcnow() miner.status = "ONLINE" self.session.commit() diff --git a/apps/coordinator-api/src/app/services/receipts.py b/apps/coordinator-api/src/app/services/receipts.py index 5f4396b6..145e1cba 100644 --- a/apps/coordinator-api/src/app/services/receipts.py +++ b/apps/coordinator-api/src/app/services/receipts.py @@ -35,24 +35,60 @@ class ReceiptService: ) -> Dict[str, Any] | None: if self._signer is None: return None + metrics = result_metrics or {} + result_payload = job_result or {} + unit_type = _first_present([ + metrics.get("unit_type"), + result_payload.get("unit_type"), + ], default="gpu_seconds") + + units = _coerce_float(_first_present([ + metrics.get("units"), + result_payload.get("units"), + ])) + if units is None: + duration_ms = _coerce_float(metrics.get("duration_ms")) + if duration_ms is not None: + units = duration_ms / 1000.0 + else: + duration_seconds = _coerce_float(_first_present([ + metrics.get("duration_seconds"), + metrics.get("compute_time"), + result_payload.get("execution_time"), + result_payload.get("duration"), + ])) + units = duration_seconds + if units is None: + units = 0.0 + + unit_price = _coerce_float(_first_present([ + metrics.get("unit_price"), + result_payload.get("unit_price"), + ])) + if unit_price is None: + unit_price = 0.02 + + price = _coerce_float(_first_present([ + metrics.get("price"), + result_payload.get("price"), + metrics.get("aitbc_earned"), + result_payload.get("aitbc_earned"), + metrics.get("cost"), + result_payload.get("cost"), + ])) + if price is None: + price = round(units * unit_price, 6) payload = { "version": "1.0", "receipt_id": token_hex(16), "job_id": job.id, "provider": miner_id, "client": job.client_id, - "units": _first_present([ - (result_metrics or {}).get("units"), - (job_result or {}).get("units"), - ], default=0.0), - "unit_type": _first_present([ - (result_metrics or {}).get("unit_type"), - (job_result or {}).get("unit_type"), - ], default="gpu_seconds"), - "price": _first_present([ - (result_metrics or {}).get("price"), - (job_result or {}).get("price"), - ]), + "status": job.state.value, + "units": units, + "unit_type": unit_type, + "unit_price": unit_price, + "price": price, "started_at": int(job.requested_at.timestamp()) if job.requested_at else int(datetime.utcnow().timestamp()), "completed_at": int(datetime.utcnow().timestamp()), "metadata": { @@ -105,3 +141,13 @@ def _first_present(values: list[Optional[Any]], default: Optional[Any] = None) - if value is not None: return value return default + + +def _coerce_float(value: Any) -> Optional[float]: + """Coerce a value to float, returning None if not possible""" + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None diff --git a/apps/coordinator-api/src/app/storage/db_pg.py b/apps/coordinator-api/src/app/storage/db_pg.py new file mode 100644 index 00000000..d6f00fa8 --- /dev/null +++ b/apps/coordinator-api/src/app/storage/db_pg.py @@ -0,0 +1,223 @@ +"""PostgreSQL database module for Coordinator API""" + +from sqlalchemy import create_engine, MetaData +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import StaticPool +import psycopg2 +from psycopg2.extras import RealDictCursor +from typing import Generator, Optional, Dict, Any, List +import json +from datetime import datetime +from decimal import Decimal + +from .config_pg import settings + +# SQLAlchemy setup for complex queries +engine = create_engine( + settings.database_url, + echo=settings.debug, + pool_pre_ping=True, + pool_recycle=300, +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# Direct PostgreSQL connection for performance +def get_pg_connection(): + """Get direct PostgreSQL connection""" + return psycopg2.connect( + host="localhost", + database="aitbc_coordinator", + user="aitbc_user", + password="aitbc_password", + port=5432, + cursor_factory=RealDictCursor + ) + +def get_db() -> Generator[Session, None, None]: + """Get database session""" + db = SessionLocal() + try: + yield db + finally: + db.close() + +class PostgreSQLAdapter: + """PostgreSQL adapter for high-performance operations""" + + def __init__(self): + self.connection = get_pg_connection() + + def execute_query(self, query: str, params: tuple = None) -> List[Dict[str, Any]]: + """Execute a query and return results""" + with self.connection.cursor() as cursor: + cursor.execute(query, params) + return cursor.fetchall() + + def execute_update(self, query: str, params: tuple = None) -> int: + """Execute an update/insert/delete query""" + with self.connection.cursor() as cursor: + cursor.execute(query, params) + self.connection.commit() + return cursor.rowcount + + def execute_batch(self, query: str, params_list: List[tuple]) -> int: + """Execute batch insert/update""" + with self.connection.cursor() as cursor: + cursor.executemany(query, params_list) + self.connection.commit() + return cursor.rowcount + + def get_job_by_id(self, job_id: str) -> Optional[Dict[str, Any]]: + """Get job by ID""" + query = "SELECT * FROM job WHERE id = %s" + results = self.execute_query(query, (job_id,)) + return results[0] if results else None + + def get_available_miners(self, region: Optional[str] = None) -> List[Dict[str, Any]]: + """Get available miners""" + if region: + query = """ + SELECT * FROM miner + WHERE status = 'active' + AND inflight < concurrency + AND (region = %s OR region IS NULL) + ORDER BY last_heartbeat DESC + """ + return self.execute_query(query, (region,)) + else: + query = """ + SELECT * FROM miner + WHERE status = 'active' + AND inflight < concurrency + ORDER BY last_heartbeat DESC + """ + return self.execute_query(query) + + def get_pending_jobs(self, limit: int = 100) -> List[Dict[str, Any]]: + """Get pending jobs""" + query = """ + SELECT * FROM job + WHERE state = 'pending' + AND expires_at > NOW() + ORDER BY requested_at ASC + LIMIT %s + """ + return self.execute_query(query, (limit,)) + + def update_job_state(self, job_id: str, state: str, **kwargs) -> bool: + """Update job state""" + set_clauses = ["state = %s"] + params = [state, job_id] + + for key, value in kwargs.items(): + set_clauses.append(f"{key} = %s") + params.insert(-1, value) + + query = f""" + UPDATE job + SET {', '.join(set_clauses)}, updated_at = NOW() + WHERE id = %s + """ + + return self.execute_update(query, params) > 0 + + def get_marketplace_offers(self, status: str = "active") -> List[Dict[str, Any]]: + """Get marketplace offers""" + query = """ + SELECT * FROM marketplaceoffer + WHERE status = %s + ORDER BY price ASC, created_at DESC + """ + return self.execute_query(query, (status,)) + + def get_user_wallets(self, user_id: str) -> List[Dict[str, Any]]: + """Get user wallets""" + query = """ + SELECT * FROM wallet + WHERE user_id = %s + ORDER BY created_at DESC + """ + return self.execute_query(query, (user_id,)) + + def create_job(self, job_data: Dict[str, Any]) -> str: + """Create a new job""" + query = """ + INSERT INTO job (id, client_id, state, payload, constraints, + ttl_seconds, requested_at, expires_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """ + result = self.execute_query(query, ( + job_data['id'], + job_data['client_id'], + job_data['state'], + json.dumps(job_data['payload']), + json.dumps(job_data.get('constraints', {})), + job_data['ttl_seconds'], + job_data['requested_at'], + job_data['expires_at'] + )) + return result[0]['id'] + + def cleanup_expired_jobs(self) -> int: + """Clean up expired jobs""" + query = """ + UPDATE job + SET state = 'expired', updated_at = NOW() + WHERE state = 'pending' + AND expires_at < NOW() + """ + return self.execute_update(query) + + def get_miner_stats(self, miner_id: str) -> Optional[Dict[str, Any]]: + """Get miner statistics""" + query = """ + SELECT + COUNT(*) as total_jobs, + COUNT(CASE WHEN state = 'completed' THEN 1 END) as completed_jobs, + COUNT(CASE WHEN state = 'failed' THEN 1 END) as failed_jobs, + AVG(CASE WHEN state = 'completed' THEN EXTRACT(EPOCH FROM (updated_at - requested_at)) END) as avg_duration_seconds + FROM job + WHERE assigned_miner_id = %s + """ + results = self.execute_query(query, (miner_id,)) + return results[0] if results else None + + def close(self): + """Close the connection""" + if self.connection: + self.connection.close() + +# Global adapter instance +db_adapter = PostgreSQLAdapter() + +# Database initialization +def init_db(): + """Initialize database tables""" + # Import models here to avoid circular imports + from .models import Base + + # Create all tables + Base.metadata.create_all(bind=engine) + + print("✅ PostgreSQL database initialized successfully!") + +# Health check +def check_db_health() -> Dict[str, Any]: + """Check database health""" + try: + result = db_adapter.execute_query("SELECT 1 as health_check") + return { + "status": "healthy", + "database": "postgresql", + "timestamp": datetime.utcnow().isoformat() + } + except Exception as e: + return { + "status": "unhealthy", + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } diff --git a/apps/explorer-web/public/mock/blocks.json b/apps/explorer-web/public/mock/blocks.json index 9c4b32bb..2d9ff907 100644 --- a/apps/explorer-web/public/mock/blocks.json +++ b/apps/explorer-web/public/mock/blocks.json @@ -1,23 +1,32 @@ -[ - { - "height": 12045, - "hash": "0x7a3f5bf5c3b8ed5d6f77a42b8ab9a421e91e23f4d2a3f6a1d4b5c6d7e8f90123", - "timestamp": "2025-09-27T01:58:12Z", - "txCount": 8, - "proposer": "miner-alpha" - }, - { - "height": 12044, - "hash": "0x5dd4e7a2b88c56f4cbb8f6e21d332e2f1a765e8d9c0b12a34567890abcdef012", - "timestamp": "2025-09-27T01:56:43Z", - "txCount": 11, - "proposer": "miner-beta" - }, - { - "height": 12043, - "hash": "0x1b9d2c3f4e5a67890b12c34d56e78f90a1b2c3d4e5f60718293a4b5c6d7e8f90", - "timestamp": "2025-09-27T01:54:16Z", - "txCount": 4, - "proposer": "miner-gamma" - } -] +{ + "items": [ + { + "height": 0, + "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "2025-01-01T00:00:00Z", + "txCount": 1, + "proposer": "genesis" + }, + { + "height": 12045, + "hash": "0x7a3f5bf5c3b8ed5d6f77a42b8ab9a421e91e23f4d2a3f6a1d4b5c6d7e8f90123", + "timestamp": "2025-09-27T01:58:12Z", + "txCount": 8, + "proposer": "miner-alpha" + }, + { + "height": 12044, + "hash": "0x5dd4e7a2b88c56f4cbb8f6e21d332e2f1a765e8d9c0b12a34567890abcdef012", + "timestamp": "2025-09-27T01:56:43Z", + "txCount": 11, + "proposer": "miner-beta" + }, + { + "height": 12043, + "hash": "0x1b9d2c3f4e5a67890b12c34d56e78f90a1b2c3d4e5f60718293a4b5c6d7e8f90", + "timestamp": "2025-09-27T01:54:16Z", + "txCount": 4, + "proposer": "miner-gamma" + } + ] +} diff --git a/apps/explorer-web/public/mock/receipts.json b/apps/explorer-web/public/mock/receipts.json index 6d05a4a1..a7e7d9a3 100644 --- a/apps/explorer-web/public/mock/receipts.json +++ b/apps/explorer-web/public/mock/receipts.json @@ -1,18 +1,20 @@ -[ - { - "jobId": "job-0001", - "receiptId": "rcpt-123", - "miner": "miner-alpha", - "coordinator": "coordinator-001", - "issuedAt": "2025-09-27T01:52:22Z", - "status": "Attested" - }, - { - "jobId": "job-0002", - "receiptId": "rcpt-124", - "miner": "miner-beta", - "coordinator": "coordinator-001", - "issuedAt": "2025-09-27T01:45:18Z", - "status": "Pending" - } -] +{ + "items": [ + { + "jobId": "job-0001", + "receiptId": "rcpt-123", + "miner": "miner-alpha", + "coordinator": "coordinator-001", + "issuedAt": "2025-09-27T01:52:22Z", + "status": "Attested" + }, + { + "jobId": "job-0002", + "receiptId": "rcpt-124", + "miner": "miner-beta", + "coordinator": "coordinator-001", + "issuedAt": "2025-09-27T01:45:18Z", + "status": "Pending" + } + ] +} diff --git a/apps/explorer-web/public/mock/transactions.json b/apps/explorer-web/public/mock/transactions.json index b56227b0..ccdc666a 100644 --- a/apps/explorer-web/public/mock/transactions.json +++ b/apps/explorer-web/public/mock/transactions.json @@ -1,18 +1,20 @@ -[ - { - "hash": "0xabc1230000000000000000000000000000000000000000000000000000000001", - "block": 12045, - "from": "0xfeedfacefeedfacefeedfacefeedfacefeedface", - "to": "0xcafebabecafebabecafebabecafebabecafebabe", - "value": "12.5 AIT", - "status": "Succeeded" - }, - { - "hash": "0xabc1230000000000000000000000000000000000000000000000000000000002", - "block": 12044, - "from": "0xdeadc0dedeadc0dedeadc0dedeadc0dedeadc0de", - "to": "0x8badf00d8badf00d8badf00d8badf00d8badf00d", - "value": "3.1 AIT", - "status": "Pending" - } -] +{ + "items": [ + { + "hash": "0xabc1230000000000000000000000000000000000000000000000000000000001", + "block": 12045, + "from": "0xfeedfacefeedfacefeedfacefeedfacefeedface", + "to": "0xcafebabecafebabecafebabecafebabecafebabe", + "value": "12.5 AIT", + "status": "Succeeded" + }, + { + "hash": "0xabc1230000000000000000000000000000000000000000000000000000000002", + "block": 12044, + "from": "0xdeadc0dedeadc0dedeadc0dedeadc0dedeadc0de", + "to": "0x8badf00d8badf00d8badf00d8badf00d8badf00d", + "value": "3.1 AIT", + "status": "Pending" + } + ] +} diff --git a/apps/explorer-web/src/components/dataModeToggle.ts b/apps/explorer-web/src/components/dataModeToggle.ts index 4dba9f49..88242d53 100644 --- a/apps/explorer-web/src/components/dataModeToggle.ts +++ b/apps/explorer-web/src/components/dataModeToggle.ts @@ -1,4 +1,4 @@ -import { CONFIG, type DataMode } from "../config"; +import { config, type DataMode } from "../config"; import { getDataMode, setDataMode } from "../lib/mockData"; const LABELS: Record = { @@ -44,7 +44,7 @@ function renderControls(mode: DataMode): string { - ${mode === "mock" ? "Static JSON samples" : `Coordinator API (${CONFIG.apiBaseUrl})`} + ${mode === "mock" ? "Static JSON samples" : `Coordinator API (${config.apiBaseUrl})`} `; } diff --git a/apps/explorer-web/src/config.ts b/apps/explorer-web/src/config.ts index 7b5b3ca0..0511c762 100644 --- a/apps/explorer-web/src/config.ts +++ b/apps/explorer-web/src/config.ts @@ -6,11 +6,11 @@ export interface ExplorerConfig { apiBaseUrl: string; } -export const CONFIG: ExplorerConfig = { +export const config = { // Base URL for the coordinator API - apiBaseUrl: "https://aitbc.bubuit.net/api", + apiBaseUrl: import.meta.env.VITE_COORDINATOR_API ?? 'https://aitbc.bubuit.net/api', // Base path for mock data files (used by fetchMock) - mockBasePath: "/explorer/mock", + mockBasePath: '/explorer/mock', // Default data mode: "live" or "mock" - dataMode: "live" as "live" | "mock", -}; + dataMode: 'live', // Changed from 'mock' to 'live' +} as const; diff --git a/apps/explorer-web/src/lib/mockData.ts b/apps/explorer-web/src/lib/mockData.ts index ded93b21..2bfd2bbc 100644 --- a/apps/explorer-web/src/lib/mockData.ts +++ b/apps/explorer-web/src/lib/mockData.ts @@ -1,4 +1,4 @@ -import { CONFIG, type DataMode } from "../config"; +import { config, type DataMode } from "../config"; import { notifyError } from "../components/notifications"; import type { BlockListResponse, @@ -29,9 +29,20 @@ function loadStoredMode(): DataMode | null { return null; } -const initialMode = loadStoredMode() ?? CONFIG.dataMode; +// Force live mode - ignore stale localStorage +const storedMode = loadStoredMode(); +const initialMode = storedMode === "mock" ? "live" : (storedMode ?? config.dataMode); let currentMode: DataMode = initialMode; +// Clear any cached mock mode preference +if (storedMode === "mock" && typeof window !== "undefined") { + try { + window.localStorage.setItem(STORAGE_KEY, "live"); + } catch (error) { + console.warn("[Explorer] Failed to update cached mode", error); + } +} + function syncDocumentMode(mode: DataMode): void { if (typeof document !== "undefined") { document.documentElement.dataset.mode = mode; @@ -63,7 +74,7 @@ export async function fetchBlocks(): Promise { } try { - const response = await fetch(`${CONFIG.apiBaseUrl}/explorer/blocks`); + const response = await fetch(`${config.apiBaseUrl}/explorer/blocks`); if (!response.ok) { throw new Error(`Failed to fetch blocks: ${response.status} ${response.statusText}`); } @@ -87,7 +98,7 @@ export async function fetchTransactions(): Promise { } try { - const response = await fetch(`${CONFIG.apiBaseUrl}/explorer/transactions`); + const response = await fetch(`${config.apiBaseUrl}/explorer/transactions`); if (!response.ok) { throw new Error(`Failed to fetch transactions: ${response.status} ${response.statusText}`); } @@ -111,7 +122,7 @@ export async function fetchAddresses(): Promise { } try { - const response = await fetch(`${CONFIG.apiBaseUrl}/explorer/addresses`); + const response = await fetch(`${config.apiBaseUrl}/explorer/addresses`); if (!response.ok) { throw new Error(`Failed to fetch addresses: ${response.status} ${response.statusText}`); } @@ -135,7 +146,7 @@ export async function fetchReceipts(): Promise { } try { - const response = await fetch(`${CONFIG.apiBaseUrl}/explorer/receipts`); + const response = await fetch(`${config.apiBaseUrl}/explorer/receipts`); if (!response.ok) { throw new Error(`Failed to fetch receipts: ${response.status} ${response.statusText}`); } @@ -153,7 +164,7 @@ export async function fetchReceipts(): Promise { } async function fetchMock(resource: string): Promise { - const url = `${CONFIG.mockBasePath}/${resource}.json`; + const url = `${config.mockBasePath}/${resource}.json`; try { const response = await fetch(url); if (!response.ok) { diff --git a/apps/explorer-web/src/lib/models.ts b/apps/explorer-web/src/lib/models.ts index 527b4a4a..093ba858 100644 --- a/apps/explorer-web/src/lib/models.ts +++ b/apps/explorer-web/src/lib/models.ts @@ -41,13 +41,26 @@ export interface AddressListResponse { export interface ReceiptSummary { receiptId: string; + jobId?: string; miner: string; coordinator: string; issuedAt: string; status: string; payload?: { + job_id?: string; + provider?: string; + client?: string; + units?: number; + unit_type?: string; + unit_price?: number; + price?: number; minerSignature?: string; coordinatorSignature?: string; + signature?: { + alg?: string; + key_id?: string; + sig?: string; + }; }; } diff --git a/apps/explorer-web/src/main.ts b/apps/explorer-web/src/main.ts index 8f1e4c7c..94336b6a 100644 --- a/apps/explorer-web/src/main.ts +++ b/apps/explorer-web/src/main.ts @@ -8,8 +8,6 @@ import { blocksTitle, renderBlocksPage, initBlocksPage } from "./pages/blocks"; import { transactionsTitle, renderTransactionsPage, initTransactionsPage } from "./pages/transactions"; import { addressesTitle, renderAddressesPage, initAddressesPage } from "./pages/addresses"; import { receiptsTitle, renderReceiptsPage, initReceiptsPage } from "./pages/receipts"; -import { initDataModeToggle } from "./components/dataModeToggle"; -import { getDataMode } from "./lib/mockData"; import { initNotifications } from "./components/notifications"; type PageConfig = { @@ -68,7 +66,6 @@ function render(): void { ${siteFooter()} `; - initDataModeToggle(render); void page?.init?.(); } diff --git a/apps/explorer-web/src/pages/addresses.ts b/apps/explorer-web/src/pages/addresses.ts index 8b3293fa..d3ec116d 100644 --- a/apps/explorer-web/src/pages/addresses.ts +++ b/apps/explorer-web/src/pages/addresses.ts @@ -8,7 +8,7 @@ export function renderAddressesPage(): string {

Address Lookup

-

Enter an account address to view recent transactions, balances, and receipt history (mock results shown below).

+

Live address data from the AITBC coordinator API.