@bcts/provenance-mark 1.0.0-alpha.10
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 +48 -0
- package/README.md +15 -0
- package/dist/index.cjs +1703 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +711 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +711 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +1700 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +1657 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +78 -0
- package/src/crypto-utils.ts +75 -0
- package/src/date.ts +216 -0
- package/src/error.ts +141 -0
- package/src/generator.ts +182 -0
- package/src/index.ts +90 -0
- package/src/mark-info.ts +126 -0
- package/src/mark.ts +597 -0
- package/src/resolution.ts +294 -0
- package/src/rng-state.ts +68 -0
- package/src/seed.ts +98 -0
- package/src/utils.ts +103 -0
- package/src/validate.ts +449 -0
- package/src/xoshiro256starstar.ts +150 -0
package/src/validate.ts
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// Ported from provenance-mark-rust/src/validate.rs
|
|
2
|
+
|
|
3
|
+
import type { ProvenanceMark } from "./mark.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format for validation report output.
|
|
7
|
+
*/
|
|
8
|
+
export enum ValidationReportFormat {
|
|
9
|
+
/** Human-readable text format */
|
|
10
|
+
Text = "text",
|
|
11
|
+
/** Compact JSON format (no whitespace) */
|
|
12
|
+
JsonCompact = "json-compact",
|
|
13
|
+
/** Pretty-printed JSON format (with indentation) */
|
|
14
|
+
JsonPretty = "json-pretty",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Issue flagged during validation.
|
|
19
|
+
*/
|
|
20
|
+
export type ValidationIssue =
|
|
21
|
+
| { type: "HashMismatch"; expected: string; actual: string }
|
|
22
|
+
| { type: "KeyMismatch" }
|
|
23
|
+
| { type: "SequenceGap"; expected: number; actual: number }
|
|
24
|
+
| { type: "DateOrdering"; previous: string; next: string }
|
|
25
|
+
| { type: "NonGenesisAtZero" }
|
|
26
|
+
| { type: "InvalidGenesisKey" };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format a validation issue as a string.
|
|
30
|
+
*/
|
|
31
|
+
export function formatValidationIssue(issue: ValidationIssue): string {
|
|
32
|
+
switch (issue.type) {
|
|
33
|
+
case "HashMismatch":
|
|
34
|
+
return `hash mismatch: expected ${issue.expected}, got ${issue.actual}`;
|
|
35
|
+
case "KeyMismatch":
|
|
36
|
+
return "key mismatch: current hash was not generated from next key";
|
|
37
|
+
case "SequenceGap":
|
|
38
|
+
return `sequence number gap: expected ${issue.expected}, got ${issue.actual}`;
|
|
39
|
+
case "DateOrdering":
|
|
40
|
+
return `date must be equal or later: previous is ${issue.previous}, next is ${issue.next}`;
|
|
41
|
+
case "NonGenesisAtZero":
|
|
42
|
+
return "non-genesis mark at sequence 0";
|
|
43
|
+
case "InvalidGenesisKey":
|
|
44
|
+
return "genesis mark must have key equal to chain_id";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A mark with any issues flagged during validation.
|
|
50
|
+
*/
|
|
51
|
+
export interface FlaggedMark {
|
|
52
|
+
mark: ProvenanceMark;
|
|
53
|
+
issues: ValidationIssue[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Report for a contiguous sequence of marks within a chain.
|
|
58
|
+
*/
|
|
59
|
+
export interface SequenceReport {
|
|
60
|
+
startSeq: number;
|
|
61
|
+
endSeq: number;
|
|
62
|
+
marks: FlaggedMark[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Report for a chain of marks with the same chain ID.
|
|
67
|
+
*/
|
|
68
|
+
export interface ChainReport {
|
|
69
|
+
chainId: Uint8Array;
|
|
70
|
+
hasGenesis: boolean;
|
|
71
|
+
marks: ProvenanceMark[];
|
|
72
|
+
sequences: SequenceReport[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the chain ID as a hex string for display.
|
|
77
|
+
*/
|
|
78
|
+
export function chainIdHex(report: ChainReport): string {
|
|
79
|
+
return hexEncode(report.chainId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Complete validation report.
|
|
84
|
+
*/
|
|
85
|
+
export interface ValidationReport {
|
|
86
|
+
marks: ProvenanceMark[];
|
|
87
|
+
chains: ChainReport[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if the validation report has any issues.
|
|
92
|
+
*/
|
|
93
|
+
export function hasIssues(report: ValidationReport): boolean {
|
|
94
|
+
// Missing genesis is considered an issue
|
|
95
|
+
for (const chain of report.chains) {
|
|
96
|
+
if (!chain.hasGenesis) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for validation issues in marks
|
|
102
|
+
for (const chain of report.chains) {
|
|
103
|
+
for (const seq of chain.sequences) {
|
|
104
|
+
for (const mark of seq.marks) {
|
|
105
|
+
if (mark.issues.length > 0) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Multiple chains or sequences are also considered issues
|
|
113
|
+
if (report.chains.length > 1) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (report.chains.length === 1 && report.chains[0].sequences.length > 1) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if the validation report contains interesting information.
|
|
126
|
+
*/
|
|
127
|
+
function isInteresting(report: ValidationReport): boolean {
|
|
128
|
+
// Not interesting if empty
|
|
129
|
+
if (report.chains.length === 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if any chain is missing genesis
|
|
134
|
+
for (const chain of report.chains) {
|
|
135
|
+
if (!chain.hasGenesis) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Not interesting if single chain with single perfect sequence
|
|
141
|
+
if (report.chains.length === 1) {
|
|
142
|
+
const chain = report.chains[0];
|
|
143
|
+
if (chain.sequences.length === 1) {
|
|
144
|
+
const seq = chain.sequences[0];
|
|
145
|
+
// Check if the sequence has no issues
|
|
146
|
+
if (seq.marks.every((m) => m.issues.length === 0)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format the validation report as human-readable text.
|
|
157
|
+
*/
|
|
158
|
+
function formatText(report: ValidationReport): string {
|
|
159
|
+
if (!isInteresting(report)) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
|
|
165
|
+
// Report summary
|
|
166
|
+
lines.push(`Total marks: ${report.marks.length}`);
|
|
167
|
+
lines.push(`Chains: ${report.chains.length}`);
|
|
168
|
+
lines.push("");
|
|
169
|
+
|
|
170
|
+
// Report each chain
|
|
171
|
+
for (let chainIdx = 0; chainIdx < report.chains.length; chainIdx++) {
|
|
172
|
+
const chain = report.chains[chainIdx];
|
|
173
|
+
|
|
174
|
+
// Show short chain ID (first 4 bytes)
|
|
175
|
+
const chainIdStr = chainIdHex(chain);
|
|
176
|
+
const shortChainId = chainIdStr.length > 8 ? chainIdStr.slice(0, 8) : chainIdStr;
|
|
177
|
+
|
|
178
|
+
lines.push(`Chain ${chainIdx + 1}: ${shortChainId}`);
|
|
179
|
+
|
|
180
|
+
if (!chain.hasGenesis) {
|
|
181
|
+
lines.push(" Warning: No genesis mark found");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Report each sequence
|
|
185
|
+
for (const seq of chain.sequences) {
|
|
186
|
+
// Report each mark in the sequence
|
|
187
|
+
for (const flaggedMark of seq.marks) {
|
|
188
|
+
const mark = flaggedMark.mark;
|
|
189
|
+
const shortId = mark.identifier();
|
|
190
|
+
const seqNum = mark.seq();
|
|
191
|
+
|
|
192
|
+
// Build the mark line with annotations
|
|
193
|
+
const annotations: string[] = [];
|
|
194
|
+
|
|
195
|
+
// Check if it's genesis
|
|
196
|
+
if (mark.isGenesis()) {
|
|
197
|
+
annotations.push("genesis mark");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add issue annotations
|
|
201
|
+
for (const issue of flaggedMark.issues) {
|
|
202
|
+
let issueStr: string;
|
|
203
|
+
switch (issue.type) {
|
|
204
|
+
case "SequenceGap":
|
|
205
|
+
issueStr = `gap: ${issue.expected} missing`;
|
|
206
|
+
break;
|
|
207
|
+
case "DateOrdering":
|
|
208
|
+
issueStr = `date ${issue.previous} < ${issue.next}`;
|
|
209
|
+
break;
|
|
210
|
+
case "HashMismatch":
|
|
211
|
+
issueStr = "hash mismatch";
|
|
212
|
+
break;
|
|
213
|
+
case "KeyMismatch":
|
|
214
|
+
issueStr = "key mismatch";
|
|
215
|
+
break;
|
|
216
|
+
case "NonGenesisAtZero":
|
|
217
|
+
issueStr = "non-genesis at seq 0";
|
|
218
|
+
break;
|
|
219
|
+
case "InvalidGenesisKey":
|
|
220
|
+
issueStr = "invalid genesis key";
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
annotations.push(issueStr);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Format the line
|
|
227
|
+
if (annotations.length === 0) {
|
|
228
|
+
lines.push(` ${seqNum}: ${shortId}`);
|
|
229
|
+
} else {
|
|
230
|
+
lines.push(` ${seqNum}: ${shortId} (${annotations.join(", ")})`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lines.push("");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.join("\n").trimEnd();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format the validation report.
|
|
243
|
+
*/
|
|
244
|
+
export function formatReport(report: ValidationReport, format: ValidationReportFormat): string {
|
|
245
|
+
switch (format) {
|
|
246
|
+
case ValidationReportFormat.Text:
|
|
247
|
+
return formatText(report);
|
|
248
|
+
case ValidationReportFormat.JsonCompact:
|
|
249
|
+
return JSON.stringify(reportToJSON(report));
|
|
250
|
+
case ValidationReportFormat.JsonPretty:
|
|
251
|
+
return JSON.stringify(reportToJSON(report), null, 2);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Convert a report to a JSON-serializable object.
|
|
257
|
+
*/
|
|
258
|
+
function reportToJSON(report: ValidationReport): unknown {
|
|
259
|
+
return {
|
|
260
|
+
marks: report.marks.map((m) => m.toUrlEncoding()),
|
|
261
|
+
chains: report.chains.map((chain) => ({
|
|
262
|
+
chain_id: hexEncode(chain.chainId),
|
|
263
|
+
has_genesis: chain.hasGenesis,
|
|
264
|
+
marks: chain.marks.map((m) => m.toUrlEncoding()),
|
|
265
|
+
sequences: chain.sequences.map((seq) => ({
|
|
266
|
+
start_seq: seq.startSeq,
|
|
267
|
+
end_seq: seq.endSeq,
|
|
268
|
+
marks: seq.marks.map((fm) => ({
|
|
269
|
+
mark: fm.mark.toUrlEncoding(),
|
|
270
|
+
issues: fm.issues,
|
|
271
|
+
})),
|
|
272
|
+
})),
|
|
273
|
+
})),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build sequence bins for a chain.
|
|
279
|
+
*/
|
|
280
|
+
function buildSequenceBins(marks: ProvenanceMark[]): SequenceReport[] {
|
|
281
|
+
const sequences: SequenceReport[] = [];
|
|
282
|
+
let currentSequence: FlaggedMark[] = [];
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < marks.length; i++) {
|
|
285
|
+
const mark = marks[i];
|
|
286
|
+
|
|
287
|
+
if (i === 0) {
|
|
288
|
+
// First mark starts a sequence
|
|
289
|
+
currentSequence.push({ mark, issues: [] });
|
|
290
|
+
} else {
|
|
291
|
+
const prev = marks[i - 1];
|
|
292
|
+
|
|
293
|
+
// Check if this mark follows the previous one
|
|
294
|
+
try {
|
|
295
|
+
prev.precedesOpt(mark);
|
|
296
|
+
// Continues the current sequence
|
|
297
|
+
currentSequence.push({ mark, issues: [] });
|
|
298
|
+
} catch (e) {
|
|
299
|
+
// Breaks the sequence - save current and start new
|
|
300
|
+
if (currentSequence.length > 0) {
|
|
301
|
+
sequences.push(createSequenceReport(currentSequence));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Parse the error to determine the issue type
|
|
305
|
+
const issue = parseValidationError(e, prev, mark);
|
|
306
|
+
|
|
307
|
+
// Start new sequence with this mark, flagged with the issue
|
|
308
|
+
currentSequence = [{ mark, issues: [issue] }];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add the final sequence
|
|
314
|
+
if (currentSequence.length > 0) {
|
|
315
|
+
sequences.push(createSequenceReport(currentSequence));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return sequences;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse a validation error into a ValidationIssue.
|
|
323
|
+
*/
|
|
324
|
+
function parseValidationError(
|
|
325
|
+
e: unknown,
|
|
326
|
+
prev: ProvenanceMark,
|
|
327
|
+
next: ProvenanceMark,
|
|
328
|
+
): ValidationIssue {
|
|
329
|
+
const message = e instanceof Error ? e.message : "";
|
|
330
|
+
|
|
331
|
+
if (message !== "" && message.includes("non-genesis mark at sequence 0")) {
|
|
332
|
+
return { type: "NonGenesisAtZero" };
|
|
333
|
+
}
|
|
334
|
+
if (message !== "" && message.includes("genesis mark must have key equal to chain_id")) {
|
|
335
|
+
return { type: "InvalidGenesisKey" };
|
|
336
|
+
}
|
|
337
|
+
if (message !== "" && message.includes("sequence gap")) {
|
|
338
|
+
const seqGapRegex = /expected (\d+), got (\d+)/;
|
|
339
|
+
const match = seqGapRegex.exec(message);
|
|
340
|
+
if (match !== null) {
|
|
341
|
+
return {
|
|
342
|
+
type: "SequenceGap",
|
|
343
|
+
expected: parseInt(match[1], 10),
|
|
344
|
+
actual: parseInt(match[2], 10),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (message !== "" && message.includes("date ordering")) {
|
|
349
|
+
return {
|
|
350
|
+
type: "DateOrdering",
|
|
351
|
+
previous: prev.date().toISOString(),
|
|
352
|
+
next: next.date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (message !== "" && message.includes("hash mismatch")) {
|
|
356
|
+
const hashRegex = /expected: (\w+), actual: (\w+)/;
|
|
357
|
+
const match = hashRegex.exec(message);
|
|
358
|
+
if (match !== null) {
|
|
359
|
+
return { type: "HashMismatch", expected: match[1], actual: match[2] };
|
|
360
|
+
}
|
|
361
|
+
return { type: "HashMismatch", expected: "", actual: "" };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Fallback
|
|
365
|
+
return { type: "KeyMismatch" };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Create a sequence report from flagged marks.
|
|
370
|
+
*/
|
|
371
|
+
function createSequenceReport(marks: FlaggedMark[]): SequenceReport {
|
|
372
|
+
const startSeq = marks.length > 0 ? marks[0].mark.seq() : 0;
|
|
373
|
+
const endSeq = marks.length > 0 ? marks[marks.length - 1].mark.seq() : 0;
|
|
374
|
+
return { startSeq, endSeq, marks };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Validate a collection of provenance marks.
|
|
379
|
+
*/
|
|
380
|
+
export function validate(marks: ProvenanceMark[]): ValidationReport {
|
|
381
|
+
// Deduplicate exact duplicates
|
|
382
|
+
const seen = new Set<string>();
|
|
383
|
+
const deduplicatedMarks: ProvenanceMark[] = [];
|
|
384
|
+
for (const mark of marks) {
|
|
385
|
+
const key = mark.toUrlEncoding();
|
|
386
|
+
if (!seen.has(key)) {
|
|
387
|
+
seen.add(key);
|
|
388
|
+
deduplicatedMarks.push(mark);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Bin marks by chain ID
|
|
393
|
+
const chainBins = new Map<string, ProvenanceMark[]>();
|
|
394
|
+
for (const mark of deduplicatedMarks) {
|
|
395
|
+
const chainIdKey = hexEncode(mark.chainId());
|
|
396
|
+
const bin = chainBins.get(chainIdKey);
|
|
397
|
+
if (bin !== undefined) {
|
|
398
|
+
bin.push(mark);
|
|
399
|
+
} else {
|
|
400
|
+
chainBins.set(chainIdKey, [mark]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Process each chain
|
|
405
|
+
const chains: ChainReport[] = [];
|
|
406
|
+
for (const [chainIdKey, chainMarks] of chainBins) {
|
|
407
|
+
// Sort by sequence number
|
|
408
|
+
chainMarks.sort((a, b) => a.seq() - b.seq());
|
|
409
|
+
|
|
410
|
+
// Check for genesis mark
|
|
411
|
+
const hasGenesis =
|
|
412
|
+
chainMarks.length > 0 && chainMarks[0].seq() === 0 && chainMarks[0].isGenesis();
|
|
413
|
+
|
|
414
|
+
// Build sequence bins
|
|
415
|
+
const sequences = buildSequenceBins(chainMarks);
|
|
416
|
+
|
|
417
|
+
chains.push({
|
|
418
|
+
chainId: hexDecode(chainIdKey),
|
|
419
|
+
hasGenesis,
|
|
420
|
+
marks: chainMarks,
|
|
421
|
+
sequences,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Sort chains by chain ID for consistent output
|
|
426
|
+
chains.sort((a, b) => hexEncode(a.chainId).localeCompare(hexEncode(b.chainId)));
|
|
427
|
+
|
|
428
|
+
return { marks: deduplicatedMarks, chains };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Helper function to encode bytes as hex.
|
|
433
|
+
*/
|
|
434
|
+
function hexEncode(bytes: Uint8Array): string {
|
|
435
|
+
return Array.from(bytes)
|
|
436
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
437
|
+
.join("");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Helper function to decode hex to bytes.
|
|
442
|
+
*/
|
|
443
|
+
function hexDecode(hex: string): Uint8Array {
|
|
444
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
445
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
446
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
447
|
+
}
|
|
448
|
+
return bytes;
|
|
449
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Ported from provenance-mark-rust/src/xoshiro256starstar.rs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Xoshiro256** PRNG implementation.
|
|
5
|
+
* A fast, high-quality pseudorandom number generator.
|
|
6
|
+
*/
|
|
7
|
+
export class Xoshiro256StarStar {
|
|
8
|
+
private s: [bigint, bigint, bigint, bigint];
|
|
9
|
+
|
|
10
|
+
private constructor(s: [bigint, bigint, bigint, bigint]) {
|
|
11
|
+
this.s = s;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the internal state as an array of 4 u64 values.
|
|
16
|
+
*/
|
|
17
|
+
toState(): [bigint, bigint, bigint, bigint] {
|
|
18
|
+
return [...this.s] as [bigint, bigint, bigint, bigint];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new PRNG from a state array.
|
|
23
|
+
*/
|
|
24
|
+
static fromState(state: [bigint, bigint, bigint, bigint]): Xoshiro256StarStar {
|
|
25
|
+
return new Xoshiro256StarStar([...state] as [bigint, bigint, bigint, bigint]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serialize the state to 32 bytes (little-endian).
|
|
30
|
+
*/
|
|
31
|
+
toData(): Uint8Array {
|
|
32
|
+
const data = new Uint8Array(32);
|
|
33
|
+
for (let i = 0; i < 4; i++) {
|
|
34
|
+
const val = this.s[i];
|
|
35
|
+
for (let j = 0; j < 8; j++) {
|
|
36
|
+
data[i * 8 + j] = Number((val >> BigInt(j * 8)) & 0xffn);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new PRNG from 32 bytes of seed data (little-endian).
|
|
44
|
+
*/
|
|
45
|
+
static fromData(data: Uint8Array): Xoshiro256StarStar {
|
|
46
|
+
if (data.length !== 32) {
|
|
47
|
+
throw new Error(`expected 32 bytes, got ${data.length}`);
|
|
48
|
+
}
|
|
49
|
+
const s: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n];
|
|
50
|
+
for (let i = 0; i < 4; i++) {
|
|
51
|
+
let val = 0n;
|
|
52
|
+
for (let j = 0; j < 8; j++) {
|
|
53
|
+
val |= BigInt(data[i * 8 + j]) << BigInt(j * 8);
|
|
54
|
+
}
|
|
55
|
+
s[i] = val;
|
|
56
|
+
}
|
|
57
|
+
return new Xoshiro256StarStar(s);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate the next u64 value.
|
|
62
|
+
*/
|
|
63
|
+
nextU64(): bigint {
|
|
64
|
+
const result = this.starstarU64(this.s[1]);
|
|
65
|
+
this.advance();
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate the next u32 value (upper bits of u64 for better quality).
|
|
71
|
+
*/
|
|
72
|
+
nextU32(): number {
|
|
73
|
+
return Number((this.nextU64() >> 32n) & 0xffffffffn);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate the next byte.
|
|
78
|
+
*/
|
|
79
|
+
nextByte(): number {
|
|
80
|
+
return Number(this.nextU64() & 0xffn);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate the next n bytes.
|
|
85
|
+
*/
|
|
86
|
+
nextBytes(len: number): Uint8Array {
|
|
87
|
+
const bytes = new Uint8Array(len);
|
|
88
|
+
for (let i = 0; i < len; i++) {
|
|
89
|
+
bytes[i] = this.nextByte();
|
|
90
|
+
}
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fill a buffer with random bytes.
|
|
96
|
+
*/
|
|
97
|
+
fillBytes(dest: Uint8Array): void {
|
|
98
|
+
// Use fill_bytes_via_next strategy - fill 8 bytes at a time
|
|
99
|
+
let i = 0;
|
|
100
|
+
while (i + 8 <= dest.length) {
|
|
101
|
+
const val = this.nextU64();
|
|
102
|
+
for (let j = 0; j < 8; j++) {
|
|
103
|
+
dest[i + j] = Number((val >> BigInt(j * 8)) & 0xffn);
|
|
104
|
+
}
|
|
105
|
+
i += 8;
|
|
106
|
+
}
|
|
107
|
+
// Handle remaining bytes
|
|
108
|
+
if (i < dest.length) {
|
|
109
|
+
const val = this.nextU64();
|
|
110
|
+
for (let j = 0; i < dest.length; i++, j++) {
|
|
111
|
+
dest[i] = Number((val >> BigInt(j * 8)) & 0xffn);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The starstar transformation: x * 5, rotate left 7, * 9
|
|
118
|
+
*/
|
|
119
|
+
private starstarU64(x: bigint): bigint {
|
|
120
|
+
const mask64 = 0xffffffffffffffffn;
|
|
121
|
+
const mul5 = (x * 5n) & mask64;
|
|
122
|
+
const rotated = this.rotateLeft64(mul5, 7n);
|
|
123
|
+
return (rotated * 9n) & mask64;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Rotate a 64-bit value left by n bits.
|
|
128
|
+
*/
|
|
129
|
+
private rotateLeft64(x: bigint, n: bigint): bigint {
|
|
130
|
+
const mask64 = 0xffffffffffffffffn;
|
|
131
|
+
return ((x << n) | (x >> (64n - n))) & mask64;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Advance the PRNG state.
|
|
136
|
+
*/
|
|
137
|
+
private advance(): void {
|
|
138
|
+
const mask64 = 0xffffffffffffffffn;
|
|
139
|
+
const t = (this.s[1] << 17n) & mask64;
|
|
140
|
+
|
|
141
|
+
this.s[2] ^= this.s[0];
|
|
142
|
+
this.s[3] ^= this.s[1];
|
|
143
|
+
this.s[1] ^= this.s[2];
|
|
144
|
+
this.s[0] ^= this.s[3];
|
|
145
|
+
|
|
146
|
+
this.s[2] ^= t;
|
|
147
|
+
|
|
148
|
+
this.s[3] = this.rotateLeft64(this.s[3], 45n);
|
|
149
|
+
}
|
|
150
|
+
}
|