@ruvector/edge-net 0.1.7 → 0.2.1

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,621 @@
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
+ // Ensure genesis exists
197
+ if (!this.transactions.has('genesis')) {
198
+ this.createGenesis();
199
+ }
200
+
201
+ const tips = Array.from(this.tips);
202
+
203
+ // Fallback to genesis if no tips available
204
+ if (tips.length === 0) {
205
+ return ['genesis'];
206
+ }
207
+
208
+ // Return all tips if we have fewer than requested
209
+ if (tips.length <= count) {
210
+ return [...tips]; // Return copy to prevent mutation issues
211
+ }
212
+
213
+ // Weighted random selection based on cumulative weight
214
+ const selected = new Set();
215
+ const weights = tips.map(tipId => {
216
+ const tx = this.transactions.get(tipId);
217
+ return tx ? Math.max(tx.cumulativeWeight, 1) : 1;
218
+ });
219
+
220
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
221
+
222
+ // Safety: prevent infinite loop
223
+ let attempts = 0;
224
+ const maxAttempts = count * 10;
225
+
226
+ while (selected.size < count && selected.size < tips.length && attempts < maxAttempts) {
227
+ let random = Math.random() * totalWeight;
228
+
229
+ for (let i = 0; i < tips.length; i++) {
230
+ random -= weights[i];
231
+ if (random <= 0) {
232
+ selected.add(tips[i]);
233
+ break;
234
+ }
235
+ }
236
+ attempts++;
237
+ }
238
+
239
+ // Ensure we have at least one valid parent
240
+ const result = Array.from(selected);
241
+ if (result.length === 0) {
242
+ result.push(tips[0] || 'genesis');
243
+ }
244
+
245
+ return result;
246
+ }
247
+
248
+ /**
249
+ * Add transaction to QDAG
250
+ */
251
+ addTransaction(tx) {
252
+ // Validate transaction with detailed error
253
+ const validation = this.validateTransaction(tx, { returnError: true });
254
+ if (!validation.valid) {
255
+ throw new Error(`Invalid transaction: ${validation.error}`);
256
+ }
257
+
258
+ // Check for duplicates
259
+ if (this.transactions.has(tx.id)) {
260
+ return this.transactions.get(tx.id);
261
+ }
262
+
263
+ // Store transaction
264
+ this.transactions.set(tx.id, tx);
265
+ this.tips.add(tx.id);
266
+ this.stats.transactions++;
267
+
268
+ // Update indices
269
+ if (tx.issuer) {
270
+ if (!this.byIssuer.has(tx.issuer)) {
271
+ this.byIssuer.set(tx.issuer, new Set());
272
+ }
273
+ this.byIssuer.get(tx.issuer).add(tx.id);
274
+ }
275
+
276
+ if (!this.byType.has(tx.type)) {
277
+ this.byType.set(tx.type, new Set());
278
+ }
279
+ this.byType.get(tx.type).add(tx.id);
280
+
281
+ // Update parent references
282
+ for (const parentId of tx.parents) {
283
+ if (!this.children.has(parentId)) {
284
+ this.children.set(parentId, new Set());
285
+ }
286
+ this.children.get(parentId).add(tx.id);
287
+
288
+ // Remove parent from tips
289
+ this.tips.delete(parentId);
290
+ }
291
+
292
+ // Update weights
293
+ this.updateWeights(tx.id);
294
+
295
+ // Check for confirmations
296
+ this.checkConfirmations();
297
+
298
+ this.emit('transaction', tx);
299
+
300
+ return tx;
301
+ }
302
+
303
+ /**
304
+ * Create and add a new transaction
305
+ */
306
+ createTransaction(type, payload, options = {}) {
307
+ const parents = options.parents || this.selectTips(2);
308
+
309
+ const tx = new Transaction({
310
+ type,
311
+ payload,
312
+ parents,
313
+ issuer: options.issuer || this.nodeId,
314
+ proof: options.proof,
315
+ });
316
+
317
+ if (options.privateKey) {
318
+ tx.sign(options.privateKey);
319
+ }
320
+
321
+ return this.addTransaction(tx);
322
+ }
323
+
324
+ /**
325
+ * Validate transaction
326
+ * @returns {boolean|{valid: boolean, error: string}}
327
+ */
328
+ validateTransaction(tx, options = {}) {
329
+ const returnError = options.returnError || false;
330
+ const fail = (msg) => returnError ? { valid: false, error: msg } : false;
331
+ const pass = () => returnError ? { valid: true, error: null } : true;
332
+
333
+ // Check required fields
334
+ if (!tx.id) {
335
+ return fail('Missing transaction id');
336
+ }
337
+ if (!tx.timestamp) {
338
+ return fail('Missing transaction timestamp');
339
+ }
340
+ if (!tx.type) {
341
+ return fail('Missing transaction type');
342
+ }
343
+
344
+ // Genesis transactions don't need parents
345
+ if (tx.type === 'genesis') {
346
+ return pass();
347
+ }
348
+
349
+ // Ensure genesis exists before validating non-genesis transactions
350
+ if (!this.transactions.has('genesis')) {
351
+ this.createGenesis();
352
+ }
353
+
354
+ // Check parents exist
355
+ if (!tx.parents || tx.parents.length === 0) {
356
+ return fail('Non-genesis transaction must have at least one parent');
357
+ }
358
+
359
+ for (const parentId of tx.parents) {
360
+ if (!this.transactions.has(parentId)) {
361
+ return fail(`Parent transaction not found: ${parentId}`);
362
+ }
363
+ }
364
+
365
+ // Check no cycles (parents must be older or equal for simultaneous txs)
366
+ for (const parentId of tx.parents) {
367
+ const parent = this.transactions.get(parentId);
368
+ // Allow equal timestamps (transactions created at same time)
369
+ if (parent && parent.timestamp > tx.timestamp) {
370
+ return fail(`Parent ${parentId} has future timestamp`);
371
+ }
372
+ }
373
+
374
+ return pass();
375
+ }
376
+
377
+ /**
378
+ * Update cumulative weights
379
+ */
380
+ updateWeights(txId) {
381
+ const tx = this.transactions.get(txId);
382
+ if (!tx) return;
383
+
384
+ // Update weight of this transaction
385
+ tx.cumulativeWeight = tx.weight;
386
+
387
+ // Add weight of all children
388
+ const children = this.children.get(txId);
389
+ if (children) {
390
+ for (const childId of children) {
391
+ const child = this.transactions.get(childId);
392
+ if (child) {
393
+ tx.cumulativeWeight += child.cumulativeWeight;
394
+ }
395
+ }
396
+ }
397
+
398
+ // Propagate to parents
399
+ for (const parentId of tx.parents) {
400
+ this.updateWeights(parentId);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Check for newly confirmed transactions
406
+ */
407
+ checkConfirmations() {
408
+ for (const [txId, tx] of this.transactions) {
409
+ if (!tx.confirmed && tx.cumulativeWeight >= this.confirmationThreshold) {
410
+ tx.confirmed = true;
411
+ tx.confirmedAt = Date.now();
412
+
413
+ this.confirmed.add(txId);
414
+ this.stats.confirmed++;
415
+
416
+ // Update average confirmation time
417
+ const confirmTime = tx.confirmedAt - tx.timestamp;
418
+ this.stats.avgConfirmationTime =
419
+ (this.stats.avgConfirmationTime * (this.stats.confirmed - 1) + confirmTime) /
420
+ this.stats.confirmed;
421
+
422
+ this.emit('confirmed', tx);
423
+ }
424
+ }
425
+
426
+ this.stats.tips = this.tips.size;
427
+ }
428
+
429
+ /**
430
+ * Get transaction by ID
431
+ */
432
+ getTransaction(txId) {
433
+ return this.transactions.get(txId);
434
+ }
435
+
436
+ /**
437
+ * Get transactions by issuer
438
+ */
439
+ getByIssuer(issuer) {
440
+ const txIds = this.byIssuer.get(issuer) || new Set();
441
+ return Array.from(txIds).map(id => this.transactions.get(id));
442
+ }
443
+
444
+ /**
445
+ * Get transactions by type
446
+ */
447
+ getByType(type) {
448
+ const txIds = this.byType.get(type) || new Set();
449
+ return Array.from(txIds).map(id => this.transactions.get(id));
450
+ }
451
+
452
+ /**
453
+ * Get current tips
454
+ */
455
+ getTips() {
456
+ return Array.from(this.tips).map(id => this.transactions.get(id));
457
+ }
458
+
459
+ /**
460
+ * Get confirmed transactions
461
+ */
462
+ getConfirmed() {
463
+ return Array.from(this.confirmed).map(id => this.transactions.get(id));
464
+ }
465
+
466
+ /**
467
+ * Prune old transactions
468
+ */
469
+ prune() {
470
+ const cutoff = Date.now() - this.pruneAge;
471
+ let pruned = 0;
472
+
473
+ for (const [txId, tx] of this.transactions) {
474
+ if (tx.type === 'genesis') continue;
475
+
476
+ if (tx.confirmed && tx.confirmedAt < cutoff) {
477
+ // Remove from storage
478
+ this.transactions.delete(txId);
479
+ this.confirmed.delete(txId);
480
+ this.tips.delete(txId);
481
+
482
+ // Clean up indices
483
+ if (tx.issuer && this.byIssuer.has(tx.issuer)) {
484
+ this.byIssuer.get(tx.issuer).delete(txId);
485
+ }
486
+ if (this.byType.has(tx.type)) {
487
+ this.byType.get(tx.type).delete(txId);
488
+ }
489
+
490
+ this.children.delete(txId);
491
+
492
+ pruned++;
493
+ }
494
+ }
495
+
496
+ if (pruned > 0) {
497
+ this.emit('pruned', { count: pruned });
498
+ }
499
+
500
+ return pruned;
501
+ }
502
+
503
+ /**
504
+ * Get QDAG statistics
505
+ */
506
+ getStats() {
507
+ return {
508
+ id: this.id,
509
+ ...this.stats,
510
+ size: this.transactions.size,
511
+ memoryUsage: process.memoryUsage?.().heapUsed,
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Export QDAG for synchronization
517
+ */
518
+ export(since = 0) {
519
+ const transactions = [];
520
+
521
+ for (const [txId, tx] of this.transactions) {
522
+ if (tx.timestamp >= since) {
523
+ transactions.push(tx.toJSON());
524
+ }
525
+ }
526
+
527
+ return {
528
+ id: this.id,
529
+ timestamp: Date.now(),
530
+ transactions,
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Import transactions from another node
536
+ */
537
+ import(data) {
538
+ let imported = 0;
539
+
540
+ // Sort by timestamp to maintain order
541
+ const sorted = data.transactions.sort((a, b) => a.timestamp - b.timestamp);
542
+
543
+ for (const txData of sorted) {
544
+ try {
545
+ const tx = Transaction.fromJSON(txData);
546
+ if (!this.transactions.has(tx.id)) {
547
+ this.addTransaction(tx);
548
+ imported++;
549
+ }
550
+ } catch (error) {
551
+ console.error('[QDAG] Import error:', error.message);
552
+ }
553
+ }
554
+
555
+ this.emit('imported', { count: imported, from: data.id });
556
+
557
+ return imported;
558
+ }
559
+
560
+ /**
561
+ * Merge with another QDAG
562
+ */
563
+ merge(other) {
564
+ return this.import(other.export());
565
+ }
566
+ }
567
+
568
+ // ============================================
569
+ // TASK TRANSACTION HELPERS
570
+ // ============================================
571
+
572
+ /**
573
+ * Create a task submission transaction
574
+ */
575
+ export function createTaskTransaction(qdag, task, options = {}) {
576
+ return qdag.createTransaction('task', {
577
+ taskId: task.id,
578
+ type: task.type,
579
+ data: task.data,
580
+ priority: task.priority || 'medium',
581
+ reward: task.reward || 0,
582
+ deadline: task.deadline,
583
+ }, options);
584
+ }
585
+
586
+ /**
587
+ * Create a task completion/reward transaction
588
+ */
589
+ export function createRewardTransaction(qdag, taskTxId, result, options = {}) {
590
+ const taskTx = qdag.getTransaction(taskTxId);
591
+ if (!taskTx) throw new Error('Task transaction not found');
592
+
593
+ return qdag.createTransaction('reward', {
594
+ taskTxId,
595
+ result,
596
+ worker: options.worker,
597
+ reward: taskTx.payload.reward || 0,
598
+ completedAt: Date.now(),
599
+ }, {
600
+ ...options,
601
+ parents: [taskTxId, ...qdag.selectTips(1)],
602
+ });
603
+ }
604
+
605
+ /**
606
+ * Create a credit transfer transaction
607
+ */
608
+ export function createTransferTransaction(qdag, from, to, amount, options = {}) {
609
+ return qdag.createTransaction('transfer', {
610
+ from,
611
+ to,
612
+ amount,
613
+ memo: options.memo,
614
+ }, options);
615
+ }
616
+
617
+ // ============================================
618
+ // EXPORTS
619
+ // ============================================
620
+
621
+ export default QDAG;
Binary file