Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

TypeScript server `ctx.random()`: first draw of every transaction is heavily biased (signed shift in `pcg32` seeding)

Aperta Adatta ai principianti
#6,247 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@Tyagiquamar ci sta già lavorando.

Dal 16/10/2026.

  • #6316 di @Tyagiquamar — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
javascript, typescript
Ambito
backend

Direzione di ricerca

Il bug si trova in pcg32 in crates/bindings-typescript/src/server/rng.ts, intorno alla riga 71, dove la rotazione usa un >> con segno su un valore a 32 bit. Inizia eseguendo il repro autonomo nell'issue per confermare il primo sorteggio distorto. È completato quando si passa a uno shift senza segno, si aggiunge un test di distribuzione sui seed timestamp ai test dell'RNG e si conferma che i numeri del repro corrispondono a with >>>.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Summary

In the TypeScript server bindings, the first value ctx.random() returns in a transaction is far from uniform. It lands below 0.001 about 28% of the time instead of 0.1%, and below 0.01 about 34% of the time instead of 1%. The second draw is closer but still off (7.7% instead of 10% below 0.1).

Any module that makes a single low-probability roll at the start of a reducer gets that outcome far too often. We noticed because a 1-in-1,000 boss drop in our game was dropping more than half the time.

Verified on spacetimedb 2.10.2, 2.11.0 and 2.12.0 (npm, dist/server/index.mjs).

Cause

pcg32 in crates/bindings-typescript/src/server/rng.ts rotates with a signed shift:

https://github.com/clockworklabs/SpacetimeDB/blob/v2.12.0/crates/bindings-typescript/src/server/rng.ts#L71

return (xorshifted >> rot) | (xorshifted << (32 - rot));

xorshifted is an unsigned 32-bit value held in a JS number. >> sign-extends it, so the rotation smears the top bit across the seed handed to xoroshiro128plus. Seeds derived from nearby timestamps then start the generator in correlated states, and the first outputs pile up near 0.

An unsigned shift fixes it:

return ((xorshifted >>> rot) | (xorshifted << (32 - rot))) >>> 0;

Reproduction

Standalone, mirroring rng.ts with the bundled pure-rand (npm install pure-rand@7 && node repro.mjs):

import { xoroshiro128plus, unsafeUniformIntDistribution } from "pure-rand";

function pcg32(state, fixed) {
  const MUL = 6364136223846793005n, INC = 11634580027462260723n;
  state = BigInt.asUintN(64, state * MUL + INC);
  const xorshifted = Number(BigInt.asUintN(32, ((state >> 18n) ^ state) >> 27n));
  const rot = Number(BigInt.asUintN(32, state >> 59n));
  return fixed ? ((xorshifted >>> rot) | (xorshifted << (32 - rot))) >>> 0 : (xorshifted >> rot) | (xorshifted << (32 - rot));
}
function makeRandom(micros, fixed) {
  const rng = xoroshiro128plus(pcg32(micros, fixed));
  return () => {
    const g1 = unsafeUniformIntDistribution(0, (1 << 26) - 1, rng);
    const g2 = unsafeUniformIntDistribution(0, (1 << 27) - 1, rng);
    return (g1 * 2 ** 27 + g2) * 2 ** -53;
  };
}

const N = 1_000_000, base = 1_791_964_800_000_000n; // microsecond timestamps, as ReducerCtx seeds them
for (const fixed of [false, true]) {
  const thresholds = [0.001, 0.01, 0.1, 0.5];
  const first = thresholds.map(() => 0), second = thresholds.map(() => 0);
  for (let i = 0; i < N; i++) {
    const random = makeRandom(base + BigInt(i) * 7919n, fixed);
    const a = random(), b = random();
    thresholds.forEach((t, k) => { if (a < t) first[k]++; if (b < t) second[k]++; });
  }
  console.log(fixed ? "with >>>" : "as shipped");
  thresholds.forEach((t, k) => console.log(`  P(draw < ${t}): first ${(first[k] / N * 100).toFixed(2)}%, second ${(second[k] / N * 100).toFixed(2)}%`));
}

Output:

as shipped
  P(draw < 0.001): first 28.20%, second 0.07%
  P(draw < 0.01): first 33.71%, second 0.77%
  P(draw < 0.1): first 43.48%, second 7.67%
  P(draw < 0.5): first 69.50%, second 53.43%
with >>>
  P(draw < 0.001): first 0.10%, second 0.10%
  P(draw < 0.01): first 1.00%, second 1.00%
  P(draw < 0.1): first 9.99%, second 10.01%
  P(draw < 0.5): first 49.98%, second 50.09%

We got the same numbers by lifting makeRandom straight out of the published dist/server/index.mjs.

Impact

Every TypeScript module using ctx.random() (and so integerInRange, fill, uuid v4 and the rest) gets a biased first draw in each reducer, scheduled reducer and procedure, and a skewed second one. In our game that meant boss drops, upgrade success rolls and rare resource finds all succeeded far more often than their stated odds.

Suggested: fix the shift and add a distribution test over timestamp seeds. The current tests replace the RNG with fixed values, so they can't see this. Since the seed sequence changes, it may also be worth a release note for modules that depend on reproducing past rolls.

Workaround

We patch that one line in the installed dist/server/index.mjs before building our module until a fixed release is out.

Thanks! Found by one of our players, @Kiira1.

Lingua principale
Rust
Stelle
25.3k
Fork
1.1k
Merge medio
3g 31m
PR unite (30g)
66

Preparare l'ambiente

  • Include un Dockerfile o un file Docker Compose
  • Ha un modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di clockworklabs/SpacetimeDB

Tutte le issue di clockworklabs/SpacetimeDB

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.