#!/usr/bin/env node /** * @ruvector/edge-net Contribution Daemon * * Real CPU contribution daemon that runs in the background and earns QDAG credits. * Connects to the relay server and sends contribution_credit messages periodically. * * Usage: * npx @ruvector/edge-net contribute # Start daemon (foreground) * npx @ruvector/edge-net contribute --daemon # Start daemon (background) * npx @ruvector/edge-net contribute --stop # Stop daemon * npx @ruvector/edge-net contribute --status # Show daemon status * npx @ruvector/edge-net contribute --cpu 50 # Set CPU limit (default: 50%) * npx @ruvector/edge-net contribute --key # Use specific public key */ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { webcrypto } from 'crypto'; import { performance } from 'perf_hooks'; import { homedir, cpus } from 'os'; import { spawn } from 'child_process'; import WebSocket from 'ws'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Relay server URL const RELAY_URL = process.env.RELAY_URL || 'wss://edge-net-relay-875130704813.us-central1.run.app'; // Contribution settings const DEFAULT_CPU_LIMIT = 50; // Default 50% CPU const CONTRIBUTION_INTERVAL = 30000; // Report every 30 seconds const RECONNECT_DELAY = 5000; // Reconnect after 5 seconds on disconnect const HEARTBEAT_INTERVAL = 15000; // Heartbeat every 15 seconds // ANSI colors const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', magenta: '\x1b[35m', }; // Config directory function getConfigDir() { const dir = join(homedir(), '.ruvector'); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); return dir; } function getPidFile() { return join(getConfigDir(), 'contribute-daemon.pid'); } function getLogFile() { return join(getConfigDir(), 'contribute-daemon.log'); } function getStateFile() { return join(getConfigDir(), 'contribute-state.json'); } // Parse command line arguments function parseArgs(args) { const opts = { daemon: false, stop: false, status: false, cpu: DEFAULT_CPU_LIMIT, key: null, site: 'cli-contributor', help: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--daemon': case '-d': opts.daemon = true; break; case '--stop': opts.stop = true; break; case '--status': opts.status = true; break; case '--cpu': opts.cpu = parseInt(args[++i]) || DEFAULT_CPU_LIMIT; break; case '--key': opts.key = args[++i]; break; case '--site': opts.site = args[++i]; break; case '--help': case '-h': opts.help = true; break; } } return opts; } // Load or generate identity async function loadIdentity(opts) { const identitiesDir = join(getConfigDir(), 'identities'); if (!existsSync(identitiesDir)) mkdirSync(identitiesDir, { recursive: true }); const metaPath = join(identitiesDir, `${opts.site}.meta.json`); // If --key is provided, use it directly if (opts.key) { return { publicKey: opts.key, shortId: `pi:${opts.key.slice(0, 16)}`, siteId: opts.site, }; } // Try to load existing identity if (existsSync(metaPath)) { const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); return { publicKey: meta.publicKey, shortId: meta.shortId, siteId: meta.siteId, }; } // Generate new identity using WASM const { createRequire } = await import('module'); const require = createRequire(import.meta.url); // Setup polyfills if (typeof globalThis.crypto === 'undefined') { globalThis.crypto = webcrypto; } console.log(`${c.dim}Generating new identity...${c.reset}`); const wasm = require('./node/ruvector_edge_net.cjs'); const piKey = new wasm.PiKey(); const publicKey = Array.from(piKey.getPublicKey()) .map(b => b.toString(16).padStart(2, '0')) .join(''); const meta = { version: 1, siteId: opts.site, shortId: piKey.getShortId(), publicKey, createdAt: new Date().toISOString(), lastUsed: new Date().toISOString(), totalSessions: 1, totalContributions: 0, }; writeFileSync(metaPath, JSON.stringify(meta, null, 2)); piKey.free(); return { publicKey: meta.publicKey, shortId: meta.shortId, siteId: meta.siteId, }; } // Load daemon state function loadState() { const stateFile = getStateFile(); if (existsSync(stateFile)) { return JSON.parse(readFileSync(stateFile, 'utf-8')); } return { totalCredits: 0, totalContributions: 0, totalSeconds: 0, startTime: null, lastSync: null, }; } // Save daemon state function saveState(state) { writeFileSync(getStateFile(), JSON.stringify(state, null, 2)); } // Measure real CPU usage function measureCpuUsage(durationMs = 1000) { return new Promise((resolve) => { const startCpus = cpus(); const startTotal = startCpus.reduce((acc, cpu) => { const times = cpu.times; return acc + times.user + times.nice + times.sys + times.idle + times.irq; }, 0); const startIdle = startCpus.reduce((acc, cpu) => acc + cpu.times.idle, 0); setTimeout(() => { const endCpus = cpus(); const endTotal = endCpus.reduce((acc, cpu) => { const times = cpu.times; return acc + times.user + times.nice + times.sys + times.idle + times.irq; }, 0); const endIdle = endCpus.reduce((acc, cpu) => acc + cpu.times.idle, 0); const totalDiff = endTotal - startTotal; const idleDiff = endIdle - startIdle; const cpuUsage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0; resolve(Math.min(100, Math.max(0, cpuUsage))); }, durationMs); }); } // Real CPU work (compute hashes) function doRealWork(cpuLimit, durationMs) { return new Promise((resolve) => { const startTime = Date.now(); const workInterval = 10; // Work in 10ms chunks const workRatio = cpuLimit / 100; let computeUnits = 0; const doWork = () => { const now = Date.now(); if (now - startTime >= durationMs) { resolve(computeUnits); return; } // Do actual CPU work (hash computation) const workStart = Date.now(); const workTime = workInterval * workRatio; while (Date.now() - workStart < workTime) { // Real cryptographic work let data = new Uint8Array(64); for (let i = 0; i < 64; i++) { data[i] = (i * 7 + computeUnits) & 0xff; } // Simple hash-like operation (real CPU work) for (let j = 0; j < 100; j++) { let hash = 0; for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash + data[i]) | 0; } data[0] = hash & 0xff; } computeUnits++; } // Rest period to respect CPU limit const restTime = workInterval * (1 - workRatio); setTimeout(doWork, restTime); }; doWork(); }); } // Contribution daemon class class ContributionDaemon { constructor(identity, cpuLimit) { this.identity = identity; this.cpuLimit = cpuLimit; this.ws = null; this.state = loadState(); this.isRunning = false; this.nodeId = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; this.contributionTimer = null; this.heartbeatTimer = null; this.reconnectTimer = null; this.sessionStart = Date.now(); this.sessionCredits = 0; this.sessionContributions = 0; } async start() { this.isRunning = true; this.state.startTime = Date.now(); saveState(this.state); console.log(`\n${c.cyan}${c.bold}Edge-Net Contribution Daemon${c.reset}`); console.log(`${c.dim}Real CPU contribution to earn QDAG credits${c.reset}\n`); console.log(`${c.bold}Configuration:${c.reset}`); console.log(` ${c.cyan}Identity:${c.reset} ${this.identity.shortId}`); console.log(` ${c.cyan}Public Key:${c.reset} ${this.identity.publicKey.slice(0, 16)}...`); console.log(` ${c.cyan}CPU Limit:${c.reset} ${this.cpuLimit}%`); console.log(` ${c.cyan}Relay:${c.reset} ${RELAY_URL}`); console.log(` ${c.cyan}Interval:${c.reset} ${CONTRIBUTION_INTERVAL / 1000}s\n`); await this.connect(); // Handle shutdown process.on('SIGINT', () => this.stop('SIGINT')); process.on('SIGTERM', () => this.stop('SIGTERM')); } async connect() { if (!this.isRunning) return; console.log(`${c.dim}Connecting to relay...${c.reset}`); try { this.ws = new WebSocket(RELAY_URL); this.ws.on('open', () => { console.log(`${c.green}Connected to relay${c.reset}`); // Register with relay this.send({ type: 'register', nodeId: this.nodeId, publicKey: this.identity.publicKey, capabilities: ['compute', 'cli-daemon'], version: '1.0.0', }); // Start heartbeat this.startHeartbeat(); // Request initial balance from QDAG setTimeout(() => { this.send({ type: 'ledger_sync', nodeId: this.nodeId, publicKey: this.identity.publicKey, }); }, 500); }); this.ws.on('message', (data) => this.handleMessage(data.toString())); this.ws.on('close', () => { console.log(`${c.yellow}Disconnected from relay${c.reset}`); this.stopHeartbeat(); this.stopContributing(); this.scheduleReconnect(); }); this.ws.on('error', (err) => { console.log(`${c.red}WebSocket error: ${err.message}${c.reset}`); }); } catch (err) { console.log(`${c.red}Connection failed: ${err.message}${c.reset}`); this.scheduleReconnect(); } } handleMessage(data) { try { const msg = JSON.parse(data); switch (msg.type) { case 'welcome': console.log(`${c.green}Registered as ${msg.nodeId}${c.reset}`); console.log(`${c.dim}Network: ${msg.networkState?.activeNodes || 0} active nodes${c.reset}`); this.startContributing(); break; case 'ledger_sync_response': const earned = Number(msg.ledger?.earned || 0) / 1e9; const spent = Number(msg.ledger?.spent || 0) / 1e9; const available = earned - spent; console.log(`${c.cyan}QDAG Balance: ${available.toFixed(4)} rUv${c.reset} (earned: ${earned.toFixed(4)}, spent: ${spent.toFixed(4)})`); this.state.totalCredits = earned; saveState(this.state); break; case 'contribution_credit_success': const credited = msg.credited || 0; this.sessionCredits += credited; this.sessionContributions++; this.state.totalCredits = Number(msg.balance?.earned || 0) / 1e9; this.state.totalContributions++; this.state.lastSync = Date.now(); saveState(this.state); const balance = Number(msg.balance?.available || 0) / 1e9; console.log(`${c.green}+${credited.toFixed(4)} rUv${c.reset} | Balance: ${balance.toFixed(4)} rUv | Total: ${this.state.totalContributions} contributions`); break; case 'contribution_credit_error': console.log(`${c.yellow}Contribution rejected: ${msg.error}${c.reset}`); break; case 'time_crystal_sync': // Silently handle time crystal sync break; case 'heartbeat_ack': // Heartbeat acknowledged break; case 'error': console.log(`${c.red}Relay error: ${msg.message}${c.reset}`); break; default: // Ignore unknown messages } } catch (err) { console.log(`${c.red}Error parsing message: ${err.message}${c.reset}`); } } send(msg) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(msg)); } } startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { this.send({ type: 'heartbeat' }); }, HEARTBEAT_INTERVAL); } stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } startContributing() { this.stopContributing(); console.log(`${c.cyan}Starting contribution loop (CPU: ${this.cpuLimit}%)${c.reset}\n`); // Immediate first contribution this.contribute(); // Then continue every interval this.contributionTimer = setInterval(() => { this.contribute(); }, CONTRIBUTION_INTERVAL); } stopContributing() { if (this.contributionTimer) { clearInterval(this.contributionTimer); this.contributionTimer = null; } } async contribute() { const startTime = Date.now(); // Do real CPU work for 5 seconds console.log(`${c.dim}[${new Date().toLocaleTimeString()}] Working...${c.reset}`); await doRealWork(this.cpuLimit, 5000); // Measure actual CPU usage const cpuUsage = await measureCpuUsage(1000); const contributionSeconds = 30; // Claiming 30 seconds since last report const effectiveCpu = Math.min(this.cpuLimit, cpuUsage); // Send contribution credit request this.send({ type: 'contribution_credit', nodeId: this.nodeId, publicKey: this.identity.publicKey, contributionSeconds, cpuUsage: Math.round(effectiveCpu), timestamp: Date.now(), }); this.state.totalSeconds += contributionSeconds; saveState(this.state); } scheduleReconnect() { if (!this.isRunning) return; if (this.reconnectTimer) return; console.log(`${c.dim}Reconnecting in ${RECONNECT_DELAY / 1000}s...${c.reset}`); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect(); }, RECONNECT_DELAY); } stop(signal = 'unknown') { console.log(`\n${c.yellow}Stopping daemon (${signal})...${c.reset}`); this.isRunning = false; this.stopContributing(); this.stopHeartbeat(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } // Save final state saveState(this.state); // Print session summary const sessionDuration = (Date.now() - this.sessionStart) / 1000; console.log(`\n${c.bold}Session Summary:${c.reset}`); console.log(` ${c.cyan}Duration:${c.reset} ${Math.round(sessionDuration)}s`); console.log(` ${c.cyan}Contributions:${c.reset} ${this.sessionContributions}`); console.log(` ${c.cyan}Credits:${c.reset} ${this.sessionCredits.toFixed(4)} rUv`); console.log(` ${c.cyan}Total Earned:${c.reset} ${this.state.totalCredits.toFixed(4)} rUv\n`); // Remove PID file const pidFile = getPidFile(); if (existsSync(pidFile)) { unlinkSync(pidFile); } process.exit(0); } } // Show daemon status async function showStatus(opts) { const pidFile = getPidFile(); const state = loadState(); const identity = await loadIdentity(opts); console.log(`\n${c.cyan}${c.bold}Edge-Net Contribution Daemon Status${c.reset}\n`); // Check if daemon is running if (existsSync(pidFile)) { const pid = parseInt(readFileSync(pidFile, 'utf-8').trim()); try { process.kill(pid, 0); // Check if process exists console.log(`${c.green}Status: Running${c.reset} (PID: ${pid})`); } catch { console.log(`${c.yellow}Status: Stale PID file${c.reset} (process not found)`); unlinkSync(pidFile); } } else { console.log(`${c.dim}Status: Not running${c.reset}`); } console.log(`\n${c.bold}Identity:${c.reset}`); console.log(` ${c.cyan}Short ID:${c.reset} ${identity.shortId}`); console.log(` ${c.cyan}Public Key:${c.reset} ${identity.publicKey.slice(0, 32)}...`); console.log(`\n${c.bold}Statistics:${c.reset}`); console.log(` ${c.cyan}Total Credits:${c.reset} ${state.totalCredits?.toFixed(4) || 0} rUv`); console.log(` ${c.cyan}Total Contributions:${c.reset} ${state.totalContributions || 0}`); console.log(` ${c.cyan}Total Time:${c.reset} ${state.totalSeconds || 0}s`); if (state.lastSync) { const lastSync = new Date(state.lastSync); console.log(` ${c.cyan}Last Sync:${c.reset} ${lastSync.toLocaleString()}`); } console.log(`\n${c.bold}Files:${c.reset}`); console.log(` ${c.dim}State:${c.reset} ${getStateFile()}`); console.log(` ${c.dim}Log:${c.reset} ${getLogFile()}`); console.log(` ${c.dim}PID:${c.reset} ${pidFile}\n`); } // Stop daemon function stopDaemon() { const pidFile = getPidFile(); if (!existsSync(pidFile)) { console.log(`${c.yellow}Daemon is not running${c.reset}`); return; } const pid = parseInt(readFileSync(pidFile, 'utf-8').trim()); try { process.kill(pid, 'SIGTERM'); console.log(`${c.green}Sent SIGTERM to daemon (PID: ${pid})${c.reset}`); // Wait a bit and check if it stopped setTimeout(() => { try { process.kill(pid, 0); console.log(`${c.yellow}Daemon still running, sending SIGKILL...${c.reset}`); process.kill(pid, 'SIGKILL'); } catch { console.log(`${c.green}Daemon stopped${c.reset}`); } if (existsSync(pidFile)) { unlinkSync(pidFile); } }, 2000); } catch { console.log(`${c.yellow}Process not found, cleaning up...${c.reset}`); unlinkSync(pidFile); } } // Start daemon in background function startDaemonBackground(args) { const pidFile = getPidFile(); const logFile = getLogFile(); if (existsSync(pidFile)) { const pid = parseInt(readFileSync(pidFile, 'utf-8').trim()); try { process.kill(pid, 0); console.log(`${c.yellow}Daemon already running (PID: ${pid})${c.reset}`); console.log(`${c.dim}Use --stop to stop it first${c.reset}`); return; } catch { unlinkSync(pidFile); } } console.log(`${c.cyan}Starting daemon in background...${c.reset}`); const out = require('fs').openSync(logFile, 'a'); const err = require('fs').openSync(logFile, 'a'); // Remove --daemon flag and respawn const filteredArgs = args.filter(a => a !== '--daemon' && a !== '-d'); const child = spawn(process.execPath, [__filename, ...filteredArgs], { detached: true, stdio: ['ignore', out, err], }); writeFileSync(pidFile, String(child.pid)); child.unref(); console.log(`${c.green}Daemon started (PID: ${child.pid})${c.reset}`); console.log(`${c.dim}Log file: ${logFile}${c.reset}`); console.log(`${c.dim}Use --status to check status${c.reset}`); console.log(`${c.dim}Use --stop to stop daemon${c.reset}`); } // Print help function printHelp() { console.log(` ${c.cyan}${c.bold}Edge-Net Contribution Daemon${c.reset} ${c.dim}Contribute CPU to earn QDAG credits${c.reset} ${c.bold}USAGE:${c.reset} npx @ruvector/edge-net contribute [options] ${c.bold}OPTIONS:${c.reset} ${c.yellow}--daemon, -d${c.reset} Run in background (detached) ${c.yellow}--stop${c.reset} Stop running daemon ${c.yellow}--status${c.reset} Show daemon status ${c.yellow}--cpu ${c.reset} CPU usage limit (default: 50) ${c.yellow}--key ${c.reset} Use specific public key ${c.yellow}--site ${c.reset} Site identifier (default: cli-contributor) ${c.yellow}--help, -h${c.reset} Show this help ${c.bold}EXAMPLES:${c.reset} ${c.dim}# Start contributing in foreground${c.reset} $ npx @ruvector/edge-net contribute ${c.dim}# Start as background daemon with 30% CPU${c.reset} $ npx @ruvector/edge-net contribute --daemon --cpu 30 ${c.dim}# Use specific public key${c.reset} $ npx @ruvector/edge-net contribute --key 38a3bcd1732fe04c... ${c.dim}# Check status${c.reset} $ npx @ruvector/edge-net contribute --status ${c.dim}# Stop daemon${c.reset} $ npx @ruvector/edge-net contribute --stop ${c.bold}HOW IT WORKS:${c.reset} 1. Daemon connects to Edge-Net relay server 2. Every 30 seconds, does real CPU work 3. Reports contribution to relay 4. Relay credits QDAG (Firestore) with earned rUv 5. Credits persist and sync across all devices ${c.bold}CREDIT RATE:${c.reset} Base rate: ~0.047 rUv/second of contribution Max rate: ~0.05 rUv/second (180 rUv/hour max) Formula: contributionSeconds * 0.047 * (cpuUsage / 100) `); } // Main entry point async function main() { const args = process.argv.slice(2); // Filter out 'contribute' if passed const filteredArgs = args.filter(a => a !== 'contribute'); const opts = parseArgs(filteredArgs); if (opts.help) { printHelp(); return; } if (opts.status) { await showStatus(opts); return; } if (opts.stop) { stopDaemon(); return; } if (opts.daemon) { startDaemonBackground(filteredArgs); return; } // Start daemon in foreground try { const identity = await loadIdentity(opts); const daemon = new ContributionDaemon(identity, opts.cpu); await daemon.start(); } catch (err) { console.error(`${c.red}Error: ${err.message}${c.reset}`); process.exit(1); } } main().catch(err => { console.error(`${c.red}Fatal error: ${err.message}${c.reset}`); process.exit(1); });