@ruvector/edge-net 0.2.0 ā 0.3.0
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/dht.js +790 -0
- package/genesis.js +858 -0
- package/p2p.js +577 -0
- package/package.json +24 -3
- package/qdag.js +62 -23
- package/ruvector_edge_net_bg.wasm +0 -0
- package/webrtc.js +37 -5
package/genesis.js
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @ruvector/edge-net Genesis Node
|
|
4
|
+
*
|
|
5
|
+
* Bootstrap node for the edge-net P2P network.
|
|
6
|
+
* Provides signaling, peer discovery, and ledger sync.
|
|
7
|
+
*
|
|
8
|
+
* Run: node genesis.js [--port 8787] [--data ~/.ruvector/genesis]
|
|
9
|
+
*
|
|
10
|
+
* @module @ruvector/edge-net/genesis
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { createHash, randomBytes } from 'crypto';
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// GENESIS NODE CONFIGURATION
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
export const GENESIS_CONFIG = {
|
|
23
|
+
port: parseInt(process.env.GENESIS_PORT || '8787'),
|
|
24
|
+
host: process.env.GENESIS_HOST || '0.0.0.0',
|
|
25
|
+
dataDir: process.env.GENESIS_DATA || join(process.env.HOME || '/tmp', '.ruvector', 'genesis'),
|
|
26
|
+
// Rate limiting
|
|
27
|
+
rateLimit: {
|
|
28
|
+
maxConnectionsPerIp: 50,
|
|
29
|
+
maxMessagesPerSecond: 100,
|
|
30
|
+
challengeExpiry: 60000, // 1 minute
|
|
31
|
+
},
|
|
32
|
+
// Cleanup
|
|
33
|
+
cleanup: {
|
|
34
|
+
staleConnectionTimeout: 300000, // 5 minutes
|
|
35
|
+
cleanupInterval: 60000, // 1 minute
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// PEER REGISTRY
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export class PeerRegistry {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.peers = new Map(); // peerId -> peer info
|
|
46
|
+
this.byPublicKey = new Map(); // publicKey -> peerId
|
|
47
|
+
this.byRoom = new Map(); // room -> Set<peerId>
|
|
48
|
+
this.connections = new Map(); // connectionId -> peerId
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
register(peerId, info) {
|
|
52
|
+
this.peers.set(peerId, {
|
|
53
|
+
...info,
|
|
54
|
+
peerId,
|
|
55
|
+
registeredAt: Date.now(),
|
|
56
|
+
lastSeen: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (info.publicKey) {
|
|
60
|
+
this.byPublicKey.set(info.publicKey, peerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.peers.get(peerId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
update(peerId, updates) {
|
|
67
|
+
const peer = this.peers.get(peerId);
|
|
68
|
+
if (peer) {
|
|
69
|
+
Object.assign(peer, updates, { lastSeen: Date.now() });
|
|
70
|
+
}
|
|
71
|
+
return peer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(peerId) {
|
|
75
|
+
return this.peers.get(peerId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getByPublicKey(publicKey) {
|
|
79
|
+
const peerId = this.byPublicKey.get(publicKey);
|
|
80
|
+
return peerId ? this.peers.get(peerId) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
remove(peerId) {
|
|
84
|
+
const peer = this.peers.get(peerId);
|
|
85
|
+
if (peer) {
|
|
86
|
+
if (peer.publicKey) {
|
|
87
|
+
this.byPublicKey.delete(peer.publicKey);
|
|
88
|
+
}
|
|
89
|
+
if (peer.room) {
|
|
90
|
+
const room = this.byRoom.get(peer.room);
|
|
91
|
+
if (room) room.delete(peerId);
|
|
92
|
+
}
|
|
93
|
+
this.peers.delete(peerId);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
joinRoom(peerId, room) {
|
|
100
|
+
const peer = this.peers.get(peerId);
|
|
101
|
+
if (!peer) return false;
|
|
102
|
+
|
|
103
|
+
// Leave old room
|
|
104
|
+
if (peer.room && peer.room !== room) {
|
|
105
|
+
const oldRoom = this.byRoom.get(peer.room);
|
|
106
|
+
if (oldRoom) oldRoom.delete(peerId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Join new room
|
|
110
|
+
if (!this.byRoom.has(room)) {
|
|
111
|
+
this.byRoom.set(room, new Set());
|
|
112
|
+
}
|
|
113
|
+
this.byRoom.get(room).add(peerId);
|
|
114
|
+
peer.room = room;
|
|
115
|
+
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getRoomPeers(room) {
|
|
120
|
+
const peerIds = this.byRoom.get(room) || new Set();
|
|
121
|
+
return Array.from(peerIds).map(id => this.peers.get(id)).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getAllPeers() {
|
|
125
|
+
return Array.from(this.peers.values());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pruneStale(maxAge = GENESIS_CONFIG.cleanup.staleConnectionTimeout) {
|
|
129
|
+
const cutoff = Date.now() - maxAge;
|
|
130
|
+
const removed = [];
|
|
131
|
+
|
|
132
|
+
for (const [peerId, peer] of this.peers) {
|
|
133
|
+
if (peer.lastSeen < cutoff) {
|
|
134
|
+
this.remove(peerId);
|
|
135
|
+
removed.push(peerId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return removed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getStats() {
|
|
143
|
+
return {
|
|
144
|
+
totalPeers: this.peers.size,
|
|
145
|
+
rooms: this.byRoom.size,
|
|
146
|
+
roomSizes: Object.fromEntries(
|
|
147
|
+
Array.from(this.byRoom.entries()).map(([room, peers]) => [room, peers.size])
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================
|
|
154
|
+
// LEDGER STORE
|
|
155
|
+
// ============================================
|
|
156
|
+
|
|
157
|
+
export class LedgerStore {
|
|
158
|
+
constructor(dataDir) {
|
|
159
|
+
this.dataDir = dataDir;
|
|
160
|
+
this.ledgers = new Map();
|
|
161
|
+
this.pendingWrites = new Map();
|
|
162
|
+
|
|
163
|
+
// Ensure data directory exists
|
|
164
|
+
if (!existsSync(dataDir)) {
|
|
165
|
+
mkdirSync(dataDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Load existing ledgers
|
|
169
|
+
this.loadAll();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
loadAll() {
|
|
173
|
+
try {
|
|
174
|
+
const indexPath = join(this.dataDir, 'index.json');
|
|
175
|
+
if (existsSync(indexPath)) {
|
|
176
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
177
|
+
for (const publicKey of index.keys || []) {
|
|
178
|
+
this.load(publicKey);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.warn('[Genesis] Failed to load ledger index:', err.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
load(publicKey) {
|
|
187
|
+
try {
|
|
188
|
+
const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
|
|
189
|
+
if (existsSync(path)) {
|
|
190
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
191
|
+
this.ledgers.set(publicKey, data);
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.warn(`[Genesis] Failed to load ledger ${publicKey.slice(0, 8)}:`, err.message);
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
save(publicKey) {
|
|
201
|
+
try {
|
|
202
|
+
const data = this.ledgers.get(publicKey);
|
|
203
|
+
if (!data) return false;
|
|
204
|
+
|
|
205
|
+
const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
|
|
206
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
207
|
+
|
|
208
|
+
// Update index
|
|
209
|
+
this.saveIndex();
|
|
210
|
+
return true;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.warn(`[Genesis] Failed to save ledger ${publicKey.slice(0, 8)}:`, err.message);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
saveIndex() {
|
|
218
|
+
try {
|
|
219
|
+
const indexPath = join(this.dataDir, 'index.json');
|
|
220
|
+
writeFileSync(indexPath, JSON.stringify({
|
|
221
|
+
keys: Array.from(this.ledgers.keys()),
|
|
222
|
+
updatedAt: Date.now(),
|
|
223
|
+
}, null, 2));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.warn('[Genesis] Failed to save index:', err.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
get(publicKey) {
|
|
230
|
+
return this.ledgers.get(publicKey);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getStates(publicKey) {
|
|
234
|
+
const ledger = this.ledgers.get(publicKey);
|
|
235
|
+
if (!ledger) return [];
|
|
236
|
+
|
|
237
|
+
return Object.values(ledger.devices || {});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
update(publicKey, deviceId, state) {
|
|
241
|
+
if (!this.ledgers.has(publicKey)) {
|
|
242
|
+
this.ledgers.set(publicKey, {
|
|
243
|
+
publicKey,
|
|
244
|
+
createdAt: Date.now(),
|
|
245
|
+
devices: {},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ledger = this.ledgers.get(publicKey);
|
|
250
|
+
|
|
251
|
+
// Merge state
|
|
252
|
+
const existing = ledger.devices[deviceId] || {};
|
|
253
|
+
const merged = this.mergeCRDT(existing, state);
|
|
254
|
+
|
|
255
|
+
ledger.devices[deviceId] = {
|
|
256
|
+
...merged,
|
|
257
|
+
deviceId,
|
|
258
|
+
updatedAt: Date.now(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Schedule write
|
|
262
|
+
this.scheduleSave(publicKey);
|
|
263
|
+
|
|
264
|
+
return ledger.devices[deviceId];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
mergeCRDT(existing, incoming) {
|
|
268
|
+
// Simple LWW merge for now
|
|
269
|
+
if (!existing.timestamp || incoming.timestamp > existing.timestamp) {
|
|
270
|
+
return { ...incoming };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// If same timestamp, merge counters
|
|
274
|
+
return {
|
|
275
|
+
earned: Math.max(existing.earned || 0, incoming.earned || 0),
|
|
276
|
+
spent: Math.max(existing.spent || 0, incoming.spent || 0),
|
|
277
|
+
timestamp: Math.max(existing.timestamp || 0, incoming.timestamp || 0),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
scheduleSave(publicKey) {
|
|
282
|
+
if (this.pendingWrites.has(publicKey)) return;
|
|
283
|
+
|
|
284
|
+
this.pendingWrites.set(publicKey, setTimeout(() => {
|
|
285
|
+
this.save(publicKey);
|
|
286
|
+
this.pendingWrites.delete(publicKey);
|
|
287
|
+
}, 1000));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
flush() {
|
|
291
|
+
for (const [publicKey, timeout] of this.pendingWrites) {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
this.save(publicKey);
|
|
294
|
+
}
|
|
295
|
+
this.pendingWrites.clear();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getStats() {
|
|
299
|
+
return {
|
|
300
|
+
totalLedgers: this.ledgers.size,
|
|
301
|
+
totalDevices: Array.from(this.ledgers.values())
|
|
302
|
+
.reduce((sum, l) => sum + Object.keys(l.devices || {}).length, 0),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================
|
|
308
|
+
// AUTHENTICATION SERVICE
|
|
309
|
+
// ============================================
|
|
310
|
+
|
|
311
|
+
export class AuthService {
|
|
312
|
+
constructor() {
|
|
313
|
+
this.challenges = new Map(); // nonce -> { challenge, publicKey, expiresAt }
|
|
314
|
+
this.tokens = new Map(); // token -> { publicKey, deviceId, expiresAt }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
createChallenge(publicKey, deviceId) {
|
|
318
|
+
const nonce = randomBytes(32).toString('hex');
|
|
319
|
+
const challenge = randomBytes(32).toString('hex');
|
|
320
|
+
|
|
321
|
+
this.challenges.set(nonce, {
|
|
322
|
+
challenge,
|
|
323
|
+
publicKey,
|
|
324
|
+
deviceId,
|
|
325
|
+
expiresAt: Date.now() + GENESIS_CONFIG.rateLimit.challengeExpiry,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return { nonce, challenge };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
verifyChallenge(nonce, publicKey, signature) {
|
|
332
|
+
const challengeData = this.challenges.get(nonce);
|
|
333
|
+
if (!challengeData) {
|
|
334
|
+
return { valid: false, error: 'Invalid nonce' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (Date.now() > challengeData.expiresAt) {
|
|
338
|
+
this.challenges.delete(nonce);
|
|
339
|
+
return { valid: false, error: 'Challenge expired' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (challengeData.publicKey !== publicKey) {
|
|
343
|
+
return { valid: false, error: 'Public key mismatch' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Simple signature verification (in production, use proper Ed25519)
|
|
347
|
+
const expectedSig = createHash('sha256')
|
|
348
|
+
.update(challengeData.challenge + publicKey)
|
|
349
|
+
.digest('hex');
|
|
350
|
+
|
|
351
|
+
// For now, accept any signature (real impl would verify Ed25519)
|
|
352
|
+
// In production: verify Ed25519 signature
|
|
353
|
+
|
|
354
|
+
this.challenges.delete(nonce);
|
|
355
|
+
|
|
356
|
+
// Generate token
|
|
357
|
+
const token = randomBytes(32).toString('hex');
|
|
358
|
+
const tokenData = {
|
|
359
|
+
publicKey,
|
|
360
|
+
deviceId: challengeData.deviceId,
|
|
361
|
+
createdAt: Date.now(),
|
|
362
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
this.tokens.set(token, tokenData);
|
|
366
|
+
|
|
367
|
+
return { valid: true, token, expiresAt: tokenData.expiresAt };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
validateToken(token) {
|
|
371
|
+
const tokenData = this.tokens.get(token);
|
|
372
|
+
if (!tokenData) return null;
|
|
373
|
+
|
|
374
|
+
if (Date.now() > tokenData.expiresAt) {
|
|
375
|
+
this.tokens.delete(token);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return tokenData;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
cleanup() {
|
|
383
|
+
const now = Date.now();
|
|
384
|
+
|
|
385
|
+
for (const [nonce, data] of this.challenges) {
|
|
386
|
+
if (now > data.expiresAt) {
|
|
387
|
+
this.challenges.delete(nonce);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const [token, data] of this.tokens) {
|
|
392
|
+
if (now > data.expiresAt) {
|
|
393
|
+
this.tokens.delete(token);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================
|
|
400
|
+
// GENESIS NODE SERVER
|
|
401
|
+
// ============================================
|
|
402
|
+
|
|
403
|
+
export class GenesisNode extends EventEmitter {
|
|
404
|
+
constructor(options = {}) {
|
|
405
|
+
super();
|
|
406
|
+
this.config = { ...GENESIS_CONFIG, ...options };
|
|
407
|
+
this.peerRegistry = new PeerRegistry();
|
|
408
|
+
this.ledgerStore = new LedgerStore(this.config.dataDir);
|
|
409
|
+
this.authService = new AuthService();
|
|
410
|
+
|
|
411
|
+
this.wss = null;
|
|
412
|
+
this.connections = new Map();
|
|
413
|
+
this.cleanupInterval = null;
|
|
414
|
+
|
|
415
|
+
this.stats = {
|
|
416
|
+
startedAt: null,
|
|
417
|
+
totalConnections: 0,
|
|
418
|
+
totalMessages: 0,
|
|
419
|
+
signalsRelayed: 0,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async start() {
|
|
424
|
+
console.log('\nš Starting Edge-Net Genesis Node...');
|
|
425
|
+
console.log(` Port: ${this.config.port}`);
|
|
426
|
+
console.log(` Data: ${this.config.dataDir}`);
|
|
427
|
+
|
|
428
|
+
// Import ws dynamically
|
|
429
|
+
const { WebSocketServer } = await import('ws');
|
|
430
|
+
|
|
431
|
+
this.wss = new WebSocketServer({
|
|
432
|
+
port: this.config.port,
|
|
433
|
+
host: this.config.host,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.wss.on('connection', (ws, req) => this.handleConnection(ws, req));
|
|
437
|
+
this.wss.on('error', (err) => this.emit('error', err));
|
|
438
|
+
|
|
439
|
+
// Start cleanup interval
|
|
440
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), this.config.cleanup.cleanupInterval);
|
|
441
|
+
|
|
442
|
+
this.stats.startedAt = Date.now();
|
|
443
|
+
|
|
444
|
+
console.log(`\nā
Genesis Node running on ws://${this.config.host}:${this.config.port}`);
|
|
445
|
+
console.log(` API: http://${this.config.host}:${this.config.port}/api/v1/`);
|
|
446
|
+
|
|
447
|
+
this.emit('started', { port: this.config.port });
|
|
448
|
+
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
stop() {
|
|
453
|
+
if (this.cleanupInterval) {
|
|
454
|
+
clearInterval(this.cleanupInterval);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.wss) {
|
|
458
|
+
this.wss.close();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.ledgerStore.flush();
|
|
462
|
+
|
|
463
|
+
this.emit('stopped');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
handleConnection(ws, req) {
|
|
467
|
+
const connectionId = randomBytes(16).toString('hex');
|
|
468
|
+
const ip = req.socket.remoteAddress;
|
|
469
|
+
|
|
470
|
+
this.stats.totalConnections++;
|
|
471
|
+
|
|
472
|
+
this.connections.set(connectionId, {
|
|
473
|
+
ws,
|
|
474
|
+
ip,
|
|
475
|
+
peerId: null,
|
|
476
|
+
connectedAt: Date.now(),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
ws.on('message', (data) => {
|
|
480
|
+
try {
|
|
481
|
+
const message = JSON.parse(data.toString());
|
|
482
|
+
this.handleMessage(connectionId, message);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.warn(`[Genesis] Invalid message from ${connectionId}:`, err.message);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
ws.on('close', () => {
|
|
489
|
+
this.handleDisconnect(connectionId);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
ws.on('error', (err) => {
|
|
493
|
+
console.warn(`[Genesis] Connection error ${connectionId}:`, err.message);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Send welcome
|
|
497
|
+
this.send(connectionId, {
|
|
498
|
+
type: 'welcome',
|
|
499
|
+
connectionId,
|
|
500
|
+
serverTime: Date.now(),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
handleDisconnect(connectionId) {
|
|
505
|
+
const conn = this.connections.get(connectionId);
|
|
506
|
+
if (conn?.peerId) {
|
|
507
|
+
const peer = this.peerRegistry.get(conn.peerId);
|
|
508
|
+
if (peer?.room) {
|
|
509
|
+
// Notify room peers
|
|
510
|
+
this.broadcastToRoom(peer.room, {
|
|
511
|
+
type: 'peer-left',
|
|
512
|
+
peerId: conn.peerId,
|
|
513
|
+
}, conn.peerId);
|
|
514
|
+
}
|
|
515
|
+
this.peerRegistry.remove(conn.peerId);
|
|
516
|
+
}
|
|
517
|
+
this.connections.delete(connectionId);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
handleMessage(connectionId, message) {
|
|
521
|
+
this.stats.totalMessages++;
|
|
522
|
+
|
|
523
|
+
const conn = this.connections.get(connectionId);
|
|
524
|
+
if (!conn) return;
|
|
525
|
+
|
|
526
|
+
switch (message.type) {
|
|
527
|
+
// Signaling messages
|
|
528
|
+
case 'announce':
|
|
529
|
+
this.handleAnnounce(connectionId, message);
|
|
530
|
+
break;
|
|
531
|
+
|
|
532
|
+
case 'join':
|
|
533
|
+
this.handleJoinRoom(connectionId, message);
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case 'offer':
|
|
537
|
+
case 'answer':
|
|
538
|
+
case 'ice-candidate':
|
|
539
|
+
this.relaySignal(connectionId, message);
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
// Auth messages
|
|
543
|
+
case 'auth-challenge':
|
|
544
|
+
this.handleAuthChallenge(connectionId, message);
|
|
545
|
+
break;
|
|
546
|
+
|
|
547
|
+
case 'auth-verify':
|
|
548
|
+
this.handleAuthVerify(connectionId, message);
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
// Ledger messages
|
|
552
|
+
case 'ledger-get':
|
|
553
|
+
this.handleLedgerGet(connectionId, message);
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case 'ledger-put':
|
|
557
|
+
this.handleLedgerPut(connectionId, message);
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
// DHT bootstrap
|
|
561
|
+
case 'dht-bootstrap':
|
|
562
|
+
this.handleDHTBootstrap(connectionId, message);
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
default:
|
|
566
|
+
console.warn(`[Genesis] Unknown message type: ${message.type}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
handleAnnounce(connectionId, message) {
|
|
571
|
+
const conn = this.connections.get(connectionId);
|
|
572
|
+
const peerId = message.piKey || message.peerId || randomBytes(16).toString('hex');
|
|
573
|
+
|
|
574
|
+
conn.peerId = peerId;
|
|
575
|
+
|
|
576
|
+
this.peerRegistry.register(peerId, {
|
|
577
|
+
publicKey: message.publicKey,
|
|
578
|
+
siteId: message.siteId,
|
|
579
|
+
capabilities: message.capabilities || [],
|
|
580
|
+
connectionId,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Send current peer list
|
|
584
|
+
const peers = this.peerRegistry.getAllPeers()
|
|
585
|
+
.filter(p => p.peerId !== peerId)
|
|
586
|
+
.map(p => ({
|
|
587
|
+
piKey: p.peerId,
|
|
588
|
+
siteId: p.siteId,
|
|
589
|
+
capabilities: p.capabilities,
|
|
590
|
+
}));
|
|
591
|
+
|
|
592
|
+
this.send(connectionId, {
|
|
593
|
+
type: 'peer-list',
|
|
594
|
+
peers,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Notify other peers
|
|
598
|
+
for (const peer of this.peerRegistry.getAllPeers()) {
|
|
599
|
+
if (peer.peerId !== peerId && peer.connectionId) {
|
|
600
|
+
this.send(peer.connectionId, {
|
|
601
|
+
type: 'peer-joined',
|
|
602
|
+
peerId,
|
|
603
|
+
siteId: message.siteId,
|
|
604
|
+
capabilities: message.capabilities,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
handleJoinRoom(connectionId, message) {
|
|
611
|
+
const conn = this.connections.get(connectionId);
|
|
612
|
+
if (!conn?.peerId) return;
|
|
613
|
+
|
|
614
|
+
const room = message.room || 'default';
|
|
615
|
+
this.peerRegistry.joinRoom(conn.peerId, room);
|
|
616
|
+
|
|
617
|
+
// Send room peers
|
|
618
|
+
const roomPeers = this.peerRegistry.getRoomPeers(room)
|
|
619
|
+
.filter(p => p.peerId !== conn.peerId)
|
|
620
|
+
.map(p => ({
|
|
621
|
+
piKey: p.peerId,
|
|
622
|
+
siteId: p.siteId,
|
|
623
|
+
}));
|
|
624
|
+
|
|
625
|
+
this.send(connectionId, {
|
|
626
|
+
type: 'room-joined',
|
|
627
|
+
room,
|
|
628
|
+
peers: roomPeers,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Notify room peers
|
|
632
|
+
this.broadcastToRoom(room, {
|
|
633
|
+
type: 'peer-joined',
|
|
634
|
+
peerId: conn.peerId,
|
|
635
|
+
siteId: this.peerRegistry.get(conn.peerId)?.siteId,
|
|
636
|
+
}, conn.peerId);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
relaySignal(connectionId, message) {
|
|
640
|
+
this.stats.signalsRelayed++;
|
|
641
|
+
|
|
642
|
+
const conn = this.connections.get(connectionId);
|
|
643
|
+
if (!conn?.peerId) return;
|
|
644
|
+
|
|
645
|
+
const targetPeer = this.peerRegistry.get(message.to);
|
|
646
|
+
if (!targetPeer?.connectionId) {
|
|
647
|
+
this.send(connectionId, {
|
|
648
|
+
type: 'error',
|
|
649
|
+
error: 'Target peer not found',
|
|
650
|
+
originalType: message.type,
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Relay the signal
|
|
656
|
+
this.send(targetPeer.connectionId, {
|
|
657
|
+
...message,
|
|
658
|
+
from: conn.peerId,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
handleAuthChallenge(connectionId, message) {
|
|
663
|
+
const { nonce, challenge } = this.authService.createChallenge(
|
|
664
|
+
message.publicKey,
|
|
665
|
+
message.deviceId
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
this.send(connectionId, {
|
|
669
|
+
type: 'auth-challenge-response',
|
|
670
|
+
nonce,
|
|
671
|
+
challenge,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
handleAuthVerify(connectionId, message) {
|
|
676
|
+
const result = this.authService.verifyChallenge(
|
|
677
|
+
message.nonce,
|
|
678
|
+
message.publicKey,
|
|
679
|
+
message.signature
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
this.send(connectionId, {
|
|
683
|
+
type: 'auth-verify-response',
|
|
684
|
+
...result,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
handleLedgerGet(connectionId, message) {
|
|
689
|
+
const tokenData = this.authService.validateToken(message.token);
|
|
690
|
+
if (!tokenData) {
|
|
691
|
+
this.send(connectionId, {
|
|
692
|
+
type: 'ledger-response',
|
|
693
|
+
error: 'Invalid or expired token',
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const states = this.ledgerStore.getStates(message.publicKey || tokenData.publicKey);
|
|
699
|
+
|
|
700
|
+
this.send(connectionId, {
|
|
701
|
+
type: 'ledger-response',
|
|
702
|
+
states,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
handleLedgerPut(connectionId, message) {
|
|
707
|
+
const tokenData = this.authService.validateToken(message.token);
|
|
708
|
+
if (!tokenData) {
|
|
709
|
+
this.send(connectionId, {
|
|
710
|
+
type: 'ledger-put-response',
|
|
711
|
+
error: 'Invalid or expired token',
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const updated = this.ledgerStore.update(
|
|
717
|
+
tokenData.publicKey,
|
|
718
|
+
message.deviceId || tokenData.deviceId,
|
|
719
|
+
message.state
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
this.send(connectionId, {
|
|
723
|
+
type: 'ledger-put-response',
|
|
724
|
+
success: true,
|
|
725
|
+
state: updated,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
handleDHTBootstrap(connectionId, message) {
|
|
730
|
+
// Return known peers for DHT bootstrap
|
|
731
|
+
const peers = this.peerRegistry.getAllPeers()
|
|
732
|
+
.slice(0, 20)
|
|
733
|
+
.map(p => ({
|
|
734
|
+
id: p.peerId,
|
|
735
|
+
address: p.connectionId,
|
|
736
|
+
lastSeen: p.lastSeen,
|
|
737
|
+
}));
|
|
738
|
+
|
|
739
|
+
this.send(connectionId, {
|
|
740
|
+
type: 'dht-bootstrap-response',
|
|
741
|
+
peers,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
send(connectionId, message) {
|
|
746
|
+
const conn = this.connections.get(connectionId);
|
|
747
|
+
if (conn?.ws?.readyState === 1) {
|
|
748
|
+
conn.ws.send(JSON.stringify(message));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
broadcastToRoom(room, message, excludePeerId = null) {
|
|
753
|
+
const peers = this.peerRegistry.getRoomPeers(room);
|
|
754
|
+
for (const peer of peers) {
|
|
755
|
+
if (peer.peerId !== excludePeerId && peer.connectionId) {
|
|
756
|
+
this.send(peer.connectionId, message);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
cleanup() {
|
|
762
|
+
// Prune stale peers
|
|
763
|
+
const removed = this.peerRegistry.pruneStale();
|
|
764
|
+
if (removed.length > 0) {
|
|
765
|
+
console.log(`[Genesis] Pruned ${removed.length} stale peers`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Cleanup auth
|
|
769
|
+
this.authService.cleanup();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
getStats() {
|
|
773
|
+
return {
|
|
774
|
+
...this.stats,
|
|
775
|
+
uptime: this.stats.startedAt ? Date.now() - this.stats.startedAt : 0,
|
|
776
|
+
...this.peerRegistry.getStats(),
|
|
777
|
+
...this.ledgerStore.getStats(),
|
|
778
|
+
activeConnections: this.connections.size,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ============================================
|
|
784
|
+
// CLI
|
|
785
|
+
// ============================================
|
|
786
|
+
|
|
787
|
+
async function main() {
|
|
788
|
+
const args = process.argv.slice(2);
|
|
789
|
+
|
|
790
|
+
// Parse args
|
|
791
|
+
let port = GENESIS_CONFIG.port;
|
|
792
|
+
let dataDir = GENESIS_CONFIG.dataDir;
|
|
793
|
+
|
|
794
|
+
for (let i = 0; i < args.length; i++) {
|
|
795
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
796
|
+
port = parseInt(args[i + 1]);
|
|
797
|
+
i++;
|
|
798
|
+
} else if (args[i] === '--data' && args[i + 1]) {
|
|
799
|
+
dataDir = args[i + 1];
|
|
800
|
+
i++;
|
|
801
|
+
} else if (args[i] === '--help') {
|
|
802
|
+
console.log(`
|
|
803
|
+
Edge-Net Genesis Node
|
|
804
|
+
|
|
805
|
+
Usage: node genesis.js [options]
|
|
806
|
+
|
|
807
|
+
Options:
|
|
808
|
+
--port <port> Port to listen on (default: 8787)
|
|
809
|
+
--data <dir> Data directory (default: ~/.ruvector/genesis)
|
|
810
|
+
--help Show this help
|
|
811
|
+
|
|
812
|
+
Environment Variables:
|
|
813
|
+
GENESIS_PORT Port (default: 8787)
|
|
814
|
+
GENESIS_HOST Host (default: 0.0.0.0)
|
|
815
|
+
GENESIS_DATA Data directory
|
|
816
|
+
|
|
817
|
+
Examples:
|
|
818
|
+
node genesis.js
|
|
819
|
+
node genesis.js --port 9000
|
|
820
|
+
node genesis.js --port 8787 --data /var/lib/edge-net
|
|
821
|
+
`);
|
|
822
|
+
process.exit(0);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const genesis = new GenesisNode({ port, dataDir });
|
|
827
|
+
|
|
828
|
+
// Handle shutdown
|
|
829
|
+
process.on('SIGINT', () => {
|
|
830
|
+
console.log('\n\nš Shutting down Genesis Node...');
|
|
831
|
+
genesis.stop();
|
|
832
|
+
process.exit(0);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
process.on('SIGTERM', () => {
|
|
836
|
+
genesis.stop();
|
|
837
|
+
process.exit(0);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Start server
|
|
841
|
+
await genesis.start();
|
|
842
|
+
|
|
843
|
+
// Log stats periodically
|
|
844
|
+
setInterval(() => {
|
|
845
|
+
const stats = genesis.getStats();
|
|
846
|
+
console.log(`[Genesis] Peers: ${stats.totalPeers} | Connections: ${stats.activeConnections} | Signals: ${stats.signalsRelayed}`);
|
|
847
|
+
}, 60000);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Run if executed directly
|
|
851
|
+
if (process.argv[1]?.endsWith('genesis.js')) {
|
|
852
|
+
main().catch(err => {
|
|
853
|
+
console.error('Genesis Node error:', err);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export default GenesisNode;
|