Files
wifi-densepose/vendor/ruvector/examples/edge-net/pkg/genesis.js

859 lines
24 KiB
JavaScript

#!/usr/bin/env node
/**
* @ruvector/edge-net Genesis Node
*
* Bootstrap node for the edge-net P2P network.
* Provides signaling, peer discovery, and ledger sync.
*
* Run: node genesis.js [--port 8787] [--data ~/.ruvector/genesis]
*
* @module @ruvector/edge-net/genesis
*/
import { EventEmitter } from 'events';
import { createHash, randomBytes } from 'crypto';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
// ============================================
// GENESIS NODE CONFIGURATION
// ============================================
export const GENESIS_CONFIG = {
port: parseInt(process.env.GENESIS_PORT || '8787'),
host: process.env.GENESIS_HOST || '0.0.0.0',
dataDir: process.env.GENESIS_DATA || join(process.env.HOME || '/tmp', '.ruvector', 'genesis'),
// Rate limiting
rateLimit: {
maxConnectionsPerIp: 50,
maxMessagesPerSecond: 100,
challengeExpiry: 60000, // 1 minute
},
// Cleanup
cleanup: {
staleConnectionTimeout: 300000, // 5 minutes
cleanupInterval: 60000, // 1 minute
},
};
// ============================================
// PEER REGISTRY
// ============================================
export class PeerRegistry {
constructor() {
this.peers = new Map(); // peerId -> peer info
this.byPublicKey = new Map(); // publicKey -> peerId
this.byRoom = new Map(); // room -> Set<peerId>
this.connections = new Map(); // connectionId -> peerId
}
register(peerId, info) {
this.peers.set(peerId, {
...info,
peerId,
registeredAt: Date.now(),
lastSeen: Date.now(),
});
if (info.publicKey) {
this.byPublicKey.set(info.publicKey, peerId);
}
return this.peers.get(peerId);
}
update(peerId, updates) {
const peer = this.peers.get(peerId);
if (peer) {
Object.assign(peer, updates, { lastSeen: Date.now() });
}
return peer;
}
get(peerId) {
return this.peers.get(peerId);
}
getByPublicKey(publicKey) {
const peerId = this.byPublicKey.get(publicKey);
return peerId ? this.peers.get(peerId) : null;
}
remove(peerId) {
const peer = this.peers.get(peerId);
if (peer) {
if (peer.publicKey) {
this.byPublicKey.delete(peer.publicKey);
}
if (peer.room) {
const room = this.byRoom.get(peer.room);
if (room) room.delete(peerId);
}
this.peers.delete(peerId);
return true;
}
return false;
}
joinRoom(peerId, room) {
const peer = this.peers.get(peerId);
if (!peer) return false;
// Leave old room
if (peer.room && peer.room !== room) {
const oldRoom = this.byRoom.get(peer.room);
if (oldRoom) oldRoom.delete(peerId);
}
// Join new room
if (!this.byRoom.has(room)) {
this.byRoom.set(room, new Set());
}
this.byRoom.get(room).add(peerId);
peer.room = room;
return true;
}
getRoomPeers(room) {
const peerIds = this.byRoom.get(room) || new Set();
return Array.from(peerIds).map(id => this.peers.get(id)).filter(Boolean);
}
getAllPeers() {
return Array.from(this.peers.values());
}
pruneStale(maxAge = GENESIS_CONFIG.cleanup.staleConnectionTimeout) {
const cutoff = Date.now() - maxAge;
const removed = [];
for (const [peerId, peer] of this.peers) {
if (peer.lastSeen < cutoff) {
this.remove(peerId);
removed.push(peerId);
}
}
return removed;
}
getStats() {
return {
totalPeers: this.peers.size,
rooms: this.byRoom.size,
roomSizes: Object.fromEntries(
Array.from(this.byRoom.entries()).map(([room, peers]) => [room, peers.size])
),
};
}
}
// ============================================
// LEDGER STORE
// ============================================
export class LedgerStore {
constructor(dataDir) {
this.dataDir = dataDir;
this.ledgers = new Map();
this.pendingWrites = new Map();
// Ensure data directory exists
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
}
// Load existing ledgers
this.loadAll();
}
loadAll() {
try {
const indexPath = join(this.dataDir, 'index.json');
if (existsSync(indexPath)) {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
for (const publicKey of index.keys || []) {
this.load(publicKey);
}
}
} catch (err) {
console.warn('[Genesis] Failed to load ledger index:', err.message);
}
}
load(publicKey) {
try {
const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
if (existsSync(path)) {
const data = JSON.parse(readFileSync(path, 'utf8'));
this.ledgers.set(publicKey, data);
return data;
}
} catch (err) {
console.warn(`[Genesis] Failed to load ledger ${publicKey.slice(0, 8)}:`, err.message);
}
return null;
}
save(publicKey) {
try {
const data = this.ledgers.get(publicKey);
if (!data) return false;
const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
writeFileSync(path, JSON.stringify(data, null, 2));
// Update index
this.saveIndex();
return true;
} catch (err) {
console.warn(`[Genesis] Failed to save ledger ${publicKey.slice(0, 8)}:`, err.message);
return false;
}
}
saveIndex() {
try {
const indexPath = join(this.dataDir, 'index.json');
writeFileSync(indexPath, JSON.stringify({
keys: Array.from(this.ledgers.keys()),
updatedAt: Date.now(),
}, null, 2));
} catch (err) {
console.warn('[Genesis] Failed to save index:', err.message);
}
}
get(publicKey) {
return this.ledgers.get(publicKey);
}
getStates(publicKey) {
const ledger = this.ledgers.get(publicKey);
if (!ledger) return [];
return Object.values(ledger.devices || {});
}
update(publicKey, deviceId, state) {
if (!this.ledgers.has(publicKey)) {
this.ledgers.set(publicKey, {
publicKey,
createdAt: Date.now(),
devices: {},
});
}
const ledger = this.ledgers.get(publicKey);
// Merge state
const existing = ledger.devices[deviceId] || {};
const merged = this.mergeCRDT(existing, state);
ledger.devices[deviceId] = {
...merged,
deviceId,
updatedAt: Date.now(),
};
// Schedule write
this.scheduleSave(publicKey);
return ledger.devices[deviceId];
}
mergeCRDT(existing, incoming) {
// Simple LWW merge for now
if (!existing.timestamp || incoming.timestamp > existing.timestamp) {
return { ...incoming };
}
// If same timestamp, merge counters
return {
earned: Math.max(existing.earned || 0, incoming.earned || 0),
spent: Math.max(existing.spent || 0, incoming.spent || 0),
timestamp: Math.max(existing.timestamp || 0, incoming.timestamp || 0),
};
}
scheduleSave(publicKey) {
if (this.pendingWrites.has(publicKey)) return;
this.pendingWrites.set(publicKey, setTimeout(() => {
this.save(publicKey);
this.pendingWrites.delete(publicKey);
}, 1000));
}
flush() {
for (const [publicKey, timeout] of this.pendingWrites) {
clearTimeout(timeout);
this.save(publicKey);
}
this.pendingWrites.clear();
}
getStats() {
return {
totalLedgers: this.ledgers.size,
totalDevices: Array.from(this.ledgers.values())
.reduce((sum, l) => sum + Object.keys(l.devices || {}).length, 0),
};
}
}
// ============================================
// AUTHENTICATION SERVICE
// ============================================
export class AuthService {
constructor() {
this.challenges = new Map(); // nonce -> { challenge, publicKey, expiresAt }
this.tokens = new Map(); // token -> { publicKey, deviceId, expiresAt }
}
createChallenge(publicKey, deviceId) {
const nonce = randomBytes(32).toString('hex');
const challenge = randomBytes(32).toString('hex');
this.challenges.set(nonce, {
challenge,
publicKey,
deviceId,
expiresAt: Date.now() + GENESIS_CONFIG.rateLimit.challengeExpiry,
});
return { nonce, challenge };
}
verifyChallenge(nonce, publicKey, signature) {
const challengeData = this.challenges.get(nonce);
if (!challengeData) {
return { valid: false, error: 'Invalid nonce' };
}
if (Date.now() > challengeData.expiresAt) {
this.challenges.delete(nonce);
return { valid: false, error: 'Challenge expired' };
}
if (challengeData.publicKey !== publicKey) {
return { valid: false, error: 'Public key mismatch' };
}
// Simple signature verification (in production, use proper Ed25519)
const expectedSig = createHash('sha256')
.update(challengeData.challenge + publicKey)
.digest('hex');
// For now, accept any signature (real impl would verify Ed25519)
// In production: verify Ed25519 signature
this.challenges.delete(nonce);
// Generate token
const token = randomBytes(32).toString('hex');
const tokenData = {
publicKey,
deviceId: challengeData.deviceId,
createdAt: Date.now(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
};
this.tokens.set(token, tokenData);
return { valid: true, token, expiresAt: tokenData.expiresAt };
}
validateToken(token) {
const tokenData = this.tokens.get(token);
if (!tokenData) return null;
if (Date.now() > tokenData.expiresAt) {
this.tokens.delete(token);
return null;
}
return tokenData;
}
cleanup() {
const now = Date.now();
for (const [nonce, data] of this.challenges) {
if (now > data.expiresAt) {
this.challenges.delete(nonce);
}
}
for (const [token, data] of this.tokens) {
if (now > data.expiresAt) {
this.tokens.delete(token);
}
}
}
}
// ============================================
// GENESIS NODE SERVER
// ============================================
export class GenesisNode extends EventEmitter {
constructor(options = {}) {
super();
this.config = { ...GENESIS_CONFIG, ...options };
this.peerRegistry = new PeerRegistry();
this.ledgerStore = new LedgerStore(this.config.dataDir);
this.authService = new AuthService();
this.wss = null;
this.connections = new Map();
this.cleanupInterval = null;
this.stats = {
startedAt: null,
totalConnections: 0,
totalMessages: 0,
signalsRelayed: 0,
};
}
async start() {
console.log('\n🌐 Starting Edge-Net Genesis Node...');
console.log(` Port: ${this.config.port}`);
console.log(` Data: ${this.config.dataDir}`);
// Import ws dynamically
const { WebSocketServer } = await import('ws');
this.wss = new WebSocketServer({
port: this.config.port,
host: this.config.host,
});
this.wss.on('connection', (ws, req) => this.handleConnection(ws, req));
this.wss.on('error', (err) => this.emit('error', err));
// Start cleanup interval
this.cleanupInterval = setInterval(() => this.cleanup(), this.config.cleanup.cleanupInterval);
this.stats.startedAt = Date.now();
console.log(`\n✅ Genesis Node running on ws://${this.config.host}:${this.config.port}`);
console.log(` API: http://${this.config.host}:${this.config.port}/api/v1/`);
this.emit('started', { port: this.config.port });
return this;
}
stop() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
if (this.wss) {
this.wss.close();
}
this.ledgerStore.flush();
this.emit('stopped');
}
handleConnection(ws, req) {
const connectionId = randomBytes(16).toString('hex');
const ip = req.socket.remoteAddress;
this.stats.totalConnections++;
this.connections.set(connectionId, {
ws,
ip,
peerId: null,
connectedAt: Date.now(),
});
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(connectionId, message);
} catch (err) {
console.warn(`[Genesis] Invalid message from ${connectionId}:`, err.message);
}
});
ws.on('close', () => {
this.handleDisconnect(connectionId);
});
ws.on('error', (err) => {
console.warn(`[Genesis] Connection error ${connectionId}:`, err.message);
});
// Send welcome
this.send(connectionId, {
type: 'welcome',
connectionId,
serverTime: Date.now(),
});
}
handleDisconnect(connectionId) {
const conn = this.connections.get(connectionId);
if (conn?.peerId) {
const peer = this.peerRegistry.get(conn.peerId);
if (peer?.room) {
// Notify room peers
this.broadcastToRoom(peer.room, {
type: 'peer-left',
peerId: conn.peerId,
}, conn.peerId);
}
this.peerRegistry.remove(conn.peerId);
}
this.connections.delete(connectionId);
}
handleMessage(connectionId, message) {
this.stats.totalMessages++;
const conn = this.connections.get(connectionId);
if (!conn) return;
switch (message.type) {
// Signaling messages
case 'announce':
this.handleAnnounce(connectionId, message);
break;
case 'join':
this.handleJoinRoom(connectionId, message);
break;
case 'offer':
case 'answer':
case 'ice-candidate':
this.relaySignal(connectionId, message);
break;
// Auth messages
case 'auth-challenge':
this.handleAuthChallenge(connectionId, message);
break;
case 'auth-verify':
this.handleAuthVerify(connectionId, message);
break;
// Ledger messages
case 'ledger-get':
this.handleLedgerGet(connectionId, message);
break;
case 'ledger-put':
this.handleLedgerPut(connectionId, message);
break;
// DHT bootstrap
case 'dht-bootstrap':
this.handleDHTBootstrap(connectionId, message);
break;
default:
console.warn(`[Genesis] Unknown message type: ${message.type}`);
}
}
handleAnnounce(connectionId, message) {
const conn = this.connections.get(connectionId);
const peerId = message.piKey || message.peerId || randomBytes(16).toString('hex');
conn.peerId = peerId;
this.peerRegistry.register(peerId, {
publicKey: message.publicKey,
siteId: message.siteId,
capabilities: message.capabilities || [],
connectionId,
});
// Send current peer list
const peers = this.peerRegistry.getAllPeers()
.filter(p => p.peerId !== peerId)
.map(p => ({
piKey: p.peerId,
siteId: p.siteId,
capabilities: p.capabilities,
}));
this.send(connectionId, {
type: 'peer-list',
peers,
});
// Notify other peers
for (const peer of this.peerRegistry.getAllPeers()) {
if (peer.peerId !== peerId && peer.connectionId) {
this.send(peer.connectionId, {
type: 'peer-joined',
peerId,
siteId: message.siteId,
capabilities: message.capabilities,
});
}
}
}
handleJoinRoom(connectionId, message) {
const conn = this.connections.get(connectionId);
if (!conn?.peerId) return;
const room = message.room || 'default';
this.peerRegistry.joinRoom(conn.peerId, room);
// Send room peers
const roomPeers = this.peerRegistry.getRoomPeers(room)
.filter(p => p.peerId !== conn.peerId)
.map(p => ({
piKey: p.peerId,
siteId: p.siteId,
}));
this.send(connectionId, {
type: 'room-joined',
room,
peers: roomPeers,
});
// Notify room peers
this.broadcastToRoom(room, {
type: 'peer-joined',
peerId: conn.peerId,
siteId: this.peerRegistry.get(conn.peerId)?.siteId,
}, conn.peerId);
}
relaySignal(connectionId, message) {
this.stats.signalsRelayed++;
const conn = this.connections.get(connectionId);
if (!conn?.peerId) return;
const targetPeer = this.peerRegistry.get(message.to);
if (!targetPeer?.connectionId) {
this.send(connectionId, {
type: 'error',
error: 'Target peer not found',
originalType: message.type,
});
return;
}
// Relay the signal
this.send(targetPeer.connectionId, {
...message,
from: conn.peerId,
});
}
handleAuthChallenge(connectionId, message) {
const { nonce, challenge } = this.authService.createChallenge(
message.publicKey,
message.deviceId
);
this.send(connectionId, {
type: 'auth-challenge-response',
nonce,
challenge,
});
}
handleAuthVerify(connectionId, message) {
const result = this.authService.verifyChallenge(
message.nonce,
message.publicKey,
message.signature
);
this.send(connectionId, {
type: 'auth-verify-response',
...result,
});
}
handleLedgerGet(connectionId, message) {
const tokenData = this.authService.validateToken(message.token);
if (!tokenData) {
this.send(connectionId, {
type: 'ledger-response',
error: 'Invalid or expired token',
});
return;
}
const states = this.ledgerStore.getStates(message.publicKey || tokenData.publicKey);
this.send(connectionId, {
type: 'ledger-response',
states,
});
}
handleLedgerPut(connectionId, message) {
const tokenData = this.authService.validateToken(message.token);
if (!tokenData) {
this.send(connectionId, {
type: 'ledger-put-response',
error: 'Invalid or expired token',
});
return;
}
const updated = this.ledgerStore.update(
tokenData.publicKey,
message.deviceId || tokenData.deviceId,
message.state
);
this.send(connectionId, {
type: 'ledger-put-response',
success: true,
state: updated,
});
}
handleDHTBootstrap(connectionId, message) {
// Return known peers for DHT bootstrap
const peers = this.peerRegistry.getAllPeers()
.slice(0, 20)
.map(p => ({
id: p.peerId,
address: p.connectionId,
lastSeen: p.lastSeen,
}));
this.send(connectionId, {
type: 'dht-bootstrap-response',
peers,
});
}
send(connectionId, message) {
const conn = this.connections.get(connectionId);
if (conn?.ws?.readyState === 1) {
conn.ws.send(JSON.stringify(message));
}
}
broadcastToRoom(room, message, excludePeerId = null) {
const peers = this.peerRegistry.getRoomPeers(room);
for (const peer of peers) {
if (peer.peerId !== excludePeerId && peer.connectionId) {
this.send(peer.connectionId, message);
}
}
}
cleanup() {
// Prune stale peers
const removed = this.peerRegistry.pruneStale();
if (removed.length > 0) {
console.log(`[Genesis] Pruned ${removed.length} stale peers`);
}
// Cleanup auth
this.authService.cleanup();
}
getStats() {
return {
...this.stats,
uptime: this.stats.startedAt ? Date.now() - this.stats.startedAt : 0,
...this.peerRegistry.getStats(),
...this.ledgerStore.getStats(),
activeConnections: this.connections.size,
};
}
}
// ============================================
// CLI
// ============================================
async function main() {
const args = process.argv.slice(2);
// Parse args
let port = GENESIS_CONFIG.port;
let dataDir = GENESIS_CONFIG.dataDir;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' && args[i + 1]) {
port = parseInt(args[i + 1]);
i++;
} else if (args[i] === '--data' && args[i + 1]) {
dataDir = args[i + 1];
i++;
} else if (args[i] === '--help') {
console.log(`
Edge-Net Genesis Node
Usage: node genesis.js [options]
Options:
--port <port> Port to listen on (default: 8787)
--data <dir> Data directory (default: ~/.ruvector/genesis)
--help Show this help
Environment Variables:
GENESIS_PORT Port (default: 8787)
GENESIS_HOST Host (default: 0.0.0.0)
GENESIS_DATA Data directory
Examples:
node genesis.js
node genesis.js --port 9000
node genesis.js --port 8787 --data /var/lib/edge-net
`);
process.exit(0);
}
}
const genesis = new GenesisNode({ port, dataDir });
// Handle shutdown
process.on('SIGINT', () => {
console.log('\n\n🛑 Shutting down Genesis Node...');
genesis.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
genesis.stop();
process.exit(0);
});
// Start server
await genesis.start();
// Log stats periodically
setInterval(() => {
const stats = genesis.getStats();
console.log(`[Genesis] Peers: ${stats.totalPeers} | Connections: ${stats.activeConnections} | Signals: ${stats.signalsRelayed}`);
}, 60000);
}
// Run if executed directly
if (process.argv[1]?.endsWith('genesis.js')) {
main().catch(err => {
console.error('Genesis Node error:', err);
process.exit(1);
});
}
export default GenesisNode;