yakmesh 1.1.0 → 1.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/CHANGELOG.md +32 -0
- package/mesh/temporal-encoder.js +383 -0
- package/package.json +1 -1
- package/test-tme.mjs +383 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to YAKMESH™ will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **TME (Temporal Mesh Encoding)**: Novel packet resilience system unique to YAKMESH
|
|
12
|
+
- Exploits atomic time synchronization as the redundancy dimension
|
|
13
|
+
- Cryptographic temporal chaining binds data to specific points in time
|
|
14
|
+
- Mesh topology-aware encoding for intelligent path diversity
|
|
15
|
+
- NOT erasure coding - a fundamentally new approach to packet loss recovery
|
|
16
|
+
- **TemporalSlice**: Atomic unit of TME with cryptographic time binding
|
|
17
|
+
- Temporal hash includes: data + timestamp + sequence + mesh position
|
|
18
|
+
- Chain integrity via prevTemporalHash linking
|
|
19
|
+
- Tamper detection on deserialization
|
|
20
|
+
- **TemporalStream**: Message slicing and reassembly with temporal properties
|
|
21
|
+
- Configurable slice size and timing intervals
|
|
22
|
+
- Completion tracking and missing slice detection
|
|
23
|
+
- Temporal chain validation
|
|
24
|
+
- **TemporalReconstructor**: Recovery system using timing proofs
|
|
25
|
+
- Consensus verification from multiple mesh neighbors
|
|
26
|
+
- Missing slice attestation via timing proofs
|
|
27
|
+
- Partial reconstruction capabilities
|
|
28
|
+
- **TemporalMeshEncoder**: High-level API for TME operations
|
|
29
|
+
- Full encode/decode lifecycle management
|
|
30
|
+
- Statistics tracking (slices sent/received, completion rates)
|
|
31
|
+
- Stream status monitoring
|
|
32
|
+
- New test suite: 18 TME-specific tests (test-tme.mjs)
|
|
33
|
+
|
|
34
|
+
### Philosophy
|
|
35
|
+
- "Time IS the redundancy dimension" - unlike Walrus/Red Stuff 2D erasure coding
|
|
36
|
+
- Designed for real-time mesh networks with atomic clock sync
|
|
37
|
+
- Leverages YAKMESH's unique post-quantum + atomic timing combination
|
|
38
|
+
|
|
8
39
|
## [1.1.0] - 2026-01-14
|
|
9
40
|
|
|
10
41
|
### Added
|
|
@@ -90,3 +121,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
90
121
|
[1.0.1]: https://github.com/yakmesh/yakmesh/releases/tag/v1.0.1
|
|
91
122
|
[1.0.0]: https://github.com/yakmesh/yakmesh/releases/tag/v1.0.0
|
|
92
123
|
|
|
124
|
+
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESH™ Temporal Mesh Encoding (TME)
|
|
3
|
+
*
|
|
4
|
+
* A novel approach to packet resilience that exploits YAKMESH's unique capabilities:
|
|
5
|
+
* - Atomic time synchronization (PTP/PCIe-level nanosecond precision)
|
|
6
|
+
* - Post-quantum ML-DSA-65 signatures for cryptographic time binding
|
|
7
|
+
* - Mesh topology awareness for intelligent path diversity
|
|
8
|
+
*
|
|
9
|
+
* Instead of traditional erasure coding (encoding data across space),
|
|
10
|
+
* TME encodes data across TIME - using the mesh's synchronized clocks
|
|
11
|
+
* as the redundancy dimension.
|
|
12
|
+
*
|
|
13
|
+
* Key Innovation: "Time IS the redundancy dimension"
|
|
14
|
+
* - Temporal slicing with cryptographic chaining
|
|
15
|
+
* - Predictive reconstruction from timing proofs
|
|
16
|
+
* - Mesh heartbeat differential encoding
|
|
17
|
+
*
|
|
18
|
+
* @module mesh/temporal-encoder
|
|
19
|
+
* @license MIT
|
|
20
|
+
* @copyright 2026 YAKMESH Contributors
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { randomBytes, createHash } from 'crypto';
|
|
24
|
+
|
|
25
|
+
const TME_CONFIG = {
|
|
26
|
+
defaultSliceIntervalNs: 50_000_000,
|
|
27
|
+
maxSlicesPerStream: 256,
|
|
28
|
+
reconstructionWindowNs: 500_000_000,
|
|
29
|
+
timingToleranceNs: 5_000_000,
|
|
30
|
+
hashAlgorithm: 'sha256',
|
|
31
|
+
temporalHashLength: 32,
|
|
32
|
+
minSlicesForReconstruction: 0.6,
|
|
33
|
+
maxMissingConsecutive: 3,
|
|
34
|
+
minPathDiversity: 2,
|
|
35
|
+
maxPathReuse: 3,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class TemporalSlice {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.data = Buffer.from(options.data);
|
|
41
|
+
this.timestamp = BigInt(options.timestamp);
|
|
42
|
+
this.sequenceNumber = options.sequenceNumber;
|
|
43
|
+
this.streamId = options.streamId;
|
|
44
|
+
this.prevTemporalHash = options.prevTemporalHash || Buffer.alloc(32);
|
|
45
|
+
this.meshPosition = options.meshPosition || [0, 0, 0];
|
|
46
|
+
this.createdAt = Date.now();
|
|
47
|
+
this.temporalHash = this._computeTemporalHash();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_computeTemporalHash() {
|
|
51
|
+
const hash = createHash(TME_CONFIG.hashAlgorithm);
|
|
52
|
+
hash.update(this.data);
|
|
53
|
+
const timeBuffer = Buffer.alloc(8);
|
|
54
|
+
timeBuffer.writeBigUInt64BE(this.timestamp);
|
|
55
|
+
hash.update(timeBuffer);
|
|
56
|
+
const seqBuffer = Buffer.alloc(4);
|
|
57
|
+
seqBuffer.writeUInt32BE(this.sequenceNumber);
|
|
58
|
+
hash.update(seqBuffer);
|
|
59
|
+
hash.update(this.streamId);
|
|
60
|
+
hash.update(this.prevTemporalHash);
|
|
61
|
+
const posBuffer = Buffer.alloc(12);
|
|
62
|
+
posBuffer.writeFloatBE(this.meshPosition[0], 0);
|
|
63
|
+
posBuffer.writeFloatBE(this.meshPosition[1], 4);
|
|
64
|
+
posBuffer.writeFloatBE(this.meshPosition[2], 8);
|
|
65
|
+
hash.update(posBuffer);
|
|
66
|
+
return hash.digest();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
verify() {
|
|
70
|
+
const computed = this._computeTemporalHash();
|
|
71
|
+
return computed.equals(this.temporalHash);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
serialize() {
|
|
75
|
+
return {
|
|
76
|
+
data: this.data.toString('base64'),
|
|
77
|
+
timestamp: this.timestamp.toString(),
|
|
78
|
+
sequenceNumber: this.sequenceNumber,
|
|
79
|
+
streamId: this.streamId,
|
|
80
|
+
prevTemporalHash: this.prevTemporalHash.toString('hex'),
|
|
81
|
+
temporalHash: this.temporalHash.toString('hex'),
|
|
82
|
+
meshPosition: this.meshPosition,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static deserialize(obj) {
|
|
87
|
+
const slice = new TemporalSlice({
|
|
88
|
+
data: Buffer.from(obj.data, 'base64'),
|
|
89
|
+
timestamp: BigInt(obj.timestamp),
|
|
90
|
+
sequenceNumber: obj.sequenceNumber,
|
|
91
|
+
streamId: obj.streamId,
|
|
92
|
+
prevTemporalHash: Buffer.from(obj.prevTemporalHash, 'hex'),
|
|
93
|
+
meshPosition: obj.meshPosition,
|
|
94
|
+
});
|
|
95
|
+
const expectedHash = Buffer.from(obj.temporalHash, 'hex');
|
|
96
|
+
if (!slice.temporalHash.equals(expectedHash)) {
|
|
97
|
+
throw new Error('Temporal hash mismatch - slice may be corrupted or tampered');
|
|
98
|
+
}
|
|
99
|
+
return slice;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class TemporalStream {
|
|
104
|
+
constructor(options = {}) {
|
|
105
|
+
this.streamId = options.streamId || this._generateStreamId();
|
|
106
|
+
this.sliceSize = options.sliceSize || 1024;
|
|
107
|
+
this.baseTimestamp = BigInt(options.baseTimestamp || Date.now() * 1_000_000);
|
|
108
|
+
this.sliceIntervalNs = options.sliceIntervalNs || TME_CONFIG.defaultSliceIntervalNs;
|
|
109
|
+
this.slices = new Map();
|
|
110
|
+
this.totalSlices = 0;
|
|
111
|
+
this.isComplete = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_generateStreamId() {
|
|
115
|
+
return randomBytes(16).toString('hex');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
encode(message, meshPosition = [0, 0, 0]) {
|
|
119
|
+
const data = Buffer.from(message);
|
|
120
|
+
const slices = [];
|
|
121
|
+
let prevHash = Buffer.alloc(32);
|
|
122
|
+
this.totalSlices = Math.ceil(data.length / this.sliceSize);
|
|
123
|
+
if (this.totalSlices > TME_CONFIG.maxSlicesPerStream) {
|
|
124
|
+
throw new Error('Message too large: requires ' + this.totalSlices + ' slices, max is ' + TME_CONFIG.maxSlicesPerStream);
|
|
125
|
+
}
|
|
126
|
+
for (let i = 0; i < this.totalSlices; i++) {
|
|
127
|
+
const start = i * this.sliceSize;
|
|
128
|
+
const end = Math.min(start + this.sliceSize, data.length);
|
|
129
|
+
const sliceData = data.slice(start, end);
|
|
130
|
+
const timestamp = this.baseTimestamp + BigInt(i * this.sliceIntervalNs);
|
|
131
|
+
const slice = new TemporalSlice({
|
|
132
|
+
data: sliceData,
|
|
133
|
+
timestamp,
|
|
134
|
+
sequenceNumber: i,
|
|
135
|
+
streamId: this.streamId,
|
|
136
|
+
prevTemporalHash: prevHash,
|
|
137
|
+
meshPosition,
|
|
138
|
+
});
|
|
139
|
+
slices.push(slice);
|
|
140
|
+
this.slices.set(i, slice);
|
|
141
|
+
prevHash = slice.temporalHash;
|
|
142
|
+
}
|
|
143
|
+
this.isComplete = true;
|
|
144
|
+
return slices;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
addSlice(slice) {
|
|
148
|
+
if (slice.streamId !== this.streamId) return false;
|
|
149
|
+
if (!slice.verify()) return false;
|
|
150
|
+
if (slice.sequenceNumber > 0 && this.slices.has(slice.sequenceNumber - 1)) {
|
|
151
|
+
const prevSlice = this.slices.get(slice.sequenceNumber - 1);
|
|
152
|
+
if (!slice.prevTemporalHash.equals(prevSlice.temporalHash)) return false;
|
|
153
|
+
}
|
|
154
|
+
this.slices.set(slice.sequenceNumber, slice);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
canReconstruct() {
|
|
159
|
+
if (this.totalSlices === 0) return false;
|
|
160
|
+
return (this.slices.size / this.totalSlices) >= TME_CONFIG.minSlicesForReconstruction;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getMissingSlices() {
|
|
164
|
+
const missing = [];
|
|
165
|
+
for (let i = 0; i < this.totalSlices; i++) {
|
|
166
|
+
if (!this.slices.has(i)) missing.push(i);
|
|
167
|
+
}
|
|
168
|
+
return missing;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getCompletionPercent() {
|
|
172
|
+
if (this.totalSlices === 0) return 0;
|
|
173
|
+
return (this.slices.size / this.totalSlices) * 100;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class TemporalReconstructor {
|
|
178
|
+
constructor() {
|
|
179
|
+
this.streams = new Map();
|
|
180
|
+
this.timingProofs = new Map();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
registerStream(stream) {
|
|
184
|
+
this.streams.set(stream.streamId, stream);
|
|
185
|
+
this.timingProofs.set(stream.streamId, new Map());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
addTimingProof(streamId, sequenceNumber, proof) {
|
|
189
|
+
if (!this.timingProofs.has(streamId)) {
|
|
190
|
+
this.timingProofs.set(streamId, new Map());
|
|
191
|
+
}
|
|
192
|
+
const streamProofs = this.timingProofs.get(streamId);
|
|
193
|
+
if (!streamProofs.has(sequenceNumber)) {
|
|
194
|
+
streamProofs.set(sequenceNumber, []);
|
|
195
|
+
}
|
|
196
|
+
streamProofs.get(sequenceNumber).push({
|
|
197
|
+
nodeId: proof.nodeId,
|
|
198
|
+
timestamp: BigInt(proof.timestamp),
|
|
199
|
+
temporalHash: Buffer.from(proof.temporalHash, 'hex'),
|
|
200
|
+
signature: proof.signature,
|
|
201
|
+
receivedAt: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
verifyMissingSlice(streamId, sequenceNumber) {
|
|
206
|
+
const streamProofs = this.timingProofs.get(streamId);
|
|
207
|
+
if (!streamProofs || !streamProofs.has(sequenceNumber)) return null;
|
|
208
|
+
const proofs = streamProofs.get(sequenceNumber);
|
|
209
|
+
if (proofs.length < 2) return null;
|
|
210
|
+
const hashCounts = new Map();
|
|
211
|
+
for (const proof of proofs) {
|
|
212
|
+
const hashHex = proof.temporalHash.toString('hex');
|
|
213
|
+
hashCounts.set(hashHex, (hashCounts.get(hashHex) || 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
let consensusHash = null;
|
|
216
|
+
let maxCount = 0;
|
|
217
|
+
for (const [hash, count] of hashCounts) {
|
|
218
|
+
if (count > maxCount) {
|
|
219
|
+
maxCount = count;
|
|
220
|
+
consensusHash = hash;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (maxCount >= 2) {
|
|
224
|
+
return { verified: true, consensusHash, proofCount: maxCount, totalProofs: proofs.length };
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
reconstruct(streamId) {
|
|
230
|
+
const stream = this.streams.get(streamId);
|
|
231
|
+
if (!stream) return { success: false, error: 'Stream not found' };
|
|
232
|
+
const missing = stream.getMissingSlices();
|
|
233
|
+
if (missing.length === 0) return this._assembleComplete(stream);
|
|
234
|
+
if (!stream.canReconstruct()) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: 'Insufficient slices for reconstruction',
|
|
238
|
+
received: stream.slices.size,
|
|
239
|
+
required: Math.ceil(stream.totalSlices * TME_CONFIG.minSlicesForReconstruction),
|
|
240
|
+
total: stream.totalSlices,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const reconstructed = [];
|
|
244
|
+
const unrecoverable = [];
|
|
245
|
+
for (const seq of missing) {
|
|
246
|
+
const result = this._interpolateSlice(stream, seq);
|
|
247
|
+
if (result.success) reconstructed.push(seq);
|
|
248
|
+
else unrecoverable.push(seq);
|
|
249
|
+
}
|
|
250
|
+
if (unrecoverable.length > 0) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: 'Some slices unrecoverable',
|
|
254
|
+
reconstructed,
|
|
255
|
+
unrecoverable,
|
|
256
|
+
completionPercent: stream.getCompletionPercent(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return this._assembleComplete(stream);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_interpolateSlice(stream, sequenceNumber) {
|
|
263
|
+
const prev = stream.slices.get(sequenceNumber - 1);
|
|
264
|
+
const next = stream.slices.get(sequenceNumber + 1);
|
|
265
|
+
if (prev && next) {
|
|
266
|
+
const expectedHash = next.prevTemporalHash;
|
|
267
|
+
return { success: false, expectedHash: expectedHash.toString('hex'), reason: 'Need data from mesh neighbors' };
|
|
268
|
+
}
|
|
269
|
+
return { success: false, reason: 'Insufficient surrounding slices' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_assembleComplete(stream) {
|
|
273
|
+
const sortedSlices = Array.from(stream.slices.entries())
|
|
274
|
+
.sort((a, b) => a[0] - b[0])
|
|
275
|
+
.map(([_, slice]) => slice);
|
|
276
|
+
for (let i = 1; i < sortedSlices.length; i++) {
|
|
277
|
+
const prev = sortedSlices[i - 1];
|
|
278
|
+
const curr = sortedSlices[i];
|
|
279
|
+
if (!curr.prevTemporalHash.equals(prev.temporalHash)) {
|
|
280
|
+
return { success: false, error: 'Temporal chain broken at slice ' + i };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const data = Buffer.concat(sortedSlices.map(s => s.data));
|
|
284
|
+
return { success: true, data, sliceCount: sortedSlices.length, streamId: stream.streamId };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
class TemporalMeshEncoder {
|
|
289
|
+
constructor(options = {}) {
|
|
290
|
+
this.nodeId = options.nodeId || randomBytes(16).toString('hex');
|
|
291
|
+
this.meshPosition = options.meshPosition || [0, 0, 0];
|
|
292
|
+
this.reconstructor = new TemporalReconstructor();
|
|
293
|
+
this.outboundStreams = new Map();
|
|
294
|
+
this.inboundStreams = new Map();
|
|
295
|
+
this.stats = {
|
|
296
|
+
slicesSent: 0,
|
|
297
|
+
slicesReceived: 0,
|
|
298
|
+
streamsCompleted: 0,
|
|
299
|
+
reconstructionAttempts: 0,
|
|
300
|
+
successfulReconstructions: 0,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
encode(message, options = {}) {
|
|
305
|
+
const stream = new TemporalStream({
|
|
306
|
+
sliceSize: options.sliceSize || 1024,
|
|
307
|
+
baseTimestamp: options.baseTimestamp,
|
|
308
|
+
sliceIntervalNs: options.sliceIntervalNs,
|
|
309
|
+
});
|
|
310
|
+
const slices = stream.encode(message, this.meshPosition);
|
|
311
|
+
this.outboundStreams.set(stream.streamId, stream);
|
|
312
|
+
this.stats.slicesSent += slices.length;
|
|
313
|
+
return {
|
|
314
|
+
streamId: stream.streamId,
|
|
315
|
+
slices: slices.map(s => s.serialize()),
|
|
316
|
+
metadata: {
|
|
317
|
+
totalSlices: stream.totalSlices,
|
|
318
|
+
sliceSize: stream.sliceSize,
|
|
319
|
+
sliceIntervalNs: stream.sliceIntervalNs,
|
|
320
|
+
baseTimestamp: stream.baseTimestamp.toString(),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
initReceive(metadata) {
|
|
326
|
+
const stream = new TemporalStream({
|
|
327
|
+
streamId: metadata.streamId,
|
|
328
|
+
sliceSize: metadata.sliceSize,
|
|
329
|
+
baseTimestamp: BigInt(metadata.baseTimestamp),
|
|
330
|
+
sliceIntervalNs: metadata.sliceIntervalNs,
|
|
331
|
+
});
|
|
332
|
+
stream.totalSlices = metadata.totalSlices;
|
|
333
|
+
this.inboundStreams.set(metadata.streamId, stream);
|
|
334
|
+
this.reconstructor.registerStream(stream);
|
|
335
|
+
return stream.streamId;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
receiveSlice(serializedSlice) {
|
|
339
|
+
try {
|
|
340
|
+
const slice = TemporalSlice.deserialize(serializedSlice);
|
|
341
|
+
const stream = this.inboundStreams.get(slice.streamId);
|
|
342
|
+
if (!stream) return { accepted: false, error: 'Unknown stream' };
|
|
343
|
+
const added = stream.addSlice(slice);
|
|
344
|
+
if (added) this.stats.slicesReceived++;
|
|
345
|
+
const completionPercent = stream.getCompletionPercent();
|
|
346
|
+
const streamComplete = completionPercent === 100;
|
|
347
|
+
if (streamComplete) this.stats.streamsCompleted++;
|
|
348
|
+
return { accepted: added, streamComplete, completionPercent, missing: stream.getMissingSlices() };
|
|
349
|
+
} catch (err) {
|
|
350
|
+
return { accepted: false, error: err.message };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
addTimingProof(streamId, sequenceNumber, proof) {
|
|
355
|
+
this.reconstructor.addTimingProof(streamId, sequenceNumber, proof);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
decode(streamId) {
|
|
359
|
+
this.stats.reconstructionAttempts++;
|
|
360
|
+
const result = this.reconstructor.reconstruct(streamId);
|
|
361
|
+
if (result.success) this.stats.successfulReconstructions++;
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getStreamStatus(streamId) {
|
|
366
|
+
const stream = this.inboundStreams.get(streamId);
|
|
367
|
+
if (!stream) return null;
|
|
368
|
+
return {
|
|
369
|
+
streamId,
|
|
370
|
+
totalSlices: stream.totalSlices,
|
|
371
|
+
receivedSlices: stream.slices.size,
|
|
372
|
+
completionPercent: stream.getCompletionPercent(),
|
|
373
|
+
missing: stream.getMissingSlices(),
|
|
374
|
+
canReconstruct: stream.canReconstruct(),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getStats() {
|
|
379
|
+
return { ...this.stats };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export { TME_CONFIG, TemporalSlice, TemporalStream, TemporalReconstructor, TemporalMeshEncoder };
|
package/package.json
CHANGED
package/test-tme.mjs
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESH™ Temporal Mesh Encoding (TME) Test Suite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
import {
|
|
7
|
+
TME_CONFIG,
|
|
8
|
+
TemporalSlice,
|
|
9
|
+
TemporalStream,
|
|
10
|
+
TemporalReconstructor,
|
|
11
|
+
TemporalMeshEncoder,
|
|
12
|
+
} from './mesh/temporal-encoder.js';
|
|
13
|
+
|
|
14
|
+
// Test utilities
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
|
|
18
|
+
function test(name, fn) {
|
|
19
|
+
try {
|
|
20
|
+
fn();
|
|
21
|
+
console.log('✅ ' + name);
|
|
22
|
+
passed++;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.log('❌ ' + name + ': ' + err.message);
|
|
25
|
+
failed++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function section(name) {
|
|
30
|
+
console.log('\n─── ' + name + ' ───\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('╔══════════════════════════════════════════════════════╗');
|
|
34
|
+
console.log('║ TME (TEMPORAL MESH ENCODING) TEST SUITE ║');
|
|
35
|
+
console.log('╚══════════════════════════════════════════════════════╝');
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
section('TemporalSlice Tests');
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
test('TemporalSlice creates with valid temporal hash', () => {
|
|
42
|
+
const slice = new TemporalSlice({
|
|
43
|
+
data: Buffer.from('Hello TME'),
|
|
44
|
+
timestamp: BigInt(Date.now() * 1_000_000),
|
|
45
|
+
sequenceNumber: 0,
|
|
46
|
+
streamId: 'test-stream-123',
|
|
47
|
+
meshPosition: [1.0, 2.0, 3.0],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert(slice.temporalHash.length === 32, 'Temporal hash should be 32 bytes');
|
|
51
|
+
assert(slice.verify(), 'Slice should verify');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('TemporalSlice serializes and deserializes correctly', () => {
|
|
55
|
+
const original = new TemporalSlice({
|
|
56
|
+
data: Buffer.from('Test data for serialization'),
|
|
57
|
+
timestamp: BigInt(1234567890000000000n),
|
|
58
|
+
sequenceNumber: 5,
|
|
59
|
+
streamId: 'serialize-test',
|
|
60
|
+
meshPosition: [10.5, 20.5, 30.5],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const serialized = original.serialize();
|
|
64
|
+
const deserialized = TemporalSlice.deserialize(serialized);
|
|
65
|
+
|
|
66
|
+
assert(deserialized.data.equals(original.data), 'Data should match');
|
|
67
|
+
assert(deserialized.timestamp === original.timestamp, 'Timestamp should match');
|
|
68
|
+
assert(deserialized.sequenceNumber === original.sequenceNumber, 'Sequence should match');
|
|
69
|
+
assert(deserialized.streamId === original.streamId, 'StreamId should match');
|
|
70
|
+
assert(deserialized.temporalHash.equals(original.temporalHash), 'Temporal hash should match');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('TemporalSlice rejects tampered data', () => {
|
|
74
|
+
const slice = new TemporalSlice({
|
|
75
|
+
data: Buffer.from('Original data'),
|
|
76
|
+
timestamp: BigInt(Date.now() * 1_000_000),
|
|
77
|
+
sequenceNumber: 0,
|
|
78
|
+
streamId: 'tamper-test',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const serialized = slice.serialize();
|
|
82
|
+
serialized.data = Buffer.from('Tampered data').toString('base64');
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
TemporalSlice.deserialize(serialized);
|
|
86
|
+
assert(false, 'Should have thrown');
|
|
87
|
+
} catch (err) {
|
|
88
|
+
assert(err.message.includes('mismatch'), 'Should detect tampering');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('TemporalSlice chains with prevTemporalHash', () => {
|
|
93
|
+
const slice1 = new TemporalSlice({
|
|
94
|
+
data: Buffer.from('First slice'),
|
|
95
|
+
timestamp: BigInt(1000000000n),
|
|
96
|
+
sequenceNumber: 0,
|
|
97
|
+
streamId: 'chain-test',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const slice2 = new TemporalSlice({
|
|
101
|
+
data: Buffer.from('Second slice'),
|
|
102
|
+
timestamp: BigInt(1050000000n),
|
|
103
|
+
sequenceNumber: 1,
|
|
104
|
+
streamId: 'chain-test',
|
|
105
|
+
prevTemporalHash: slice1.temporalHash,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert(!slice2.prevTemporalHash.equals(Buffer.alloc(32)), 'Should have prev hash');
|
|
109
|
+
assert(slice2.prevTemporalHash.equals(slice1.temporalHash), 'Prev hash should match slice1');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
section('TemporalStream Tests');
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
test('TemporalStream encodes message into slices', () => {
|
|
117
|
+
const stream = new TemporalStream({ sliceSize: 10 });
|
|
118
|
+
const message = 'Hello, this is a test message for TME encoding!';
|
|
119
|
+
const slices = stream.encode(message);
|
|
120
|
+
|
|
121
|
+
assert(slices.length === Math.ceil(message.length / 10), 'Should create correct number of slices');
|
|
122
|
+
assert(stream.totalSlices === slices.length, 'totalSlices should match');
|
|
123
|
+
assert(stream.isComplete, 'Stream should be marked complete');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('TemporalStream maintains temporal chain integrity', () => {
|
|
127
|
+
const stream = new TemporalStream({ sliceSize: 5 });
|
|
128
|
+
const slices = stream.encode('1234567890ABCDEF');
|
|
129
|
+
|
|
130
|
+
for (let i = 1; i < slices.length; i++) {
|
|
131
|
+
const prev = slices[i - 1];
|
|
132
|
+
const curr = slices[i];
|
|
133
|
+
assert(curr.prevTemporalHash.equals(prev.temporalHash), 'Chain should be linked at slice ' + i);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('TemporalStream rejects message exceeding max slices', () => {
|
|
138
|
+
const stream = new TemporalStream({ sliceSize: 1 });
|
|
139
|
+
const hugeMessage = 'x'.repeat(TME_CONFIG.maxSlicesPerStream + 1);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
stream.encode(hugeMessage);
|
|
143
|
+
assert(false, 'Should have thrown');
|
|
144
|
+
} catch (err) {
|
|
145
|
+
assert(err.message.includes('too large'), 'Should reject oversized message');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('TemporalStream addSlice validates slice integrity', () => {
|
|
150
|
+
const stream = new TemporalStream({ sliceSize: 10 });
|
|
151
|
+
const slices = stream.encode('Test message');
|
|
152
|
+
|
|
153
|
+
// Create a new stream to receive
|
|
154
|
+
const receiverStream = new TemporalStream({
|
|
155
|
+
streamId: stream.streamId,
|
|
156
|
+
sliceSize: 10,
|
|
157
|
+
});
|
|
158
|
+
receiverStream.totalSlices = stream.totalSlices;
|
|
159
|
+
|
|
160
|
+
// Should accept valid slice
|
|
161
|
+
const added = receiverStream.addSlice(slices[0]);
|
|
162
|
+
assert(added, 'Should accept valid slice');
|
|
163
|
+
|
|
164
|
+
// Should reject slice from different stream
|
|
165
|
+
const wrongSlice = new TemporalSlice({
|
|
166
|
+
data: Buffer.from('Wrong'),
|
|
167
|
+
timestamp: BigInt(Date.now() * 1_000_000),
|
|
168
|
+
sequenceNumber: 1,
|
|
169
|
+
streamId: 'wrong-stream',
|
|
170
|
+
});
|
|
171
|
+
const rejected = receiverStream.addSlice(wrongSlice);
|
|
172
|
+
assert(!rejected, 'Should reject slice from wrong stream');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('TemporalStream tracks completion percentage', () => {
|
|
176
|
+
const stream = new TemporalStream({ sliceSize: 5 });
|
|
177
|
+
const slices = stream.encode('12345678901234567890'); // 20 chars = 4 slices
|
|
178
|
+
|
|
179
|
+
const receiverStream = new TemporalStream({
|
|
180
|
+
streamId: stream.streamId,
|
|
181
|
+
sliceSize: 5,
|
|
182
|
+
});
|
|
183
|
+
receiverStream.totalSlices = 4;
|
|
184
|
+
|
|
185
|
+
assert(receiverStream.getCompletionPercent() === 0, 'Should start at 0%');
|
|
186
|
+
|
|
187
|
+
receiverStream.addSlice(slices[0]);
|
|
188
|
+
assert(receiverStream.getCompletionPercent() === 25, 'Should be 25% after 1 slice');
|
|
189
|
+
|
|
190
|
+
receiverStream.addSlice(slices[1]);
|
|
191
|
+
assert(receiverStream.getCompletionPercent() === 50, 'Should be 50% after 2 slices');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('TemporalStream detects missing slices', () => {
|
|
195
|
+
const stream = new TemporalStream({ sliceSize: 5 });
|
|
196
|
+
const slices = stream.encode('12345678901234567890');
|
|
197
|
+
|
|
198
|
+
const receiverStream = new TemporalStream({
|
|
199
|
+
streamId: stream.streamId,
|
|
200
|
+
sliceSize: 5,
|
|
201
|
+
});
|
|
202
|
+
receiverStream.totalSlices = 4;
|
|
203
|
+
|
|
204
|
+
receiverStream.addSlice(slices[0]);
|
|
205
|
+
receiverStream.addSlice(slices[3]); // Skip 1 and 2
|
|
206
|
+
|
|
207
|
+
const missing = receiverStream.getMissingSlices();
|
|
208
|
+
assert(missing.length === 2, 'Should have 2 missing slices');
|
|
209
|
+
assert(missing.includes(1), 'Should be missing slice 1');
|
|
210
|
+
assert(missing.includes(2), 'Should be missing slice 2');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
section('TemporalReconstructor Tests');
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
test('TemporalReconstructor assembles complete stream', () => {
|
|
218
|
+
const stream = new TemporalStream({ sliceSize: 10 });
|
|
219
|
+
const message = 'Hello TME World!';
|
|
220
|
+
const slices = stream.encode(message);
|
|
221
|
+
|
|
222
|
+
const reconstructor = new TemporalReconstructor();
|
|
223
|
+
reconstructor.registerStream(stream);
|
|
224
|
+
|
|
225
|
+
const result = reconstructor.reconstruct(stream.streamId);
|
|
226
|
+
assert(result.success, 'Should reconstruct successfully');
|
|
227
|
+
assert(result.data.toString() === message, 'Data should match original');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('TemporalReconstructor reports insufficient slices', () => {
|
|
231
|
+
const stream = new TemporalStream({ sliceSize: 5 });
|
|
232
|
+
stream.encode('12345678901234567890'); // 4 slices
|
|
233
|
+
|
|
234
|
+
// Create receiver with only 1 slice (25% < 60% threshold)
|
|
235
|
+
const receiverStream = new TemporalStream({
|
|
236
|
+
streamId: stream.streamId,
|
|
237
|
+
sliceSize: 5,
|
|
238
|
+
});
|
|
239
|
+
receiverStream.totalSlices = 4;
|
|
240
|
+
receiverStream.slices.set(0, stream.slices.get(0));
|
|
241
|
+
|
|
242
|
+
const reconstructor = new TemporalReconstructor();
|
|
243
|
+
reconstructor.registerStream(receiverStream);
|
|
244
|
+
|
|
245
|
+
const result = reconstructor.reconstruct(receiverStream.streamId);
|
|
246
|
+
assert(!result.success, 'Should fail with insufficient slices');
|
|
247
|
+
assert(result.error.includes('Insufficient'), 'Should report insufficient');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('TemporalReconstructor verifies missing slice via timing proofs', () => {
|
|
251
|
+
const reconstructor = new TemporalReconstructor();
|
|
252
|
+
const streamId = 'proof-test';
|
|
253
|
+
|
|
254
|
+
reconstructor.timingProofs.set(streamId, new Map());
|
|
255
|
+
|
|
256
|
+
// Add 3 timing proofs that agree on the hash
|
|
257
|
+
const consensusHash = '0'.repeat(64);
|
|
258
|
+
reconstructor.addTimingProof(streamId, 5, {
|
|
259
|
+
nodeId: 'node-a',
|
|
260
|
+
timestamp: Date.now() * 1_000_000,
|
|
261
|
+
temporalHash: consensusHash,
|
|
262
|
+
signature: 'sig-a',
|
|
263
|
+
});
|
|
264
|
+
reconstructor.addTimingProof(streamId, 5, {
|
|
265
|
+
nodeId: 'node-b',
|
|
266
|
+
timestamp: Date.now() * 1_000_000,
|
|
267
|
+
temporalHash: consensusHash,
|
|
268
|
+
signature: 'sig-b',
|
|
269
|
+
});
|
|
270
|
+
reconstructor.addTimingProof(streamId, 5, {
|
|
271
|
+
nodeId: 'node-c',
|
|
272
|
+
timestamp: Date.now() * 1_000_000,
|
|
273
|
+
temporalHash: 'f'.repeat(64), // Dissenting
|
|
274
|
+
signature: 'sig-c',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const verification = reconstructor.verifyMissingSlice(streamId, 5);
|
|
278
|
+
assert(verification !== null, 'Should have verification');
|
|
279
|
+
assert(verification.verified, 'Should be verified');
|
|
280
|
+
assert(verification.consensusHash === consensusHash, 'Should have consensus hash');
|
|
281
|
+
assert(verification.proofCount === 2, 'Should have 2 agreeing proofs');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// =============================================================================
|
|
285
|
+
section('TemporalMeshEncoder Tests (End-to-End)');
|
|
286
|
+
// =============================================================================
|
|
287
|
+
|
|
288
|
+
test('TemporalMeshEncoder full encode/decode cycle', () => {
|
|
289
|
+
const sender = new TemporalMeshEncoder({ meshPosition: [1, 2, 3] });
|
|
290
|
+
const receiver = new TemporalMeshEncoder({ meshPosition: [4, 5, 6] });
|
|
291
|
+
|
|
292
|
+
const message = 'This is a complete TME transmission test!';
|
|
293
|
+
const encoded = sender.encode(message, { sliceSize: 10 });
|
|
294
|
+
|
|
295
|
+
// Receiver initializes stream
|
|
296
|
+
receiver.initReceive({
|
|
297
|
+
streamId: encoded.streamId,
|
|
298
|
+
...encoded.metadata,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Receive all slices
|
|
302
|
+
for (const slice of encoded.slices) {
|
|
303
|
+
receiver.receiveSlice(slice);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Decode
|
|
307
|
+
const decoded = receiver.decode(encoded.streamId);
|
|
308
|
+
assert(decoded.success, 'Should decode successfully');
|
|
309
|
+
assert(decoded.data.toString() === message, 'Message should match');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('TemporalMeshEncoder tracks statistics', () => {
|
|
313
|
+
const encoder = new TemporalMeshEncoder();
|
|
314
|
+
|
|
315
|
+
encoder.encode('Message 1', { sliceSize: 5 });
|
|
316
|
+
encoder.encode('Message 2', { sliceSize: 5 });
|
|
317
|
+
|
|
318
|
+
const stats = encoder.getStats();
|
|
319
|
+
assert(stats.slicesSent > 0, 'Should track slices sent');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('TemporalMeshEncoder handles partial reception', () => {
|
|
323
|
+
const sender = new TemporalMeshEncoder();
|
|
324
|
+
const receiver = new TemporalMeshEncoder();
|
|
325
|
+
|
|
326
|
+
const message = '12345678901234567890'; // 20 chars
|
|
327
|
+
const encoded = sender.encode(message, { sliceSize: 5 }); // 4 slices
|
|
328
|
+
|
|
329
|
+
receiver.initReceive({
|
|
330
|
+
streamId: encoded.streamId,
|
|
331
|
+
...encoded.metadata,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Only receive slices 0 and 3 (skip 1 and 2)
|
|
335
|
+
receiver.receiveSlice(encoded.slices[0]);
|
|
336
|
+
receiver.receiveSlice(encoded.slices[3]);
|
|
337
|
+
|
|
338
|
+
const status = receiver.getStreamStatus(encoded.streamId);
|
|
339
|
+
assert(status.receivedSlices === 2, 'Should have 2 slices');
|
|
340
|
+
assert(status.completionPercent === 50, 'Should be 50% complete');
|
|
341
|
+
assert(status.missing.length === 2, 'Should have 2 missing');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('TemporalMeshEncoder rejects unknown stream slice', () => {
|
|
345
|
+
const receiver = new TemporalMeshEncoder();
|
|
346
|
+
|
|
347
|
+
const fakeSlice = new TemporalSlice({
|
|
348
|
+
data: Buffer.from('Fake'),
|
|
349
|
+
timestamp: BigInt(Date.now() * 1_000_000),
|
|
350
|
+
sequenceNumber: 0,
|
|
351
|
+
streamId: 'unknown-stream',
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const result = receiver.receiveSlice(fakeSlice.serialize());
|
|
355
|
+
assert(!result.accepted, 'Should reject unknown stream');
|
|
356
|
+
assert(result.error === 'Unknown stream', 'Should report unknown stream');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('TemporalMeshEncoder detects stream completion', () => {
|
|
360
|
+
const sender = new TemporalMeshEncoder();
|
|
361
|
+
const receiver = new TemporalMeshEncoder();
|
|
362
|
+
|
|
363
|
+
const encoded = sender.encode('Short', { sliceSize: 10 }); // 1 slice
|
|
364
|
+
|
|
365
|
+
receiver.initReceive({
|
|
366
|
+
streamId: encoded.streamId,
|
|
367
|
+
...encoded.metadata,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = receiver.receiveSlice(encoded.slices[0]);
|
|
371
|
+
assert(result.streamComplete, 'Should detect completion');
|
|
372
|
+
assert(result.completionPercent === 100, 'Should be 100%');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// Summary
|
|
377
|
+
// =============================================================================
|
|
378
|
+
|
|
379
|
+
console.log('\n╔══════════════════════════════════════════════════════╗');
|
|
380
|
+
console.log('║ RESULTS: ' + passed + ' passed, ' + failed + ' failed ║');
|
|
381
|
+
console.log('╚══════════════════════════════════════════════════════╝');
|
|
382
|
+
|
|
383
|
+
process.exit(failed > 0 ? 1 : 0);
|