@ruvector/edge-net 0.1.0 ā 0.1.2
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/README.md +119 -0
- package/cli.js +287 -108
- package/index.js +104 -0
- package/join.html +985 -0
- package/join.js +1333 -0
- package/network.js +820 -0
- package/networks.js +817 -0
- package/node/ruvector_edge_net.cjs +8126 -0
- package/node/ruvector_edge_net.d.ts +2289 -0
- package/node/ruvector_edge_net_bg.wasm +0 -0
- package/node/ruvector_edge_net_bg.wasm.d.ts +625 -0
- package/package.json +17 -3
- package/webrtc.js +964 -0
package/network.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Edge-Net Network Module
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Bootstrap node discovery
|
|
7
|
+
* - Peer announcement protocol
|
|
8
|
+
* - QDAG contribution recording
|
|
9
|
+
* - Contribution verification
|
|
10
|
+
* - P2P message routing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash, randomBytes } from 'crypto';
|
|
14
|
+
import { promises as fs } from 'fs';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { join, dirname } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Network configuration
|
|
23
|
+
const NETWORK_CONFIG = {
|
|
24
|
+
// Genesis nodes on Google Cloud (multi-region)
|
|
25
|
+
bootstrapNodes: [
|
|
26
|
+
{ id: 'genesis-us-central1', host: 'edge-net-genesis-us.ruvector.dev', port: 9000, region: 'us-central1', cloud: 'gcp' },
|
|
27
|
+
{ id: 'genesis-europe-west1', host: 'edge-net-genesis-eu.ruvector.dev', port: 9000, region: 'europe-west1', cloud: 'gcp' },
|
|
28
|
+
{ id: 'genesis-asia-east1', host: 'edge-net-genesis-asia.ruvector.dev', port: 9000, region: 'asia-east1', cloud: 'gcp' },
|
|
29
|
+
],
|
|
30
|
+
// Local network simulation for offline/testing
|
|
31
|
+
localSimulation: true,
|
|
32
|
+
// Peer discovery interval (ms)
|
|
33
|
+
discoveryInterval: 30000,
|
|
34
|
+
// Heartbeat interval (ms)
|
|
35
|
+
heartbeatInterval: 10000,
|
|
36
|
+
// Max peers per node
|
|
37
|
+
maxPeers: 50,
|
|
38
|
+
// QDAG sync interval (ms)
|
|
39
|
+
qdagSyncInterval: 5000,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Data directories
|
|
43
|
+
function getNetworkDir() {
|
|
44
|
+
return join(homedir(), '.ruvector', 'network');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getPeersFile() {
|
|
48
|
+
return join(getNetworkDir(), 'peers.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getQDAGFile() {
|
|
52
|
+
return join(getNetworkDir(), 'qdag.json');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Ensure directories exist
|
|
56
|
+
async function ensureDirectories() {
|
|
57
|
+
await fs.mkdir(getNetworkDir(), { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Peer Discovery and Management
|
|
62
|
+
*/
|
|
63
|
+
export class PeerManager {
|
|
64
|
+
constructor(localIdentity) {
|
|
65
|
+
this.localIdentity = localIdentity;
|
|
66
|
+
this.peers = new Map();
|
|
67
|
+
this.bootstrapNodes = NETWORK_CONFIG.bootstrapNodes;
|
|
68
|
+
this.discoveryInterval = null;
|
|
69
|
+
this.heartbeatInterval = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async initialize() {
|
|
73
|
+
await ensureDirectories();
|
|
74
|
+
await this.loadPeers();
|
|
75
|
+
|
|
76
|
+
// Start discovery and heartbeat
|
|
77
|
+
if (!NETWORK_CONFIG.localSimulation) {
|
|
78
|
+
this.startDiscovery();
|
|
79
|
+
this.startHeartbeat();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async loadPeers() {
|
|
86
|
+
try {
|
|
87
|
+
const data = await fs.readFile(getPeersFile(), 'utf-8');
|
|
88
|
+
const peers = JSON.parse(data);
|
|
89
|
+
for (const peer of peers) {
|
|
90
|
+
this.peers.set(peer.piKey, peer);
|
|
91
|
+
}
|
|
92
|
+
console.log(` š” Loaded ${this.peers.size} known peers`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// No peers file yet
|
|
95
|
+
console.log(' š” Starting fresh peer list');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async savePeers() {
|
|
100
|
+
const peers = Array.from(this.peers.values());
|
|
101
|
+
await fs.writeFile(getPeersFile(), JSON.stringify(peers, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Announce this node to the network
|
|
106
|
+
*/
|
|
107
|
+
async announce() {
|
|
108
|
+
const announcement = {
|
|
109
|
+
type: 'announce',
|
|
110
|
+
piKey: this.localIdentity.piKey,
|
|
111
|
+
publicKey: this.localIdentity.publicKey,
|
|
112
|
+
siteId: this.localIdentity.siteId,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
capabilities: ['compute', 'storage', 'verify'],
|
|
115
|
+
version: '0.1.1',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Sign the announcement
|
|
119
|
+
announcement.signature = this.signMessage(JSON.stringify(announcement));
|
|
120
|
+
|
|
121
|
+
// In local simulation, just record ourselves
|
|
122
|
+
if (NETWORK_CONFIG.localSimulation) {
|
|
123
|
+
await this.registerPeer({
|
|
124
|
+
...announcement,
|
|
125
|
+
lastSeen: Date.now(),
|
|
126
|
+
verified: true,
|
|
127
|
+
});
|
|
128
|
+
return announcement;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// In production, broadcast to bootstrap nodes
|
|
132
|
+
for (const bootstrap of this.bootstrapNodes) {
|
|
133
|
+
try {
|
|
134
|
+
await this.sendToNode(bootstrap, announcement);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Bootstrap node unreachable
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return announcement;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a peer in the local peer table
|
|
145
|
+
*/
|
|
146
|
+
async registerPeer(peer) {
|
|
147
|
+
const existing = this.peers.get(peer.piKey);
|
|
148
|
+
|
|
149
|
+
if (existing) {
|
|
150
|
+
// Update last seen
|
|
151
|
+
existing.lastSeen = Date.now();
|
|
152
|
+
existing.verified = peer.verified || existing.verified;
|
|
153
|
+
} else {
|
|
154
|
+
// New peer
|
|
155
|
+
this.peers.set(peer.piKey, {
|
|
156
|
+
piKey: peer.piKey,
|
|
157
|
+
publicKey: peer.publicKey,
|
|
158
|
+
siteId: peer.siteId,
|
|
159
|
+
capabilities: peer.capabilities || [],
|
|
160
|
+
firstSeen: Date.now(),
|
|
161
|
+
lastSeen: Date.now(),
|
|
162
|
+
verified: peer.verified || false,
|
|
163
|
+
contributions: 0,
|
|
164
|
+
});
|
|
165
|
+
console.log(` š New peer: ${peer.siteId} (Ļ:${peer.piKey.slice(0, 8)})`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await this.savePeers();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get active peers (seen in last 5 minutes)
|
|
173
|
+
*/
|
|
174
|
+
getActivePeers() {
|
|
175
|
+
const cutoff = Date.now() - 300000; // 5 minutes
|
|
176
|
+
return Array.from(this.peers.values()).filter(p => p.lastSeen > cutoff);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all known peers
|
|
181
|
+
*/
|
|
182
|
+
getAllPeers() {
|
|
183
|
+
return Array.from(this.peers.values());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Verify a peer's identity
|
|
188
|
+
*/
|
|
189
|
+
async verifyPeer(peer) {
|
|
190
|
+
// Request identity proof
|
|
191
|
+
const challenge = randomBytes(32).toString('hex');
|
|
192
|
+
const response = await this.requestProof(peer, challenge);
|
|
193
|
+
|
|
194
|
+
if (response && this.verifyProof(peer.publicKey, challenge, response)) {
|
|
195
|
+
peer.verified = true;
|
|
196
|
+
await this.savePeers();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sign a message with local identity
|
|
204
|
+
*/
|
|
205
|
+
signMessage(message) {
|
|
206
|
+
// Simplified signing (in production uses Ed25519)
|
|
207
|
+
const hash = createHash('sha256')
|
|
208
|
+
.update(this.localIdentity.piKey)
|
|
209
|
+
.update(message)
|
|
210
|
+
.digest('hex');
|
|
211
|
+
return hash;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Verify a signature
|
|
216
|
+
*/
|
|
217
|
+
verifySignature(publicKey, message, signature) {
|
|
218
|
+
// Simplified verification
|
|
219
|
+
return signature && signature.length === 64;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
startDiscovery() {
|
|
223
|
+
this.discoveryInterval = setInterval(async () => {
|
|
224
|
+
await this.discoverPeers();
|
|
225
|
+
}, NETWORK_CONFIG.discoveryInterval);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
startHeartbeat() {
|
|
229
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
230
|
+
await this.announce();
|
|
231
|
+
}, NETWORK_CONFIG.heartbeatInterval);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async discoverPeers() {
|
|
235
|
+
// Request peer lists from known peers
|
|
236
|
+
for (const peer of this.getActivePeers()) {
|
|
237
|
+
try {
|
|
238
|
+
const newPeers = await this.requestPeerList(peer);
|
|
239
|
+
for (const newPeer of newPeers) {
|
|
240
|
+
await this.registerPeer(newPeer);
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// Peer unreachable
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Placeholder network methods (implemented in production with WebRTC/WebSocket)
|
|
249
|
+
async sendToNode(node, message) {
|
|
250
|
+
// In production: WebSocket/WebRTC connection
|
|
251
|
+
return { ok: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async requestProof(peer, challenge) {
|
|
255
|
+
// In production: Request signed proof
|
|
256
|
+
return this.signMessage(challenge);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
verifyProof(publicKey, challenge, response) {
|
|
260
|
+
return response && response.length > 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async requestPeerList(peer) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
stop() {
|
|
268
|
+
if (this.discoveryInterval) clearInterval(this.discoveryInterval);
|
|
269
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* QDAG (Quantum DAG) Contribution Ledger
|
|
275
|
+
*
|
|
276
|
+
* A directed acyclic graph that records all contributions
|
|
277
|
+
* with cryptographic verification and consensus
|
|
278
|
+
*/
|
|
279
|
+
export class QDAGLedger {
|
|
280
|
+
constructor(peerManager) {
|
|
281
|
+
this.peerManager = peerManager;
|
|
282
|
+
this.nodes = new Map(); // DAG nodes
|
|
283
|
+
this.tips = new Set(); // Current tips (unconfirmed)
|
|
284
|
+
this.confirmed = new Set(); // Confirmed nodes
|
|
285
|
+
this.pendingContributions = [];
|
|
286
|
+
this.syncInterval = null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async initialize() {
|
|
290
|
+
await this.loadLedger();
|
|
291
|
+
|
|
292
|
+
if (!NETWORK_CONFIG.localSimulation) {
|
|
293
|
+
this.startSync();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async loadLedger() {
|
|
300
|
+
try {
|
|
301
|
+
const data = await fs.readFile(getQDAGFile(), 'utf-8');
|
|
302
|
+
const ledger = JSON.parse(data);
|
|
303
|
+
|
|
304
|
+
for (const node of ledger.nodes || []) {
|
|
305
|
+
this.nodes.set(node.id, node);
|
|
306
|
+
}
|
|
307
|
+
this.tips = new Set(ledger.tips || []);
|
|
308
|
+
this.confirmed = new Set(ledger.confirmed || []);
|
|
309
|
+
|
|
310
|
+
console.log(` š Loaded QDAG: ${this.nodes.size} nodes, ${this.confirmed.size} confirmed`);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
// Create genesis node
|
|
313
|
+
const genesis = this.createNode({
|
|
314
|
+
type: 'genesis',
|
|
315
|
+
timestamp: Date.now(),
|
|
316
|
+
message: 'Edge-Net QDAG Genesis',
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
this.nodes.set(genesis.id, genesis);
|
|
320
|
+
this.tips.add(genesis.id);
|
|
321
|
+
this.confirmed.add(genesis.id);
|
|
322
|
+
|
|
323
|
+
await this.saveLedger();
|
|
324
|
+
console.log(' š Created QDAG genesis block');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async saveLedger() {
|
|
329
|
+
const ledger = {
|
|
330
|
+
nodes: Array.from(this.nodes.values()),
|
|
331
|
+
tips: Array.from(this.tips),
|
|
332
|
+
confirmed: Array.from(this.confirmed),
|
|
333
|
+
savedAt: Date.now(),
|
|
334
|
+
};
|
|
335
|
+
await fs.writeFile(getQDAGFile(), JSON.stringify(ledger, null, 2));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create a new QDAG node
|
|
340
|
+
*/
|
|
341
|
+
createNode(data, parents) {
|
|
342
|
+
const nodeData = {
|
|
343
|
+
...data,
|
|
344
|
+
parents: parents,
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const id = createHash('sha256')
|
|
349
|
+
.update(JSON.stringify(nodeData))
|
|
350
|
+
.digest('hex')
|
|
351
|
+
.slice(0, 16);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
id,
|
|
355
|
+
...nodeData,
|
|
356
|
+
weight: 1,
|
|
357
|
+
confirmations: 0,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Record a contribution to the QDAG
|
|
363
|
+
*/
|
|
364
|
+
async recordContribution(contribution) {
|
|
365
|
+
// Select parent tips (2 parents for DAG structure)
|
|
366
|
+
const parents = this.selectTips(2);
|
|
367
|
+
|
|
368
|
+
// Create contribution node
|
|
369
|
+
const node = this.createNode({
|
|
370
|
+
type: 'contribution',
|
|
371
|
+
contributor: contribution.piKey,
|
|
372
|
+
siteId: contribution.siteId,
|
|
373
|
+
taskId: contribution.taskId,
|
|
374
|
+
computeUnits: contribution.computeUnits,
|
|
375
|
+
credits: contribution.credits,
|
|
376
|
+
signature: contribution.signature,
|
|
377
|
+
}, parents);
|
|
378
|
+
|
|
379
|
+
// Add to DAG
|
|
380
|
+
this.nodes.set(node.id, node);
|
|
381
|
+
|
|
382
|
+
// Update tips
|
|
383
|
+
for (const parent of parents) {
|
|
384
|
+
this.tips.delete(parent);
|
|
385
|
+
}
|
|
386
|
+
this.tips.add(node.id);
|
|
387
|
+
|
|
388
|
+
// Update parent weights (confirm path)
|
|
389
|
+
await this.updateWeights(node.id);
|
|
390
|
+
|
|
391
|
+
await this.saveLedger();
|
|
392
|
+
|
|
393
|
+
console.log(` š Recorded contribution ${node.id}: +${contribution.credits} credits`);
|
|
394
|
+
|
|
395
|
+
return node;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Select tips for new node parents
|
|
400
|
+
*/
|
|
401
|
+
selectTips(count) {
|
|
402
|
+
const tips = Array.from(this.tips);
|
|
403
|
+
if (tips.length <= count) return tips;
|
|
404
|
+
|
|
405
|
+
// Weighted random selection based on age
|
|
406
|
+
const selected = [];
|
|
407
|
+
const available = [...tips];
|
|
408
|
+
|
|
409
|
+
while (selected.length < count && available.length > 0) {
|
|
410
|
+
const idx = Math.floor(Math.random() * available.length);
|
|
411
|
+
selected.push(available[idx]);
|
|
412
|
+
available.splice(idx, 1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return selected;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Update weights along the path to genesis
|
|
420
|
+
*/
|
|
421
|
+
async updateWeights(nodeId) {
|
|
422
|
+
const visited = new Set();
|
|
423
|
+
const queue = [nodeId];
|
|
424
|
+
|
|
425
|
+
while (queue.length > 0) {
|
|
426
|
+
const id = queue.shift();
|
|
427
|
+
if (visited.has(id)) continue;
|
|
428
|
+
visited.add(id);
|
|
429
|
+
|
|
430
|
+
const node = this.nodes.get(id);
|
|
431
|
+
if (!node) continue;
|
|
432
|
+
|
|
433
|
+
node.weight = (node.weight || 0) + 1;
|
|
434
|
+
node.confirmations = (node.confirmations || 0) + 1;
|
|
435
|
+
|
|
436
|
+
// Check for confirmation threshold
|
|
437
|
+
if (node.confirmations >= 3 && !this.confirmed.has(id)) {
|
|
438
|
+
this.confirmed.add(id);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Add parents to queue
|
|
442
|
+
for (const parentId of node.parents || []) {
|
|
443
|
+
queue.push(parentId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get contribution stats for a contributor
|
|
450
|
+
*/
|
|
451
|
+
getContributorStats(piKey) {
|
|
452
|
+
const contributions = Array.from(this.nodes.values())
|
|
453
|
+
.filter(n => n.type === 'contribution' && n.contributor === piKey);
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
totalContributions: contributions.length,
|
|
457
|
+
confirmedContributions: contributions.filter(c => this.confirmed.has(c.id)).length,
|
|
458
|
+
totalCredits: contributions.reduce((sum, c) => sum + (c.credits || 0), 0),
|
|
459
|
+
totalComputeUnits: contributions.reduce((sum, c) => sum + (c.computeUnits || 0), 0),
|
|
460
|
+
firstContribution: contributions.length > 0
|
|
461
|
+
? Math.min(...contributions.map(c => c.timestamp))
|
|
462
|
+
: null,
|
|
463
|
+
lastContribution: contributions.length > 0
|
|
464
|
+
? Math.max(...contributions.map(c => c.timestamp))
|
|
465
|
+
: null,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get network-wide stats
|
|
471
|
+
*/
|
|
472
|
+
getNetworkStats() {
|
|
473
|
+
const contributions = Array.from(this.nodes.values())
|
|
474
|
+
.filter(n => n.type === 'contribution');
|
|
475
|
+
|
|
476
|
+
const contributors = new Set(contributions.map(c => c.contributor));
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
totalNodes: this.nodes.size,
|
|
480
|
+
totalContributions: contributions.length,
|
|
481
|
+
confirmedNodes: this.confirmed.size,
|
|
482
|
+
uniqueContributors: contributors.size,
|
|
483
|
+
totalCredits: contributions.reduce((sum, c) => sum + (c.credits || 0), 0),
|
|
484
|
+
totalComputeUnits: contributions.reduce((sum, c) => sum + (c.computeUnits || 0), 0),
|
|
485
|
+
currentTips: this.tips.size,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Verify contribution integrity
|
|
491
|
+
*/
|
|
492
|
+
async verifyContribution(nodeId) {
|
|
493
|
+
const node = this.nodes.get(nodeId);
|
|
494
|
+
if (!node) return { valid: false, reason: 'Node not found' };
|
|
495
|
+
|
|
496
|
+
// Verify parents exist
|
|
497
|
+
for (const parentId of node.parents || []) {
|
|
498
|
+
if (!this.nodes.has(parentId)) {
|
|
499
|
+
return { valid: false, reason: `Missing parent: ${parentId}` };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Verify signature (if peer available)
|
|
504
|
+
const peer = this.peerManager.peers.get(node.contributor);
|
|
505
|
+
if (peer && node.signature) {
|
|
506
|
+
const dataToVerify = JSON.stringify({
|
|
507
|
+
contributor: node.contributor,
|
|
508
|
+
taskId: node.taskId,
|
|
509
|
+
computeUnits: node.computeUnits,
|
|
510
|
+
credits: node.credits,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (!this.peerManager.verifySignature(peer.publicKey, dataToVerify, node.signature)) {
|
|
514
|
+
return { valid: false, reason: 'Invalid signature' };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { valid: true, confirmations: node.confirmations };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Sync QDAG with peers
|
|
523
|
+
*/
|
|
524
|
+
startSync() {
|
|
525
|
+
this.syncInterval = setInterval(async () => {
|
|
526
|
+
await this.syncWithPeers();
|
|
527
|
+
}, NETWORK_CONFIG.qdagSyncInterval);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async syncWithPeers() {
|
|
531
|
+
const activePeers = this.peerManager.getActivePeers();
|
|
532
|
+
|
|
533
|
+
for (const peer of activePeers.slice(0, 3)) {
|
|
534
|
+
try {
|
|
535
|
+
// Request missing nodes from peer
|
|
536
|
+
const peerTips = await this.requestTips(peer);
|
|
537
|
+
for (const tipId of peerTips) {
|
|
538
|
+
if (!this.nodes.has(tipId)) {
|
|
539
|
+
const node = await this.requestNode(peer, tipId);
|
|
540
|
+
if (node) {
|
|
541
|
+
await this.mergeNode(node);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
// Peer sync failed
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async requestTips(peer) {
|
|
552
|
+
// In production: Request tips via P2P
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async requestNode(peer, nodeId) {
|
|
557
|
+
// In production: Request specific node via P2P
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async mergeNode(node) {
|
|
562
|
+
if (this.nodes.has(node.id)) return;
|
|
563
|
+
|
|
564
|
+
// Verify node before merging
|
|
565
|
+
const verification = await this.verifyContribution(node.id);
|
|
566
|
+
if (!verification.valid) return;
|
|
567
|
+
|
|
568
|
+
this.nodes.set(node.id, node);
|
|
569
|
+
await this.updateWeights(node.id);
|
|
570
|
+
await this.saveLedger();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
stop() {
|
|
574
|
+
if (this.syncInterval) clearInterval(this.syncInterval);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Contribution Verifier
|
|
580
|
+
*
|
|
581
|
+
* Cross-verifies contributions between peers
|
|
582
|
+
*/
|
|
583
|
+
export class ContributionVerifier {
|
|
584
|
+
constructor(peerManager, qdagLedger) {
|
|
585
|
+
this.peerManager = peerManager;
|
|
586
|
+
this.qdag = qdagLedger;
|
|
587
|
+
this.verificationQueue = [];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Submit contribution for verification
|
|
592
|
+
*/
|
|
593
|
+
async submitContribution(contribution) {
|
|
594
|
+
// Sign the contribution
|
|
595
|
+
contribution.signature = this.peerManager.signMessage(
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
contributor: contribution.piKey,
|
|
598
|
+
taskId: contribution.taskId,
|
|
599
|
+
computeUnits: contribution.computeUnits,
|
|
600
|
+
credits: contribution.credits,
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
// Record to local QDAG
|
|
605
|
+
const node = await this.qdag.recordContribution(contribution);
|
|
606
|
+
|
|
607
|
+
// In local simulation, self-verify
|
|
608
|
+
if (NETWORK_CONFIG.localSimulation) {
|
|
609
|
+
return {
|
|
610
|
+
nodeId: node.id,
|
|
611
|
+
verified: true,
|
|
612
|
+
confirmations: 1,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// In production, broadcast for peer verification
|
|
617
|
+
const verifications = await this.broadcastForVerification(node);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
nodeId: node.id,
|
|
621
|
+
verified: verifications.filter(v => v.valid).length >= 2,
|
|
622
|
+
confirmations: verifications.length,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Broadcast contribution for peer verification
|
|
628
|
+
*/
|
|
629
|
+
async broadcastForVerification(node) {
|
|
630
|
+
const activePeers = this.peerManager.getActivePeers();
|
|
631
|
+
const verifications = [];
|
|
632
|
+
|
|
633
|
+
for (const peer of activePeers.slice(0, 5)) {
|
|
634
|
+
try {
|
|
635
|
+
const verification = await this.requestVerification(peer, node);
|
|
636
|
+
verifications.push(verification);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
// Peer verification failed
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return verifications;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async requestVerification(peer, node) {
|
|
646
|
+
// In production: Request verification via P2P
|
|
647
|
+
return { valid: true, peerId: peer.piKey };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Verify a contribution from another peer
|
|
652
|
+
*/
|
|
653
|
+
async verifyFromPeer(contribution, requestingPeer) {
|
|
654
|
+
// Verify signature
|
|
655
|
+
const valid = this.peerManager.verifySignature(
|
|
656
|
+
requestingPeer.publicKey,
|
|
657
|
+
JSON.stringify({
|
|
658
|
+
contributor: contribution.contributor,
|
|
659
|
+
taskId: contribution.taskId,
|
|
660
|
+
computeUnits: contribution.computeUnits,
|
|
661
|
+
credits: contribution.credits,
|
|
662
|
+
}),
|
|
663
|
+
contribution.signature
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Verify compute units are reasonable
|
|
667
|
+
const reasonable = contribution.computeUnits > 0 &&
|
|
668
|
+
contribution.computeUnits < 1000000 &&
|
|
669
|
+
contribution.credits === Math.floor(contribution.computeUnits / 100);
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
valid: valid && reasonable,
|
|
673
|
+
reason: !valid ? 'Invalid signature' : (!reasonable ? 'Unreasonable values' : 'OK'),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Network Manager - High-level API
|
|
680
|
+
*/
|
|
681
|
+
export class NetworkManager {
|
|
682
|
+
constructor(identity) {
|
|
683
|
+
this.identity = identity;
|
|
684
|
+
this.peerManager = new PeerManager(identity);
|
|
685
|
+
this.qdag = null;
|
|
686
|
+
this.verifier = null;
|
|
687
|
+
this.initialized = false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async initialize() {
|
|
691
|
+
console.log('\nš Initializing Edge-Net Network...');
|
|
692
|
+
|
|
693
|
+
await this.peerManager.initialize();
|
|
694
|
+
|
|
695
|
+
this.qdag = new QDAGLedger(this.peerManager);
|
|
696
|
+
await this.qdag.initialize();
|
|
697
|
+
|
|
698
|
+
this.verifier = new ContributionVerifier(this.peerManager, this.qdag);
|
|
699
|
+
|
|
700
|
+
// Announce to network
|
|
701
|
+
await this.peerManager.announce();
|
|
702
|
+
|
|
703
|
+
this.initialized = true;
|
|
704
|
+
console.log('ā
Network initialized\n');
|
|
705
|
+
|
|
706
|
+
return this;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Record a compute contribution
|
|
711
|
+
*/
|
|
712
|
+
async recordContribution(taskId, computeUnits) {
|
|
713
|
+
const credits = Math.floor(computeUnits / 100);
|
|
714
|
+
|
|
715
|
+
const contribution = {
|
|
716
|
+
piKey: this.identity.piKey,
|
|
717
|
+
siteId: this.identity.siteId,
|
|
718
|
+
taskId,
|
|
719
|
+
computeUnits,
|
|
720
|
+
credits,
|
|
721
|
+
timestamp: Date.now(),
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
return await this.verifier.submitContribution(contribution);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Get stats for this contributor
|
|
729
|
+
*/
|
|
730
|
+
getMyStats() {
|
|
731
|
+
return this.qdag.getContributorStats(this.identity.piKey);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Get network-wide stats
|
|
736
|
+
*/
|
|
737
|
+
getNetworkStats() {
|
|
738
|
+
return this.qdag.getNetworkStats();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Get connected peers
|
|
743
|
+
*/
|
|
744
|
+
getPeers() {
|
|
745
|
+
return this.peerManager.getAllPeers();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Stop network services
|
|
750
|
+
*/
|
|
751
|
+
stop() {
|
|
752
|
+
this.peerManager.stop();
|
|
753
|
+
this.qdag.stop();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// CLI interface
|
|
758
|
+
async function main() {
|
|
759
|
+
const args = process.argv.slice(2);
|
|
760
|
+
const command = args[0];
|
|
761
|
+
|
|
762
|
+
if (command === 'stats') {
|
|
763
|
+
// Show network stats
|
|
764
|
+
await ensureDirectories();
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const data = await fs.readFile(getQDAGFile(), 'utf-8');
|
|
768
|
+
const ledger = JSON.parse(data);
|
|
769
|
+
|
|
770
|
+
console.log('\nš Edge-Net Network Statistics\n');
|
|
771
|
+
console.log(` Total Nodes: ${ledger.nodes?.length || 0}`);
|
|
772
|
+
console.log(` Confirmed: ${ledger.confirmed?.length || 0}`);
|
|
773
|
+
console.log(` Current Tips: ${ledger.tips?.length || 0}`);
|
|
774
|
+
|
|
775
|
+
const contributions = (ledger.nodes || []).filter(n => n.type === 'contribution');
|
|
776
|
+
const contributors = new Set(contributions.map(c => c.contributor));
|
|
777
|
+
|
|
778
|
+
console.log(` Contributions: ${contributions.length}`);
|
|
779
|
+
console.log(` Contributors: ${contributors.size}`);
|
|
780
|
+
console.log(` Total Credits: ${contributions.reduce((s, c) => s + (c.credits || 0), 0)}`);
|
|
781
|
+
console.log();
|
|
782
|
+
} catch (err) {
|
|
783
|
+
console.log('No QDAG data found. Start contributing to initialize the network.');
|
|
784
|
+
}
|
|
785
|
+
} else if (command === 'peers') {
|
|
786
|
+
// Show known peers
|
|
787
|
+
await ensureDirectories();
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const data = await fs.readFile(getPeersFile(), 'utf-8');
|
|
791
|
+
const peers = JSON.parse(data);
|
|
792
|
+
|
|
793
|
+
console.log('\nš„ Known Peers\n');
|
|
794
|
+
for (const peer of peers) {
|
|
795
|
+
const status = (Date.now() - peer.lastSeen) < 300000 ? 'š¢' : 'āŖ';
|
|
796
|
+
console.log(` ${status} ${peer.siteId} (Ļ:${peer.piKey.slice(0, 8)})`);
|
|
797
|
+
console.log(` First seen: ${new Date(peer.firstSeen).toLocaleString()}`);
|
|
798
|
+
console.log(` Last seen: ${new Date(peer.lastSeen).toLocaleString()}`);
|
|
799
|
+
console.log(` Verified: ${peer.verified ? 'ā
' : 'ā'}`);
|
|
800
|
+
console.log();
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.log('No peers found. Join the network to discover peers.');
|
|
804
|
+
}
|
|
805
|
+
} else if (command === 'help' || !command) {
|
|
806
|
+
console.log(`
|
|
807
|
+
Edge-Net Network Module
|
|
808
|
+
|
|
809
|
+
Commands:
|
|
810
|
+
stats Show network statistics
|
|
811
|
+
peers Show known peers
|
|
812
|
+
help Show this help
|
|
813
|
+
|
|
814
|
+
The network module is used internally by the join CLI.
|
|
815
|
+
To join the network: npx edge-net-join --generate
|
|
816
|
+
`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
main().catch(console.error);
|