@ruvector/edge-net 0.1.7 → 0.2.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/qdag.js ADDED
@@ -0,0 +1,582 @@
1
+ /**
2
+ * @ruvector/edge-net QDAG (Quantum DAG) Implementation
3
+ *
4
+ * Directed Acyclic Graph for distributed consensus and task tracking
5
+ * Inspired by IOTA Tangle and DAG-based blockchains
6
+ *
7
+ * Features:
8
+ * - Tip selection algorithm
9
+ * - Proof of contribution verification
10
+ * - Transaction validation
11
+ * - Network synchronization
12
+ *
13
+ * @module @ruvector/edge-net/qdag
14
+ */
15
+
16
+ import { EventEmitter } from 'events';
17
+ import { randomBytes, createHash, createHmac } from 'crypto';
18
+
19
+ // ============================================
20
+ // TRANSACTION
21
+ // ============================================
22
+
23
+ /**
24
+ * QDAG Transaction
25
+ */
26
+ export class Transaction {
27
+ constructor(data = {}) {
28
+ this.id = data.id || `tx-${randomBytes(16).toString('hex')}`;
29
+ this.timestamp = data.timestamp || Date.now();
30
+ this.type = data.type || 'generic'; // 'genesis', 'task', 'reward', 'transfer'
31
+
32
+ // Links to parent transactions (must reference 2 tips)
33
+ this.parents = data.parents || [];
34
+
35
+ // Transaction payload
36
+ this.payload = data.payload || {};
37
+
38
+ // Proof of contribution
39
+ this.proof = data.proof || null;
40
+
41
+ // Issuer
42
+ this.issuer = data.issuer || null;
43
+ this.signature = data.signature || null;
44
+
45
+ // Computed fields
46
+ this.hash = data.hash || this.computeHash();
47
+ this.weight = data.weight || 1;
48
+ this.cumulativeWeight = data.cumulativeWeight || 1;
49
+ this.confirmed = data.confirmed || false;
50
+ this.confirmedAt = data.confirmedAt || null;
51
+ }
52
+
53
+ /**
54
+ * Compute transaction hash
55
+ */
56
+ computeHash() {
57
+ const content = JSON.stringify({
58
+ id: this.id,
59
+ timestamp: this.timestamp,
60
+ type: this.type,
61
+ parents: this.parents,
62
+ payload: this.payload,
63
+ proof: this.proof,
64
+ issuer: this.issuer,
65
+ });
66
+
67
+ return createHash('sha256').update(content).digest('hex');
68
+ }
69
+
70
+ /**
71
+ * Sign transaction
72
+ */
73
+ sign(privateKey) {
74
+ const hmac = createHmac('sha256', privateKey);
75
+ hmac.update(this.hash);
76
+ this.signature = hmac.digest('hex');
77
+ return this.signature;
78
+ }
79
+
80
+ /**
81
+ * Verify signature
82
+ */
83
+ verify(publicKey) {
84
+ if (!this.signature) return false;
85
+
86
+ const hmac = createHmac('sha256', publicKey);
87
+ hmac.update(this.hash);
88
+ const expected = hmac.digest('hex');
89
+
90
+ return this.signature === expected;
91
+ }
92
+
93
+ /**
94
+ * Serialize transaction
95
+ */
96
+ toJSON() {
97
+ return {
98
+ id: this.id,
99
+ timestamp: this.timestamp,
100
+ type: this.type,
101
+ parents: this.parents,
102
+ payload: this.payload,
103
+ proof: this.proof,
104
+ issuer: this.issuer,
105
+ signature: this.signature,
106
+ hash: this.hash,
107
+ weight: this.weight,
108
+ cumulativeWeight: this.cumulativeWeight,
109
+ confirmed: this.confirmed,
110
+ confirmedAt: this.confirmedAt,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Deserialize transaction
116
+ */
117
+ static fromJSON(json) {
118
+ return new Transaction(json);
119
+ }
120
+ }
121
+
122
+ // ============================================
123
+ // QDAG (Quantum DAG)
124
+ // ============================================
125
+
126
+ /**
127
+ * QDAG - Directed Acyclic Graph for distributed consensus
128
+ */
129
+ export class QDAG extends EventEmitter {
130
+ constructor(options = {}) {
131
+ super();
132
+ this.id = options.id || `qdag-${randomBytes(8).toString('hex')}`;
133
+ this.nodeId = options.nodeId;
134
+
135
+ // Transaction storage
136
+ this.transactions = new Map();
137
+ this.tips = new Set(); // Unconfirmed transactions
138
+ this.confirmed = new Set(); // Confirmed transactions
139
+
140
+ // Indices
141
+ this.byIssuer = new Map(); // issuer -> Set<txId>
142
+ this.byType = new Map(); // type -> Set<txId>
143
+ this.children = new Map(); // txId -> Set<childTxId>
144
+
145
+ // Configuration
146
+ this.confirmationThreshold = options.confirmationThreshold || 5;
147
+ this.maxTips = options.maxTips || 100;
148
+ this.pruneAge = options.pruneAge || 24 * 60 * 60 * 1000; // 24 hours
149
+
150
+ // Stats
151
+ this.stats = {
152
+ transactions: 0,
153
+ confirmed: 0,
154
+ tips: 0,
155
+ avgConfirmationTime: 0,
156
+ };
157
+
158
+ // Create genesis if needed
159
+ if (options.createGenesis !== false) {
160
+ this.createGenesis();
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Create genesis transaction
166
+ */
167
+ createGenesis() {
168
+ const genesis = new Transaction({
169
+ id: 'genesis',
170
+ type: 'genesis',
171
+ parents: [],
172
+ payload: {
173
+ message: 'QDAG Genesis',
174
+ timestamp: Date.now(),
175
+ },
176
+ issuer: 'system',
177
+ });
178
+
179
+ genesis.confirmed = true;
180
+ genesis.confirmedAt = Date.now();
181
+ genesis.cumulativeWeight = this.confirmationThreshold + 1;
182
+
183
+ this.transactions.set(genesis.id, genesis);
184
+ this.tips.add(genesis.id);
185
+ this.confirmed.add(genesis.id);
186
+
187
+ this.emit('genesis', genesis);
188
+
189
+ return genesis;
190
+ }
191
+
192
+ /**
193
+ * Select tips for new transaction (weighted random walk)
194
+ */
195
+ selectTips(count = 2) {
196
+ const tips = Array.from(this.tips);
197
+
198
+ if (tips.length === 0) {
199
+ return ['genesis'];
200
+ }
201
+
202
+ if (tips.length <= count) {
203
+ return tips;
204
+ }
205
+
206
+ // Weighted random selection based on cumulative weight
207
+ const selected = new Set();
208
+ const weights = tips.map(tipId => {
209
+ const tx = this.transactions.get(tipId);
210
+ return tx ? tx.cumulativeWeight : 1;
211
+ });
212
+
213
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
214
+
215
+ while (selected.size < count && selected.size < tips.length) {
216
+ let random = Math.random() * totalWeight;
217
+
218
+ for (let i = 0; i < tips.length; i++) {
219
+ random -= weights[i];
220
+ if (random <= 0) {
221
+ selected.add(tips[i]);
222
+ break;
223
+ }
224
+ }
225
+ }
226
+
227
+ return Array.from(selected);
228
+ }
229
+
230
+ /**
231
+ * Add transaction to QDAG
232
+ */
233
+ addTransaction(tx) {
234
+ // Validate transaction
235
+ if (!this.validateTransaction(tx)) {
236
+ throw new Error('Invalid transaction');
237
+ }
238
+
239
+ // Check for duplicates
240
+ if (this.transactions.has(tx.id)) {
241
+ return this.transactions.get(tx.id);
242
+ }
243
+
244
+ // Store transaction
245
+ this.transactions.set(tx.id, tx);
246
+ this.tips.add(tx.id);
247
+ this.stats.transactions++;
248
+
249
+ // Update indices
250
+ if (tx.issuer) {
251
+ if (!this.byIssuer.has(tx.issuer)) {
252
+ this.byIssuer.set(tx.issuer, new Set());
253
+ }
254
+ this.byIssuer.get(tx.issuer).add(tx.id);
255
+ }
256
+
257
+ if (!this.byType.has(tx.type)) {
258
+ this.byType.set(tx.type, new Set());
259
+ }
260
+ this.byType.get(tx.type).add(tx.id);
261
+
262
+ // Update parent references
263
+ for (const parentId of tx.parents) {
264
+ if (!this.children.has(parentId)) {
265
+ this.children.set(parentId, new Set());
266
+ }
267
+ this.children.get(parentId).add(tx.id);
268
+
269
+ // Remove parent from tips
270
+ this.tips.delete(parentId);
271
+ }
272
+
273
+ // Update weights
274
+ this.updateWeights(tx.id);
275
+
276
+ // Check for confirmations
277
+ this.checkConfirmations();
278
+
279
+ this.emit('transaction', tx);
280
+
281
+ return tx;
282
+ }
283
+
284
+ /**
285
+ * Create and add a new transaction
286
+ */
287
+ createTransaction(type, payload, options = {}) {
288
+ const parents = options.parents || this.selectTips(2);
289
+
290
+ const tx = new Transaction({
291
+ type,
292
+ payload,
293
+ parents,
294
+ issuer: options.issuer || this.nodeId,
295
+ proof: options.proof,
296
+ });
297
+
298
+ if (options.privateKey) {
299
+ tx.sign(options.privateKey);
300
+ }
301
+
302
+ return this.addTransaction(tx);
303
+ }
304
+
305
+ /**
306
+ * Validate transaction
307
+ */
308
+ validateTransaction(tx) {
309
+ // Check required fields
310
+ if (!tx.id || !tx.timestamp || !tx.type) {
311
+ return false;
312
+ }
313
+
314
+ // Check parents exist (except genesis)
315
+ if (tx.type !== 'genesis') {
316
+ if (tx.parents.length === 0) {
317
+ return false;
318
+ }
319
+
320
+ for (const parentId of tx.parents) {
321
+ if (!this.transactions.has(parentId)) {
322
+ return false;
323
+ }
324
+ }
325
+ }
326
+
327
+ // Check no cycles (parents must be older)
328
+ for (const parentId of tx.parents) {
329
+ const parent = this.transactions.get(parentId);
330
+ if (parent && parent.timestamp >= tx.timestamp) {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ return true;
336
+ }
337
+
338
+ /**
339
+ * Update cumulative weights
340
+ */
341
+ updateWeights(txId) {
342
+ const tx = this.transactions.get(txId);
343
+ if (!tx) return;
344
+
345
+ // Update weight of this transaction
346
+ tx.cumulativeWeight = tx.weight;
347
+
348
+ // Add weight of all children
349
+ const children = this.children.get(txId);
350
+ if (children) {
351
+ for (const childId of children) {
352
+ const child = this.transactions.get(childId);
353
+ if (child) {
354
+ tx.cumulativeWeight += child.cumulativeWeight;
355
+ }
356
+ }
357
+ }
358
+
359
+ // Propagate to parents
360
+ for (const parentId of tx.parents) {
361
+ this.updateWeights(parentId);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Check for newly confirmed transactions
367
+ */
368
+ checkConfirmations() {
369
+ for (const [txId, tx] of this.transactions) {
370
+ if (!tx.confirmed && tx.cumulativeWeight >= this.confirmationThreshold) {
371
+ tx.confirmed = true;
372
+ tx.confirmedAt = Date.now();
373
+
374
+ this.confirmed.add(txId);
375
+ this.stats.confirmed++;
376
+
377
+ // Update average confirmation time
378
+ const confirmTime = tx.confirmedAt - tx.timestamp;
379
+ this.stats.avgConfirmationTime =
380
+ (this.stats.avgConfirmationTime * (this.stats.confirmed - 1) + confirmTime) /
381
+ this.stats.confirmed;
382
+
383
+ this.emit('confirmed', tx);
384
+ }
385
+ }
386
+
387
+ this.stats.tips = this.tips.size;
388
+ }
389
+
390
+ /**
391
+ * Get transaction by ID
392
+ */
393
+ getTransaction(txId) {
394
+ return this.transactions.get(txId);
395
+ }
396
+
397
+ /**
398
+ * Get transactions by issuer
399
+ */
400
+ getByIssuer(issuer) {
401
+ const txIds = this.byIssuer.get(issuer) || new Set();
402
+ return Array.from(txIds).map(id => this.transactions.get(id));
403
+ }
404
+
405
+ /**
406
+ * Get transactions by type
407
+ */
408
+ getByType(type) {
409
+ const txIds = this.byType.get(type) || new Set();
410
+ return Array.from(txIds).map(id => this.transactions.get(id));
411
+ }
412
+
413
+ /**
414
+ * Get current tips
415
+ */
416
+ getTips() {
417
+ return Array.from(this.tips).map(id => this.transactions.get(id));
418
+ }
419
+
420
+ /**
421
+ * Get confirmed transactions
422
+ */
423
+ getConfirmed() {
424
+ return Array.from(this.confirmed).map(id => this.transactions.get(id));
425
+ }
426
+
427
+ /**
428
+ * Prune old transactions
429
+ */
430
+ prune() {
431
+ const cutoff = Date.now() - this.pruneAge;
432
+ let pruned = 0;
433
+
434
+ for (const [txId, tx] of this.transactions) {
435
+ if (tx.type === 'genesis') continue;
436
+
437
+ if (tx.confirmed && tx.confirmedAt < cutoff) {
438
+ // Remove from storage
439
+ this.transactions.delete(txId);
440
+ this.confirmed.delete(txId);
441
+ this.tips.delete(txId);
442
+
443
+ // Clean up indices
444
+ if (tx.issuer && this.byIssuer.has(tx.issuer)) {
445
+ this.byIssuer.get(tx.issuer).delete(txId);
446
+ }
447
+ if (this.byType.has(tx.type)) {
448
+ this.byType.get(tx.type).delete(txId);
449
+ }
450
+
451
+ this.children.delete(txId);
452
+
453
+ pruned++;
454
+ }
455
+ }
456
+
457
+ if (pruned > 0) {
458
+ this.emit('pruned', { count: pruned });
459
+ }
460
+
461
+ return pruned;
462
+ }
463
+
464
+ /**
465
+ * Get QDAG statistics
466
+ */
467
+ getStats() {
468
+ return {
469
+ id: this.id,
470
+ ...this.stats,
471
+ size: this.transactions.size,
472
+ memoryUsage: process.memoryUsage?.().heapUsed,
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Export QDAG for synchronization
478
+ */
479
+ export(since = 0) {
480
+ const transactions = [];
481
+
482
+ for (const [txId, tx] of this.transactions) {
483
+ if (tx.timestamp >= since) {
484
+ transactions.push(tx.toJSON());
485
+ }
486
+ }
487
+
488
+ return {
489
+ id: this.id,
490
+ timestamp: Date.now(),
491
+ transactions,
492
+ };
493
+ }
494
+
495
+ /**
496
+ * Import transactions from another node
497
+ */
498
+ import(data) {
499
+ let imported = 0;
500
+
501
+ // Sort by timestamp to maintain order
502
+ const sorted = data.transactions.sort((a, b) => a.timestamp - b.timestamp);
503
+
504
+ for (const txData of sorted) {
505
+ try {
506
+ const tx = Transaction.fromJSON(txData);
507
+ if (!this.transactions.has(tx.id)) {
508
+ this.addTransaction(tx);
509
+ imported++;
510
+ }
511
+ } catch (error) {
512
+ console.error('[QDAG] Import error:', error.message);
513
+ }
514
+ }
515
+
516
+ this.emit('imported', { count: imported, from: data.id });
517
+
518
+ return imported;
519
+ }
520
+
521
+ /**
522
+ * Merge with another QDAG
523
+ */
524
+ merge(other) {
525
+ return this.import(other.export());
526
+ }
527
+ }
528
+
529
+ // ============================================
530
+ // TASK TRANSACTION HELPERS
531
+ // ============================================
532
+
533
+ /**
534
+ * Create a task submission transaction
535
+ */
536
+ export function createTaskTransaction(qdag, task, options = {}) {
537
+ return qdag.createTransaction('task', {
538
+ taskId: task.id,
539
+ type: task.type,
540
+ data: task.data,
541
+ priority: task.priority || 'medium',
542
+ reward: task.reward || 0,
543
+ deadline: task.deadline,
544
+ }, options);
545
+ }
546
+
547
+ /**
548
+ * Create a task completion/reward transaction
549
+ */
550
+ export function createRewardTransaction(qdag, taskTxId, result, options = {}) {
551
+ const taskTx = qdag.getTransaction(taskTxId);
552
+ if (!taskTx) throw new Error('Task transaction not found');
553
+
554
+ return qdag.createTransaction('reward', {
555
+ taskTxId,
556
+ result,
557
+ worker: options.worker,
558
+ reward: taskTx.payload.reward || 0,
559
+ completedAt: Date.now(),
560
+ }, {
561
+ ...options,
562
+ parents: [taskTxId, ...qdag.selectTips(1)],
563
+ });
564
+ }
565
+
566
+ /**
567
+ * Create a credit transfer transaction
568
+ */
569
+ export function createTransferTransaction(qdag, from, to, amount, options = {}) {
570
+ return qdag.createTransaction('transfer', {
571
+ from,
572
+ to,
573
+ amount,
574
+ memo: options.memo,
575
+ }, options);
576
+ }
577
+
578
+ // ============================================
579
+ // EXPORTS
580
+ // ============================================
581
+
582
+ export default QDAG;