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.
- package/CHANGELOG.md +65 -75
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/discord.md +74 -0
- package/mesh/beacon-broadcast.js +655 -0
- package/mesh/echo-ranging.js +612 -0
- package/mesh/phantom-routing.js +700 -0
- package/mesh/pulse-sync.js +618 -0
- package/mesh/temporal-encoder.js +383 -0
- package/package.json +82 -51
- package/test-novel-systems.mjs +398 -0
- package/test-tme.mjs +383 -0
|
@@ -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
|
+
};
|