@ruvector/edge-net 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1536 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ruvector/edge-net Genesis Node - Production Deployment
4
+ *
5
+ * Production-ready bootstrap node for the Edge-Net P2P network.
6
+ * Features:
7
+ * - Persistent service with graceful shutdown
8
+ * - Firebase registration as known bootstrap node
9
+ * - DHT routing table maintenance
10
+ * - Peer discovery request handling
11
+ * - Health check HTTP endpoint
12
+ * - Structured JSON logging for monitoring
13
+ * - Automatic reconnection and recovery
14
+ *
15
+ * Environment Variables:
16
+ * GENESIS_PORT - WebSocket port (default: 8787)
17
+ * GENESIS_HOST - Bind address (default: 0.0.0.0)
18
+ * GENESIS_DATA - Data directory (default: /data/genesis)
19
+ * GENESIS_NODE_ID - Fixed node ID (optional, auto-generated if not set)
20
+ * HEALTH_PORT - Health check HTTP port (default: 8788)
21
+ * LOG_LEVEL - Logging level: debug, info, warn, error (default: info)
22
+ * LOG_FORMAT - Log format: json, text (default: json)
23
+ * FIREBASE_API_KEY - Firebase API key (optional)
24
+ * FIREBASE_PROJECT_ID - Firebase project ID (optional)
25
+ * METRICS_ENABLED - Enable Prometheus metrics (default: true)
26
+ *
27
+ * @module @ruvector/edge-net/deploy/genesis-prod
28
+ */
29
+
30
+ import { EventEmitter } from 'events';
31
+ import { createHash, randomBytes } from 'crypto';
32
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
33
+ import { join, dirname } from 'path';
34
+ import { fileURLToPath } from 'url';
35
+ import http from 'http';
36
+
37
+ // Resolve paths
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = dirname(__filename);
40
+
41
+ // ============================================
42
+ // CONFIGURATION
43
+ // ============================================
44
+
45
+ const CONFIG = {
46
+ // Network
47
+ port: parseInt(process.env.GENESIS_PORT || '8787'),
48
+ host: process.env.GENESIS_HOST || '0.0.0.0',
49
+ healthPort: parseInt(process.env.HEALTH_PORT || '8788'),
50
+
51
+ // Storage
52
+ dataDir: process.env.GENESIS_DATA || '/data/genesis',
53
+
54
+ // Identity
55
+ nodeId: process.env.GENESIS_NODE_ID || null,
56
+
57
+ // Logging
58
+ logLevel: process.env.LOG_LEVEL || 'info',
59
+ logFormat: process.env.LOG_FORMAT || 'json',
60
+
61
+ // Features
62
+ metricsEnabled: process.env.METRICS_ENABLED !== 'false',
63
+
64
+ // Rate limiting
65
+ rateLimit: {
66
+ maxConnectionsPerIp: parseInt(process.env.MAX_CONN_PER_IP || '50'),
67
+ maxMessagesPerSecond: parseInt(process.env.MAX_MSG_PER_SEC || '100'),
68
+ challengeExpiry: 60000,
69
+ },
70
+
71
+ // Cleanup
72
+ cleanup: {
73
+ staleConnectionTimeout: 300000, // 5 minutes
74
+ cleanupInterval: 60000, // 1 minute
75
+ },
76
+
77
+ // DHT
78
+ dht: {
79
+ maxRoutingTableSize: 1000,
80
+ bucketRefreshInterval: 60000,
81
+ announceInterval: 300000, // 5 minutes
82
+ },
83
+
84
+ // Firebase (optional bootstrap registration)
85
+ firebase: {
86
+ enabled: !!(process.env.FIREBASE_API_KEY && process.env.FIREBASE_PROJECT_ID),
87
+ apiKey: process.env.FIREBASE_API_KEY,
88
+ projectId: process.env.FIREBASE_PROJECT_ID,
89
+ authDomain: process.env.FIREBASE_AUTH_DOMAIN,
90
+ registrationInterval: 60000, // Re-register every minute
91
+ },
92
+ };
93
+
94
+ // ============================================
95
+ // STRUCTURED LOGGER
96
+ // ============================================
97
+
98
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
99
+
100
+ class Logger {
101
+ constructor(name, config) {
102
+ this.name = name;
103
+ this.level = LOG_LEVELS[config.logLevel] || LOG_LEVELS.info;
104
+ this.format = config.logFormat;
105
+ }
106
+
107
+ _log(level, message, meta = {}) {
108
+ if (LOG_LEVELS[level] < this.level) return;
109
+
110
+ const entry = {
111
+ timestamp: new Date().toISOString(),
112
+ level,
113
+ service: this.name,
114
+ message,
115
+ ...meta,
116
+ };
117
+
118
+ if (this.format === 'json') {
119
+ console.log(JSON.stringify(entry));
120
+ } else {
121
+ const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
122
+ console.log(`[${entry.timestamp}] ${level.toUpperCase()} [${this.name}] ${message}${metaStr}`);
123
+ }
124
+ }
125
+
126
+ debug(msg, meta) { this._log('debug', msg, meta); }
127
+ info(msg, meta) { this._log('info', msg, meta); }
128
+ warn(msg, meta) { this._log('warn', msg, meta); }
129
+ error(msg, meta) { this._log('error', msg, meta); }
130
+ }
131
+
132
+ const log = new Logger('genesis-node', CONFIG);
133
+
134
+ // ============================================
135
+ // METRICS COLLECTOR
136
+ // ============================================
137
+
138
+ class MetricsCollector {
139
+ constructor() {
140
+ this.counters = {
141
+ connections_total: 0,
142
+ connections_active: 0,
143
+ messages_received: 0,
144
+ messages_sent: 0,
145
+ signals_relayed: 0,
146
+ dht_lookups: 0,
147
+ dht_stores: 0,
148
+ auth_challenges: 0,
149
+ auth_successes: 0,
150
+ auth_failures: 0,
151
+ errors_total: 0,
152
+ };
153
+
154
+ this.gauges = {
155
+ peers_registered: 0,
156
+ rooms_active: 0,
157
+ ledgers_stored: 0,
158
+ uptime_seconds: 0,
159
+ };
160
+
161
+ this.histograms = {
162
+ message_latency_ms: [],
163
+ };
164
+
165
+ this.startTime = Date.now();
166
+ }
167
+
168
+ inc(counter, value = 1) {
169
+ if (this.counters[counter] !== undefined) {
170
+ this.counters[counter] += value;
171
+ }
172
+ }
173
+
174
+ dec(counter, value = 1) {
175
+ if (this.counters[counter] !== undefined) {
176
+ this.counters[counter] -= value;
177
+ }
178
+ }
179
+
180
+ set(gauge, value) {
181
+ if (this.gauges[gauge] !== undefined) {
182
+ this.gauges[gauge] = value;
183
+ }
184
+ }
185
+
186
+ observe(histogram, value) {
187
+ if (this.histograms[histogram]) {
188
+ this.histograms[histogram].push(value);
189
+ // Keep last 1000 observations
190
+ if (this.histograms[histogram].length > 1000) {
191
+ this.histograms[histogram].shift();
192
+ }
193
+ }
194
+ }
195
+
196
+ getMetrics() {
197
+ this.gauges.uptime_seconds = Math.floor((Date.now() - this.startTime) / 1000);
198
+
199
+ return {
200
+ counters: { ...this.counters },
201
+ gauges: { ...this.gauges },
202
+ histograms: Object.fromEntries(
203
+ Object.entries(this.histograms).map(([k, v]) => [
204
+ k,
205
+ {
206
+ count: v.length,
207
+ avg: v.length ? v.reduce((a, b) => a + b, 0) / v.length : 0,
208
+ max: v.length ? Math.max(...v) : 0,
209
+ min: v.length ? Math.min(...v) : 0,
210
+ },
211
+ ])
212
+ ),
213
+ };
214
+ }
215
+
216
+ toPrometheus() {
217
+ const lines = [];
218
+
219
+ // Counters
220
+ for (const [name, value] of Object.entries(this.counters)) {
221
+ lines.push(`# TYPE genesis_${name} counter`);
222
+ lines.push(`genesis_${name} ${value}`);
223
+ }
224
+
225
+ // Gauges
226
+ this.gauges.uptime_seconds = Math.floor((Date.now() - this.startTime) / 1000);
227
+ for (const [name, value] of Object.entries(this.gauges)) {
228
+ lines.push(`# TYPE genesis_${name} gauge`);
229
+ lines.push(`genesis_${name} ${value}`);
230
+ }
231
+
232
+ return lines.join('\n');
233
+ }
234
+ }
235
+
236
+ // ============================================
237
+ // PEER REGISTRY (Enhanced)
238
+ // ============================================
239
+
240
+ class PeerRegistry {
241
+ constructor(metrics) {
242
+ this.peers = new Map();
243
+ this.byPublicKey = new Map();
244
+ this.byRoom = new Map();
245
+ this.connections = new Map();
246
+ this.ipConnections = new Map(); // Track connections per IP
247
+ this.metrics = metrics;
248
+ }
249
+
250
+ register(peerId, info) {
251
+ this.peers.set(peerId, {
252
+ ...info,
253
+ peerId,
254
+ registeredAt: Date.now(),
255
+ lastSeen: Date.now(),
256
+ });
257
+
258
+ if (info.publicKey) {
259
+ this.byPublicKey.set(info.publicKey, peerId);
260
+ }
261
+
262
+ this.metrics.set('peers_registered', this.peers.size);
263
+ return this.peers.get(peerId);
264
+ }
265
+
266
+ update(peerId, updates) {
267
+ const peer = this.peers.get(peerId);
268
+ if (peer) {
269
+ Object.assign(peer, updates, { lastSeen: Date.now() });
270
+ }
271
+ return peer;
272
+ }
273
+
274
+ get(peerId) {
275
+ return this.peers.get(peerId);
276
+ }
277
+
278
+ getByPublicKey(publicKey) {
279
+ const peerId = this.byPublicKey.get(publicKey);
280
+ return peerId ? this.peers.get(peerId) : null;
281
+ }
282
+
283
+ remove(peerId) {
284
+ const peer = this.peers.get(peerId);
285
+ if (peer) {
286
+ if (peer.publicKey) {
287
+ this.byPublicKey.delete(peer.publicKey);
288
+ }
289
+ if (peer.room) {
290
+ const room = this.byRoom.get(peer.room);
291
+ if (room) room.delete(peerId);
292
+ }
293
+ this.peers.delete(peerId);
294
+ this.metrics.set('peers_registered', this.peers.size);
295
+ return true;
296
+ }
297
+ return false;
298
+ }
299
+
300
+ joinRoom(peerId, room) {
301
+ const peer = this.peers.get(peerId);
302
+ if (!peer) return false;
303
+
304
+ if (peer.room && peer.room !== room) {
305
+ const oldRoom = this.byRoom.get(peer.room);
306
+ if (oldRoom) oldRoom.delete(peerId);
307
+ }
308
+
309
+ if (!this.byRoom.has(room)) {
310
+ this.byRoom.set(room, new Set());
311
+ }
312
+ this.byRoom.get(room).add(peerId);
313
+ peer.room = room;
314
+
315
+ this.metrics.set('rooms_active', this.byRoom.size);
316
+ return true;
317
+ }
318
+
319
+ getRoomPeers(room) {
320
+ const peerIds = this.byRoom.get(room) || new Set();
321
+ return Array.from(peerIds).map(id => this.peers.get(id)).filter(Boolean);
322
+ }
323
+
324
+ getAllPeers() {
325
+ return Array.from(this.peers.values());
326
+ }
327
+
328
+ pruneStale(maxAge = CONFIG.cleanup.staleConnectionTimeout) {
329
+ const cutoff = Date.now() - maxAge;
330
+ const removed = [];
331
+
332
+ for (const [peerId, peer] of this.peers) {
333
+ if (peer.lastSeen < cutoff) {
334
+ this.remove(peerId);
335
+ removed.push(peerId);
336
+ }
337
+ }
338
+
339
+ return removed;
340
+ }
341
+
342
+ trackIpConnection(ip, connectionId) {
343
+ if (!this.ipConnections.has(ip)) {
344
+ this.ipConnections.set(ip, new Set());
345
+ }
346
+ this.ipConnections.get(ip).add(connectionId);
347
+ }
348
+
349
+ removeIpConnection(ip, connectionId) {
350
+ const conns = this.ipConnections.get(ip);
351
+ if (conns) {
352
+ conns.delete(connectionId);
353
+ if (conns.size === 0) {
354
+ this.ipConnections.delete(ip);
355
+ }
356
+ }
357
+ }
358
+
359
+ getIpConnectionCount(ip) {
360
+ return this.ipConnections.get(ip)?.size || 0;
361
+ }
362
+
363
+ getStats() {
364
+ return {
365
+ totalPeers: this.peers.size,
366
+ rooms: this.byRoom.size,
367
+ roomSizes: Object.fromEntries(
368
+ Array.from(this.byRoom.entries()).map(([room, peers]) => [room, peers.size])
369
+ ),
370
+ };
371
+ }
372
+ }
373
+
374
+ // ============================================
375
+ // LEDGER STORE (Enhanced)
376
+ // ============================================
377
+
378
+ class LedgerStore {
379
+ constructor(dataDir, metrics) {
380
+ this.dataDir = dataDir;
381
+ this.ledgers = new Map();
382
+ this.pendingWrites = new Map();
383
+ this.metrics = metrics;
384
+
385
+ if (!existsSync(dataDir)) {
386
+ mkdirSync(dataDir, { recursive: true });
387
+ }
388
+
389
+ this.loadAll();
390
+ }
391
+
392
+ loadAll() {
393
+ try {
394
+ const indexPath = join(this.dataDir, 'index.json');
395
+ if (existsSync(indexPath)) {
396
+ const index = JSON.parse(readFileSync(indexPath, 'utf8'));
397
+ for (const publicKey of index.keys || []) {
398
+ this.load(publicKey);
399
+ }
400
+ log.info('Loaded ledger index', { count: index.keys?.length || 0 });
401
+ }
402
+ } catch (err) {
403
+ log.warn('Failed to load ledger index', { error: err.message });
404
+ }
405
+ }
406
+
407
+ load(publicKey) {
408
+ try {
409
+ const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
410
+ if (existsSync(path)) {
411
+ const data = JSON.parse(readFileSync(path, 'utf8'));
412
+ this.ledgers.set(publicKey, data);
413
+ return data;
414
+ }
415
+ } catch (err) {
416
+ log.warn('Failed to load ledger', { publicKey: publicKey.slice(0, 8), error: err.message });
417
+ }
418
+ return null;
419
+ }
420
+
421
+ save(publicKey) {
422
+ try {
423
+ const data = this.ledgers.get(publicKey);
424
+ if (!data) return false;
425
+
426
+ const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
427
+ writeFileSync(path, JSON.stringify(data, null, 2));
428
+ this.saveIndex();
429
+ return true;
430
+ } catch (err) {
431
+ log.warn('Failed to save ledger', { publicKey: publicKey.slice(0, 8), error: err.message });
432
+ return false;
433
+ }
434
+ }
435
+
436
+ saveIndex() {
437
+ try {
438
+ const indexPath = join(this.dataDir, 'index.json');
439
+ writeFileSync(indexPath, JSON.stringify({
440
+ keys: Array.from(this.ledgers.keys()),
441
+ updatedAt: Date.now(),
442
+ }, null, 2));
443
+ } catch (err) {
444
+ log.warn('Failed to save index', { error: err.message });
445
+ }
446
+ }
447
+
448
+ get(publicKey) {
449
+ return this.ledgers.get(publicKey);
450
+ }
451
+
452
+ getStates(publicKey) {
453
+ const ledger = this.ledgers.get(publicKey);
454
+ if (!ledger) return [];
455
+ return Object.values(ledger.devices || {});
456
+ }
457
+
458
+ update(publicKey, deviceId, state) {
459
+ if (!this.ledgers.has(publicKey)) {
460
+ this.ledgers.set(publicKey, {
461
+ publicKey,
462
+ createdAt: Date.now(),
463
+ devices: {},
464
+ });
465
+ }
466
+
467
+ const ledger = this.ledgers.get(publicKey);
468
+ const existing = ledger.devices[deviceId] || {};
469
+ const merged = this.mergeCRDT(existing, state);
470
+
471
+ ledger.devices[deviceId] = {
472
+ ...merged,
473
+ deviceId,
474
+ updatedAt: Date.now(),
475
+ };
476
+
477
+ this.scheduleSave(publicKey);
478
+ this.metrics.set('ledgers_stored', this.ledgers.size);
479
+
480
+ return ledger.devices[deviceId];
481
+ }
482
+
483
+ mergeCRDT(existing, incoming) {
484
+ if (!existing.timestamp || incoming.timestamp > existing.timestamp) {
485
+ return { ...incoming };
486
+ }
487
+
488
+ return {
489
+ earned: Math.max(existing.earned || 0, incoming.earned || 0),
490
+ spent: Math.max(existing.spent || 0, incoming.spent || 0),
491
+ timestamp: Math.max(existing.timestamp || 0, incoming.timestamp || 0),
492
+ };
493
+ }
494
+
495
+ scheduleSave(publicKey) {
496
+ if (this.pendingWrites.has(publicKey)) return;
497
+
498
+ this.pendingWrites.set(publicKey, setTimeout(() => {
499
+ this.save(publicKey);
500
+ this.pendingWrites.delete(publicKey);
501
+ }, 1000));
502
+ }
503
+
504
+ flush() {
505
+ for (const [publicKey, timeout] of this.pendingWrites) {
506
+ clearTimeout(timeout);
507
+ this.save(publicKey);
508
+ }
509
+ this.pendingWrites.clear();
510
+ }
511
+
512
+ getStats() {
513
+ return {
514
+ totalLedgers: this.ledgers.size,
515
+ totalDevices: Array.from(this.ledgers.values())
516
+ .reduce((sum, l) => sum + Object.keys(l.devices || {}).length, 0),
517
+ };
518
+ }
519
+ }
520
+
521
+ // ============================================
522
+ // AUTH SERVICE
523
+ // ============================================
524
+
525
+ class AuthService {
526
+ constructor(metrics) {
527
+ this.challenges = new Map();
528
+ this.tokens = new Map();
529
+ this.metrics = metrics;
530
+ }
531
+
532
+ createChallenge(publicKey, deviceId) {
533
+ const nonce = randomBytes(32).toString('hex');
534
+ const challenge = randomBytes(32).toString('hex');
535
+
536
+ this.challenges.set(nonce, {
537
+ challenge,
538
+ publicKey,
539
+ deviceId,
540
+ expiresAt: Date.now() + CONFIG.rateLimit.challengeExpiry,
541
+ });
542
+
543
+ this.metrics.inc('auth_challenges');
544
+ return { nonce, challenge };
545
+ }
546
+
547
+ verifyChallenge(nonce, publicKey, signature) {
548
+ const challengeData = this.challenges.get(nonce);
549
+ if (!challengeData) {
550
+ this.metrics.inc('auth_failures');
551
+ return { valid: false, error: 'Invalid nonce' };
552
+ }
553
+
554
+ if (Date.now() > challengeData.expiresAt) {
555
+ this.challenges.delete(nonce);
556
+ this.metrics.inc('auth_failures');
557
+ return { valid: false, error: 'Challenge expired' };
558
+ }
559
+
560
+ if (challengeData.publicKey !== publicKey) {
561
+ this.metrics.inc('auth_failures');
562
+ return { valid: false, error: 'Public key mismatch' };
563
+ }
564
+
565
+ this.challenges.delete(nonce);
566
+
567
+ const token = randomBytes(32).toString('hex');
568
+ const tokenData = {
569
+ publicKey,
570
+ deviceId: challengeData.deviceId,
571
+ createdAt: Date.now(),
572
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000,
573
+ };
574
+
575
+ this.tokens.set(token, tokenData);
576
+ this.metrics.inc('auth_successes');
577
+
578
+ return { valid: true, token, expiresAt: tokenData.expiresAt };
579
+ }
580
+
581
+ validateToken(token) {
582
+ const tokenData = this.tokens.get(token);
583
+ if (!tokenData) return null;
584
+
585
+ if (Date.now() > tokenData.expiresAt) {
586
+ this.tokens.delete(token);
587
+ return null;
588
+ }
589
+
590
+ return tokenData;
591
+ }
592
+
593
+ cleanup() {
594
+ const now = Date.now();
595
+
596
+ for (const [nonce, data] of this.challenges) {
597
+ if (now > data.expiresAt) {
598
+ this.challenges.delete(nonce);
599
+ }
600
+ }
601
+
602
+ for (const [token, data] of this.tokens) {
603
+ if (now > data.expiresAt) {
604
+ this.tokens.delete(token);
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ // ============================================
611
+ // DHT ROUTING TABLE
612
+ // ============================================
613
+
614
+ const K = 20;
615
+ const ID_BITS = 160;
616
+
617
+ function xorDistance(id1, id2) {
618
+ const buf1 = Buffer.from(id1, 'hex');
619
+ const buf2 = Buffer.from(id2, 'hex');
620
+ const result = Buffer.alloc(Math.max(buf1.length, buf2.length));
621
+
622
+ for (let i = 0; i < result.length; i++) {
623
+ result[i] = (buf1[i] || 0) ^ (buf2[i] || 0);
624
+ }
625
+
626
+ return result.toString('hex');
627
+ }
628
+
629
+ function getBucketIndex(distance) {
630
+ const buf = Buffer.from(distance, 'hex');
631
+
632
+ for (let i = 0; i < buf.length; i++) {
633
+ if (buf[i] !== 0) {
634
+ for (let j = 7; j >= 0; j--) {
635
+ if (buf[i] & (1 << j)) {
636
+ return (buf.length - i - 1) * 8 + j;
637
+ }
638
+ }
639
+ }
640
+ }
641
+
642
+ return 0;
643
+ }
644
+
645
+ class DHTRoutingTable {
646
+ constructor(localId, metrics) {
647
+ this.localId = localId;
648
+ this.buckets = new Array(ID_BITS).fill(null).map(() => []);
649
+ this.metrics = metrics;
650
+ }
651
+
652
+ add(peer) {
653
+ if (peer.id === this.localId) return false;
654
+
655
+ const distance = xorDistance(this.localId, peer.id);
656
+ const bucketIndex = getBucketIndex(distance);
657
+ const bucket = this.buckets[bucketIndex];
658
+
659
+ const existingIndex = bucket.findIndex(p => p.id === peer.id);
660
+ if (existingIndex !== -1) {
661
+ bucket.splice(existingIndex, 1);
662
+ bucket.push({ ...peer, lastSeen: Date.now() });
663
+ return true;
664
+ }
665
+
666
+ if (bucket.length < K) {
667
+ bucket.push({ ...peer, lastSeen: Date.now() });
668
+ return true;
669
+ }
670
+
671
+ return false;
672
+ }
673
+
674
+ remove(peerId) {
675
+ const distance = xorDistance(this.localId, peerId);
676
+ const bucketIndex = getBucketIndex(distance);
677
+ const bucket = this.buckets[bucketIndex];
678
+
679
+ const index = bucket.findIndex(p => p.id === peerId);
680
+ if (index !== -1) {
681
+ bucket.splice(index, 1);
682
+ return true;
683
+ }
684
+ return false;
685
+ }
686
+
687
+ findClosest(targetId, count = K) {
688
+ const candidates = [];
689
+
690
+ for (const bucket of this.buckets) {
691
+ candidates.push(...bucket);
692
+ }
693
+
694
+ return candidates
695
+ .map(p => ({
696
+ ...p,
697
+ distance: xorDistance(p.id, targetId),
698
+ }))
699
+ .sort((a, b) => a.distance.localeCompare(b.distance))
700
+ .slice(0, count);
701
+ }
702
+
703
+ getAllPeers() {
704
+ const peers = [];
705
+ for (const bucket of this.buckets) {
706
+ peers.push(...bucket);
707
+ }
708
+ return peers;
709
+ }
710
+
711
+ prune(maxAge = 300000) {
712
+ const cutoff = Date.now() - maxAge;
713
+ let removed = 0;
714
+
715
+ for (const bucket of this.buckets) {
716
+ for (let i = bucket.length - 1; i >= 0; i--) {
717
+ if (bucket[i].lastSeen < cutoff) {
718
+ bucket.splice(i, 1);
719
+ removed++;
720
+ }
721
+ }
722
+ }
723
+
724
+ return removed;
725
+ }
726
+
727
+ getStats() {
728
+ let totalPeers = 0;
729
+ let bucketsUsed = 0;
730
+
731
+ for (const bucket of this.buckets) {
732
+ if (bucket.length > 0) {
733
+ totalPeers += bucket.length;
734
+ bucketsUsed++;
735
+ }
736
+ }
737
+
738
+ return { totalPeers, bucketsUsed, bucketCount: this.buckets.length };
739
+ }
740
+ }
741
+
742
+ // ============================================
743
+ // FIREBASE BOOTSTRAP REGISTRATION
744
+ // ============================================
745
+
746
+ class FirebaseBootstrapRegistration {
747
+ constructor(nodeId, config, metrics) {
748
+ this.nodeId = nodeId;
749
+ this.config = config;
750
+ this.metrics = metrics;
751
+ this.app = null;
752
+ this.db = null;
753
+ this.isRegistered = false;
754
+ this.registrationInterval = null;
755
+ }
756
+
757
+ async connect() {
758
+ if (!this.config.firebase.enabled) {
759
+ log.info('Firebase registration disabled');
760
+ return false;
761
+ }
762
+
763
+ try {
764
+ const { initializeApp, getApps } = await import('firebase/app');
765
+ const { getFirestore, doc, setDoc, serverTimestamp } = await import('firebase/firestore');
766
+
767
+ const firebaseConfig = {
768
+ apiKey: this.config.firebase.apiKey,
769
+ projectId: this.config.firebase.projectId,
770
+ authDomain: this.config.firebase.authDomain || `${this.config.firebase.projectId}.firebaseapp.com`,
771
+ };
772
+
773
+ const apps = getApps();
774
+ this.app = apps.length ? apps[0] : initializeApp(firebaseConfig);
775
+ this.db = getFirestore(this.app);
776
+
777
+ this.firebase = { doc, setDoc, serverTimestamp };
778
+
779
+ await this.register();
780
+
781
+ // Re-register periodically
782
+ this.registrationInterval = setInterval(
783
+ () => this.register(),
784
+ this.config.firebase.registrationInterval
785
+ );
786
+
787
+ log.info('Firebase bootstrap registration enabled');
788
+ return true;
789
+
790
+ } catch (error) {
791
+ log.warn('Firebase connection failed', { error: error.message });
792
+ return false;
793
+ }
794
+ }
795
+
796
+ async register() {
797
+ if (!this.db) return;
798
+
799
+ try {
800
+ const { doc, setDoc, serverTimestamp } = this.firebase;
801
+
802
+ const bootstrapRef = doc(this.db, 'edgenet_bootstrap_nodes', this.nodeId);
803
+
804
+ await setDoc(bootstrapRef, {
805
+ nodeId: this.nodeId,
806
+ type: 'genesis',
807
+ host: this.config.host === '0.0.0.0' ? null : this.config.host,
808
+ port: this.config.port,
809
+ capabilities: ['signaling', 'dht', 'ledger', 'discovery'],
810
+ online: true,
811
+ lastSeen: serverTimestamp(),
812
+ version: '1.0.0',
813
+ }, { merge: true });
814
+
815
+ this.isRegistered = true;
816
+ log.debug('Registered as bootstrap node');
817
+
818
+ } catch (error) {
819
+ log.warn('Bootstrap registration failed', { error: error.message });
820
+ }
821
+ }
822
+
823
+ async unregister() {
824
+ if (!this.db || !this.isRegistered) return;
825
+
826
+ try {
827
+ const { doc, setDoc } = this.firebase;
828
+
829
+ const bootstrapRef = doc(this.db, 'edgenet_bootstrap_nodes', this.nodeId);
830
+ await setDoc(bootstrapRef, { online: false }, { merge: true });
831
+
832
+ log.info('Unregistered from bootstrap nodes');
833
+ } catch (error) {
834
+ log.warn('Bootstrap unregistration failed', { error: error.message });
835
+ }
836
+ }
837
+
838
+ stop() {
839
+ if (this.registrationInterval) {
840
+ clearInterval(this.registrationInterval);
841
+ }
842
+ }
843
+ }
844
+
845
+ // ============================================
846
+ // HEALTH CHECK SERVER
847
+ // ============================================
848
+
849
+ class HealthCheckServer {
850
+ constructor(config, metrics, getStatus) {
851
+ this.config = config;
852
+ this.metrics = metrics;
853
+ this.getStatus = getStatus;
854
+ this.server = null;
855
+ }
856
+
857
+ start() {
858
+ this.server = http.createServer((req, res) => {
859
+ const url = new URL(req.url, `http://${req.headers.host}`);
860
+
861
+ switch (url.pathname) {
862
+ case '/health':
863
+ case '/healthz':
864
+ this.handleHealth(req, res);
865
+ break;
866
+
867
+ case '/ready':
868
+ case '/readyz':
869
+ this.handleReady(req, res);
870
+ break;
871
+
872
+ case '/metrics':
873
+ this.handleMetrics(req, res);
874
+ break;
875
+
876
+ case '/status':
877
+ this.handleStatus(req, res);
878
+ break;
879
+
880
+ default:
881
+ res.writeHead(404);
882
+ res.end('Not Found');
883
+ }
884
+ });
885
+
886
+ this.server.listen(this.config.healthPort, () => {
887
+ log.info('Health check server started', { port: this.config.healthPort });
888
+ });
889
+ }
890
+
891
+ handleHealth(req, res) {
892
+ res.writeHead(200, { 'Content-Type': 'application/json' });
893
+ res.end(JSON.stringify({ status: 'healthy', timestamp: Date.now() }));
894
+ }
895
+
896
+ handleReady(req, res) {
897
+ const status = this.getStatus();
898
+ const ready = status.isRunning;
899
+
900
+ res.writeHead(ready ? 200 : 503, { 'Content-Type': 'application/json' });
901
+ res.end(JSON.stringify({ ready, timestamp: Date.now() }));
902
+ }
903
+
904
+ handleMetrics(req, res) {
905
+ if (this.config.metricsEnabled) {
906
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
907
+ res.end(this.metrics.toPrometheus());
908
+ } else {
909
+ res.writeHead(404);
910
+ res.end('Metrics disabled');
911
+ }
912
+ }
913
+
914
+ handleStatus(req, res) {
915
+ const status = this.getStatus();
916
+ res.writeHead(200, { 'Content-Type': 'application/json' });
917
+ res.end(JSON.stringify(status, null, 2));
918
+ }
919
+
920
+ stop() {
921
+ if (this.server) {
922
+ this.server.close();
923
+ }
924
+ }
925
+ }
926
+
927
+ // ============================================
928
+ // PRODUCTION GENESIS NODE
929
+ // ============================================
930
+
931
+ class ProductionGenesisNode extends EventEmitter {
932
+ constructor() {
933
+ super();
934
+
935
+ // Generate or use fixed node ID
936
+ this.nodeId = CONFIG.nodeId || createHash('sha1').update(randomBytes(32)).digest('hex');
937
+
938
+ // Initialize components
939
+ this.metrics = new MetricsCollector();
940
+ this.peerRegistry = new PeerRegistry(this.metrics);
941
+ this.ledgerStore = new LedgerStore(CONFIG.dataDir, this.metrics);
942
+ this.authService = new AuthService(this.metrics);
943
+ this.dhtRouting = new DHTRoutingTable(this.nodeId, this.metrics);
944
+
945
+ // Firebase registration
946
+ this.firebaseRegistration = new FirebaseBootstrapRegistration(
947
+ this.nodeId, CONFIG, this.metrics
948
+ );
949
+
950
+ // Health check
951
+ this.healthServer = new HealthCheckServer(
952
+ CONFIG, this.metrics, () => this.getStatus()
953
+ );
954
+
955
+ // WebSocket server
956
+ this.wss = null;
957
+ this.connections = new Map();
958
+
959
+ // Timers
960
+ this.cleanupInterval = null;
961
+ this.statsInterval = null;
962
+ this.dhtRefreshInterval = null;
963
+
964
+ // State
965
+ this.isRunning = false;
966
+ this.startedAt = null;
967
+ }
968
+
969
+ async start() {
970
+ log.info('Starting Production Genesis Node', {
971
+ nodeId: this.nodeId.slice(0, 16),
972
+ port: CONFIG.port,
973
+ dataDir: CONFIG.dataDir,
974
+ });
975
+
976
+ // Start health check server
977
+ this.healthServer.start();
978
+
979
+ // Connect to Firebase for bootstrap registration
980
+ await this.firebaseRegistration.connect();
981
+
982
+ // Start WebSocket server
983
+ const { WebSocketServer } = await import('ws');
984
+
985
+ this.wss = new WebSocketServer({
986
+ port: CONFIG.port,
987
+ host: CONFIG.host,
988
+ perMessageDeflate: false,
989
+ });
990
+
991
+ this.wss.on('connection', (ws, req) => this.handleConnection(ws, req));
992
+ this.wss.on('error', (err) => {
993
+ log.error('WebSocket server error', { error: err.message });
994
+ this.metrics.inc('errors_total');
995
+ });
996
+
997
+ // Start cleanup interval
998
+ this.cleanupInterval = setInterval(
999
+ () => this.cleanup(),
1000
+ CONFIG.cleanup.cleanupInterval
1001
+ );
1002
+
1003
+ // Start stats logging interval
1004
+ this.statsInterval = setInterval(
1005
+ () => this.logStats(),
1006
+ 60000
1007
+ );
1008
+
1009
+ // Start DHT refresh interval
1010
+ this.dhtRefreshInterval = setInterval(
1011
+ () => this.refreshDHT(),
1012
+ CONFIG.dht.bucketRefreshInterval
1013
+ );
1014
+
1015
+ this.isRunning = true;
1016
+ this.startedAt = Date.now();
1017
+
1018
+ log.info('Genesis Node started successfully', {
1019
+ wsEndpoint: `ws://${CONFIG.host}:${CONFIG.port}`,
1020
+ healthEndpoint: `http://${CONFIG.host}:${CONFIG.healthPort}/health`,
1021
+ metricsEndpoint: `http://${CONFIG.host}:${CONFIG.healthPort}/metrics`,
1022
+ });
1023
+
1024
+ this.emit('started', { nodeId: this.nodeId, port: CONFIG.port });
1025
+
1026
+ return this;
1027
+ }
1028
+
1029
+ async stop() {
1030
+ log.info('Shutting down Genesis Node...');
1031
+
1032
+ this.isRunning = false;
1033
+
1034
+ // Stop intervals
1035
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
1036
+ if (this.statsInterval) clearInterval(this.statsInterval);
1037
+ if (this.dhtRefreshInterval) clearInterval(this.dhtRefreshInterval);
1038
+
1039
+ // Unregister from Firebase
1040
+ await this.firebaseRegistration.unregister();
1041
+ this.firebaseRegistration.stop();
1042
+
1043
+ // Close WebSocket server
1044
+ if (this.wss) {
1045
+ this.wss.close();
1046
+ }
1047
+
1048
+ // Stop health server
1049
+ this.healthServer.stop();
1050
+
1051
+ // Flush ledger data
1052
+ this.ledgerStore.flush();
1053
+
1054
+ log.info('Genesis Node stopped');
1055
+ this.emit('stopped');
1056
+ }
1057
+
1058
+ handleConnection(ws, req) {
1059
+ const connectionId = randomBytes(16).toString('hex');
1060
+ const ip = req.headers['x-forwarded-for']?.split(',')[0].trim() ||
1061
+ req.socket.remoteAddress;
1062
+
1063
+ // Rate limiting by IP
1064
+ if (this.peerRegistry.getIpConnectionCount(ip) >= CONFIG.rateLimit.maxConnectionsPerIp) {
1065
+ log.warn('Rate limit exceeded', { ip, connectionId });
1066
+ ws.close(1008, 'Rate limit exceeded');
1067
+ return;
1068
+ }
1069
+
1070
+ this.metrics.inc('connections_total');
1071
+ this.metrics.inc('connections_active');
1072
+
1073
+ this.connections.set(connectionId, {
1074
+ ws,
1075
+ ip,
1076
+ peerId: null,
1077
+ connectedAt: Date.now(),
1078
+ messageCount: 0,
1079
+ lastMessageTime: Date.now(),
1080
+ });
1081
+
1082
+ this.peerRegistry.trackIpConnection(ip, connectionId);
1083
+
1084
+ log.debug('New connection', { connectionId: connectionId.slice(0, 8), ip });
1085
+
1086
+ ws.on('message', (data) => {
1087
+ try {
1088
+ const conn = this.connections.get(connectionId);
1089
+ if (!conn) return;
1090
+
1091
+ // Rate limiting by message frequency
1092
+ const now = Date.now();
1093
+ if (now - conn.lastMessageTime < 10) { // 100 msg/sec max
1094
+ conn.messageCount++;
1095
+ if (conn.messageCount > CONFIG.rateLimit.maxMessagesPerSecond) {
1096
+ log.warn('Message rate limit exceeded', { connectionId: connectionId.slice(0, 8) });
1097
+ ws.close(1008, 'Message rate limit exceeded');
1098
+ return;
1099
+ }
1100
+ } else {
1101
+ conn.messageCount = 1;
1102
+ conn.lastMessageTime = now;
1103
+ }
1104
+
1105
+ const message = JSON.parse(data.toString());
1106
+ this.handleMessage(connectionId, message);
1107
+
1108
+ } catch (err) {
1109
+ log.warn('Invalid message', { connectionId: connectionId.slice(0, 8), error: err.message });
1110
+ this.metrics.inc('errors_total');
1111
+ }
1112
+ });
1113
+
1114
+ ws.on('close', () => {
1115
+ this.handleDisconnect(connectionId);
1116
+ });
1117
+
1118
+ ws.on('error', (err) => {
1119
+ log.warn('Connection error', { connectionId: connectionId.slice(0, 8), error: err.message });
1120
+ this.metrics.inc('errors_total');
1121
+ });
1122
+
1123
+ // Send welcome
1124
+ this.send(connectionId, {
1125
+ type: 'welcome',
1126
+ connectionId,
1127
+ nodeId: this.nodeId,
1128
+ serverTime: Date.now(),
1129
+ capabilities: ['signaling', 'dht', 'ledger', 'discovery'],
1130
+ });
1131
+ }
1132
+
1133
+ handleDisconnect(connectionId) {
1134
+ const conn = this.connections.get(connectionId);
1135
+ if (!conn) return;
1136
+
1137
+ if (conn.peerId) {
1138
+ const peer = this.peerRegistry.get(conn.peerId);
1139
+ if (peer?.room) {
1140
+ this.broadcastToRoom(peer.room, {
1141
+ type: 'peer-left',
1142
+ peerId: conn.peerId,
1143
+ }, conn.peerId);
1144
+ }
1145
+ this.peerRegistry.remove(conn.peerId);
1146
+ this.dhtRouting.remove(conn.peerId);
1147
+ }
1148
+
1149
+ this.peerRegistry.removeIpConnection(conn.ip, connectionId);
1150
+ this.connections.delete(connectionId);
1151
+ this.metrics.dec('connections_active');
1152
+
1153
+ log.debug('Connection closed', { connectionId: connectionId.slice(0, 8) });
1154
+ }
1155
+
1156
+ handleMessage(connectionId, message) {
1157
+ this.metrics.inc('messages_received');
1158
+
1159
+ const conn = this.connections.get(connectionId);
1160
+ if (!conn) return;
1161
+
1162
+ switch (message.type) {
1163
+ case 'announce':
1164
+ this.handleAnnounce(connectionId, message);
1165
+ break;
1166
+
1167
+ case 'join':
1168
+ this.handleJoinRoom(connectionId, message);
1169
+ break;
1170
+
1171
+ case 'offer':
1172
+ case 'answer':
1173
+ case 'ice-candidate':
1174
+ this.relaySignal(connectionId, message);
1175
+ break;
1176
+
1177
+ case 'auth-challenge':
1178
+ this.handleAuthChallenge(connectionId, message);
1179
+ break;
1180
+
1181
+ case 'auth-verify':
1182
+ this.handleAuthVerify(connectionId, message);
1183
+ break;
1184
+
1185
+ case 'ledger-get':
1186
+ this.handleLedgerGet(connectionId, message);
1187
+ break;
1188
+
1189
+ case 'ledger-put':
1190
+ this.handleLedgerPut(connectionId, message);
1191
+ break;
1192
+
1193
+ case 'dht-bootstrap':
1194
+ this.handleDHTBootstrap(connectionId, message);
1195
+ break;
1196
+
1197
+ case 'dht-find-node':
1198
+ this.handleDHTFindNode(connectionId, message);
1199
+ break;
1200
+
1201
+ case 'dht-store':
1202
+ this.handleDHTStore(connectionId, message);
1203
+ break;
1204
+
1205
+ case 'ping':
1206
+ this.send(connectionId, { type: 'pong', timestamp: Date.now() });
1207
+ break;
1208
+
1209
+ default:
1210
+ log.debug('Unknown message type', { type: message.type });
1211
+ }
1212
+ }
1213
+
1214
+ handleAnnounce(connectionId, message) {
1215
+ const conn = this.connections.get(connectionId);
1216
+ const peerId = message.peerId || message.piKey || randomBytes(16).toString('hex');
1217
+
1218
+ conn.peerId = peerId;
1219
+
1220
+ this.peerRegistry.register(peerId, {
1221
+ publicKey: message.publicKey,
1222
+ siteId: message.siteId,
1223
+ capabilities: message.capabilities || [],
1224
+ connectionId,
1225
+ });
1226
+
1227
+ // Add to DHT routing table
1228
+ this.dhtRouting.add({
1229
+ id: peerId,
1230
+ address: connectionId,
1231
+ lastSeen: Date.now(),
1232
+ });
1233
+
1234
+ // Send current peer list
1235
+ const peers = this.peerRegistry.getAllPeers()
1236
+ .filter(p => p.peerId !== peerId)
1237
+ .slice(0, 50)
1238
+ .map(p => ({
1239
+ piKey: p.peerId,
1240
+ siteId: p.siteId,
1241
+ capabilities: p.capabilities,
1242
+ }));
1243
+
1244
+ this.send(connectionId, {
1245
+ type: 'peer-list',
1246
+ peers,
1247
+ });
1248
+
1249
+ // Notify other peers
1250
+ for (const peer of this.peerRegistry.getAllPeers()) {
1251
+ if (peer.peerId !== peerId && peer.connectionId) {
1252
+ this.send(peer.connectionId, {
1253
+ type: 'peer-joined',
1254
+ peerId,
1255
+ siteId: message.siteId,
1256
+ capabilities: message.capabilities,
1257
+ });
1258
+ }
1259
+ }
1260
+
1261
+ log.debug('Peer announced', { peerId: peerId.slice(0, 8), capabilities: message.capabilities });
1262
+ }
1263
+
1264
+ handleJoinRoom(connectionId, message) {
1265
+ const conn = this.connections.get(connectionId);
1266
+ if (!conn?.peerId) return;
1267
+
1268
+ const room = message.room || 'default';
1269
+ this.peerRegistry.joinRoom(conn.peerId, room);
1270
+
1271
+ const roomPeers = this.peerRegistry.getRoomPeers(room)
1272
+ .filter(p => p.peerId !== conn.peerId)
1273
+ .map(p => ({
1274
+ piKey: p.peerId,
1275
+ siteId: p.siteId,
1276
+ }));
1277
+
1278
+ this.send(connectionId, {
1279
+ type: 'room-joined',
1280
+ room,
1281
+ peers: roomPeers,
1282
+ });
1283
+
1284
+ this.broadcastToRoom(room, {
1285
+ type: 'peer-joined',
1286
+ peerId: conn.peerId,
1287
+ siteId: this.peerRegistry.get(conn.peerId)?.siteId,
1288
+ }, conn.peerId);
1289
+ }
1290
+
1291
+ relaySignal(connectionId, message) {
1292
+ this.metrics.inc('signals_relayed');
1293
+
1294
+ const conn = this.connections.get(connectionId);
1295
+ if (!conn?.peerId) return;
1296
+
1297
+ const targetPeer = this.peerRegistry.get(message.to);
1298
+ if (!targetPeer?.connectionId) {
1299
+ this.send(connectionId, {
1300
+ type: 'error',
1301
+ error: 'Target peer not found',
1302
+ originalType: message.type,
1303
+ });
1304
+ return;
1305
+ }
1306
+
1307
+ this.send(targetPeer.connectionId, {
1308
+ ...message,
1309
+ from: conn.peerId,
1310
+ });
1311
+
1312
+ this.metrics.inc('messages_sent');
1313
+ }
1314
+
1315
+ handleAuthChallenge(connectionId, message) {
1316
+ const { nonce, challenge } = this.authService.createChallenge(
1317
+ message.publicKey,
1318
+ message.deviceId
1319
+ );
1320
+
1321
+ this.send(connectionId, {
1322
+ type: 'auth-challenge-response',
1323
+ nonce,
1324
+ challenge,
1325
+ });
1326
+ }
1327
+
1328
+ handleAuthVerify(connectionId, message) {
1329
+ const result = this.authService.verifyChallenge(
1330
+ message.nonce,
1331
+ message.publicKey,
1332
+ message.signature
1333
+ );
1334
+
1335
+ this.send(connectionId, {
1336
+ type: 'auth-verify-response',
1337
+ ...result,
1338
+ });
1339
+ }
1340
+
1341
+ handleLedgerGet(connectionId, message) {
1342
+ const tokenData = this.authService.validateToken(message.token);
1343
+ if (!tokenData) {
1344
+ this.send(connectionId, {
1345
+ type: 'ledger-response',
1346
+ error: 'Invalid or expired token',
1347
+ });
1348
+ return;
1349
+ }
1350
+
1351
+ const states = this.ledgerStore.getStates(message.publicKey || tokenData.publicKey);
1352
+
1353
+ this.send(connectionId, {
1354
+ type: 'ledger-response',
1355
+ states,
1356
+ });
1357
+ }
1358
+
1359
+ handleLedgerPut(connectionId, message) {
1360
+ const tokenData = this.authService.validateToken(message.token);
1361
+ if (!tokenData) {
1362
+ this.send(connectionId, {
1363
+ type: 'ledger-put-response',
1364
+ error: 'Invalid or expired token',
1365
+ });
1366
+ return;
1367
+ }
1368
+
1369
+ const updated = this.ledgerStore.update(
1370
+ tokenData.publicKey,
1371
+ message.deviceId || tokenData.deviceId,
1372
+ message.state
1373
+ );
1374
+
1375
+ this.send(connectionId, {
1376
+ type: 'ledger-put-response',
1377
+ success: true,
1378
+ state: updated,
1379
+ });
1380
+ }
1381
+
1382
+ handleDHTBootstrap(connectionId, message) {
1383
+ this.metrics.inc('dht_lookups');
1384
+
1385
+ const peers = this.dhtRouting.getAllPeers()
1386
+ .slice(0, 20)
1387
+ .map(p => ({
1388
+ id: p.id,
1389
+ address: p.address,
1390
+ lastSeen: p.lastSeen,
1391
+ }));
1392
+
1393
+ this.send(connectionId, {
1394
+ type: 'dht-bootstrap-response',
1395
+ nodeId: this.nodeId,
1396
+ peers,
1397
+ });
1398
+ }
1399
+
1400
+ handleDHTFindNode(connectionId, message) {
1401
+ this.metrics.inc('dht_lookups');
1402
+
1403
+ const closest = this.dhtRouting.findClosest(message.target, K);
1404
+
1405
+ this.send(connectionId, {
1406
+ type: 'dht-find-node-response',
1407
+ target: message.target,
1408
+ nodes: closest.map(p => ({
1409
+ id: p.id,
1410
+ address: p.address,
1411
+ distance: p.distance,
1412
+ })),
1413
+ });
1414
+ }
1415
+
1416
+ handleDHTStore(connectionId, message) {
1417
+ this.metrics.inc('dht_stores');
1418
+
1419
+ // For now, just acknowledge - full DHT storage would be added here
1420
+ this.send(connectionId, {
1421
+ type: 'dht-store-response',
1422
+ success: true,
1423
+ key: message.key,
1424
+ });
1425
+ }
1426
+
1427
+ send(connectionId, message) {
1428
+ const conn = this.connections.get(connectionId);
1429
+ if (conn?.ws?.readyState === 1) {
1430
+ conn.ws.send(JSON.stringify(message));
1431
+ this.metrics.inc('messages_sent');
1432
+ }
1433
+ }
1434
+
1435
+ broadcastToRoom(room, message, excludePeerId = null) {
1436
+ const peers = this.peerRegistry.getRoomPeers(room);
1437
+ for (const peer of peers) {
1438
+ if (peer.peerId !== excludePeerId && peer.connectionId) {
1439
+ this.send(peer.connectionId, message);
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ cleanup() {
1445
+ // Prune stale peers
1446
+ const removed = this.peerRegistry.pruneStale();
1447
+ if (removed.length > 0) {
1448
+ log.info('Pruned stale peers', { count: removed.length });
1449
+ }
1450
+
1451
+ // Prune DHT routing table
1452
+ const dhtRemoved = this.dhtRouting.prune();
1453
+ if (dhtRemoved > 0) {
1454
+ log.debug('Pruned DHT entries', { count: dhtRemoved });
1455
+ }
1456
+
1457
+ // Cleanup auth
1458
+ this.authService.cleanup();
1459
+ }
1460
+
1461
+ refreshDHT() {
1462
+ // Periodic DHT maintenance would go here
1463
+ log.debug('DHT refresh', this.dhtRouting.getStats());
1464
+ }
1465
+
1466
+ logStats() {
1467
+ const stats = this.getStatus();
1468
+ log.info('Node statistics', {
1469
+ peers: stats.peers.total,
1470
+ connections: stats.connections,
1471
+ dht: stats.dht.totalPeers,
1472
+ uptime: Math.floor((Date.now() - this.startedAt) / 1000),
1473
+ });
1474
+ }
1475
+
1476
+ getStatus() {
1477
+ return {
1478
+ nodeId: this.nodeId,
1479
+ isRunning: this.isRunning,
1480
+ startedAt: this.startedAt,
1481
+ uptime: this.startedAt ? Date.now() - this.startedAt : 0,
1482
+ connections: this.connections.size,
1483
+ peers: this.peerRegistry.getStats(),
1484
+ ledger: this.ledgerStore.getStats(),
1485
+ dht: this.dhtRouting.getStats(),
1486
+ metrics: this.metrics.getMetrics(),
1487
+ firebase: {
1488
+ enabled: CONFIG.firebase.enabled,
1489
+ registered: this.firebaseRegistration.isRegistered,
1490
+ },
1491
+ };
1492
+ }
1493
+ }
1494
+
1495
+ // ============================================
1496
+ // MAIN
1497
+ // ============================================
1498
+
1499
+ async function main() {
1500
+ log.info('Production Genesis Node starting...', {
1501
+ version: '1.0.0',
1502
+ nodeEnv: process.env.NODE_ENV,
1503
+ });
1504
+
1505
+ const genesis = new ProductionGenesisNode();
1506
+
1507
+ // Handle shutdown signals
1508
+ const shutdown = async (signal) => {
1509
+ log.info('Received shutdown signal', { signal });
1510
+ await genesis.stop();
1511
+ process.exit(0);
1512
+ };
1513
+
1514
+ process.on('SIGINT', () => shutdown('SIGINT'));
1515
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
1516
+
1517
+ // Handle uncaught errors
1518
+ process.on('uncaughtException', (err) => {
1519
+ log.error('Uncaught exception', { error: err.message, stack: err.stack });
1520
+ process.exit(1);
1521
+ });
1522
+
1523
+ process.on('unhandledRejection', (reason, promise) => {
1524
+ log.error('Unhandled rejection', { reason: String(reason) });
1525
+ });
1526
+
1527
+ // Start the node
1528
+ await genesis.start();
1529
+ }
1530
+
1531
+ main().catch(err => {
1532
+ log.error('Fatal error', { error: err.message, stack: err.stack });
1533
+ process.exit(1);
1534
+ });
1535
+
1536
+ export default ProductionGenesisNode;