@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.
- package/deploy/.env.example +97 -0
- package/deploy/DEPLOY.md +481 -0
- package/deploy/Dockerfile +99 -0
- package/deploy/docker-compose.yml +162 -0
- package/deploy/genesis-prod.js +1536 -0
- package/deploy/health-check.js +187 -0
- package/deploy/prometheus.yml +38 -0
- package/firebase-signaling.js +41 -2
- package/package.json +8 -1
- package/real-workers.js +9 -4
- package/scheduler.js +8 -4
- package/tests/distributed-workers-test.js +1609 -0
- package/tests/p2p-migration-test.js +1102 -0
- package/tests/webrtc-peer-test.js +686 -0
- package/webrtc.js +693 -40
|
@@ -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;
|