yakmesh 1.1.0 → 1.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.
@@ -0,0 +1,612 @@
1
+ /**
2
+ * YAKMESH™ ECHO - Encrypted Coordinate Heuristic Oracle
3
+ *
4
+ * A novel topology discovery and latency mapping system that:
5
+ * - Maps mesh topology WITHOUT exposing node positions
6
+ * - Uses encrypted timing probes for secure latency measurement
7
+ * - Builds probabilistic coordinate space from RTT measurements
8
+ * - Enables route optimization through virtual coordinates
9
+ *
10
+ * Key Innovation: "Privacy-preserving network cartography"
11
+ * - Nodes learn relative distances without revealing absolute positions
12
+ * - Encrypted probes prevent eavesdroppers from mapping the network
13
+ * - Temporal signatures ensure probe authenticity
14
+ *
15
+ * @module mesh/echo-ranging
16
+ * @license MIT
17
+ * @copyright 2026 YAKMESH Contributors
18
+ * @trademark ECHO™ is a trademark of YAKMESH
19
+ */
20
+
21
+ import { randomBytes, createHash, createCipheriv, createDecipheriv } from 'crypto';
22
+ import { sha3_256 } from '@noble/hashes/sha3.js';
23
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
24
+
25
+ const ECHO_CONFIG = {
26
+ // Probe settings
27
+ probeIntervalMs: 30000, // Send probes every 30s
28
+ probeTimeoutMs: 5000, // Probe timeout
29
+ maxProbesPerPeer: 10, // Rolling window for RTT averaging
30
+
31
+ // Coordinate space
32
+ dimensions: 8, // Virtual coordinate dimensions
33
+ coordinatePrecision: 1000, // Microsecond precision
34
+ maxCoordinateValue: 1000000, // Max coordinate value
35
+
36
+ // Convergence
37
+ convergenceThreshold: 0.01, // 1% change threshold
38
+ adaptationRate: 0.25, // How fast coordinates adapt
39
+
40
+ // Security
41
+ encryptionAlgorithm: 'aes-256-gcm',
42
+ probeNonceSize: 12,
43
+ authTagLength: 16,
44
+ };
45
+
46
+ /**
47
+ * Encrypted probe for measuring latency
48
+ */
49
+ class EchoProbe {
50
+ constructor(options) {
51
+ this.probeId = options.probeId || bytesToHex(randomBytes(16));
52
+ this.sourceNodeId = options.sourceNodeId;
53
+ this.targetNodeId = options.targetNodeId;
54
+ this.sendTime = options.sendTime || process.hrtime.bigint();
55
+ this.sequence = options.sequence || 0;
56
+ this.encrypted = options.encrypted || false;
57
+ this.payload = options.payload || null;
58
+ }
59
+
60
+ /**
61
+ * Encrypt probe payload for secure transmission
62
+ */
63
+ encrypt(sharedSecret) {
64
+ const nonce = randomBytes(ECHO_CONFIG.probeNonceSize);
65
+ const cipher = createCipheriv(
66
+ ECHO_CONFIG.encryptionAlgorithm,
67
+ this._deriveKey(sharedSecret, 'echo-probe'),
68
+ nonce,
69
+ { authTagLength: ECHO_CONFIG.authTagLength }
70
+ );
71
+
72
+ const plaintext = JSON.stringify({
73
+ probeId: this.probeId,
74
+ sourceNodeId: this.sourceNodeId,
75
+ sendTime: this.sendTime.toString(),
76
+ sequence: this.sequence,
77
+ });
78
+
79
+ const encrypted = Buffer.concat([
80
+ cipher.update(plaintext, 'utf8'),
81
+ cipher.final(),
82
+ ]);
83
+
84
+ this.payload = {
85
+ nonce: nonce.toString('hex'),
86
+ data: encrypted.toString('hex'),
87
+ tag: cipher.getAuthTag().toString('hex'),
88
+ };
89
+ this.encrypted = true;
90
+
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Decrypt probe payload
96
+ */
97
+ static decrypt(encryptedProbe, sharedSecret) {
98
+ if (!encryptedProbe.payload || !encryptedProbe.encrypted) {
99
+ throw new Error('Probe is not encrypted');
100
+ }
101
+
102
+ const nonce = Buffer.from(encryptedProbe.payload.nonce, 'hex');
103
+ const data = Buffer.from(encryptedProbe.payload.data, 'hex');
104
+ const tag = Buffer.from(encryptedProbe.payload.tag, 'hex');
105
+
106
+ const decipher = createDecipheriv(
107
+ ECHO_CONFIG.encryptionAlgorithm,
108
+ EchoProbe.prototype._deriveKey(sharedSecret, 'echo-probe'),
109
+ nonce,
110
+ { authTagLength: ECHO_CONFIG.authTagLength }
111
+ );
112
+ decipher.setAuthTag(tag);
113
+
114
+ const decrypted = Buffer.concat([
115
+ decipher.update(data),
116
+ decipher.final(),
117
+ ]);
118
+
119
+ const parsed = JSON.parse(decrypted.toString('utf8'));
120
+
121
+ return new EchoProbe({
122
+ probeId: parsed.probeId,
123
+ sourceNodeId: parsed.sourceNodeId,
124
+ targetNodeId: encryptedProbe.targetNodeId,
125
+ sendTime: BigInt(parsed.sendTime),
126
+ sequence: parsed.sequence,
127
+ encrypted: false,
128
+ });
129
+ }
130
+
131
+ _deriveKey(secret, context) {
132
+ return createHash('sha256')
133
+ .update(secret)
134
+ .update(context)
135
+ .digest();
136
+ }
137
+
138
+ serialize() {
139
+ return {
140
+ probeId: this.probeId,
141
+ sourceNodeId: this.sourceNodeId,
142
+ targetNodeId: this.targetNodeId,
143
+ sequence: this.sequence,
144
+ encrypted: this.encrypted,
145
+ payload: this.payload,
146
+ };
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Echo response (pong)
152
+ */
153
+ class EchoResponse {
154
+ constructor(options) {
155
+ this.probeId = options.probeId;
156
+ this.responderNodeId = options.responderNodeId;
157
+ this.receiveTime = options.receiveTime || process.hrtime.bigint();
158
+ this.respondTime = options.respondTime || process.hrtime.bigint();
159
+ this.processingDelay = options.processingDelay || 0n;
160
+ this.coordinates = options.coordinates || null;
161
+ }
162
+
163
+ /**
164
+ * Calculate processing delay
165
+ */
166
+ calculateProcessingDelay() {
167
+ this.processingDelay = this.respondTime - this.receiveTime;
168
+ return this.processingDelay;
169
+ }
170
+
171
+ serialize() {
172
+ return {
173
+ probeId: this.probeId,
174
+ responderNodeId: this.responderNodeId,
175
+ processingDelay: this.processingDelay.toString(),
176
+ coordinates: this.coordinates,
177
+ };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Virtual coordinate system using Vivaldi-inspired algorithm
183
+ * Each node maintains coordinates that reflect network latency
184
+ */
185
+ class VirtualCoordinates {
186
+ constructor(dimensions = ECHO_CONFIG.dimensions) {
187
+ this.dimensions = dimensions;
188
+ this.coordinates = new Array(dimensions).fill(0);
189
+ this.error = 1.0; // Estimated coordinate error
190
+ this.updateCount = 0;
191
+
192
+ // Initialize with small random values
193
+ for (let i = 0; i < dimensions; i++) {
194
+ this.coordinates[i] = (Math.random() - 0.5) * 100;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Update coordinates based on measured RTT to a peer
200
+ */
201
+ update(peerCoordinates, measuredRtt, peerError = 0.5) {
202
+ // Calculate predicted distance
203
+ const predictedDistance = this.distanceTo(peerCoordinates);
204
+
205
+ // Calculate error (difference between predicted and actual)
206
+ const error = measuredRtt - predictedDistance;
207
+
208
+ // Relative error for weighting
209
+ const relativeError = Math.abs(error) / measuredRtt;
210
+
211
+ // Combined error weight (lower is more confident)
212
+ const weight = this.error / (this.error + peerError);
213
+
214
+ // Adaptive learning rate
215
+ const adaptationRate = ECHO_CONFIG.adaptationRate * weight;
216
+
217
+ // Update coordinates
218
+ const unitVector = this._unitVector(peerCoordinates);
219
+ for (let i = 0; i < this.dimensions; i++) {
220
+ this.coordinates[i] += adaptationRate * error * unitVector[i];
221
+
222
+ // Clamp to valid range
223
+ this.coordinates[i] = Math.max(
224
+ -ECHO_CONFIG.maxCoordinateValue,
225
+ Math.min(ECHO_CONFIG.maxCoordinateValue, this.coordinates[i])
226
+ );
227
+ }
228
+
229
+ // Update local error estimate
230
+ this.error = relativeError * weight + this.error * (1 - weight);
231
+ this.updateCount++;
232
+
233
+ return {
234
+ predictedDistance,
235
+ measuredRtt,
236
+ error,
237
+ newCoordinates: [...this.coordinates],
238
+ confidenceError: this.error,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Calculate Euclidean distance to another coordinate
244
+ */
245
+ distanceTo(other) {
246
+ if (!other || other.length !== this.dimensions) {
247
+ return Infinity;
248
+ }
249
+
250
+ let sum = 0;
251
+ for (let i = 0; i < this.dimensions; i++) {
252
+ const diff = this.coordinates[i] - other[i];
253
+ sum += diff * diff;
254
+ }
255
+ return Math.sqrt(sum);
256
+ }
257
+
258
+ /**
259
+ * Get unit vector pointing toward peer
260
+ */
261
+ _unitVector(peerCoordinates) {
262
+ const direction = [];
263
+ let magnitude = 0;
264
+
265
+ for (let i = 0; i < this.dimensions; i++) {
266
+ const diff = peerCoordinates[i] - this.coordinates[i];
267
+ direction.push(diff);
268
+ magnitude += diff * diff;
269
+ }
270
+
271
+ magnitude = Math.sqrt(magnitude);
272
+ if (magnitude === 0) {
273
+ // Random direction if at same point
274
+ return new Array(this.dimensions).fill(0).map(() => Math.random() - 0.5);
275
+ }
276
+
277
+ return direction.map(d => d / magnitude);
278
+ }
279
+
280
+ /**
281
+ * Check if coordinates have converged
282
+ */
283
+ hasConverged(previousCoordinates) {
284
+ if (!previousCoordinates) return false;
285
+
286
+ const distance = this.distanceTo(previousCoordinates);
287
+ const magnitude = Math.sqrt(
288
+ this.coordinates.reduce((sum, c) => sum + c * c, 0)
289
+ );
290
+
291
+ return distance / (magnitude || 1) < ECHO_CONFIG.convergenceThreshold;
292
+ }
293
+
294
+ serialize() {
295
+ return {
296
+ coordinates: [...this.coordinates],
297
+ error: this.error,
298
+ updateCount: this.updateCount,
299
+ };
300
+ }
301
+
302
+ static deserialize(obj) {
303
+ const vc = new VirtualCoordinates(obj.coordinates.length);
304
+ vc.coordinates = [...obj.coordinates];
305
+ vc.error = obj.error;
306
+ vc.updateCount = obj.updateCount;
307
+ return vc;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * RTT measurement with statistical analysis
313
+ */
314
+ class LatencyTracker {
315
+ constructor(maxSamples = ECHO_CONFIG.maxProbesPerPeer) {
316
+ this.maxSamples = maxSamples;
317
+ this.samples = [];
318
+ this.lastUpdate = 0;
319
+ }
320
+
321
+ addSample(rttNs) {
322
+ this.samples.push({
323
+ rtt: rttNs,
324
+ timestamp: Date.now(),
325
+ });
326
+
327
+ // Keep rolling window
328
+ if (this.samples.length > this.maxSamples) {
329
+ this.samples.shift();
330
+ }
331
+
332
+ this.lastUpdate = Date.now();
333
+ }
334
+
335
+ getStats() {
336
+ if (this.samples.length === 0) {
337
+ return null;
338
+ }
339
+
340
+ const rtts = this.samples.map(s => s.rtt);
341
+ const sorted = [...rtts].sort((a, b) => Number(a - b));
342
+
343
+ const sum = rtts.reduce((a, b) => a + b, 0n);
344
+ const mean = sum / BigInt(rtts.length);
345
+
346
+ const median = sorted[Math.floor(sorted.length / 2)];
347
+ const min = sorted[0];
348
+ const max = sorted[sorted.length - 1];
349
+
350
+ // Calculate standard deviation
351
+ const squaredDiffs = rtts.map(r => {
352
+ const diff = r - mean;
353
+ return diff * diff;
354
+ });
355
+ const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0n) / BigInt(rtts.length);
356
+ const stdDev = BigInt(Math.floor(Math.sqrt(Number(avgSquaredDiff))));
357
+
358
+ // P95
359
+ const p95Index = Math.floor(sorted.length * 0.95);
360
+ const p95 = sorted[p95Index] || max;
361
+
362
+ return {
363
+ mean,
364
+ median,
365
+ min,
366
+ max,
367
+ stdDev,
368
+ p95,
369
+ sampleCount: this.samples.length,
370
+ lastUpdate: this.lastUpdate,
371
+ };
372
+ }
373
+
374
+ getMeanMs() {
375
+ const stats = this.getStats();
376
+ if (!stats) return Infinity;
377
+ return Number(stats.mean) / 1_000_000; // ns to ms
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Main ECHO ranging system
383
+ */
384
+ class EchoRanging {
385
+ constructor(options = {}) {
386
+ this.nodeId = options.nodeId || bytesToHex(randomBytes(16));
387
+ this.coordinates = new VirtualCoordinates(options.dimensions);
388
+ this.peerLatencies = new Map(); // peerId -> LatencyTracker
389
+ this.peerCoordinates = new Map(); // peerId -> coordinates
390
+ this.pendingProbes = new Map(); // probeId -> { probe, sentAt }
391
+ this.sharedSecrets = new Map(); // peerId -> shared secret
392
+ this.probeSequence = 0;
393
+
394
+ this.stats = {
395
+ probesSent: 0,
396
+ probesReceived: 0,
397
+ responsesReceived: 0,
398
+ coordinateUpdates: 0,
399
+ encryptedProbes: 0,
400
+ };
401
+
402
+ // Callbacks
403
+ this.onSendProbe = options.onSendProbe || (() => {});
404
+ this.onCoordinateUpdate = options.onCoordinateUpdate || (() => {});
405
+ }
406
+
407
+ /**
408
+ * Set shared secret for encrypted probes with a peer
409
+ */
410
+ setSharedSecret(peerId, secret) {
411
+ this.sharedSecrets.set(peerId, secret);
412
+ }
413
+
414
+ /**
415
+ * Create and send a probe to a peer
416
+ */
417
+ createProbe(targetNodeId) {
418
+ const probe = new EchoProbe({
419
+ sourceNodeId: this.nodeId,
420
+ targetNodeId,
421
+ sequence: this.probeSequence++,
422
+ });
423
+
424
+ // Encrypt if we have a shared secret
425
+ const secret = this.sharedSecrets.get(targetNodeId);
426
+ if (secret) {
427
+ probe.encrypt(secret);
428
+ this.stats.encryptedProbes++;
429
+ }
430
+
431
+ // Track pending probe
432
+ this.pendingProbes.set(probe.probeId, {
433
+ probe,
434
+ sentAt: process.hrtime.bigint(),
435
+ targetNodeId,
436
+ });
437
+
438
+ // Set timeout to clean up
439
+ setTimeout(() => {
440
+ this.pendingProbes.delete(probe.probeId);
441
+ }, ECHO_CONFIG.probeTimeoutMs);
442
+
443
+ this.stats.probesSent++;
444
+
445
+ return probe.serialize();
446
+ }
447
+
448
+ /**
449
+ * Handle incoming probe (generate response)
450
+ */
451
+ handleProbe(probeData, fromNodeId) {
452
+ const receiveTime = process.hrtime.bigint();
453
+ this.stats.probesReceived++;
454
+
455
+ let probe;
456
+
457
+ // Decrypt if encrypted
458
+ if (probeData.encrypted) {
459
+ const secret = this.sharedSecrets.get(fromNodeId);
460
+ if (!secret) {
461
+ return { error: 'No shared secret for decryption' };
462
+ }
463
+ try {
464
+ probe = EchoProbe.decrypt(probeData, secret);
465
+ } catch (err) {
466
+ return { error: 'Decryption failed: ' + err.message };
467
+ }
468
+ } else {
469
+ probe = new EchoProbe(probeData);
470
+ }
471
+
472
+ // Create response
473
+ const response = new EchoResponse({
474
+ probeId: probe.probeId,
475
+ responderNodeId: this.nodeId,
476
+ receiveTime,
477
+ respondTime: process.hrtime.bigint(),
478
+ coordinates: this.coordinates.serialize().coordinates,
479
+ });
480
+ response.calculateProcessingDelay();
481
+
482
+ return response.serialize();
483
+ }
484
+
485
+ /**
486
+ * Handle probe response
487
+ */
488
+ handleResponse(responseData) {
489
+ const receiveTime = process.hrtime.bigint();
490
+
491
+ const pending = this.pendingProbes.get(responseData.probeId);
492
+ if (!pending) {
493
+ return { error: 'Unknown probe or timeout' };
494
+ }
495
+
496
+ // Calculate RTT (accounting for processing delay)
497
+ const processingDelay = BigInt(responseData.processingDelay || '0');
498
+ const rttNs = receiveTime - pending.sentAt - processingDelay;
499
+
500
+ // Get or create latency tracker
501
+ const peerId = pending.targetNodeId;
502
+ if (!this.peerLatencies.has(peerId)) {
503
+ this.peerLatencies.set(peerId, new LatencyTracker());
504
+ }
505
+ const tracker = this.peerLatencies.get(peerId);
506
+ tracker.addSample(rttNs);
507
+
508
+ // Store peer coordinates
509
+ if (responseData.coordinates) {
510
+ this.peerCoordinates.set(peerId, responseData.coordinates);
511
+
512
+ // Update our coordinates based on measurement
513
+ const rttMs = Number(rttNs) / 1_000_000;
514
+ const result = this.coordinates.update(
515
+ responseData.coordinates,
516
+ rttMs
517
+ );
518
+
519
+ this.stats.coordinateUpdates++;
520
+ this.onCoordinateUpdate(result);
521
+ }
522
+
523
+ this.pendingProbes.delete(responseData.probeId);
524
+ this.stats.responsesReceived++;
525
+
526
+ return {
527
+ peerId,
528
+ rttNs,
529
+ rttMs: Number(rttNs) / 1_000_000,
530
+ latencyStats: tracker.getStats(),
531
+ coordinates: this.coordinates.serialize(),
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Get estimated latency to a peer without probing
537
+ */
538
+ estimateLatency(peerId) {
539
+ const peerCoords = this.peerCoordinates.get(peerId);
540
+ if (!peerCoords) {
541
+ return null;
542
+ }
543
+
544
+ return this.coordinates.distanceTo(peerCoords);
545
+ }
546
+
547
+ /**
548
+ * Get best route through known peers
549
+ */
550
+ findBestRoute(targetPeerId, availablePeers) {
551
+ const targetCoords = this.peerCoordinates.get(targetPeerId);
552
+ if (!targetCoords) {
553
+ return null;
554
+ }
555
+
556
+ const routes = availablePeers
557
+ .filter(p => this.peerCoordinates.has(p))
558
+ .map(peerId => {
559
+ const peerCoords = this.peerCoordinates.get(peerId);
560
+ const toHop = this.coordinates.distanceTo(peerCoords);
561
+ const hopToTarget = Math.sqrt(
562
+ peerCoords.reduce((sum, c, i) => sum + Math.pow(c - targetCoords[i], 2), 0)
563
+ );
564
+ return {
565
+ via: peerId,
566
+ estimatedLatency: toHop + hopToTarget,
567
+ directLatency: this.coordinates.distanceTo(targetCoords),
568
+ };
569
+ })
570
+ .sort((a, b) => a.estimatedLatency - b.estimatedLatency);
571
+
572
+ return routes[0] || null;
573
+ }
574
+
575
+ /**
576
+ * Get network topology summary
577
+ */
578
+ getTopology() {
579
+ const peers = [];
580
+ for (const [peerId, coords] of this.peerCoordinates) {
581
+ const latency = this.peerLatencies.get(peerId);
582
+ peers.push({
583
+ peerId,
584
+ coordinates: coords,
585
+ estimatedDistance: this.coordinates.distanceTo(coords),
586
+ latencyStats: latency ? latency.getStats() : null,
587
+ });
588
+ }
589
+
590
+ return {
591
+ self: {
592
+ nodeId: this.nodeId,
593
+ coordinates: this.coordinates.serialize(),
594
+ },
595
+ peers: peers.sort((a, b) => a.estimatedDistance - b.estimatedDistance),
596
+ stats: { ...this.stats },
597
+ };
598
+ }
599
+
600
+ getStats() {
601
+ return { ...this.stats };
602
+ }
603
+ }
604
+
605
+ export {
606
+ ECHO_CONFIG,
607
+ EchoProbe,
608
+ EchoResponse,
609
+ VirtualCoordinates,
610
+ LatencyTracker,
611
+ EchoRanging,
612
+ };