TypeScript server `ctx.random()`: first draw of every transaction is heavily biased (signed shift in `pcg32` seeding)
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 72/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- javascript, typescript
- Área
- backend
Línea de trabajo
El error está en pcg32 en crates/bindings-typescript/src/server/rng.ts, alrededor de la línea 71, donde la rotación usa un >> con signo sobre un valor de 32 bits. Empieza ejecutando el repro independiente del issue para confirmar la primera extracción sesgada. Está terminado cuando se cambia a un desplazamiento sin signo, se añade una prueba de distribución sobre semillas de timestamp a las pruebas del RNG, y se confirma que los números del repro coinciden con with >>>.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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:
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.
- Lenguaje dominante
- Rust
- Estrellas
- 25.3k
- Forks
- 1.1k
- Merge medio
- 3 d 31 min
- PR fusionados (30 d)
- 66
Preparar el entorno
- Incluye un Dockerfile o un archivo de Docker Compose
- Tiene una plantilla de pull request
- Sin guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de clockworklabs/SpacetimeDB
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
clockworklabs/SpacetimeDB#6314 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
clockworklabs/SpacetimeDB#6288 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
clockworklabs/SpacetimeDB#6146 ·
Los mantenedores suelen responder en 1 día
-
CI flake: LintsAbierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 88/100
clockworklabs/SpacetimeDB#5985 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
clockworklabs/SpacetimeDB#5943 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de clockworklabs/SpacetimeDB
Issues similares
-
build: Update rust versionPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertodependencies
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
ibm-granite-community/granite-cli#229 ·
Los mantenedores suelen responder en 1 día
-
setTargetAtTime booked ahead of time starts its curve early (values blow up before start_time)Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
orottier/web-audio-api-rs#658 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
aosama/astronomical#1318 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
bug: `shep kelpie finish all`Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
shep-pm/shep-kelpie#980 ·
Los mantenedores suelen responder en 1 día