@blockcast/fec-worker 0.1.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.
Files changed (53) hide show
  1. package/README.md +213 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/fec-worker-client.d.ts +80 -0
  4. package/dist/fec-worker-client.d.ts.map +1 -0
  5. package/dist/fec-worker-client.js +270 -0
  6. package/dist/fec-worker-client.js.map +1 -0
  7. package/dist/fec-worker-types.d.ts +252 -0
  8. package/dist/fec-worker-types.d.ts.map +1 -0
  9. package/dist/fec-worker-types.js +11 -0
  10. package/dist/fec-worker-types.js.map +1 -0
  11. package/dist/fec-worker.d.ts +14 -0
  12. package/dist/fec-worker.d.ts.map +1 -0
  13. package/dist/fec-worker.js +565 -0
  14. package/dist/fec-worker.js.map +7 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +8 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/shred-fec-worker.d.ts +20 -0
  20. package/dist/shred-fec-worker.d.ts.map +1 -0
  21. package/dist/shred-fec-worker.js +349 -0
  22. package/dist/shred-fec-worker.js.map +7 -0
  23. package/dist/shred-worker-client.d.ts +104 -0
  24. package/dist/shred-worker-client.d.ts.map +1 -0
  25. package/dist/shred-worker-client.js +181 -0
  26. package/dist/shred-worker-client.js.map +1 -0
  27. package/dist/shred-worker-types.d.ts +179 -0
  28. package/dist/shred-worker-types.d.ts.map +1 -0
  29. package/dist/shred-worker-types.js +47 -0
  30. package/dist/shred-worker-types.js.map +1 -0
  31. package/dist/transfer.d.ts +9 -0
  32. package/dist/transfer.d.ts.map +1 -0
  33. package/dist/transfer.js +13 -0
  34. package/dist/transfer.js.map +1 -0
  35. package/package.json +54 -0
  36. package/src/__tests__/fec-worker-alta.test.ts +231 -0
  37. package/src/__tests__/fec-worker-cleanup-integration.test.ts +250 -0
  38. package/src/__tests__/fec-worker-pending-queue.test.ts +315 -0
  39. package/src/__tests__/fec-worker-repair-size.test.ts +1029 -0
  40. package/src/__tests__/fec-worker-wasm-contract.test.ts +84 -0
  41. package/src/__tests__/shred-fec-worker.test.ts +448 -0
  42. package/src/fec-worker-client.test.ts +530 -0
  43. package/src/fec-worker-client.ts +309 -0
  44. package/src/fec-worker-types.test.ts +400 -0
  45. package/src/fec-worker-types.ts +268 -0
  46. package/src/fec-worker.ts +1048 -0
  47. package/src/index.ts +32 -0
  48. package/src/shred-fec-worker.ts +558 -0
  49. package/src/shred-worker-client.test.ts +182 -0
  50. package/src/shred-worker-client.ts +222 -0
  51. package/src/shred-worker-types.ts +194 -0
  52. package/src/transfer.test.ts +24 -0
  53. package/src/transfer.ts +12 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Unit tests for the deferred ALTA verification queue (08-07 Path 3).
3
+ *
4
+ * When ALTA's MAC-chain anchor is unresolvable at packet arrival (late joiner
5
+ * before first signed packet, or packet that references a not-yet-recovered
6
+ * predecessor), the packet is queued rather than failed. After every
7
+ * successful verify seeds the verifier's ring buffer, the queue drains and
8
+ * retries previously-pending entries.
9
+ *
10
+ * These tests mirror `fec-worker.ts`'s runDetachedVerify / enqueuePendingVerify
11
+ * / drainPendingVerify in a test-harness-friendly form. The real module is a
12
+ * Worker entry file (self, onmessage) and can't be imported; the inline
13
+ * simulation matches the pattern in `fec-worker-alta.test.ts`.
14
+ */
15
+
16
+ import { describe, it, expect } from "vitest";
17
+
18
+ // ── Mock verifier ─────────────────────────────────────────────────────
19
+
20
+ type Outcome = "valid" | "failed" | "pending";
21
+
22
+ /** Controllable verifier whose result depends on whether a seq is "in ring buffer". */
23
+ class MockVerifier {
24
+ ringBuffer = new Set<number>(); // seqs with raw bytes available
25
+ tamperSeqs = new Set<number>(); // seqs that should hard-fail (tamper)
26
+
27
+ verifyDetached(
28
+ auth: Uint8Array,
29
+ _payload: Uint8Array,
30
+ ): { valid: boolean; error?: string } {
31
+ const seq = seqFromAuth(auth);
32
+ if (this.tamperSeqs.has(seq)) {
33
+ return { valid: false, error: "MAC mismatch on 1 slot(s)" };
34
+ }
35
+ // Signer references seq - 2 (offset=1) as primary MAC ref. If that's in
36
+ // the ring buffer, verify passes. Else pending.
37
+ const macRef = seq - 2;
38
+ if (macRef <= 0 || this.ringBuffer.has(macRef)) {
39
+ this.ringBuffer.add(seq);
40
+ return { valid: true };
41
+ }
42
+ return {
43
+ valid: false,
44
+ error: "No trust anchor (signature absent, no MAC refs in buffer)",
45
+ };
46
+ }
47
+ }
48
+
49
+ function seqFromAuth(auth: Uint8Array): number {
50
+ return (
51
+ ((auth[0]! << 24) | (auth[1]! << 16) | (auth[2]! << 8) | auth[3]!) >>> 0
52
+ );
53
+ }
54
+
55
+ function mkAuth(seq: number): Uint8Array {
56
+ const auth = new Uint8Array(40);
57
+ auth[0] = (seq >>> 24) & 0xff;
58
+ auth[1] = (seq >>> 16) & 0xff;
59
+ auth[2] = (seq >>> 8) & 0xff;
60
+ auth[3] = seq & 0xff;
61
+ return auth;
62
+ }
63
+
64
+ // ── Inline simulation of fec-worker queue logic ──────────────────────
65
+
66
+ interface PendingVerify {
67
+ seq: number;
68
+ auth: Uint8Array;
69
+ payload: Uint8Array;
70
+ arrivalTs: number;
71
+ }
72
+
73
+ interface Harness {
74
+ verifier: MockVerifier;
75
+ queue: Map<number, PendingVerify>;
76
+ cap: number;
77
+ ttlMs: number;
78
+ now: number;
79
+ altaVerified: number;
80
+ altaFailed: number;
81
+ altaPending: number;
82
+ }
83
+
84
+ function runVerify(h: Harness, auth: Uint8Array, payload: Uint8Array): Outcome {
85
+ const r = h.verifier.verifyDetached(auth, payload);
86
+ if (r.valid) return "valid";
87
+ const err = r.error ?? "";
88
+ if (err.startsWith("No trust anchor")) return "pending";
89
+ return "failed";
90
+ }
91
+
92
+ function enqueue(h: Harness, auth: Uint8Array, payload: Uint8Array): void {
93
+ const seq = seqFromAuth(auth);
94
+ if (h.queue.has(seq)) return;
95
+
96
+ // Mirror fec-worker.ts: when the queue is at cap, sweep TTL'd entries
97
+ // first, then reject the NEW entry if still full. Never evict legitimate
98
+ // pending entries that a flood of "No trust anchor" packets could
99
+ // otherwise displace.
100
+ if (h.queue.size >= h.cap) {
101
+ for (const [staleSeq, entry] of h.queue) {
102
+ if (h.now - entry.arrivalTs > h.ttlMs) {
103
+ h.queue.delete(staleSeq);
104
+ h.altaPending++;
105
+ }
106
+ }
107
+ if (h.queue.size >= h.cap) {
108
+ h.altaPending++; // cap-rejection counts the rejected incoming
109
+ return;
110
+ }
111
+ }
112
+
113
+ h.queue.set(seq, { seq, auth, payload, arrivalTs: h.now });
114
+ }
115
+
116
+ function drain(h: Harness): void {
117
+ const keys = Array.from(h.queue.keys());
118
+ for (const seq of keys) {
119
+ const entry = h.queue.get(seq);
120
+ if (!entry) continue;
121
+ if (h.now - entry.arrivalTs > h.ttlMs) {
122
+ h.queue.delete(seq);
123
+ h.altaPending++;
124
+ continue;
125
+ }
126
+ const outcome = runVerify(h, entry.auth, entry.payload);
127
+ if (outcome === "valid") {
128
+ h.queue.delete(seq);
129
+ h.altaVerified++;
130
+ } else if (outcome === "failed") {
131
+ h.queue.delete(seq);
132
+ h.altaFailed++;
133
+ }
134
+ }
135
+ }
136
+
137
+ function processDirectSource(
138
+ h: Harness,
139
+ auth: Uint8Array,
140
+ payload: Uint8Array,
141
+ ): void {
142
+ const outcome = runVerify(h, auth, payload);
143
+ if (outcome === "valid") {
144
+ h.altaVerified++;
145
+ drain(h);
146
+ } else if (outcome === "failed") {
147
+ h.altaFailed++;
148
+ } else {
149
+ enqueue(h, auth, payload);
150
+ }
151
+ }
152
+
153
+ function makeHarness(opts?: { cap?: number; ttlMs?: number }): Harness {
154
+ return {
155
+ verifier: new MockVerifier(),
156
+ queue: new Map(),
157
+ cap: opts?.cap ?? 8,
158
+ ttlMs: opts?.ttlMs ?? 10_000,
159
+ now: 0,
160
+ altaVerified: 0,
161
+ altaFailed: 0,
162
+ altaPending: 0,
163
+ };
164
+ }
165
+
166
+ // ── Tests ─────────────────────────────────────────────────────────────
167
+
168
+ describe("deferred ALTA verification queue", () => {
169
+ it("valid direct verify increments altaVerified, queue stays empty", () => {
170
+ const h = makeHarness();
171
+ // Seed ring buffer with seq=1 so seq=3's MAC ref (seq=1) resolves.
172
+ h.verifier.ringBuffer.add(1);
173
+ processDirectSource(h, mkAuth(3), new Uint8Array(1400));
174
+ expect(h.altaVerified).toBe(1);
175
+ expect(h.altaFailed).toBe(0);
176
+ expect(h.queue.size).toBe(0);
177
+ });
178
+
179
+ it("pending direct verify enqueues, counters untouched", () => {
180
+ const h = makeHarness();
181
+ // No seed — seq=10's MAC ref (seq=8) not in ring buffer.
182
+ processDirectSource(h, mkAuth(10), new Uint8Array(1400));
183
+ expect(h.queue.size).toBe(1);
184
+ expect(h.queue.has(10)).toBe(true);
185
+ expect(h.altaVerified).toBe(0);
186
+ expect(h.altaFailed).toBe(0);
187
+ expect(h.altaPending).toBe(0);
188
+ });
189
+
190
+ it("tamper (MAC mismatch) fails immediately — not queued", () => {
191
+ const h = makeHarness();
192
+ h.verifier.ringBuffer.add(8);
193
+ h.verifier.tamperSeqs.add(10);
194
+ processDirectSource(h, mkAuth(10), new Uint8Array(1400));
195
+ expect(h.altaFailed).toBe(1);
196
+ expect(h.queue.size).toBe(0);
197
+ });
198
+
199
+ it("seed arrives → drain retries queued entry → altaVerified", () => {
200
+ const h = makeHarness();
201
+ // First: seq=10 enqueued (no anchor).
202
+ processDirectSource(h, mkAuth(10), new Uint8Array(1400));
203
+ expect(h.queue.size).toBe(1);
204
+ // Now seed: seq=8's ref (seq=6) isn't in buffer, so seq=8 itself would
205
+ // also be pending. Bootstrap via seq=2 (early-warmup path — no ref).
206
+ processDirectSource(h, mkAuth(2), new Uint8Array(1400));
207
+ expect(h.altaVerified).toBe(1); // seq=2 verified
208
+ // seq=2 is now in buffer. seq=8 would need seq=6 (not buffered),
209
+ // seq=10 needs seq=8 (not buffered yet either). Queue still has seq=10.
210
+ // Verify needs seq=4, which needs seq=2. Let's feed seq=4 and seq=8.
211
+ processDirectSource(h, mkAuth(4), new Uint8Array(1400));
212
+ processDirectSource(h, mkAuth(6), new Uint8Array(1400));
213
+ processDirectSource(h, mkAuth(8), new Uint8Array(1400));
214
+ // Now seq=8 is in the ring buffer. Next drain should resolve seq=10.
215
+ // Re-feed seq=12 to trigger drain via its successful verify.
216
+ processDirectSource(h, mkAuth(12), new Uint8Array(1400));
217
+ // After that cascade, queue should be empty and seq=10 counted verified.
218
+ expect(h.queue.size).toBe(0);
219
+ expect(h.altaVerified).toBeGreaterThanOrEqual(5);
220
+ });
221
+
222
+ it("TTL expiry → evicts as altaPending on drain", () => {
223
+ const h = makeHarness({ ttlMs: 1000 });
224
+ processDirectSource(h, mkAuth(100), new Uint8Array(1400));
225
+ expect(h.queue.size).toBe(1);
226
+ // Advance time past TTL, then trigger drain via a valid seed.
227
+ h.now = 2000;
228
+ processDirectSource(h, mkAuth(2), new Uint8Array(1400)); // bootstraps
229
+ // drain ran; seq=100 expired, counted pending.
230
+ expect(h.queue.size).toBe(0);
231
+ expect(h.altaPending).toBe(1);
232
+ expect(h.altaFailed).toBe(0);
233
+ });
234
+
235
+ it("cap rejection → NEW entry dropped as altaPending (08-08 flood resistance)", () => {
236
+ const h = makeHarness({ cap: 3 });
237
+ // Enqueue 4 pending entries at the same tick (no TTL sweep eligible).
238
+ // Pre-08-08: the 4th would evict the 1st. Post-fix: the 1st is
239
+ // retained and the 4th is rejected.
240
+ for (const seq of [100, 101, 102, 103]) {
241
+ processDirectSource(h, mkAuth(seq), new Uint8Array(1400));
242
+ }
243
+ expect(h.queue.size).toBe(3);
244
+ expect(h.queue.has(100)).toBe(true); // oldest retained
245
+ expect(h.queue.has(101)).toBe(true);
246
+ expect(h.queue.has(102)).toBe(true);
247
+ expect(h.queue.has(103)).toBe(false); // newest rejected
248
+ expect(h.altaPending).toBe(1); // rejected counts as pending
249
+ });
250
+
251
+ it("attacker flood does NOT evict legitimate pending entries", () => {
252
+ // Scenario: a legit packet at seq=500 is queued awaiting its anchor.
253
+ // An attacker then emits a burst of forged "No trust anchor" packets
254
+ // (e.g. seq 600..650). Pre-08-08 those would evict seq=500. Post-fix
255
+ // seq=500 stays put and the attacker's packets get rejected.
256
+ const h = makeHarness({ cap: 10 });
257
+
258
+ // 1. Legit pending entry lands first.
259
+ processDirectSource(h, mkAuth(500), new Uint8Array(1400));
260
+ expect(h.queue.has(500)).toBe(true);
261
+
262
+ // 2. Attacker flood: 100 packets, every one returning "pending"
263
+ // (no anchor for any of their seqs). Only the first 9 should
264
+ // land (fill queue to cap=10 with the 1 legit entry). The rest
265
+ // must be rejected without touching seq=500.
266
+ for (let seq = 600; seq < 700; seq++) {
267
+ processDirectSource(h, mkAuth(seq), new Uint8Array(1400));
268
+ }
269
+
270
+ // Legit entry survived.
271
+ expect(h.queue.has(500)).toBe(true);
272
+ // Queue filled to cap.
273
+ expect(h.queue.size).toBe(10);
274
+ // 91 attacker packets were rejected (100 attempted − 9 admitted).
275
+ expect(h.altaPending).toBe(91);
276
+ // No tamper-failure spurious counting.
277
+ expect(h.altaFailed).toBe(0);
278
+ });
279
+
280
+ it("stale entries are swept on cap pressure, making room for new arrivals", () => {
281
+ const h = makeHarness({ cap: 3, ttlMs: 1000 });
282
+ // Fill the queue at t=0.
283
+ for (const seq of [100, 101, 102]) {
284
+ processDirectSource(h, mkAuth(seq), new Uint8Array(1400));
285
+ }
286
+ expect(h.queue.size).toBe(3);
287
+
288
+ // Advance past TTL, then attempt a new enqueue.
289
+ h.now = 2000;
290
+ processDirectSource(h, mkAuth(200), new Uint8Array(1400));
291
+ // The three stale entries were swept by the pre-admission sweep, the
292
+ // new entry (seq=200) got in.
293
+ expect(h.queue.size).toBe(1);
294
+ expect(h.queue.has(200)).toBe(true);
295
+ expect(h.queue.has(100)).toBe(false);
296
+ expect(h.altaPending).toBe(3); // three TTL-swept
297
+ });
298
+
299
+ it("dedup: re-feeding same seq doesn't duplicate queue entry", () => {
300
+ const h = makeHarness();
301
+ processDirectSource(h, mkAuth(50), new Uint8Array(1400));
302
+ processDirectSource(h, mkAuth(50), new Uint8Array(1400));
303
+ expect(h.queue.size).toBe(1);
304
+ });
305
+
306
+ it("drain keeps entries that are still pending (ref still not in buffer)", () => {
307
+ const h = makeHarness();
308
+ // Enqueue seq=50 (needs seq=48, not in buffer).
309
+ processDirectSource(h, mkAuth(50), new Uint8Array(1400));
310
+ // Valid seed seq=2 triggers drain but doesn't resolve seq=50.
311
+ processDirectSource(h, mkAuth(2), new Uint8Array(1400));
312
+ expect(h.queue.size).toBe(1); // seq=50 still pending
313
+ expect(h.altaPending).toBe(0);
314
+ });
315
+ });