@bcts/uniform-resources 1.0.0-alpha.13 → 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/dist/index.cjs +153 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +154 -103
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +154 -101
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/fountain.ts +15 -35
- package/src/multipart-decoder.ts +30 -14
- package/src/multipart-encoder.ts +6 -19
- package/src/utils.ts +7 -10
- package/src/xoshiro.ts +170 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bcts/uniform-resources",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blockchain Commons Uniform Resources (UR) for TypeScript",
|
|
6
6
|
"license": "BSD-2-Clause-Patent",
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"node": ">=18.0.0"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@bcts/
|
|
70
|
+
"@bcts/crypto": "^1.0.0-alpha.14",
|
|
71
|
+
"@bcts/dcbor": "^1.0.0-alpha.14"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@bcts/eslint": "^0.1.0",
|
package/src/fountain.ts
CHANGED
|
@@ -71,6 +71,11 @@ export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
71
71
|
* This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
|
|
72
72
|
* ensuring encoder and decoder agree without explicit coordination.
|
|
73
73
|
*
|
|
74
|
+
* The algorithm matches the BC-UR reference implementation:
|
|
75
|
+
* 1. For pure parts (seqNum <= seqLen), return single fragment index
|
|
76
|
+
* 2. For mixed parts, use weighted sampling to choose degree
|
|
77
|
+
* 3. Shuffle all indices and take the first 'degree' indices
|
|
78
|
+
*
|
|
74
79
|
* @param seqNum - The sequence number (1-based)
|
|
75
80
|
* @param seqLen - Total number of pure fragments
|
|
76
81
|
* @param checksum - CRC32 checksum of the message
|
|
@@ -86,43 +91,18 @@ export function chooseFragments(seqNum: number, seqLen: number, checksum: number
|
|
|
86
91
|
const seed = createSeed(checksum, seqNum);
|
|
87
92
|
const rng = new Xoshiro256(seed);
|
|
88
93
|
|
|
89
|
-
// Choose degree
|
|
90
|
-
|
|
91
|
-
const degree = chooseDegree(rng, seqLen);
|
|
94
|
+
// Choose degree using weighted sampler (1/k distribution)
|
|
95
|
+
const degree = rng.chooseDegree(seqLen);
|
|
92
96
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
indices.add(index);
|
|
97
|
+
// Create array of all indices [0, 1, 2, ..., seqLen-1]
|
|
98
|
+
const allIndices: number[] = [];
|
|
99
|
+
for (let i = 0; i < seqLen; i++) {
|
|
100
|
+
allIndices.push(i);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Chooses the degree (number of fragments to mix) using a simplified
|
|
105
|
-
* robust soliton distribution.
|
|
106
|
-
*
|
|
107
|
-
* This ensures good coverage of fragments for efficient decoding.
|
|
108
|
-
*/
|
|
109
|
-
function chooseDegree(rng: Xoshiro256, seqLen: number): number {
|
|
110
|
-
// Use a simplified distribution that tends toward lower degrees
|
|
111
|
-
// but can occasionally include more fragments
|
|
112
|
-
const r = rng.nextDouble();
|
|
113
|
-
|
|
114
|
-
// Probability distribution favoring lower degrees
|
|
115
|
-
// Based on robust soliton distribution
|
|
116
|
-
if (r < 0.5) {
|
|
117
|
-
return 1;
|
|
118
|
-
} else if (r < 0.75) {
|
|
119
|
-
return 2;
|
|
120
|
-
} else if (r < 0.9) {
|
|
121
|
-
return Math.min(3, seqLen);
|
|
122
|
-
} else {
|
|
123
|
-
// Higher degrees are less common but help with convergence
|
|
124
|
-
return Math.min(rng.nextInt(4, seqLen + 1), seqLen);
|
|
125
|
-
}
|
|
103
|
+
// Shuffle all indices and take the first 'degree' indices
|
|
104
|
+
const shuffled = rng.shuffled(allIndices);
|
|
105
|
+
return shuffled.slice(0, degree);
|
|
126
106
|
}
|
|
127
107
|
|
|
128
108
|
/**
|
|
@@ -264,7 +244,7 @@ export class FountainDecoder {
|
|
|
264
244
|
const indices = chooseFragments(part.seqNum, this.seqLen, this.checksum);
|
|
265
245
|
|
|
266
246
|
if (indices.length === 1) {
|
|
267
|
-
// Pure fragment
|
|
247
|
+
// Pure fragment (or degree-1 mixed that acts like pure)
|
|
268
248
|
const index = indices[0];
|
|
269
249
|
if (!this.pureFragments.has(index)) {
|
|
270
250
|
this.pureFragments.set(index, part.data);
|
package/src/multipart-decoder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeCbor } from "@bcts/dcbor";
|
|
1
|
+
import { decodeCbor, MajorType, type Cbor } from "@bcts/dcbor";
|
|
2
2
|
import { InvalidSchemeError, InvalidTypeError, UnexpectedTypeError, URError } from "./error.js";
|
|
3
3
|
import { UR } from "./ur.js";
|
|
4
4
|
import { URType } from "./ur-type.js";
|
|
@@ -118,27 +118,43 @@ export class MultipartDecoder {
|
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Decodes a multipart UR's fountain part data.
|
|
121
|
+
*
|
|
122
|
+
* The multipart body is a CBOR array: [seqNum, seqLen, messageLen, checksum, data]
|
|
121
123
|
*/
|
|
122
124
|
private _decodeFountainPart(partInfo: MultipartInfo): FountainPart {
|
|
123
|
-
// Decode bytewords
|
|
124
|
-
const
|
|
125
|
+
// Decode bytewords to get CBOR data
|
|
126
|
+
const cborData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
128
|
+
// Decode the CBOR array
|
|
129
|
+
const decoded = decodeCbor(cborData);
|
|
129
130
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// The decoded value should be an array with 5 elements
|
|
132
|
+
if (decoded.type !== MajorType.Array) {
|
|
133
|
+
throw new URError("Invalid multipart data: expected CBOR array");
|
|
134
|
+
}
|
|
133
135
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
+
const items = decoded.value as Cbor[];
|
|
137
|
+
if (items.length !== 5) {
|
|
138
|
+
throw new URError(`Invalid multipart data: expected 5 elements, got ${items.length}`);
|
|
139
|
+
}
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
// Extract the fields: [seqNum, seqLen, messageLen, checksum, data]
|
|
142
|
+
const seqNum = Number(items[0].value);
|
|
143
|
+
const seqLen = Number(items[1].value);
|
|
144
|
+
const messageLen = Number(items[2].value);
|
|
145
|
+
const checksum = Number(items[3].value);
|
|
146
|
+
const data = items[4].value as Uint8Array;
|
|
147
|
+
|
|
148
|
+
// Verify seqNum and seqLen match the URL path values
|
|
149
|
+
if (seqNum !== partInfo.seqNum || seqLen !== partInfo.seqLen) {
|
|
150
|
+
throw new URError(
|
|
151
|
+
`Multipart metadata mismatch: URL says ${partInfo.seqNum}-${partInfo.seqLen}, CBOR says ${seqNum}-${seqLen}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
138
154
|
|
|
139
155
|
return {
|
|
140
|
-
seqNum
|
|
141
|
-
seqLen
|
|
156
|
+
seqNum,
|
|
157
|
+
seqLen,
|
|
142
158
|
messageLen,
|
|
143
159
|
checksum,
|
|
144
160
|
data,
|
package/src/multipart-encoder.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { UR } from "./ur.js";
|
|
|
2
2
|
import { URError } from "./error.js";
|
|
3
3
|
import { FountainEncoder, type FountainPart } from "./fountain.js";
|
|
4
4
|
import { encodeBytewords, BytewordsStyle } from "./utils.js";
|
|
5
|
+
import { cbor } from "@bcts/dcbor";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Encodes a UR as multiple parts using fountain codes.
|
|
@@ -96,28 +97,14 @@ export class MultipartEncoder {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
|
-
* Encodes part metadata and data
|
|
100
|
+
* Encodes part metadata and data as CBOR for bytewords encoding.
|
|
101
|
+
* Format: CBOR array [seqNum, seqLen, messageLen, checksum, data]
|
|
100
102
|
*/
|
|
101
103
|
private _encodePartData(part: FountainPart): Uint8Array {
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
+
// Create CBOR array with 5 elements: [seqNum, seqLen, messageLen, checksum, data]
|
|
105
|
+
const cborArray = cbor([part.seqNum, part.seqLen, part.messageLen, part.checksum, part.data]);
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
result[0] = (part.messageLen >>> 24) & 0xff;
|
|
107
|
-
result[1] = (part.messageLen >>> 16) & 0xff;
|
|
108
|
-
result[2] = (part.messageLen >>> 8) & 0xff;
|
|
109
|
-
result[3] = part.messageLen & 0xff;
|
|
110
|
-
|
|
111
|
-
// Checksum (big-endian)
|
|
112
|
-
result[4] = (part.checksum >>> 24) & 0xff;
|
|
113
|
-
result[5] = (part.checksum >>> 16) & 0xff;
|
|
114
|
-
result[6] = (part.checksum >>> 8) & 0xff;
|
|
115
|
-
result[7] = part.checksum & 0xff;
|
|
116
|
-
|
|
117
|
-
// Fragment data
|
|
118
|
-
result.set(part.data, 8);
|
|
119
|
-
|
|
120
|
-
return result;
|
|
107
|
+
return cborArray.toData();
|
|
121
108
|
}
|
|
122
109
|
|
|
123
110
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -615,7 +615,7 @@ export function encodeBytemojisIdentifier(data: Uint8Array): string {
|
|
|
615
615
|
export enum BytewordsStyle {
|
|
616
616
|
/** Full 4-letter words separated by spaces */
|
|
617
617
|
Standard = "standard",
|
|
618
|
-
/** Full 4-letter words
|
|
618
|
+
/** Full 4-letter words separated by hyphens (URI-safe) */
|
|
619
619
|
Uri = "uri",
|
|
620
620
|
/** First and last character only (minimal) - used by UR encoding */
|
|
621
621
|
Minimal = "minimal",
|
|
@@ -712,6 +712,7 @@ export function encodeBytewords(
|
|
|
712
712
|
case BytewordsStyle.Standard:
|
|
713
713
|
return words.join(" ");
|
|
714
714
|
case BytewordsStyle.Uri:
|
|
715
|
+
return words.join("-");
|
|
715
716
|
case BytewordsStyle.Minimal:
|
|
716
717
|
return words.join("");
|
|
717
718
|
}
|
|
@@ -741,19 +742,15 @@ export function decodeBytewords(
|
|
|
741
742
|
break;
|
|
742
743
|
}
|
|
743
744
|
case BytewordsStyle.Uri: {
|
|
744
|
-
// 4-character words
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
bytes = [];
|
|
749
|
-
for (let i = 0; i < lowercased.length; i += 4) {
|
|
750
|
-
const word = lowercased.slice(i, i + 4);
|
|
745
|
+
// 4-character words separated by hyphens
|
|
746
|
+
const words = lowercased.split("-");
|
|
747
|
+
bytes = words.map((word) => {
|
|
751
748
|
const index = BYTEWORDS_MAP.get(word);
|
|
752
749
|
if (index === undefined) {
|
|
753
750
|
throw new Error(`Invalid byteword: ${word}`);
|
|
754
751
|
}
|
|
755
|
-
|
|
756
|
-
}
|
|
752
|
+
return index;
|
|
753
|
+
});
|
|
757
754
|
break;
|
|
758
755
|
}
|
|
759
756
|
case BytewordsStyle.Minimal: {
|
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
|
|
34
|
-
*
|
|
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 (
|
|
40
|
+
* @param seed - The seed bytes (must be exactly 32 bytes)
|
|
37
41
|
*/
|
|
38
42
|
constructor(seed: Uint8Array) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
257
|
+
// Create 8-byte seed: seqNum (big-endian) || checksum (big-endian)
|
|
258
|
+
const seed8 = new Uint8Array(8);
|
|
166
259
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
272
|
+
// Hash with SHA-256 to get 32 bytes
|
|
273
|
+
return sha256(seed8);
|
|
180
274
|
}
|