@bcts/uniform-resources 1.0.0-alpha.12 → 1.0.0-alpha.14

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/src/xoshiro.ts CHANGED
@@ -5,8 +5,11 @@
5
5
  * for deterministic fragment selection in fountain codes.
6
6
  *
7
7
  * Reference: https://prng.di.unimi.it/
8
+ * BC-UR Reference: https://github.com/nicklockwood/fountain-codes
8
9
  */
9
10
 
11
+ import { sha256 } from "@bcts/crypto";
12
+
10
13
  const MAX_UINT64 = BigInt("0xffffffffffffffff");
11
14
 
12
15
  /**
@@ -28,25 +31,33 @@ export class Xoshiro256 {
28
31
  private s: [bigint, bigint, bigint, bigint];
29
32
 
30
33
  /**
31
- * Creates a new Xoshiro256** instance from a seed.
34
+ * Creates a new Xoshiro256** instance from a 32-byte seed.
32
35
  *
33
- * The seed is hashed using SHA-256 to initialize the state.
34
- * For consistent results across encoder/decoder, use the same seed.
36
+ * The seed must be exactly 32 bytes (256 bits). The bytes are interpreted
37
+ * using the BC-UR reference algorithm: each 8-byte chunk is read as
38
+ * big-endian then stored as little-endian for the state.
35
39
  *
36
- * @param seed - The seed bytes (any length)
40
+ * @param seed - The seed bytes (must be exactly 32 bytes)
37
41
  */
38
42
  constructor(seed: Uint8Array) {
39
- // Hash the seed using a simple hash function
40
- // In production, you'd use SHA-256 here
41
- const hash = this.hashSeed(seed);
42
-
43
- // Initialize the 4x64-bit state from the hash
44
- this.s = [
45
- this.bytesToBigInt(hash.slice(0, 8)),
46
- this.bytesToBigInt(hash.slice(8, 16)),
47
- this.bytesToBigInt(hash.slice(16, 24)),
48
- this.bytesToBigInt(hash.slice(24, 32)),
49
- ];
43
+ if (seed.length !== 32) {
44
+ throw new Error(`Seed must be 32 bytes, got ${seed.length}`);
45
+ }
46
+
47
+ // BC-UR reference implementation:
48
+ // For each 8-byte chunk, read as big-endian u64, then convert to little-endian bytes
49
+ // This effectively swaps the byte order within each 8-byte segment
50
+ const s: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n];
51
+ for (let i = 0; i < 4; i++) {
52
+ // Read 8 bytes as big-endian u64
53
+ let v = 0n;
54
+ for (let n = 0; n < 8; n++) {
55
+ v = (v << 8n) | BigInt(seed[8 * i + n] ?? 0);
56
+ }
57
+ s[i] = v;
58
+ }
59
+
60
+ this.s = s;
50
61
  }
51
62
 
52
63
  /**
@@ -59,47 +70,6 @@ export class Xoshiro256 {
59
70
  return instance;
60
71
  }
61
72
 
62
- /**
63
- * Simple hash function for seeding.
64
- * This is a basic implementation - in production use SHA-256.
65
- */
66
- private hashSeed(seed: Uint8Array): Uint8Array {
67
- // Simple hash expansion using CRC32-like operations
68
- const result = new Uint8Array(32);
69
-
70
- if (seed.length === 0) {
71
- return result;
72
- }
73
-
74
- // Expand seed to 32 bytes using a simple mixing function
75
- for (let i = 0; i < 32; i++) {
76
- let hash = 0;
77
- for (const byte of seed) {
78
- hash = (hash * 31 + byte + i) >>> 0;
79
- }
80
- // Mix the hash further
81
- hash ^= hash >>> 16;
82
- hash = (hash * 0x85ebca6b) >>> 0;
83
- hash ^= hash >>> 13;
84
- hash = (hash * 0xc2b2ae35) >>> 0;
85
- hash ^= hash >>> 16;
86
- result[i] = hash & 0xff;
87
- }
88
-
89
- return result;
90
- }
91
-
92
- /**
93
- * Converts 8 bytes to a 64-bit BigInt (little-endian).
94
- */
95
- private bytesToBigInt(bytes: Uint8Array): bigint {
96
- let result = 0n;
97
- for (let i = 7; i >= 0; i--) {
98
- result = (result << 8n) | BigInt(bytes[i] ?? 0);
99
- }
100
- return result;
101
- }
102
-
103
73
  /**
104
74
  * Generates the next 64-bit random value.
105
75
  */
@@ -121,19 +91,21 @@ export class Xoshiro256 {
121
91
 
122
92
  /**
123
93
  * Generates a random double in [0, 1).
94
+ * Matches BC-UR reference: self.next() as f64 / (u64::MAX as f64 + 1.0)
124
95
  */
125
96
  nextDouble(): number {
126
- // Use the upper 53 bits for double precision
127
97
  const value = this.next();
128
- return Number(value >> 11n) / Number(1n << 53n);
98
+ // u64::MAX as f64 + 1.0 = 18446744073709551616.0
99
+ return Number(value) / 18446744073709551616.0;
129
100
  }
130
101
 
131
102
  /**
132
- * Generates a random integer in [low, high).
103
+ * Generates a random integer in [low, high] (inclusive).
104
+ * Matches BC-UR reference: (self.next_double() * ((high - low + 1) as f64)) as u64 + low
133
105
  */
134
106
  nextInt(low: number, high: number): number {
135
- const range = high - low;
136
- return low + Math.floor(this.nextDouble() * range);
107
+ const range = high - low + 1;
108
+ return Math.floor(this.nextDouble() * range) + low;
137
109
  }
138
110
 
139
111
  /**
@@ -153,28 +125,150 @@ export class Xoshiro256 {
153
125
  }
154
126
  return result;
155
127
  }
128
+
129
+ /**
130
+ * Shuffles items by repeatedly picking random indices.
131
+ * Matches BC-UR reference implementation.
132
+ */
133
+ shuffled<T>(items: T[]): T[] {
134
+ const source = [...items];
135
+ const shuffled: T[] = [];
136
+ while (source.length > 0) {
137
+ const index = this.nextInt(0, source.length - 1);
138
+ const item = source.splice(index, 1)[0];
139
+ if (item !== undefined) {
140
+ shuffled.push(item);
141
+ }
142
+ }
143
+ return shuffled;
144
+ }
145
+
146
+ /**
147
+ * Chooses the degree (number of fragments to mix) using a weighted sampler.
148
+ * Uses the robust soliton distribution with weights [1/1, 1/2, 1/3, ..., 1/n].
149
+ * Matches BC-UR reference implementation.
150
+ */
151
+ chooseDegree(seqLen: number): number {
152
+ // Create weights: [1/1, 1/2, 1/3, ..., 1/seqLen]
153
+ const weights: number[] = [];
154
+ for (let i = 1; i <= seqLen; i++) {
155
+ weights.push(1.0 / i);
156
+ }
157
+
158
+ // Use Vose's alias method for weighted sampling
159
+ const sampler = new WeightedSampler(weights);
160
+ return sampler.next(this) + 1; // 1-indexed degree
161
+ }
156
162
  }
157
163
 
158
164
  /**
159
- * Creates a seed for the Xoshiro PRNG from message checksum and sequence number.
165
+ * Weighted sampler using Vose's alias method.
166
+ * Allows O(1) sampling from a discrete probability distribution.
167
+ */
168
+ class WeightedSampler {
169
+ private readonly aliases: number[];
170
+ private readonly probs: number[];
171
+
172
+ constructor(weights: number[]) {
173
+ const n = weights.length;
174
+ if (n === 0) {
175
+ throw new Error("Weights array cannot be empty");
176
+ }
177
+
178
+ // Normalize weights
179
+ const sum = weights.reduce((a, b) => a + b, 0);
180
+ if (sum <= 0) {
181
+ throw new Error("Weights must sum to a positive value");
182
+ }
183
+
184
+ const normalized = weights.map((w) => (w * n) / sum);
185
+
186
+ // Initialize alias table
187
+ this.aliases = Array.from<number>({ length: n }).fill(0);
188
+ this.probs = Array.from<number>({ length: n }).fill(0);
189
+
190
+ // Partition into small and large
191
+ const small: number[] = [];
192
+ const large: number[] = [];
193
+
194
+ for (let i = n - 1; i >= 0; i--) {
195
+ if (normalized[i] < 1.0) {
196
+ small.push(i);
197
+ } else {
198
+ large.push(i);
199
+ }
200
+ }
201
+
202
+ // Build the alias table
203
+ while (small.length > 0 && large.length > 0) {
204
+ const a = small.pop();
205
+ const g = large.pop();
206
+ if (a === undefined || g === undefined) break;
207
+ this.probs[a] = normalized[a] ?? 0;
208
+ this.aliases[a] = g;
209
+ const normalizedG = normalized[g] ?? 0;
210
+ const normalizedA = normalized[a] ?? 0;
211
+ normalized[g] = normalizedG + normalizedA - 1.0;
212
+ if (normalized[g] !== undefined && normalized[g] < 1.0) {
213
+ small.push(g);
214
+ } else {
215
+ large.push(g);
216
+ }
217
+ }
218
+
219
+ while (large.length > 0) {
220
+ const g = large.pop();
221
+ if (g === undefined) break;
222
+ this.probs[g] = 1.0;
223
+ }
224
+
225
+ while (small.length > 0) {
226
+ const a = small.pop();
227
+ if (a === undefined) break;
228
+ this.probs[a] = 1.0;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Sample from the distribution.
234
+ */
235
+ next(rng: Xoshiro256): number {
236
+ const r1 = rng.nextDouble();
237
+ const r2 = rng.nextDouble();
238
+ const n = this.probs.length;
239
+ const i = Math.floor(n * r1);
240
+ if (r2 < this.probs[i]) {
241
+ return i;
242
+ } else {
243
+ return this.aliases[i];
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Creates a Xoshiro256 PRNG instance from message checksum and sequence number.
250
+ *
251
+ * This creates an 8-byte seed by concatenating seqNum and checksum (both in
252
+ * big-endian), then hashes it with SHA-256 to get the 32-byte seed for Xoshiro.
160
253
  *
161
- * This ensures that both encoder and decoder produce the same random sequence
162
- * for a given message and part number.
254
+ * This matches the BC-UR reference implementation.
163
255
  */
164
256
  export function createSeed(checksum: number, seqNum: number): Uint8Array {
165
- const seed = new Uint8Array(8);
257
+ // Create 8-byte seed: seqNum (big-endian) || checksum (big-endian)
258
+ const seed8 = new Uint8Array(8);
166
259
 
167
- // Pack checksum (4 bytes, big-endian)
168
- seed[0] = (checksum >>> 24) & 0xff;
169
- seed[1] = (checksum >>> 16) & 0xff;
170
- seed[2] = (checksum >>> 8) & 0xff;
171
- seed[3] = checksum & 0xff;
260
+ // seqNum in big-endian (bytes 0-3)
261
+ seed8[0] = (seqNum >>> 24) & 0xff;
262
+ seed8[1] = (seqNum >>> 16) & 0xff;
263
+ seed8[2] = (seqNum >>> 8) & 0xff;
264
+ seed8[3] = seqNum & 0xff;
172
265
 
173
- // Pack seqNum (4 bytes, big-endian)
174
- seed[4] = (seqNum >>> 24) & 0xff;
175
- seed[5] = (seqNum >>> 16) & 0xff;
176
- seed[6] = (seqNum >>> 8) & 0xff;
177
- seed[7] = seqNum & 0xff;
266
+ // checksum in big-endian (bytes 4-7)
267
+ seed8[4] = (checksum >>> 24) & 0xff;
268
+ seed8[5] = (checksum >>> 16) & 0xff;
269
+ seed8[6] = (checksum >>> 8) & 0xff;
270
+ seed8[7] = checksum & 0xff;
178
271
 
179
- return seed;
272
+ // Hash with SHA-256 to get 32 bytes
273
+ return sha256(seed8);
180
274
  }