49 lines
1.6 KiB
JavaScript
49 lines
1.6 KiB
JavaScript
import crypto from "node:crypto";
|
|
|
|
const MASK = (1n << 64n) - 1n;
|
|
|
|
function rotateLeft(value, amount) {
|
|
const shift = BigInt(amount);
|
|
return ((value << shift) | (value >> (64n - shift))) & MASK;
|
|
}
|
|
|
|
export function deriveState(rootSeedHex, namespace) {
|
|
const key = Buffer.from(rootSeedHex, "hex");
|
|
if (key.length !== 32) throw new Error("Root seed must be exactly 256 bits");
|
|
let attempt = namespace;
|
|
for (let retry = 0; retry < 100; retry += 1) {
|
|
const bytes = crypto.createHmac("sha256", key).update(attempt, "utf8").digest();
|
|
const state = [0, 8, 16, 24].map((offset) => bytes.readBigUInt64BE(offset));
|
|
if (state.some((word) => word !== 0n)) return state;
|
|
attempt = `${namespace}/retry/${retry + 1}`;
|
|
}
|
|
throw new Error("Could not derive a non-zero xoshiro256** state");
|
|
}
|
|
|
|
export function nextUint64(state) {
|
|
const result = (rotateLeft((state[1] * 5n) & MASK, 7) * 9n) & MASK;
|
|
const temporary = (state[1] << 17n) & MASK;
|
|
state[2] ^= state[0];
|
|
state[3] ^= state[1];
|
|
state[1] ^= state[2];
|
|
state[0] ^= state[3];
|
|
state[2] ^= temporary;
|
|
state[3] = rotateLeft(state[3], 45);
|
|
return result;
|
|
}
|
|
|
|
export function sampleInteger(state, upperExclusive) {
|
|
if (!Number.isSafeInteger(upperExclusive) || upperExclusive <= 0) {
|
|
throw new TypeError("upperExclusive must be a positive safe integer");
|
|
}
|
|
const bound = BigInt(upperExclusive);
|
|
const limit = (1n << 64n) - ((1n << 64n) % bound);
|
|
let value;
|
|
do value = nextUint64(state); while (value >= limit);
|
|
return Number(value % bound);
|
|
}
|
|
|
|
export function stateHex(state) {
|
|
return state.map((word) => word.toString(16).padStart(16, "0")).join("");
|
|
}
|