@bcts/spqr 1.0.0-alpha.21
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/LICENSE +661 -0
- package/README.md +11 -0
- package/dist/index.cjs +4321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +4318 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +4312 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/authenticator.ts +163 -0
- package/src/chain.ts +522 -0
- package/src/constants.ts +90 -0
- package/src/encoding/gf.ts +190 -0
- package/src/encoding/index.ts +15 -0
- package/src/encoding/polynomial.ts +657 -0
- package/src/error.ts +75 -0
- package/src/incremental-mlkem768.ts +546 -0
- package/src/index.ts +415 -0
- package/src/kdf.ts +34 -0
- package/src/proto/index.ts +1376 -0
- package/src/proto/pq-ratchet-types.ts +195 -0
- package/src/types.ts +81 -0
- package/src/util.ts +61 -0
- package/src/v1/chunked/index.ts +60 -0
- package/src/v1/chunked/message.ts +257 -0
- package/src/v1/chunked/send-ct.ts +352 -0
- package/src/v1/chunked/send-ek.ts +285 -0
- package/src/v1/chunked/serialize.ts +278 -0
- package/src/v1/chunked/states.ts +399 -0
- package/src/v1/index.ts +9 -0
- package/src/v1/unchunked/index.ts +20 -0
- package/src/v1/unchunked/send-ct.ts +231 -0
- package/src/v1/unchunked/send-ek.ts +177 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Polynomial erasure coding over GF(2^16).
|
|
6
|
+
* Ported from the Rust SPQR implementation.
|
|
7
|
+
*
|
|
8
|
+
* The encoder splits a message into 16 parallel polynomials and can produce
|
|
9
|
+
* an unlimited number of coded chunks. Any sufficient subset of chunks
|
|
10
|
+
* allows the decoder to reconstruct the original message via Lagrange
|
|
11
|
+
* interpolation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { GF16, parallelMult } from "./gf.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Error type
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export class PolynomialError extends Error {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PolynomialError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** A point on a polynomial in GF(2^16). */
|
|
32
|
+
export interface Pt {
|
|
33
|
+
x: GF16;
|
|
34
|
+
y: GF16;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A coded chunk: an index plus exactly 32 bytes of data. */
|
|
38
|
+
export interface Chunk {
|
|
39
|
+
index: number; // u16
|
|
40
|
+
data: Uint8Array; // exactly 32 bytes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Encoder interface. */
|
|
44
|
+
export interface Encoder {
|
|
45
|
+
nextChunk(): Chunk;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Decoder interface. */
|
|
49
|
+
export interface Decoder {
|
|
50
|
+
addChunk(chunk: Chunk): void;
|
|
51
|
+
decodedMessage(): Uint8Array | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Number of polynomials used for interleaving
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const NUM_POLYS = 16;
|
|
59
|
+
const CHUNK_DATA_SIZE = 32; // 16 GF16 values * 2 bytes each
|
|
60
|
+
const MAX_MESSAGE_LENGTH = (1 << 16) * NUM_POLYS; // 1,048,576 bytes (matches Rust)
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Poly class
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A polynomial over GF(2^16) in coefficient form.
|
|
68
|
+
*
|
|
69
|
+
* Coefficients are stored in little-endian order:
|
|
70
|
+
* coefficients[0] = constant term (x^0)
|
|
71
|
+
* coefficients[1] = linear term (x^1)
|
|
72
|
+
* ...
|
|
73
|
+
*/
|
|
74
|
+
export class Poly {
|
|
75
|
+
coefficients: GF16[];
|
|
76
|
+
|
|
77
|
+
constructor(coefficients: GF16[]) {
|
|
78
|
+
this.coefficients = coefficients;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Create a zero polynomial of a given length. */
|
|
82
|
+
static zeros(len: number): Poly {
|
|
83
|
+
const coeffs: GF16[] = new Array<GF16>(len);
|
|
84
|
+
for (let i = 0; i < len; i++) {
|
|
85
|
+
coeffs[i] = GF16.ZERO;
|
|
86
|
+
}
|
|
87
|
+
return new Poly(coeffs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Number of coefficients. */
|
|
91
|
+
get length(): number {
|
|
92
|
+
return this.coefficients.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// -- Evaluation -----------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Evaluate the polynomial at a given point using a divide-and-conquer
|
|
99
|
+
* approach for computing powers of x, then a dot product.
|
|
100
|
+
*
|
|
101
|
+
* xs[0] = 1, xs[1] = x, xs[i] = xs[floor(i/2)] * xs[floor(i/2) + (i%2)]
|
|
102
|
+
*/
|
|
103
|
+
computeAt(x: GF16): GF16 {
|
|
104
|
+
const n = this.coefficients.length;
|
|
105
|
+
if (n === 0) return GF16.ZERO;
|
|
106
|
+
if (n === 1) return this.coefficients[0];
|
|
107
|
+
|
|
108
|
+
// Build powers of x
|
|
109
|
+
const xs: GF16[] = new Array<GF16>(n);
|
|
110
|
+
xs[0] = GF16.ONE;
|
|
111
|
+
if (n > 1) xs[1] = x;
|
|
112
|
+
|
|
113
|
+
for (let i = 2; i < n; i++) {
|
|
114
|
+
const half = i >>> 1;
|
|
115
|
+
const rem = i & 1;
|
|
116
|
+
xs[i] = xs[half].mul(xs[half + rem]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Dot product: sum(coefficients[i] * xs[i])
|
|
120
|
+
let result = GF16.ZERO;
|
|
121
|
+
for (let i = 0; i < n; i++) {
|
|
122
|
+
result = result.add(this.coefficients[i].mul(xs[i]));
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// -- In-place arithmetic --------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Add another polynomial to this one in-place. */
|
|
130
|
+
addAssign(other: Poly): void {
|
|
131
|
+
// Extend if necessary
|
|
132
|
+
while (this.coefficients.length < other.coefficients.length) {
|
|
133
|
+
this.coefficients.push(GF16.ZERO);
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < other.coefficients.length; i++) {
|
|
136
|
+
this.coefficients[i] = this.coefficients[i].add(other.coefficients[i]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Multiply all coefficients by a scalar in-place. */
|
|
141
|
+
multAssign(m: GF16): void {
|
|
142
|
+
parallelMult(m, this.coefficients);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// -- Serialization --------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** Serialize coefficients as big-endian u16 pairs. */
|
|
148
|
+
serialize(): Uint8Array {
|
|
149
|
+
const out = new Uint8Array(this.coefficients.length * 2);
|
|
150
|
+
for (let i = 0; i < this.coefficients.length; i++) {
|
|
151
|
+
const v = this.coefficients[i].value;
|
|
152
|
+
out[i * 2] = (v >>> 8) & 0xff;
|
|
153
|
+
out[i * 2 + 1] = v & 0xff;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Deserialize from big-endian u16 pairs. */
|
|
159
|
+
static deserialize(data: Uint8Array): Poly {
|
|
160
|
+
if (data.length % 2 !== 0) {
|
|
161
|
+
throw new PolynomialError("Poly data length must be even");
|
|
162
|
+
}
|
|
163
|
+
const n = data.length / 2;
|
|
164
|
+
const coeffs: GF16[] = new Array<GF16>(n);
|
|
165
|
+
for (let i = 0; i < n; i++) {
|
|
166
|
+
coeffs[i] = new GF16((data[i * 2] << 8) | data[i * 2 + 1]);
|
|
167
|
+
}
|
|
168
|
+
return new Poly(coeffs);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// -- Lagrange interpolation -----------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Lagrange interpolation over a set of points.
|
|
175
|
+
*
|
|
176
|
+
* Given N points (x_i, y_i), produces the unique polynomial of degree < N
|
|
177
|
+
* passing through all of them.
|
|
178
|
+
*/
|
|
179
|
+
static lagrangeInterpolate(pts: Pt[]): Poly {
|
|
180
|
+
const n = pts.length;
|
|
181
|
+
if (n === 0) return new Poly([]);
|
|
182
|
+
|
|
183
|
+
// Step 1: Compute the "master" product polynomial
|
|
184
|
+
// P(x) = PRODUCT_{i=0}^{n-1} (x - x_i)
|
|
185
|
+
//
|
|
186
|
+
// In GF(2^n), (x - x_i) = (x + x_i) since subtraction = addition.
|
|
187
|
+
// We represent (x + x_i) as [x_i, 1] (constant, linear).
|
|
188
|
+
let product = new Poly([pts[0].x, GF16.ONE]);
|
|
189
|
+
for (let i = 1; i < n; i++) {
|
|
190
|
+
product = polyMultiply(product, new Poly([pts[i].x, GF16.ONE]));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step 2: For each point, compute the Lagrange basis polynomial
|
|
194
|
+
// L_i(x) = y_i * PRODUCT_{j != i} (x - x_j) / PRODUCT_{j != i} (x_i - x_j)
|
|
195
|
+
//
|
|
196
|
+
// We obtain PRODUCT_{j != i} (x - x_j) by dividing the master product by
|
|
197
|
+
// (x - x_i) using synthetic division.
|
|
198
|
+
const result = Poly.zeros(n);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < n; i++) {
|
|
201
|
+
// Synthetic division of `product` by (x - x_i) = (x + x_i)
|
|
202
|
+
const basis = syntheticDivide(product, pts[i].x);
|
|
203
|
+
|
|
204
|
+
// Compute the denominator: PRODUCT_{j != i} (x_i - x_j)
|
|
205
|
+
// This is just basis.computeAt(x_i) since basis = product / (x - x_i)
|
|
206
|
+
// and product(x_i) = 0, so we evaluate the quotient at x_i.
|
|
207
|
+
const denom = basis.computeAt(pts[i].x);
|
|
208
|
+
|
|
209
|
+
// Scale: L_i(x) = basis(x) * y_i / denom
|
|
210
|
+
const scale = pts[i].y.div(denom);
|
|
211
|
+
const scaled = clonePoly(basis);
|
|
212
|
+
scaled.multAssign(scale);
|
|
213
|
+
|
|
214
|
+
result.addAssign(scaled);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Polynomial helpers (private)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** Multiply two polynomials (convolution over GF(2^16)). */
|
|
226
|
+
function polyMultiply(a: Poly, b: Poly): Poly {
|
|
227
|
+
if (a.length === 0 || b.length === 0) return new Poly([]);
|
|
228
|
+
const result = Poly.zeros(a.length + b.length - 1);
|
|
229
|
+
for (let i = 0; i < a.length; i++) {
|
|
230
|
+
for (let j = 0; j < b.length; j++) {
|
|
231
|
+
result.coefficients[i + j] = result.coefficients[i + j].add(
|
|
232
|
+
a.coefficients[i].mul(b.coefficients[j]),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Synthetic division: divide poly by (x - root) = (x + root) in GF(2^n).
|
|
241
|
+
*
|
|
242
|
+
* Returns the quotient polynomial (degree one less than input).
|
|
243
|
+
*/
|
|
244
|
+
function syntheticDivide(poly: Poly, root: GF16): Poly {
|
|
245
|
+
const n = poly.length;
|
|
246
|
+
if (n <= 1) return new Poly([]);
|
|
247
|
+
|
|
248
|
+
// Work from high degree down
|
|
249
|
+
const quotient: GF16[] = new Array<GF16>(n - 1);
|
|
250
|
+
let carry = GF16.ZERO;
|
|
251
|
+
|
|
252
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
253
|
+
const coeff = poly.coefficients[i].add(carry);
|
|
254
|
+
quotient[i - 1] = coeff;
|
|
255
|
+
// In GF(2^n), multiply by root (same as negated root since -root = root)
|
|
256
|
+
carry = coeff.mul(root);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return new Poly(quotient);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Clone a polynomial (shallow copy of coefficient array). */
|
|
263
|
+
function clonePoly(p: Poly): Poly {
|
|
264
|
+
return new Poly(p.coefficients.slice());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// SortedPtSet - maintains points sorted by x.value, no duplicate x's
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
class SortedPtSet {
|
|
272
|
+
private readonly pts: Pt[] = [];
|
|
273
|
+
|
|
274
|
+
get length(): number {
|
|
275
|
+
return this.pts.length;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Insert a point, maintaining sorted order. Returns false if duplicate x. */
|
|
279
|
+
insert(pt: Pt): boolean {
|
|
280
|
+
const idx = this.findIndex(pt.x.value);
|
|
281
|
+
if (idx < this.pts.length && this.pts[idx].x.value === pt.x.value) {
|
|
282
|
+
return false; // duplicate x
|
|
283
|
+
}
|
|
284
|
+
this.pts.splice(idx, 0, pt);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Binary search for the insertion point of a given x value. */
|
|
289
|
+
private findIndex(xVal: number): number {
|
|
290
|
+
let lo = 0;
|
|
291
|
+
let hi = this.pts.length;
|
|
292
|
+
while (lo < hi) {
|
|
293
|
+
const mid = (lo + hi) >>> 1;
|
|
294
|
+
if (this.pts[mid].x.value < xVal) {
|
|
295
|
+
lo = mid + 1;
|
|
296
|
+
} else {
|
|
297
|
+
hi = mid;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return lo;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Look up a point by x value. Returns undefined if not found. */
|
|
304
|
+
findByX(xVal: number): Pt | undefined {
|
|
305
|
+
const idx = this.findIndex(xVal);
|
|
306
|
+
if (idx < this.pts.length && this.pts[idx].x.value === xVal) {
|
|
307
|
+
return this.pts[idx];
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Return all points as an array (for interpolation). */
|
|
313
|
+
toArray(): Pt[] {
|
|
314
|
+
return this.pts.slice();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Serialize all points as big-endian u16 pairs (x then y). */
|
|
318
|
+
serialize(): Uint8Array {
|
|
319
|
+
const out = new Uint8Array(this.pts.length * 4);
|
|
320
|
+
for (let i = 0; i < this.pts.length; i++) {
|
|
321
|
+
const pt = this.pts[i];
|
|
322
|
+
out[i * 4] = (pt.x.value >>> 8) & 0xff;
|
|
323
|
+
out[i * 4 + 1] = pt.x.value & 0xff;
|
|
324
|
+
out[i * 4 + 2] = (pt.y.value >>> 8) & 0xff;
|
|
325
|
+
out[i * 4 + 3] = pt.y.value & 0xff;
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Deserialize from big-endian u16 pairs (x then y). */
|
|
331
|
+
static deserialize(data: Uint8Array): SortedPtSet {
|
|
332
|
+
if (data.length % 4 !== 0) {
|
|
333
|
+
throw new PolynomialError("SortedPtSet data length must be multiple of 4");
|
|
334
|
+
}
|
|
335
|
+
const set = new SortedPtSet();
|
|
336
|
+
const n = data.length / 4;
|
|
337
|
+
for (let i = 0; i < n; i++) {
|
|
338
|
+
const x = new GF16((data[i * 4] << 8) | data[i * 4 + 1]);
|
|
339
|
+
const y = new GF16((data[i * 4 + 2] << 8) | data[i * 4 + 3]);
|
|
340
|
+
set.insert({ x, y });
|
|
341
|
+
}
|
|
342
|
+
return set;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Encoder state
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
/** Points-based state: raw GF16 values for each of 16 polynomials. */
|
|
351
|
+
interface PointsState {
|
|
352
|
+
kind: "points";
|
|
353
|
+
/** 16 arrays of GF16 y-values (x is implicit: 0, 1, 2, ...). */
|
|
354
|
+
points: GF16[][];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Polys-based state: Lagrange-interpolated polynomials. */
|
|
358
|
+
interface PolysState {
|
|
359
|
+
kind: "polys";
|
|
360
|
+
polys: Poly[];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
type EncoderState = PointsState | PolysState;
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// PolyEncoder
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export class PolyEncoder implements Encoder {
|
|
370
|
+
private idx: number;
|
|
371
|
+
private state: EncoderState;
|
|
372
|
+
|
|
373
|
+
private constructor(idx: number, state: EncoderState) {
|
|
374
|
+
this.idx = idx;
|
|
375
|
+
this.state = state;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create an encoder from a message byte array.
|
|
380
|
+
*
|
|
381
|
+
* The message is split into 16 interleaved streams of GF16 values.
|
|
382
|
+
* Each pair of consecutive bytes becomes one GF16 element. If the message
|
|
383
|
+
* length is odd, it is padded with a zero byte.
|
|
384
|
+
*/
|
|
385
|
+
static encodeBytes(msg: Uint8Array): PolyEncoder {
|
|
386
|
+
if (msg.length % 2 !== 0) {
|
|
387
|
+
throw new PolynomialError("Message length must be even");
|
|
388
|
+
}
|
|
389
|
+
if (msg.length > MAX_MESSAGE_LENGTH) {
|
|
390
|
+
throw new PolynomialError("Message length exceeds maximum");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Total number of GF16 values
|
|
394
|
+
const totalValues = msg.length / 2;
|
|
395
|
+
|
|
396
|
+
// Initialize 16 point arrays
|
|
397
|
+
const points: GF16[][] = new Array<GF16[]>(NUM_POLYS);
|
|
398
|
+
for (let p = 0; p < NUM_POLYS; p++) {
|
|
399
|
+
points[p] = [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Distribute values across 16 polynomials round-robin
|
|
403
|
+
for (let i = 0; i < totalValues; i++) {
|
|
404
|
+
const poly = i % NUM_POLYS;
|
|
405
|
+
const value = (msg[i * 2] << 8) | msg[i * 2 + 1];
|
|
406
|
+
points[poly].push(new GF16(value));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return new PolyEncoder(0, { kind: "points", points });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Return the next chunk and advance the index. */
|
|
413
|
+
nextChunk(): Chunk {
|
|
414
|
+
const chunk = this.chunkAt(this.idx);
|
|
415
|
+
this.idx++;
|
|
416
|
+
return chunk;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Compute the chunk at a specific index. */
|
|
420
|
+
chunkAt(idx: number): Chunk {
|
|
421
|
+
const data = new Uint8Array(CHUNK_DATA_SIZE);
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < NUM_POLYS; i++) {
|
|
424
|
+
const totalIdx = idx * NUM_POLYS + i;
|
|
425
|
+
const poly = totalIdx % NUM_POLYS;
|
|
426
|
+
const polyIdx = Math.floor(totalIdx / NUM_POLYS);
|
|
427
|
+
const val = this.pointAt(poly, polyIdx);
|
|
428
|
+
data[i * 2] = (val.value >>> 8) & 0xff;
|
|
429
|
+
data[i * 2 + 1] = val.value & 0xff;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { index: idx & 0xffff, data };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the GF16 value for polynomial `poly` at index `idx`.
|
|
437
|
+
*
|
|
438
|
+
* If we are in Points state and the index is within range, return directly.
|
|
439
|
+
* Otherwise, convert to Polys state and evaluate.
|
|
440
|
+
*/
|
|
441
|
+
private pointAt(poly: number, idx: number): GF16 {
|
|
442
|
+
if (this.state.kind === "points") {
|
|
443
|
+
const pts = this.state.points[poly];
|
|
444
|
+
if (idx < pts.length) {
|
|
445
|
+
return pts[idx];
|
|
446
|
+
}
|
|
447
|
+
// Need to convert to polys for extrapolation
|
|
448
|
+
this.convertToPolys();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// State is now "polys"
|
|
452
|
+
const polys = (this.state as PolysState).polys;
|
|
453
|
+
return polys[poly].computeAt(new GF16(idx));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Convert from Points state to Polys state via Lagrange interpolation. */
|
|
457
|
+
private convertToPolys(): void {
|
|
458
|
+
if (this.state.kind === "polys") return;
|
|
459
|
+
|
|
460
|
+
const { points } = this.state;
|
|
461
|
+
const polys: Poly[] = new Array<Poly>(NUM_POLYS);
|
|
462
|
+
|
|
463
|
+
for (let p = 0; p < NUM_POLYS; p++) {
|
|
464
|
+
const pts: Pt[] = points[p].map((y, i) => ({
|
|
465
|
+
x: new GF16(i),
|
|
466
|
+
y,
|
|
467
|
+
}));
|
|
468
|
+
polys[p] = Poly.lagrangeInterpolate(pts);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.state = { kind: "polys", polys };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Serialize encoder state for protobuf transport. */
|
|
475
|
+
toProto(): { idx: number; pts: Uint8Array[]; polys: Uint8Array[] } {
|
|
476
|
+
if (this.state.kind === "points") {
|
|
477
|
+
const pts: Uint8Array[] = this.state.points.map((arr) => {
|
|
478
|
+
const buf = new Uint8Array(arr.length * 2);
|
|
479
|
+
for (let i = 0; i < arr.length; i++) {
|
|
480
|
+
buf[i * 2] = (arr[i].value >>> 8) & 0xff;
|
|
481
|
+
buf[i * 2 + 1] = arr[i].value & 0xff;
|
|
482
|
+
}
|
|
483
|
+
return buf;
|
|
484
|
+
});
|
|
485
|
+
return { idx: this.idx, pts, polys: [] };
|
|
486
|
+
}
|
|
487
|
+
const polys: Uint8Array[] = this.state.polys.map((p) => p.serialize());
|
|
488
|
+
return { idx: this.idx, pts: [], polys };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Restore encoder from protobuf data. */
|
|
492
|
+
static fromProto(data: { idx: number; pts: Uint8Array[]; polys: Uint8Array[] }): PolyEncoder {
|
|
493
|
+
if (data.polys.length > 0) {
|
|
494
|
+
const polys = data.polys.map((buf) => Poly.deserialize(buf));
|
|
495
|
+
return new PolyEncoder(data.idx, { kind: "polys", polys });
|
|
496
|
+
}
|
|
497
|
+
const points: GF16[][] = data.pts.map((buf) => {
|
|
498
|
+
const arr: GF16[] = [];
|
|
499
|
+
for (let i = 0; i < buf.length; i += 2) {
|
|
500
|
+
arr.push(new GF16((buf[i] << 8) | buf[i + 1]));
|
|
501
|
+
}
|
|
502
|
+
return arr;
|
|
503
|
+
});
|
|
504
|
+
return new PolyEncoder(data.idx, { kind: "points", points });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// PolyDecoder
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
export class PolyDecoder implements Decoder {
|
|
513
|
+
/** Total number of GF16 values needed (= message byte length / 2, rounded up). */
|
|
514
|
+
ptsNeeded: number;
|
|
515
|
+
private readonly pts: SortedPtSet[];
|
|
516
|
+
private _isComplete: boolean;
|
|
517
|
+
|
|
518
|
+
private constructor(ptsNeeded: number, pts: SortedPtSet[], isComplete: boolean) {
|
|
519
|
+
this.ptsNeeded = ptsNeeded;
|
|
520
|
+
this.pts = pts;
|
|
521
|
+
this._isComplete = isComplete;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create a decoder for a message of `lenBytes` bytes.
|
|
526
|
+
*
|
|
527
|
+
* The caller must know the original message length to know when enough
|
|
528
|
+
* chunks have been received.
|
|
529
|
+
*/
|
|
530
|
+
static create(lenBytes: number): PolyDecoder {
|
|
531
|
+
if (lenBytes % 2 !== 0) {
|
|
532
|
+
throw new PolynomialError("Message length must be even");
|
|
533
|
+
}
|
|
534
|
+
const ptsNeeded = lenBytes / 2;
|
|
535
|
+
|
|
536
|
+
const pts: SortedPtSet[] = new Array<SortedPtSet>(NUM_POLYS);
|
|
537
|
+
for (let i = 0; i < NUM_POLYS; i++) {
|
|
538
|
+
pts[i] = new SortedPtSet();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return new PolyDecoder(ptsNeeded, pts, false);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Whether all polynomial sets have enough points to decode. */
|
|
545
|
+
get isComplete(): boolean {
|
|
546
|
+
return this._isComplete;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Number of points necessary for polynomial `poly`.
|
|
551
|
+
*
|
|
552
|
+
* The total points (ptsNeeded) are distributed across 16 polynomials
|
|
553
|
+
* round-robin, so some may need one more point than others.
|
|
554
|
+
*/
|
|
555
|
+
private necessaryPoints(poly: number): number {
|
|
556
|
+
const base = Math.floor(this.ptsNeeded / NUM_POLYS);
|
|
557
|
+
return poly < this.ptsNeeded % NUM_POLYS ? base + 1 : base;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Add a chunk to the decoder. */
|
|
561
|
+
addChunk(chunk: Chunk): void {
|
|
562
|
+
if (this._isComplete) return;
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < NUM_POLYS; i++) {
|
|
565
|
+
const totalIdx = chunk.index * NUM_POLYS + i;
|
|
566
|
+
const poly = totalIdx % NUM_POLYS;
|
|
567
|
+
const polyIdx = Math.floor(totalIdx / NUM_POLYS);
|
|
568
|
+
|
|
569
|
+
const needed = this.necessaryPoints(poly);
|
|
570
|
+
if (this.pts[poly].length >= needed) {
|
|
571
|
+
continue; // Already have enough points for this polynomial
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const y = new GF16((chunk.data[i * 2] << 8) | chunk.data[i * 2 + 1]);
|
|
575
|
+
const pt: Pt = { x: new GF16(polyIdx), y };
|
|
576
|
+
this.pts[poly].insert(pt);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Check completeness
|
|
580
|
+
this._isComplete = this.checkComplete();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Check whether all 16 polynomial sets have enough points. */
|
|
584
|
+
private checkComplete(): boolean {
|
|
585
|
+
for (let p = 0; p < NUM_POLYS; p++) {
|
|
586
|
+
if (this.pts[p].length < this.necessaryPoints(p)) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Attempt to decode the message.
|
|
595
|
+
*
|
|
596
|
+
* Returns the decoded byte array if enough chunks have been received,
|
|
597
|
+
* or null if more chunks are needed.
|
|
598
|
+
*/
|
|
599
|
+
decodedMessage(): Uint8Array | null {
|
|
600
|
+
if (!this._isComplete) return null;
|
|
601
|
+
|
|
602
|
+
const result = new Uint8Array(this.ptsNeeded * 2);
|
|
603
|
+
|
|
604
|
+
for (let i = 0; i < this.ptsNeeded; i++) {
|
|
605
|
+
const poly = i % NUM_POLYS;
|
|
606
|
+
const polyIdx = Math.floor(i / NUM_POLYS);
|
|
607
|
+
const xVal = polyIdx;
|
|
608
|
+
|
|
609
|
+
// Try direct lookup first (fast path)
|
|
610
|
+
let val: GF16;
|
|
611
|
+
const direct = this.pts[poly].findByX(xVal);
|
|
612
|
+
if (direct !== undefined) {
|
|
613
|
+
val = direct.y;
|
|
614
|
+
} else {
|
|
615
|
+
// Fall back to Lagrange interpolation
|
|
616
|
+
const allPts = this.pts[poly].toArray();
|
|
617
|
+
const interpolated = Poly.lagrangeInterpolate(allPts);
|
|
618
|
+
val = interpolated.computeAt(new GF16(xVal));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
result[i * 2] = (val.value >>> 8) & 0xff;
|
|
622
|
+
result[i * 2 + 1] = val.value & 0xff;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Serialize decoder state for protobuf transport. */
|
|
629
|
+
toProto(): {
|
|
630
|
+
ptsNeeded: number;
|
|
631
|
+
polys: number;
|
|
632
|
+
pts: Uint8Array[];
|
|
633
|
+
isComplete: boolean;
|
|
634
|
+
} {
|
|
635
|
+
return {
|
|
636
|
+
ptsNeeded: this.ptsNeeded,
|
|
637
|
+
polys: NUM_POLYS,
|
|
638
|
+
pts: this.pts.map((s) => s.serialize()),
|
|
639
|
+
isComplete: this._isComplete,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Restore decoder from protobuf data. */
|
|
644
|
+
static fromProto(data: {
|
|
645
|
+
ptsNeeded: number;
|
|
646
|
+
polys: number;
|
|
647
|
+
pts: Uint8Array[];
|
|
648
|
+
isComplete: boolean;
|
|
649
|
+
}): PolyDecoder {
|
|
650
|
+
const pts: SortedPtSet[] = data.pts.map((buf) => SortedPtSet.deserialize(buf));
|
|
651
|
+
// Ensure we always have exactly 16 sets
|
|
652
|
+
while (pts.length < NUM_POLYS) {
|
|
653
|
+
pts.push(new SortedPtSet() as unknown as SortedPtSet);
|
|
654
|
+
}
|
|
655
|
+
return new PolyDecoder(data.ptsNeeded, pts, data.isComplete);
|
|
656
|
+
}
|
|
657
|
+
}
|