Skip to content

feat(chacha12): Consolidate PRNG for fuzzing#3232

Open
kriskowal wants to merge 12 commits into
masterfrom
kriskowal-random-chacha20
Open

feat(chacha12): Consolidate PRNG for fuzzing#3232
kriskowal wants to merge 12 commits into
masterfrom
kriskowal-random-chacha20

Conversation

@kriskowal

@kriskowal kriskowal commented May 2, 2026

Copy link
Copy Markdown
Member

Description

Adds two new workspace packages and rewires the existing
deterministic-fuzz consumers in this repo to use them.

@endo/random

A source-agnostic library of random sampling functions. Each
sampler accepts a RandomSource as its first argument:

type RandomSource = (out: Uint8Array) => void;

The shape mirrors crypto.getRandomValues (minus the return value),
so the canonical browser/Node entropy source and a
@endo/chacha12-backed source returned by makeChaCha12(seed) are
both directly usable wherever a RandomSource is expected.

Names follow the TC39
proposal-random-functions
translation Random.method -> randomMethod. Each sampler is its
own subpath export so consumers can import only what they use:

Path Exports
@endo/random all of the below
@endo/random/random.js random
@endo/random/int.js randomInt
@endo/random/uint.js randomUint8, randomUint16, randomUint24, randomUint32, randomUint53
@endo/random/fast-check.js adaptToPureRandomGenerator, adaptFromPureRandomGenerator, makeRandomTypeFromSeed
@endo/random/seeds.js bobsCoffee64

@endo/random/fast-check.js carries bidirectional adapters between
RandomSource and the pure-rand v5 RandomGenerator interface
that fast-check consumes via the randomType parameter, plus a
makeRandomTypeFromSeed builder. It imports nothing from
pure-rand or fast-check; it depends only on those interfaces
being structurally compatible.

@endo/random/seeds.js exports bobsCoffee64, a 32-byte seed
(b0b5c0ffeefacade repeated four times) shared across Endo
deterministic fuzz suites so a single seed change propagates.

@endo/chacha12

A pure-JavaScript ChaCha12 keystream. The package's sole public
entry point is makeChaCha12(key), which returns a function
(out: Uint8Array) => void that fills out with the next bytes of
the keystream. That shape matches crypto.getRandomValues and
@endo/random's RandomSource exactly, so the same function serves
either ecosystem and the two packages compose without an adapter.

ChaCha12 is the 12-round variant of Daniel J. Bernstein's ChaCha
family. The block function is identical to ChaCha20 modulo the
loop count (6 double-rounds vs 10). The reduced round count trades
cryptographic safety margin for speed. The package is positioned
and documented as a PRNG, not a cipher recommendation.

Consumer rewires

The @endo/hex encode/decode benchmarks and the @endo/ocapn
syrup and passable fuzz tests now draw deterministic bytes from
@endo/chacha12 directly, seeded from @endo/random/seeds.js's
bobsCoffee64. This removes the duplicated _xorshift.js helpers
that previously lived in packages/hex/test/ and
packages/ocapn/test/.

Most critical files to review:

  • packages/random/src/int.js — the range-aware rejection-sampling
    staircase for randomInt. Each tier picks the smallest draw
    width that covers the requested range with reject fraction
    below 0.5, capping the worst-case reject rate across the entire
    safe-integer domain.
  • packages/random/src/read-uint.js — fixed-width little-endian
    unsigned readers backed by per-width scratch buffers (sharing a
    single 8-byte buffer measured ~2.5x slower under V8's
    element-kind specialization). Each reader zeroes its scratch
    prefix after the read.
  • packages/random/src/random.jsrandom() returns
    randomUint53(source) * 2 ** -53, deterministic across runs and
    engines.
  • packages/random/fast-check.jspure-rand v5 RandomGenerator
    adapters and the randomType builder. Documents the clone()
    alias semantics for serial keystreams without a snapshot facility.
  • packages/random/seeds.js — canonical bobsCoffee64 seed.
  • packages/random/random.types.d.tsRandomSource and
    PureRandomGenerator type definitions.
  • packages/chacha12/src/chacha12.js — the block function, state
    layout, and the IETF / RFC 7539 nonce convention.
  • packages/chacha12/test/chacha12.test.js — TC1, TC4, TC8
    keystream vectors from
    draft-strombergson-chacha-test-vectors-01, with a layout note
    documenting the DJB 8-byte-IV vs IETF 12-byte-nonce
    reconciliation at counter = 0.
  • packages/random/test/random.bench.js — the cross-source
    microbenchmark; lives in @endo/random rather than
    @endo/chacha12 because it drives both keystreams through the
    same samplers (and because chacha12 cannot devDep on random
    without a cycle — random already devDeps on chacha12).
  • packages/hex/test/{decode,encode}.bench.js,
    packages/ocapn/test/{codecs/passable-fuzz,syrup/fuzz}.test.js
    consumer-side rewires.

Security Considerations

@endo/chacha12 is positioned and documented as a PRNG, not a
cipher. The README explicitly steers cipher use cases to ChaCha20
or another 20-round implementation; the cipher package keyword
has been intentionally omitted. ChaCha12 reduces the cryptographic
safety margin relative to ChaCha20 (12 rounds vs 20); for
deterministic test fixtures, property-based testing, and fuzzing —
the consumer use cases in this PR — the extra speed is the right
tradeoff, but downstream users should not adopt this package for
confidentiality or for key derivation from caller-supplied seeds.

Neither package introduces new authority surface. makeChaCha12
is a pure deterministic function of its 32-byte key; the returned
function is hardened. @endo/random's samplers are pure functions
over their source argument, with module state limited to small
per-width scratch buffers that are zeroed after each read so no
random bytes linger across calls. No I/O, no entropy source, no
global state.

The hex/ocapn rewires add a workspace devDependency only — no
runtime dependency change for consumers of those packages.

Scaling Considerations

@endo/chacha12/BENCH.md reports the cross-source microbenchmark
(packages/random/test/random.bench.js). Headline numbers on the
test bed (Node 22.22.2, AMD Ryzen AI MAX+, x64, median of 10 runs):

Workload xorshift128+ ChaCha20 ChaCha12 C12/C20
Bulk bytes (MB/s) 696 190 281 1.48x
random() (ns/call) 17.2 45.4 35.1 1.29x
randomInt(0, 99) (ns/call) 11.1 15.6 14.3 1.10x

The bulk-bytes ratio is below the naive 20/12 = 1.67x because
per-block fixed costs are identical between ChaCha20 and ChaCha12.
The sampler workloads narrow the gap further because the per-call
envelope (range-aware staircase, mask-and-reject) is shared across
all sources and amortizes the keystream difference.

No new on-chain storage, message exchange, or persistent resource
consumption.

Documentation Considerations

Self-contained docs ship with each new package:

  • packages/random/README.mdRandomSource interface, subpath
    exports, determinism guarantees, scratch-buffer rationale.
  • packages/chacha12/README.mdmakeChaCha12 API, ChaCha12 vs
    ChaCha20 framing, keystream-length bound, verification.
  • packages/chacha12/BENCH.md — full benchmark report.
  • packages/random/SECURITY.md, packages/chacha12/SECURITY.md
    standard Endo security policy.
  • .changeset/endo-chacha12.md@endo/random: minor,
    @endo/chacha12: minor, @endo/hex: patch, @endo/ocapn: patch.

No existing data or deployment is affected — both packages are new,
and the hex/ocapn changes are confined to test/benchmark helpers.
No upgrade instructions are needed for downstream users of
@endo/hex or @endo/ocapn.

Testing Considerations

packages/random/test/:

  • random.test.jsrandom(source) range invariants,
    determinism, and a pinned 4-element golden float vector.
  • int.test.js — closed-interval invariants, endpoint coverage,
    bounds and unsafe-range error paths, determinism, and a
    draw-width sweep that exercises every tier of the
    rejection-sampling staircase (1-byte through 53-bit slow path).
  • pure-rand.test.jsadaptToPureRandomGenerator /
    adaptFromPureRandomGenerator round-trip equivalence with a
    fresh twin, plus distribution-shape sanity.
  • fast-check.test.js — drives fc.assert with a
    ChaCha12-backed randomType to confirm the adapter satisfies
    fast-check's contract end-to-end.
  • random.bench.js — local microbenchmarks against an inlined
    ChaCha20 reference (_chacha20.js) and an xorshift128+
    baseline (_xorshift.js).

packages/chacha12/test/chacha12.test.js — three published
ChaCha12 keystream vectors (TC1, TC4, TC8) from
draft-strombergson-chacha-test-vectors-01, plus block-boundary
equivalence (a 192-byte single pull vs piecewise 7/57/64/32/32
pulls from a freshly-seeded twin) and RandomSource-shape
conformance.

The @endo/hex benchmarks and @endo/ocapn fuzz tests now run
against the new chacha12 source; existing assertions are preserved.
No new CI jobs required.

Compatibility Considerations

  • @endo/random and @endo/chacha12 are new packages — no prior
    usage to break.
  • @endo/hex and @endo/ocapn change only test and benchmark
    internals; their published runtime surface is unchanged. The
    duplicated _xorshift.js helpers were never exported.

Upgrade Considerations

Pure additive change for production systems:

  • No runtime dependency added to @endo/hex or @endo/ocapn
    consumers (the @endo/chacha12 and @endo/random links are
    devDependencies).
  • No data migration, no on-chain implications.
  • Not a breaking change.

@changeset-bot

changeset-bot Bot commented May 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc336d4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@endo/random Major
@endo/chacha12 Major
@endo/hex Patch
@endo/ocapn Patch
@endo/chacha12-fast-check-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gibson042 gibson042 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts and suggestions on design and implementation.

I'd really like to see a more isolated core that can be used to construct multiple interfaces such as proposal-random-functions Random and pure-rand RandomGenerator.

Comment thread .changeset/endo-chacha12.md Outdated
the extra speed is generally the right tradeoff.

The keystream is verified against three published test vectors
from `draft-strombergson-chacha-test-vectors` (TC1, TC4, TC8).

@gibson042 gibson042 May 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread packages/chacha12/BENCH.md Outdated
Comment on lines +8 to +15
| Field | Value |
| -------- | ---------------------------------------------- |
| CPU | AMD Ryzen AI MAX+ 395 w/ Radeon 8060S (32 vCPU) |
| RAM | 128 GiB |
| OS | Linux 6.14 (Ubuntu 24.04) |
| Node | 22.22.2 |
| Arch | x64 |
| Harness | `packages/chacha12/test/random.bench.js` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This near-alignment... 😐

Comment thread packages/chacha12/README.md Outdated
Comment on lines +18 to +21
This package is the ChaCha12 sibling of `@endo/chacha20` (the
20-round variant) and `@endo/xorshift` (a non-cryptographic PRNG);
choose among them by package name based on the throughput vs margin
tradeoff a given consumer needs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all three packages cross-reference the other two? It seems valuable for them to implement and maintain a consistent API, and I'm wondering whether that and other commonalities should rely upon something like importing from an @endo/prng package.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the other two packages don't even exist! Comment stands if there are plans to introduce them, but otherwise:

Suggested change
This package is the ChaCha12 sibling of `@endo/chacha20` (the
20-round variant) and `@endo/xorshift` (a non-cryptographic PRNG);
choose among them by package name based on the throughput vs margin
tradeoff a given consumer needs.

Comment thread packages/chacha12/README.md Outdated
Comment on lines +34 to +37
The PRNG method names align with the
[TC39 proposal-random-functions](https://tc39.es/proposal-random-functions/)
(Stage 1) so consumers can swap in a `Random.Seeded` instance later
without changing call sites.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the proposal is at stage 1, this claim is too strong. I'd prefer to pare down the API to the point where such a swap becomes possible in the future without breaking changes here regardless of dramatic evolution in the proposal. Even if we need all four abilities (draw sub-unitary non-negative float, draw integer from closed interval, draw fresh Uint8Array, fill Uint8Array), perhaps it makes sense to start with just a block stream here and a more ergonomic translating wrapper in the previously-suggested common package?

import { BLOCK_SIZE, makeChaCha12 } from '@endo/chacha12';
import { decodeHex } from '@endo/hex';
import { makeRandomFromBlockFiller } from '@endo/prng';

const { pullBlock }: { pullBlock: (out: Uint8Array) => void } = makeChaCha12(
  decodeHex('b0b5c0ffeefacade'.repeat(4)),
);

// `makeRandomFromBlockFiller` makes a proposal-random-functions Random from a
// `pullBlock`-shaped Uint8Array-filling function.
const prng: {
  random: () => number;
  int: (lo: number, hi: number) => number;
  bytes: (n: number) => Uint8Array;
  fillBytes: (buf: Uint8Array, start?: number, end?: number) => Uint8Array;
} = makeRandomFromBlockFiller(pullBlock, { blockSize: BLOCK_SIZE });

assert(0 <= prng.random() && prng.random() < 1);
assert(Math.abs(prng.int(10, 20) - 15) <= 5);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for bonus points, we should also be able to represent a ChaCha12 as a pure-rand RandomGenerator for compatibility with fast-check randomType.

import fc from 'fast-check';
import type { RandomType as FastCheckRandomType } from 'fast-check';
import type { RandomGenerator } from 'pure-rand/types/RandomGenerator';
import {
  BLOCK_SIZE,
  makeChaCha12Kit,
  adaptToPureRandomGenerator,
} from '@endo/chacha12';

/*
const adaptToPureRandomGenerator = ({
  getState,
  pullBlock,
}: ChaCha12Kit): RandomGenerator => {
  const buf8 = new Uint8Array(BLOCK_SIZE);
  const buf32 = new Int32Array(buf8.buffer);
  let prevBuf32Idx = 0;
  return {
    clone: () =>
      adaptToPureRandomGenerator(makeChaCha12Kit({ state: getState() })),
    next: () => {
      if (prevBuf32Idx === 0) {
        // Refill and reset.
        pullBlock(buf8);
        prevBuf32Idx = buf32.length;
      }
      prevBuf32Idx -= 1;
      return buf32[prevBuf32Idx];
    },
    getState: () => getState(),
  };
};
*/

const randomGenerator: RandomGenerator = adaptToPureRandomGenerator(
  makeChaCha12Kit({ seed: decodeHex('b0b5c0ffeefacade'.repeat(4)) }),
);

// Our fast-check adapter.
const chaCha12RandomType: FastCheckRandomType = (int32Seed: number) => {
  const chaChaSeed = new Int32Array(8).fill(int32Seed);
  return adaptToPureRandomGenerator(
    makeChaCha12Kit({ seed: new Uint8Array(chaChaSeed.buffer) }),
  );
};

// A deterministically failing PBT fed by ChaCha12.
fc.assert(
  fc.property(fc.integer(), n => n >= 0),
  { randomType: chaCha12RandomType, seed: 0xdeadbeef },
);

Comment thread packages/chacha12/README.md Outdated
Comment on lines +68 to +73
- `random()` reads 8 keystream bytes, masks the top 11 bits, and
divides by `2 ** 53`.
Each respective returned value from streams with the same seed is
equal across runs and engines because the float is computed by
dividing a 53-bit integer by an exact power of two, with no
engine-dependent rounding.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `random()` reads 8 keystream bytes, masks the top 11 bits, and
divides by `2 ** 53`.
Each respective returned value from streams with the same seed is
equal across runs and engines because the float is computed by
dividing a 53-bit integer by an exact power of two, with no
engine-dependent rounding.
- `random()` ensures that each respective returned value from streams with the
same seed is equal across runs and engines by internally constructing a 53-bit
integer and dividing it by `2 ** 53` (which avoids engine-dependent rounding).

Comment thread packages/chacha12/src/random.js Outdated
Comment on lines +85 to +90
/**
* Pulls 8 keystream bytes, masks the high 11 bits to 0, and
* divides the resulting 53-bit integer by `2 ** 53`. The result
* is a float in `[0, 1)`, deterministic for a given seed across
* engines.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Pulls 8 keystream bytes, masks the high 11 bits to 0, and
* divides the resulting 53-bit integer by `2 ** 53`. The result
* is a float in `[0, 1)`, deterministic for a given seed across
* engines.
*/
/**
* Produces a float in `[0, 1)` that is deterministic for a given seed
* across all engines by constructing an unsigned 53-bit integer and
* dividing it by `2 ** 53`.
*/

Comment thread packages/chacha12/src/random.js Outdated
Comment on lines +114 to +123
// Read a 32-bit unsigned little-endian integer from the keystream.
// Used by `int()` for ranges that fit in 32 bits, avoiding the
// float round-trip through `random()`.
const readU32 = () => {
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
const b3 = readByte();
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Read a 32-bit unsigned little-endian integer from the keystream.
// Used by `int()` for ranges that fit in 32 bits, avoiding the
// float round-trip through `random()`.
const readU32 = () => {
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
const b3 = readByte();
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
};
/** Read a 16-bit unsigned little-endian integer from the keystream. */
const readU16 = () => {
const b0 = readByte();
const b1 = readByte();
return (b0 | (b1 << 8)) >>> 0;
};
/** Read a 24-bit unsigned little-endian integer from the keystream. */
const readU24 = () => {
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
return (b0 | (b1 << 8) | (b2 << 16)) >>> 0;
};
/** Read a 32-bit unsigned little-endian integer from the keystream. */
const readU32 = () => {
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
const b3 = readByte();
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
};
/** Read a 53-bit unsigned little-endian integer from the keystream. */
const readU53 = () => {
// Assemble the integer from two 32-bit halves, masking off the top
// 11 bits of the upper half to leave 21 high bits + 32 low bits =
// 53 bits.
const lo = readU32();
const hi24 = readU24();
const hi21 = hi24 & 0x1fffff;
return hi21 * 4294967296 + lo;
};

Comment thread packages/chacha12/src/random.js Outdated
Comment on lines +91 to +112
const random = () => {
// Read 8 bytes from the buffer; assemble into a non-negative
// 53-bit integer using two halves.
//
// High 32 bits: little-endian. We mask off the top 11 bits to
// leave 21 high bits + 32 low bits = 53 bits.
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
const b3 = readByte();
const b4 = readByte();
const b5 = readByte();
const b6 = readByte();
const b7 = readByte();
const lo = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
// Mask top 11 bits of the high word to 0, keeping 21 bits.
const hi = ((b4 | (b5 << 8) | (b6 << 16) | (b7 << 24)) >>> 0) & 0x1fffff;
// hi * 2 ** 32 + lo, divided by 2 ** 53:
// = hi * 2 ** -21 + lo * 2 ** -53
// Computed as a single multiply by POW2_M53 of the 53-bit int.
return (hi * 4294967296 + lo) * POW2_M53;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const random = () => {
// Read 8 bytes from the buffer; assemble into a non-negative
// 53-bit integer using two halves.
//
// High 32 bits: little-endian. We mask off the top 11 bits to
// leave 21 high bits + 32 low bits = 53 bits.
const b0 = readByte();
const b1 = readByte();
const b2 = readByte();
const b3 = readByte();
const b4 = readByte();
const b5 = readByte();
const b6 = readByte();
const b7 = readByte();
const lo = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
// Mask top 11 bits of the high word to 0, keeping 21 bits.
const hi = ((b4 | (b5 << 8) | (b6 << 16) | (b7 << 24)) >>> 0) & 0x1fffff;
// hi * 2 ** 32 + lo, divided by 2 ** 53:
// = hi * 2 ** -21 + lo * 2 ** -53
// Computed as a single multiply by POW2_M53 of the 53-bit int.
return (hi * 4294967296 + lo) * POW2_M53;
};
const random = () => readU53() * POW2_M53;

Comment thread packages/chacha12/src/random.js Outdated
Comment on lines +143 to +172
// Fast path: ranges up to `2 ** 32` use a single 32-bit
// keystream draw and pure integer arithmetic to eliminate modulo
// bias. Discard draws in `[limit32, 2 ** 32)` so the retained
// `limit32` buckets divide evenly into `range`.
if (range <= 0x100000000) {
const limit32 = 0x100000000 - (0x100000000 % range);
for (;;) {
const u = readU32();
if (u < limit32) {
return lo + (u % range);
}
}
}
// Slow path: ranges up to `2 ** 53`. Rejection sampling on a
// 53-bit integer drawn directly from the keystream as a pair of
// 32-bit halves; the 11 high bits of the high word are masked
// off. No floating-point arithmetic is involved in selecting
// the integer.
//
// `limit53 = floor(2 ** 53 / range) * range`. Discard draws in
// `[limit53, 2 ** 53)` and return `lo + (u % range)`.
const limit53 = Math.floor(9007199254740992 / range) * range;
for (;;) {
const lo32 = readU32();
const hi21 = readU32() & 0x1fffff;
const u = hi21 * 4294967296 + lo32; // exact 53-bit non-negative int.
if (u < limit53) {
return lo + (u % range);
}
}

@gibson042 gibson042 May 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's eliminate some pathologies (e.g., int(0, 2**31) should not throw away almost half of the draws because its upper acceptable limit ends up at 2**31 + 1).

Suggested change
// Fast path: ranges up to `2 ** 32` use a single 32-bit
// keystream draw and pure integer arithmetic to eliminate modulo
// bias. Discard draws in `[limit32, 2 ** 32)` so the retained
// `limit32` buckets divide evenly into `range`.
if (range <= 0x100000000) {
const limit32 = 0x100000000 - (0x100000000 % range);
for (;;) {
const u = readU32();
if (u < limit32) {
return lo + (u % range);
}
}
}
// Slow path: ranges up to `2 ** 53`. Rejection sampling on a
// 53-bit integer drawn directly from the keystream as a pair of
// 32-bit halves; the 11 high bits of the high word are masked
// off. No floating-point arithmetic is involved in selecting
// the integer.
//
// `limit53 = floor(2 ** 53 / range) * range`. Discard draws in
// `[limit53, 2 ** 53)` and return `lo + (u % range)`.
const limit53 = Math.floor(9007199254740992 / range) * range;
for (;;) {
const lo32 = readU32();
const hi21 = readU32() & 0x1fffff;
const u = hi21 * 4294967296 + lo32; // exact 53-bit non-negative int.
if (u < limit53) {
return lo + (u % range);
}
}
// Draw the fewest bytes necessary to either match the requested range or
// cover at least two repetitions of it, discarding draws in the range
// beyond the last full repetition to avoid modulo bias while keeping the
// discard fraction acceptably low.
const { draw, limit } = (() => {
// Read one byte to cover a range of exactly 256 or up to 128.
if (range === 0x100 || range <= 0x80) {
return { draw: readByte, limit: 0x100 - (0x100 % range) };
}
// Read two bytes to cover a range of exactly 65536 or up to 32768.
if (range === 0x10000 || range <= 0x8000) {
return { draw: readU16, limit: 0x10000 - (0x10000 % range) };
}
// ...and so on.
if (range === 0x1000000 || range <= 0x800000) {
return { draw: readU24, limit: 0x1000000 - (0x1000000 % range) };
}
if (range === 0x100000000 || range <= 0x80000000) {
return { draw: readU32, limit: 0x100000000 - (0x100000000 % range) };
}
// The final limit is a 53-bit integer, which will discard just under half
// of all possible draws when the range is just slightly above 2**52.
return {
draw: readU53,
limit: Math.floor(9007199254740992 / range) * range,
};
})();
for (;;) {
const n = draw();
if (n < limit) {
return lo + (n % range);
}
}

Comment thread packages/chacha12/src/random.js Outdated
Comment on lines +224 to +231
if (!(seed instanceof Uint8Array)) {
throw TypeError('seed must be a Uint8Array');
}
if (seed.length !== 32) {
throw TypeError(
`seed must be 32 bytes (got ${seed.length}); ChaCha12 keys are 256 bits`,
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: align with patterns in chacha12.js.

Suggested change
if (!(seed instanceof Uint8Array)) {
throw TypeError('seed must be a Uint8Array');
}
if (seed.length !== 32) {
throw TypeError(
`seed must be 32 bytes (got ${seed.length}); ChaCha12 keys are 256 bits`,
);
}
if (!(seed instanceof Uint8Array) || seed.length !== 32) {
throw TypeError('ChaCha12 seed must be a 32-byte Uint8Array');
}

kriskowal added a commit that referenced this pull request May 4, 2026
Per Richard Gibson's review of #3232: pare @endo/chacha12
to the keystream primitive and introduce @endo/random as a
source-agnostic library of per-function samplers.

@endo/random exposes randomInt, randomBytes, randomFillBytes, and
random as separate tree-shakeable modules, each parameterized by a
RandomSource (an object with a single fillBytes method). Names follow
the TC39 proposal-random-functions translation Random.method ->
randomMethod. Adapters in tools/ bridge to a fixed-block-size
keystream filler, to/from pure-rand's RandomGenerator (the contract
fast-check uses), and a fast-check randomType helper. tools/test-seeds
publishes the canonical Bob's Coffee Façade seed.

@endo/chacha12 now exports BLOCK_SIZE, chacha12Block, chacha12State
(with optional nonce/counter), makeChaCha12Source, and
assertChaCha12Key only. The sampling functions and their tests have
moved to @endo/random; chacha12 keystream KAT vectors stay.

Adopted Richard's specific suggestions: range-aware rejection sampling
in randomInt (single-byte path for ranges <= 128 etc.), readU16/24/32
/53 helpers, random = readU53 * 2^-53, chacha12State(key) shorthand,
hyperlink to draft-strombergson-chacha-test-vectors-01, BENCH.md table
alignment, and a shared seed file. Strict-fork clone() semantics on
RandomSource are deferred.

Rewires @endo/hex benches and @endo/ocapn fuzz tests to draw bytes
from @endo/chacha12 through @endo/random samplers using the shared
seed. All 24 random tests, 9 chacha12 keystream tests, 13 hex tests,
and 282 ocapn tests pass under both lockdown and shims-only configs.
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch from 7fd7a67 to 9456fb1 Compare May 4, 2026 04:56
@kriskowal kriskowal requested a review from gibson042 May 4, 2026 04:57
@kriscendobot

Copy link
Copy Markdown

@gibson042 — thank you for the thorough first-round review. The PR has gone through several redesign rounds since (PR #75 in the mirror tracks the work-in-progress); each of your comments is now addressed. Mapping comment-by-comment:

Surface redesign

comment 3177082445 (README.md:37 — pare down to a block stream + translating wrapper). Adopted as the architectural pivot. @endo/chacha12 now exposes a single public entry point, makeChaCha12, that returns a (out: Uint8Array) => void function — matching crypto.getRandomValues ergonomics minus the return value, and conforming directly to @endo/random's RandomSource type. All sampling distributions (random, randomInt) live in a separate workspace package @endo/random and accept any RandomSource. No public Random class, no built-in API surface beyond the translating wrappers themselves; consumers compose what they need.

comment 3177216582 (pure-rand RandomGenerator adapter for fast-check). Implemented in @endo/random/fast-check.js. adaptToPureRandomGenerator(source) → RandomGenerator and the inverse adaptFromPureRandomGenerator(rg) → RandomSource round-trip cleanly, plus a makeRandomTypeFromSeed(makeSourceFromSeed) builder that returns a fast-check-compatible randomType function. The module imports nothing from pure-rand or fast-check; it depends only on the structural compatibility of the RandomGenerator interface.

comments 3176918766 / 3177404457 (cross-reference among packages, "the other two packages don't even exist"). Resolved per your second comment: speculative references to @endo/chacha20 / @endo/xorshift are removed throughout. The xorshift128+ and ChaCha20 baselines used by the comparative benchmark live in packages/random/test/_xorshift.js and packages/random/test/_chacha20.js as in-package test artifacts only; we are not pursuing those packages.

chacha12 keystream

comment 3177309309 (chacha12.js:177chacha12State(key) shorthand). Adopted. makeChaCha12 calls chacha12State(key) directly.

comment 3177312921 (chacha12.js:140 — optional nonce / counter). Adopted with the suggested signature chacha12State(key, nonce = undefined, counter = 0).

comment 3177399312 (random.js:231 — error-message style alignment). Adopted. All chacha12 throws now follow the chacha12 … prefix style.

@endo/random samplers

comment 3177117515 (README determinism wording for random()). Adopted verbatim in both the random() JSDoc and @endo/random/README.md: "each respective returned value from streams with the same seed is equal across runs and engines by internally constructing a 53-bit integer and dividing it by 2 ** 53 (which avoids engine-dependent rounding)".

comment 3177123420 (README:75 — misplaced sentence). Removed from the Determinism section.

comment 3177327612 (random.js:90 — JSDoc). Adopted.

comment 3177339416 (random.js:123readU16/24/32/53 helpers). Adopted. Renamed in line with the Uint16Array precedent: randomUint8, randomUint16, randomUint24, randomUint32, randomUint53 (in packages/random/src/read-uint.js). Each helper is hardened and uses a fixed-size scratch buffer per width plus a long-lived DataView for endian-correct reads.

comment 3177346557 (random.js:112random = readU53() * POW2_M53). Adopted.

comment 3177389843 (random.js:172 — range-aware int() rejection sampling). Adopted. randomInt selects a draw width matching the requested range (1, 2, 3, 4, or 8 bytes) and rejection-samples within the corresponding capacity. Per-draw reject probability p never exceeds 0.5; consecutive draws are independent so P(N > k) = p^k decays exponentially and E[N] = 1/(1−p) ≤ 2. The range === capacity short-circuits handle the exact-power case. Bench: randomInt(0, 99) reads exactly one byte per draw.

Test infrastructure and changeset

comment 3176903202 (.changeset/endo-chacha12.md:26 — hyperlink). Done.

comment 3177305214 (shared seed). Done. @endo/random/seeds.js exports the canonical bobsCoffee64 32-byte seed; the hex benches and ocapn fuzz tests import it.

comment 3176909949 (BENCH.md:15 — table near-alignment). Fixed and the table refreshed with median-of-10 numbers from a fresh measurement run.

Beyond your inline comments

A few changes the redesign rounds added that are worth flagging:

  • The RandomSource interface is a function, not an object with a method. This makes crypto.getRandomValues, makeChaCha12(seed), and any future entropy source structurally compatible without an adapter.
  • chacha12Block and chacha12State are no longer in the package's public exports. They remain in src/chacha12.js for in-package known-answer tests via relative import.
  • DataView vs shift-and-OR was benched (your tooling intuition) and the verdict split: cached module-scope DataView wins for randomUint* (21–49% faster), per-call new DataView loses for chacha12State/chacha12Block (3.4× / 12% slower from constructor cost). Both sites land on the faster choice with an explanatory comment.
  • Per-width scratch buffers in read-uint.js (one buffer per draw width rather than a shared 8-byte slice) are 2.5× faster on the integer hot paths under V8 12.4.
  • @endo/random and @endo/chacha12 are at 100% test coverage (statements / branches / functions / lines). Defensive runtime validations that survived only as "throw on bad input" unit-test surface are removed; a 256-GiB counter-overflow guard remains as a correctness invariant behind /* c8 ignore */.
  • The bench harness lives at packages/random/test/random.bench.js and refreshed numbers are in packages/chacha12/BENCH.md: chacha12 throughput is 1.48× chacha20 on bulk fills, ~1.3× on random(), ~1.1× on randomInt(0, 99) (where the rejection-sampling envelope dominates per-call cost and amortizes the keystream-cost difference).

Happy to walk through any specific decision in more depth.

kriskowal added a commit that referenced this pull request May 5, 2026
Per Richard Gibson's review of #3232: pare @endo/chacha12
to the keystream primitive and introduce @endo/random as a
source-agnostic library of per-function samplers.

@endo/random exposes randomInt, randomBytes, randomFillBytes, and
random as separate tree-shakeable modules, each parameterized by a
RandomSource (an object with a single fillBytes method). Names follow
the TC39 proposal-random-functions translation Random.method ->
randomMethod. Adapters in tools/ bridge to a fixed-block-size
keystream filler, to/from pure-rand's RandomGenerator (the contract
fast-check uses), and a fast-check randomType helper. tools/test-seeds
publishes the canonical Bob's Coffee Façade seed.

@endo/chacha12 now exports BLOCK_SIZE, chacha12Block, chacha12State
(with optional nonce/counter), makeChaCha12Source, and
assertChaCha12Key only. The sampling functions and their tests have
moved to @endo/random; chacha12 keystream KAT vectors stay.

Adopted Richard's specific suggestions: range-aware rejection sampling
in randomInt (single-byte path for ranges <= 128 etc.), readU16/24/32
/53 helpers, random = readU53 * 2^-53, chacha12State(key) shorthand,
hyperlink to draft-strombergson-chacha-test-vectors-01, BENCH.md table
alignment, and a shared seed file. Strict-fork clone() semantics on
RandomSource are deferred.

Rewires @endo/hex benches and @endo/ocapn fuzz tests to draw bytes
from @endo/chacha12 through @endo/random samplers using the shared
seed. All 24 random tests, 9 chacha12 keystream tests, 13 hex tests,
and 282 ocapn tests pass under both lockdown and shims-only configs.
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch from 9456fb1 to f40e94a Compare May 5, 2026 19:01

@gibson042 gibson042 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The packages/random/$noun.js files that just export { … } from './src/$noun.js'; clutter up the package root; I understand why we don't want to expose the src/ files directly, but perhaps we should establish a convention of storing such files in an exports/ sibling directory.

Aside from that, I think the pure-rand v8 RandomSource interface would be a much better interop target than the v5 one.

Neither of these are blocking concerns, and the rest of the comments won't need another round of review.

Comment thread packages/random/random.types.d.ts Outdated
Comment thread packages/random/random.types.d.ts Outdated
Comment on lines +15 to +36
/**
* The shape of a `pure-rand` v5 `RandomGenerator`, the contract used
* by `fast-check` to drive property-based tests via the `randomType`
* parameter. We restate it locally rather than depend on `pure-rand`
* directly so `@endo/random` stays standalone.
*
* `next()` returns a `[value, nextGenerator]` tuple where `value` is
* a 32-bit signed integer in `[min(), max()]` (typically
* `[-0x80000000, 0x7fffffff]`). The returned generator MAY be the
* same instance with mutated state.
*
* `clone()` is required by `pure-rand` v5; for serial keystreams
* without state-snapshot support the adapter returns an alias whose
* state is shared with the original.
*/
export interface PureRandomGenerator {
next(): [number, PureRandomGenerator];
unsafeNext(): number;
clone(): PureRandomGenerator;
min(): number;
max(): number;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread packages/random/fast-check.js Outdated
Comment on lines +108 to +118
/**
* Builds a `fast-check`-compatible `randomType` function from a
* factory that turns a 32-byte seed into a `RandomSource`.
* `fast-check` calls the returned function with a 32-bit signed
* integer seed and expects back a `pure-rand`-shaped
* `RandomGenerator`. The integer seed is broadcast across a 32-byte
* buffer (compatible with ChaCha-style key inputs).
*
* @param {(seed: Uint8Array) => RandomSource} makeSourceFromSeed
* @returns {(int32Seed: number) => PureRandomGenerator}
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +62 to +68
// Range-aware rejection sampling: each tier of the draw-width
// staircase covers a specific range domain. This test exercises the
// boundaries: single-byte (range <= 128 and exact 256), two-byte (up
// to 32768 and exact 65536), three-byte (up to 8388608), four-byte
// (up to 2^31 and exact 2^32), and the 53-bit slow path (range >
// 2^32). Each branch produces values in the requested closed
// interval.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want testing that verifies the expected count of read bytes for ranges of particular size?

Comment thread packages/random/test/fast-check.test.js Outdated
Comment on lines +12 to +25
test('chaCha12-backed randomType drives fc.assert successfully', t => {
// A property that is always true exercises the adapter: fast-check
// invokes `chaCha12RandomType(seed)`, pulls values via
// RandomGenerator.next() / unsafeNext(), and reports no failures.
fc.assert(
fc.property(fc.integer(), () => true),
/** @type {any} */ ({
randomType: chaCha12RandomType,
seed: 0xdeadbeef,
numRuns: 100,
}),
);
t.pass();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise here; can we extend this to verify that the value passed as randomType is called with the provided seed and that bytes are actually read from the value it returns?

kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 6, 2026
Per Richard Gibson's review of endojs/endo#3232: pare @endo/chacha12
to the keystream primitive and introduce @endo/random as a
source-agnostic library of per-function samplers.

@endo/random exposes randomInt, randomBytes, randomFillBytes, and
random as separate tree-shakeable modules, each parameterized by a
RandomSource (an object with a single fillBytes method). Names follow
the TC39 proposal-random-functions translation Random.method ->
randomMethod. Adapters in tools/ bridge to a fixed-block-size
keystream filler, to/from pure-rand's RandomGenerator (the contract
fast-check uses), and a fast-check randomType helper. tools/test-seeds
publishes the canonical Bob's Coffee Façade seed.

@endo/chacha12 now exports BLOCK_SIZE, chacha12Block, chacha12State
(with optional nonce/counter), makeChaCha12Source, and
assertChaCha12Key only. The sampling functions and their tests have
moved to @endo/random; chacha12 keystream KAT vectors stay.

Adopted Richard's specific suggestions: range-aware rejection sampling
in randomInt (single-byte path for ranges <= 128 etc.), readU16/24/32
/53 helpers, random = readU53 * 2^-53, chacha12State(key) shorthand,
hyperlink to draft-strombergson-chacha-test-vectors-01, BENCH.md table
alignment, and a shared seed file. Strict-fork clone() semantics on
RandomSource are deferred.

Rewires @endo/hex benches and @endo/ocapn fuzz tests to draw bytes
from @endo/chacha12 through @endo/random samplers using the shared
seed. All 24 random tests, 9 chacha12 keystream tests, 13 hex tests,
and 282 ocapn tests pass under both lockdown and shims-only configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch 2 times, most recently from 72e7809 to 2effced Compare May 9, 2026 00:10
@kriskowal

Copy link
Copy Markdown
Member Author

The packages/random/$noun.js files that just export { … } from './src/$noun.js'; clutter up the package root; I understand why we don't want to expose the src/ files directly, but perhaps we should establish a convention of storing such files in an exports/ sibling directory.

Aside from that, I think the pure-rand v8 RandomSource interface would be a much better interop target than the v5 one.

Neither of these are blocking concerns, and the rest of the comments won't need another round of review.

There’s a deeper reason. Not all tools and older versions of Node.js don’t recognize the exports directive in package.json, so mirroring key to value is necessary as a fallback. We could just put these thunks in src/ otherwise.

@kriskowal kriskowal requested a review from gibson042 May 10, 2026 17:24
@kriskowal

Copy link
Copy Markdown
Member Author

Another note: I pulled out the fastcheck adapters. The depth of that conversation led me to understand that there are at least two random number adapters we would want depending on the fastcheck version and that warranted a dedicated package or or packages if we sought to use our prng instead of one that ships with fastcheck.

I also missed a couple notes in the review and will finish revising Monday.

@gibson042

gibson042 commented May 11, 2026

Copy link
Copy Markdown
Member

The packages/random/$noun.js files that just export { … } from './src/$noun.js'; clutter up the package root; I understand why we don't want to expose the src/ files directly, but perhaps we should establish a convention of storing such files in an exports/ sibling directory.

There’s a deeper reason. Not all tools and older versions of Node.js don’t recognize the exports directive in package.json, so mirroring key to value is necessary as a fallback. We could just put these thunks in src/ otherwise.

What software specifically are you concerned about here? The files in question are ECMAScript modules that use import statements, which are supported in Node.js without a CLI flag since version 12.7.0, while package.json exports are supported since Node.js version 12.20.0. Is the gap between Node.js versions 12.7.0 and 12.20.0 really relevant? Just how far back does Endo support extend?

Aside from that, I think the pure-rand v8 RandomSource interface would be a much better interop target than the v5 one.

Another note: I pulled out the fastcheck adapters. The depth of that conversation led me to understand that there are at least two random number adapters we would want depending on the fastcheck version and that warranted a dedicated package or or packages if we sought to use our prng instead of one that ships with fastcheck.

I don't think we need to support anything other than the pure-rand v8 RandomSource interface, which is preferred by the latest version of fast-check and is the only one for which I have a use case (because I have seen Node.js crashes in #3053 that I suspect were ultimately caused by the PRNG internal to fast-check). And by "support" I mean that it must be possible to write source text as shown in #3232 (comment) :

import fc from 'fast-check'; // ^4.6.0
import type { RandomType as FastCheckRandomType } from 'fast-check'; // ^4.6.0
import type {
  RandomGenerator as PureRandGenerator,
} from 'pure-rand/types/RandomGenerator'; // ^8.4.0
import { $EXPORT as makeChaCha12Source } from '@endo/chacha12;
import { adaptToChaCha12Seed, adaptToPureRandV8Generator } from './adapters.js';

// Our fast-check adapter.
const chaCha12RandomType: FastCheckRandomType = (int32Seed: number) => {
  const randomGenerator: PureRandGenerator = adaptToPureRandV8Generator(
    makeChaCha12Source({ seed: adaptToChaCha12Seed(int32Seed) }),
  );
  return randomGenerator;
};

// A deterministically failing PBT fed by ChaCha12.
fc.assert(
  fc.property(fc.integer(), n => n >= 0),
  { randomType: chaCha12RandomType, seed: 0xdeadbeef },
);

And the ability to write and use those adaptToChaCha12Seed and adaptToPureRandV8Generator functions should be covered by tests even if the package doesn't export anything related to them. The reason why this matters is that the pure-rand RandomGenerator interface includes a getState method that requires @endo/chacha12 to export more than just a function whose return value has the same shape as Crypto.getRandomValues.

@kriskowal

Copy link
Copy Markdown
Member Author

The last hold out for exports support is eslint. I prompted a report that reveals there may be a workaround by using a forked plugin. endojs/endo-but-for-bots#218

kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 11, 2026
Address upstream review feedback on PR #3232 by exposing the
keystream's internal state for introspection and resumption.

`makeChaCha12(key)` now returns a `ChaCha12Generator` record
`{ next, getState, clone, fillRandomBytes }` instead of a bare
`(out: Uint8Array) => void` function.  The new shape is structurally
compatible with `pure-rand` v8's `RandomGenerator` (the contract
`fast-check@4` consumes via its `randomType` parameter):

  - `next()` returns a signed int32 in `[-0x80000000, 0x7fffffff]`
    by reading 4 little-endian keystream bytes.
  - `getState()` returns a 34-element `readonly number[]` snapshot
    `[base0..base15, counter, offset, block0..block15]`.
  - `clone()` returns a fully independent generator at the same
    position (not an alias).
  - `fillRandomBytes(out)` preserves the original byte-keystream
    entry point and conforms to `@endo/random`'s `RandomSource`.

Add `makeChaCha12FromState(state)` for deterministic resumption
from a snapshot, completing the pure-rand-style paired
`makeXxx` / `makeXxxFromState` factory convention.

Update in-tree consumers (`@endo/random`, `@endo/hex`,
`@endo/ocapn`) to destructure `fillRandomBytes` from the returned
record.  The migration is mechanical:

    -const fillRandomBytes = makeChaCha12(seed);
    +const { fillRandomBytes } = makeChaCha12(seed);

Add a new private package `@endo/chacha12-fast-check-test` that
drives `fast-check@4`'s property-based tester through the new
chacha12 surface as its `randomType` source.  The package depends
on `fast-check@^4` directly; the production-shaped fast-check /
pure-rand adapters remain in the separately-designed
`@endo/random-fast-check` package.

The new chacha12 unit tests cover the int32 contract for `next`,
the round-trip property for `getState` /
`makeChaCha12FromState` (across every offset 0..64 to catch
block-boundary off-by-ones), `clone` independence, and malformed
state rejection.  18 chacha12 tests + 7 fast-check integration
tests pass.

Refs: endojs/endo#3232 (comment)
Refs: #75

import { chacha12Block, chacha12State, makeChaCha12 } from '../src/chacha12.js';

// Inline hex helpers: `@endo/hex` already depends on `@endo/chacha12`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could break the cycle with a/the hex-test package.

kriskowal added a commit to kriskowal/garden that referenced this pull request May 14, 2026
… steward

User correction after the liaison posted an explanatory comment on
endojs/endo#3232 directly (kriskowal-identity) instead of routing
through the steward (kriscendobot-identity).

The principle: kriskowal's identity carries maintainer weight on
primary repos (endojs/endo, agoric/agoric-sdk, etc.) and is reserved
for actions that genuinely require it (reviews, approvals, merges).
Comments are bot-side bookkeeping and belong to the bot. The post-
handoff explanatory comment on an upstream PR after a boatman ferry
should land via the steward, posted under kriscendobot.

Mechanism: write a message-to-steward journal entry with the comment
body and target PR; the steward picks it up on its next cycle and
posts. Adds a small delay (one steward cycle) but preserves identity
discipline.

Touches:
- roles/boatman/AGENT.md: new norm 'Comments on primary upstream
  repos route through the steward', alongside the existing 'Source-
  side cross-link only' norm. Pushes still go under kriskowal (gated
  by identity_switch_authorized); comments do not.
- roles/liaison/AGENT.md: new operating norm 'No comments on primary
  repos under the kriskowal identity'.

Carve-out: comments on the garden's own repos (endojs/endo-but-for-bots,
kriskowal/garden) under kriskowal are fine. Those are the garden, not
primary.
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch from 4d3c969 to 04664e5 Compare May 14, 2026 04:25
Comment thread .changeset/chacha12-next-getstate.md Outdated
Comment on lines +46 to +57
### Why

[Upstream review feedback on PR #3232](https://github.com/endojs/endo/pull/3232#issuecomment-4421637048)
asked for `@endo/chacha12` to expose enough of its keystream state
to support a `pure-rand` v8 / `fast-check` v4 adapter. The closure
returned by the previous shape encapsulated `baseState`, `counter`,
`offset`, and `block` with no accessor, so an out-of-package
adapter could not snapshot or clone the keystream.

The new sibling package `@endo/chacha12-fast-check-test` (private)
exercises the surface end-to-end against `fast-check@4`'s
`randomType` parameter.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please omit gratuitous process comment.

Comment thread .changeset/endo-chacha12.md Outdated
Comment on lines +26 to +31
- `@endo/random/fast-check.js` -- bidirectional adapters between
`RandomSource` and `pure-rand`'s `RandomGenerator` (the contract
`fast-check` uses via the `randomType` parameter), plus a
`randomType` builder. Imports nothing from `pure-rand` or
`fast-check`; depends only on those interfaces being structurally
compatible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer true.

Comment thread .changeset/endo-chacha12.md Outdated
Comment on lines +35 to +42
Add `@endo/chacha12`: a pure-JavaScript ChaCha12 keystream. The
factory `makeChaCha12(key)` returns a `(out: Uint8Array) => void`
function that conforms to `@endo/random`'s `RandomSource` and to
`crypto.getRandomValues`-style ergonomics; pass it directly to the
samplers. The keystream is cross-checked against three published
ChaCha12 test vectors from
[`draft-strombergson-chacha-test-vectors-01`](https://datatracker.ietf.org/doc/html/draft-strombergson-chacha-test-vectors-01)
(TC1, TC4, TC8).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface documented here is stale. We changed to an interface consistent with pure-rand-v8, structurally, with a fillRandomBytes function that satisfies RandomSource, like crypto.getRandomBytes.

Comment thread .changeset/endo-chacha12.md Outdated
Comment on lines +44 to +48
Rewires the `@endo/hex` encode/decode benchmarks and the
`@endo/ocapn` syrup and passable fuzz tests to draw deterministic
bytes from `@endo/chacha12` directly, using the shared seed from
`@endo/random/seeds.js`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit as not interesting to package authors updating dependencies.

Comment thread .changeset/chacha12-next-getstate.md Outdated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have only made one change in this release cycle, when this PR merges. Please consolidate these changeset.

* @returns {ChaCha12Generator}
*/
const makeGenerator = (
baseState,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that we are indeed retaining this. That’s a foot-gun. Please allocate your own copy.

Comment on lines +36 to +44
// Engine-portable nanosecond timer.
const hasHrtime =
typeof globalThis.process === 'object' &&
globalThis.process !== null &&
typeof globalThis.process.hrtime === 'function' &&
typeof globalThis.process.hrtime.bigint === 'function';
const nowNs = hasHrtime
? () => Number(globalThis.process.hrtime.bigint())
: () => Date.now() * 1_000_000;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, frankly, presume that Ava tests are running under Node.js 20+.

// from `block` to `out` we route through `set` after a subarray view.
// This is essentially the same as `copySet`; included as a sanity
// probe.
const copyCopywin = (out, i, block, offset, n) => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this copyWithin

Comment thread packages/random/CHANGELOG.md Outdated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be reframed for this package and mostly deleted.

Comment thread packages/random/README.md Outdated
Comment on lines +24 to +27
| TC39 proposal | `@endo/random` |
| --- | --- |
| `Random.random()` | `random(source)` |
| `Random.int(lo, hi)` | `randomInt(source, lo, hi)` |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use prettier on this. Leave a note to do this in general for all new or altered Markdown documents going forward.

kriscendobot pushed a commit to kriskowal/garden that referenced this pull request May 14, 2026
Three recurring patterns from kriskowal's review of endojs/endo#3232:

- Changeset hygiene (extend skills/changeset-discipline): one
  changeset per release cycle, keep the body current as the PR
  evolves, omit implementation detail, no process commentary.
- Gratuitous renames (new skills/rename-discipline): leave existing
  identifiers alone unless the rename earns its place in the diff;
  a "gratuitous rename" review comment is a revert, not a defense.
- Equivalence assertion (extend skills/regression-evidence): a
  code comment or doc claim of "equivalent to X" is load-bearing
  and warrants a backing test.

builder, fixer, weaver cite rename-discipline; builder and fixer's
existing changeset-discipline and regression-evidence citations
gain one-line annotations for the new content. CLAUDE.md inventory
adds rename-discipline.
kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 14, 2026
Address upstream review feedback on PR #3232 by exposing the
keystream's internal state for introspection and resumption.

`makeChaCha12(key)` now returns a `ChaCha12Generator` record
`{ next, getState, clone, fillRandomBytes }` instead of a bare
`(out: Uint8Array) => void` function.  The new shape is structurally
compatible with `pure-rand` v8's `RandomGenerator` (the contract
`fast-check@4` consumes via its `randomType` parameter):

  - `next()` returns a signed int32 in `[-0x80000000, 0x7fffffff]`
    by reading 4 little-endian keystream bytes.
  - `getState()` returns a 34-element `readonly number[]` snapshot
    `[base0..base15, counter, offset, block0..block15]`.
  - `clone()` returns a fully independent generator at the same
    position (not an alias).
  - `fillRandomBytes(out)` preserves the original byte-keystream
    entry point and conforms to `@endo/random`'s `RandomSource`.

Add `makeChaCha12FromState(state)` for deterministic resumption
from a snapshot, completing the pure-rand-style paired
`makeXxx` / `makeXxxFromState` factory convention.

Update in-tree consumers (`@endo/random`, `@endo/hex`,
`@endo/ocapn`) to destructure `fillRandomBytes` from the returned
record.  The migration is mechanical:

    -const fillRandomBytes = makeChaCha12(seed);
    +const { fillRandomBytes } = makeChaCha12(seed);

Add a new private package `@endo/chacha12-fast-check-test` that
drives `fast-check@4`'s property-based tester through the new
chacha12 surface as its `randomType` source.  The package depends
on `fast-check@^4` directly; the production-shaped fast-check /
pure-rand adapters remain in the separately-designed
`@endo/random-fast-check` package.

The new chacha12 unit tests cover the int32 contract for `next`,
the round-trip property for `getState` /
`makeChaCha12FromState` (across every offset 0..64 to catch
block-boundary off-by-ones), `clone` independence, and malformed
state rejection.  18 chacha12 tests + 7 fast-check integration
tests pass.

Refs: endojs/endo#3232 (comment)
Refs: #75
kriscendobot added a commit to endojs/endo-but-for-bots that referenced this pull request May 14, 2026
Per upstream review comments 3239081576 and 3239082370 on
endojs/endo#3232: the module-level callable was better when named
`random`. The earlier rename to `randomNumber` was a side-effect
of importing `random as randomFloat`; the imported `random` does
not appear in the module's value namespace, so there is no
shadowing and the local `random` identifier is unambiguous.
kriscendobot added a commit to endojs/endo-but-for-bots that referenced this pull request May 14, 2026
Per upstream review comments on endojs/endo#3232:

- 3239072009: fold chacha12-next-getstate.md into endo-chacha12.md.
  We have only made one release-cycle change with this PR; a single
  changeset captures it without the synthetic before/after framing.
- 3239062983: omit the gratuitous process commentary (the "### Why
  / asked for ... / new sibling package exercises the surface"
  section).
- 3239068864: omit the "Rewires the @endo/hex / @endo/ocapn ..."
  paragraph; that is not interesting to package authors updating
  dependencies (the @endo/hex and @endo/ocapn shipped runtime
  surfaces are unchanged; only their dev fixtures were rewired).
- 3239067688: rewrite the @endo/chacha12 interface description to
  reflect the actual landed shape (ChaCha12Generator record with
  fillRandomBytes satisfying RandomSource, plus next/getState/clone
  for pure-rand v8 structural compatibility), not the stale "bare
  (out) => void" shape.
- 3239064618: drop the "pure-rand v5" wording.  We are structurally
  compatible with pure-rand v8 and fast-check v4 now.
@kriscendobot

Copy link
Copy Markdown

Force-pushed to incorporate three new fixups from the source branch since the prior tip:

  1. refactor(ocapn): revert gratuitous randomNumber rename in fuzz tests — addresses review comments 3239081576 and 3239082370. The module-level callable reads better as random than randomNumber; the earlier rename was a side-effect of import { random as randomFloat } shadowing concerns that turned out not to apply.

  2. chore(changeset): consolidate the two chacha12 changesets per review — addresses review comments 3239072009, 3239062983, 3239068864, 3239067688, and 3239064618. Folds chacha12-next-getstate.md into endo-chacha12.md, drops gratuitous process commentary and the dependent-package wording, rewrites the @endo/chacha12 interface description to reflect the actual landed ChaCha12Generator shape, and updates the pure-rand v5 wording (we are structurally compatible with pure-rand v8 and fast-check v4 now).

  3. test(random): pin random = randomUint53 * 2 ** -53 equivalence — addresses review comment 3239085874. Adds an assertion that captures the float-extraction recipe so a future implementation edit cannot silently drift the multiplier off the magic constant.

The prior tip 04664e52e is preserved as an ancestor (no rewrite of older commits, no review-thread anchor invalidated). New tip f87bf84257.

kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
Address upstream review feedback on PR #3232 by exposing the
keystream's internal state for introspection and resumption.

`makeChaCha12(key)` now returns a `ChaCha12Generator` record
`{ next, getState, clone, fillRandomBytes }` instead of a bare
`(out: Uint8Array) => void` function.  The new shape is structurally
compatible with `pure-rand` v8's `RandomGenerator` (the contract
`fast-check@4` consumes via its `randomType` parameter):

  - `next()` returns a signed int32 in `[-0x80000000, 0x7fffffff]`
    by reading 4 little-endian keystream bytes.
  - `getState()` returns a 34-element `readonly number[]` snapshot
    `[base0..base15, counter, offset, block0..block15]`.
  - `clone()` returns a fully independent generator at the same
    position (not an alias).
  - `fillRandomBytes(out)` preserves the original byte-keystream
    entry point and conforms to `@endo/random`'s `RandomSource`.

Add `makeChaCha12FromState(state)` for deterministic resumption
from a snapshot, completing the pure-rand-style paired
`makeXxx` / `makeXxxFromState` factory convention.

Update in-tree consumers (`@endo/random`, `@endo/hex`,
`@endo/ocapn`) to destructure `fillRandomBytes` from the returned
record.  The migration is mechanical:

    -const fillRandomBytes = makeChaCha12(seed);
    +const { fillRandomBytes } = makeChaCha12(seed);

Add a new private package `@endo/chacha12-fast-check-test` that
drives `fast-check@4`'s property-based tester through the new
chacha12 surface as its `randomType` source.  The package depends
on `fast-check@^4` directly; the production-shaped fast-check /
pure-rand adapters remain in the separately-designed
`@endo/random-fast-check` package.

The new chacha12 unit tests cover the int32 contract for `next`,
the round-trip property for `getState` /
`makeChaCha12FromState` (across every offset 0..64 to catch
block-boundary off-by-ones), `clone` independence, and malformed
state rejection.  18 chacha12 tests + 7 fast-check integration
tests pass.

Refs: endojs/endo#3232 (comment)
Refs: #75
kriscendobot added a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
Per upstream review comments 3239081576 and 3239082370 on
endojs/endo#3232: the module-level callable was better when named
`random`. The earlier rename to `randomNumber` was a side-effect
of importing `random as randomFloat`; the imported `random` does
not appear in the module's value namespace, so there is no
shadowing and the local `random` identifier is unambiguous.
kriscendobot added a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
Per upstream review comments on endojs/endo#3232:

- 3239072009: fold chacha12-next-getstate.md into endo-chacha12.md.
  We have only made one release-cycle change with this PR; a single
  changeset captures it without the synthetic before/after framing.
- 3239062983: omit the gratuitous process commentary (the "### Why
  / asked for ... / new sibling package exercises the surface"
  section).
- 3239068864: omit the "Rewires the @endo/hex / @endo/ocapn ..."
  paragraph; that is not interesting to package authors updating
  dependencies (the @endo/hex and @endo/ocapn shipped runtime
  surfaces are unchanged; only their dev fixtures were rewired).
- 3239067688: rewrite the @endo/chacha12 interface description to
  reflect the actual landed shape (ChaCha12Generator record with
  fillRandomBytes satisfying RandomSource, plus next/getState/clone
  for pure-rand v8 structural compatibility), not the stale "bare
  (out) => void" shape.
- 3239064618: drop the "pure-rand v5" wording.  We are structurally
  compatible with pure-rand v8 and fast-check v4 now.
kriscendobot added a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
Per upstream review comment 3239085874 on endojs/endo#3232:
add an assertion to the test suite that captures the float
extraction recipe `random(source) === randomUint53(source) * 2 ** -53`
so a future implementation edit cannot silently drift the
multiplier off the magic constant.

The test constructs three fixed-byte mock RandomSources whose
randomUint53 returns 1, 2**53 - 1, and a chosen mid-range
value, and asserts each random() output equals the integer
times 2 ** -53 exactly.
kriskowal added a commit to kriskowal/garden that referenced this pull request May 15, 2026
Comment thread packages/random/test/random.test.js Outdated
Comment on lines +46 to +57
// Pin the magic multiplier to exactly `2 ** -53`. When randomUint53
// returns 1, random() returns the multiplier itself, so this asserts
// the constant in `src/random.js` is what its accompanying comment
// claims it is.
test('random() multiplies randomUint53 by exactly 2 ** -53', t => {
/** @param {Uint8Array} out */
const oneSource = out => {
for (let i = 0; i < out.length; i += 1) out[i] = 0;
out[0] = 1;
};
t.is(random(oneSource), 2 ** -53);
});

@gibson042 gibson042 May 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strikes me as overly brittle; it presumes a particular pattern of consumption that is not required (i.e., little-endian consumption of the buffer that retains its first octet and interprets it as the least significant of the value to be scaled down). Less brittle would be something like this:

Suggested change
// Pin the magic multiplier to exactly `2 ** -53`. When randomUint53
// returns 1, random() returns the multiplier itself, so this asserts
// the constant in `src/random.js` is what its accompanying comment
// claims it is.
test('random() multiplies randomUint53 by exactly 2 ** -53', t => {
/** @param {Uint8Array} out */
const oneSource = out => {
for (let i = 0; i < out.length; i += 1) out[i] = 0;
out[0] = 1;
};
t.is(random(oneSource), 2 ** -53);
});
// We expect random() to achieve a uniform distribution by scaling down
// randomUint53() output by exactly `2 ** -53`, but don't want to overly
// constrain _how_ the former maps octets into integers (consuming 8 vs. 7, big
// vs. little endian, etc.). So we provide a source that sets every bit to
// force maximum output, and if we ever need even more assurance could also use
// sources that set every bit of four contiguous octets after a prefix of 0 to 4
// fully-clear octets (0xFFFFFFFF_00..., 0x00FFFFFF_FF00...,
// 0x0000FFFF_FFFF00..., 0x000000FF_FFFFFF00..., 0x00000000_FFFFFFFF_00...),
// exactly one of which should drive random() output to be `2 ** -21 - 2 ** -53`
// (i.e., the one in which the set octets correspond exactly with the least
// significant 32 bits for randomUint53).
test('random() multiplies randomUint53 by exactly 2 ** -53', t => {
/** @param {Uint8Array} out */
const maxSource = out => {
for (let i = 0; i < out.length; i += 1) out[i] = 0xFF;
};
t.is(random(maxSource), 1 - 2 ** -53);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But on the other hand, this brittleness could be construed as a feature that prevents refactors from accidentally slipping in breaking changes. In which case I would highlight that in the explanatory comments (but also still do the set-all-bits analog as well).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This was emitted in order to provide assurance that the max float int constant matched the value prescribed in the comment, but I like narrowing this fence. I’m going to propose both all set, none set, 52 bits set, and 53 bits set.

kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
…3 (per r3245918820)

Replace the brittle `oneSource` test (which presumed little-endian
octet consumption with a retained-first-octet pattern) with the
`maxSource` form gibson042 suggested in
endojs/endo#3232 (review comment r3245918820): set every byte to
`0xFF` so randomUint53 returns `2 ** 53 - 1`, then assert
`random(maxSource) === 1 - 2 ** -53`. This pins the multiplier
without coupling the test to any specific octet-to-integer mapping
inside randomUint53 (8 vs 7 octets, big vs little endian).
kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 15, 2026
…3 (per r3245918820)

Replace the brittle `oneSource` test (which presumed little-endian
octet consumption with a retained-first-octet pattern) with the
`maxSource` form gibson042 suggested in
endojs/endo#3232 (review comment r3245918820): set every byte to
`0xff` so randomUint53 returns `2 ** 53 - 1`, then assert
`random(maxSource) === 1 - 2 ** -53`. This pins the multiplier
without coupling the test to any specific octet-to-integer mapping
inside randomUint53 (8 vs 7 octets, big vs little endian).

The hex literal is lowercased (`0xff`) to satisfy prettier's
default; gibson042's verbatim suggestion used `0xFF`, but lowercase
is the package-wide convention and prettier's formatting rule.
kriscendobot pushed a commit to endojs/endo-but-for-bots that referenced this pull request May 20, 2026
…3 (per r3245918820)

Replace the brittle `oneSource` test (which presumed little-endian
octet consumption with a retained-first-octet pattern) with the
`maxSource` form gibson042 suggested in
endojs/endo#3232 (review comment r3245918820): set every byte to
`0xff` so randomUint53 returns `2 ** 53 - 1`, then assert
`random(maxSource) === 1 - 2 ** -53`. This pins the multiplier
without coupling the test to any specific octet-to-integer mapping
inside randomUint53 (8 vs 7 octets, big vs little endian).

The hex literal is lowercased (`0xff`) to satisfy prettier's
default; gibson042's verbatim suggestion used `0xFF`, but lowercase
is the package-wide convention and prettier's formatting rule.
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch from 6fbe4b0 to 71055ef Compare May 21, 2026 06:29
@kriskowal kriskowal requested a review from gibson042 May 21, 2026 06:38
kriskowal pushed a commit to kriskowal/garden that referenced this pull request Jun 6, 2026
…(base-drift refresh; clear CONFLICTING; random/chacha12)
kriskowal added 10 commits June 5, 2026 22:13
A source-agnostic library of random sampling functions
(`random`, `randomInt`, plus the underlying `randomUint8`,
`randomUint16`, `randomUint24`, `randomUint32`, `randomUint53`
readers).  Each function accepts a `RandomSource`:
`(out: Uint8Array) => void`, matching `crypto.getRandomValues`
minus the return value.  Names follow the TC39
proposal-random-functions translation `Random.method` ->
`randomMethod`.  Each sampler is its own module so consumers
can import only what they use.

Also ships `@endo/random/seeds.js`, exporting the canonical
`bobsCoffee32` 32-byte seed shared across Endo deterministic
fuzz suites.
A pure-JavaScript ChaCha12 keystream.  The factory
`makeChaCha12(key)` returns a `ChaCha12Generator` record
`{ next, getState, clone, fillRandomBytes }`.  The
`fillRandomBytes` method has the shape
`(out: Uint8Array) => void`, conforming to
`@endo/random`'s `RandomSource` and to
`crypto.getRandomValues`-style ergonomics; it can be passed
directly to the samplers.

The remaining methods expose the keystream's internal state
(snapshot via `getState`, independent copy via `clone`,
signed-int32 pull via `next`) so the generator satisfies
`pure-rand` v8's `RandomGenerator` interface structurally,
driving `fast-check` v4's `randomType` parameter without a
separate adapter.  A companion `makeChaCha12FromState(state)`
reconstructs a generator from a snapshot for deterministic
resumption.

The keystream is cross-checked against three published
ChaCha12 test vectors from
`draft-strombergson-chacha-test-vectors-01` (TC1, TC4, TC8).
A companion benchmark (`fill-random-bytes.bench.js`) measures
keystream throughput across engines.
A companion test package that exercises `@endo/chacha12`'s
structural `pure-rand` v8 `RandomGenerator` shape by driving
`fast-check` v4's `randomType` parameter.  Adopting the
test-package shape (separate package with its own dev-deps,
no `main`, tests under `test/`) keeps `fast-check` out of
`@endo/chacha12`'s declared surface while still proving the
interface compatibility under CI.
…bench inputs

Drop the in-tree `_xorshift.js` copy in favor of
`@endo/chacha12`'s `fillRandomBytes` seeded from
`@endo/random/seeds.js`'s `bobsCoffee32`.  The shared seed
keeps the bench's deterministic input identical across the
hex and ocapn fuzz suites without duplicating the PRNG
implementation in each test directory.
Drop the in-tree `_xorshift.js` copy in favor of
`@endo/chacha12`'s `fillRandomBytes` driving
`@endo/random/random.js`'s `random` (the `[0, 1)` float
sampler).  The shared default seed (`bobsCoffee32`) keeps
the fuzz determinism identical across the hex and ocapn
suites without duplicating the PRNG implementation per test
directory.
…ons> overlap

The original `...CompartmentOptionsArgs` rest signature
produced a single-element tuple of unions, which
`Parameters<typeof compartmentOptions>` (declared as
`CompartmentOptionsArgs`, no rest) did not overlap.  The
typedoc/typecheck pass surfaced the mismatch.  Re-typing
`args` as the union itself (with the `@this` annotation
collapsed into the same JSDoc block) restores the
overlap.
Capture why a top-level `.js` thunk module exists (legacy
`exports`-map portability and public-interface filtering)
and when it can be removed.  Surfaced by the `@endo/random`
package introduction, whose multi-entry shape forced the
question of whether each public sampler needed a thunk
versus pointing `exports` at `src/` directly.
…config and typedoc

Wire the three new packages into the workspace's composite
tsconfig so `yarn build` resolves them, and exclude
`chacha12-fast-check-test` from typedoc generation
(test-package shape: no public docs surface).
Announce `@endo/random` (major) and `@endo/chacha12` (major)
as new packages, and the `@endo/hex` and `@endo/ocapn`
(patch) test-driver swaps from the in-tree xorshift copies.
@kriskowal kriskowal force-pushed the kriskowal-random-chacha20 branch from 71055ef to 46e330a Compare June 6, 2026 05:15
kriskowal pushed a commit to kriskowal/garden that referenced this pull request Jun 6, 2026
…e-drift refresh; CONFLICTING->MERGEABLE)
kriskowal pushed a commit to kriskowal/garden that referenced this pull request Jun 6, 2026
kriskowal added 2 commits June 6, 2026 08:27
Run `yarn lint --fix` to insert numeric separators in the
documented 3-digit-group style on all numeric literals across the
random and chacha12 packages. Tests for both packages continue to
pass; the rewrites preserve value semantics.
The package-uniformity check requires per-package SECURITY.md to be
byte-identical to the skel template. The newly added chacha12 and
random packages shipped a copy that already spelled "GitHub" with
the canonical capitalization; the skel template still reads
"Github". Sync the packages to the skel until the skel is itself
updated. Lint now passes end to end.
kriskowal pushed a commit to kriskowal/garden that referenced this pull request Jun 6, 2026
kriskowal pushed a commit to kriskowal/garden that referenced this pull request Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants