TypeScript server `ctx.random()`: first draw of every transaction is heavily biased (signed shift in `pcg32` seeding)
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- javascript, typescript
- Lĩnh vực
- backend
Hướng nghiên cứu
Lỗi nằm ở pcg32 trong crates/bindings-typescript/src/server/rng.ts, khoảng dòng 71, nơi phép xoay dùng >> có dấu trên giá trị 32 bit. Bắt đầu bằng cách chạy repro độc lập trong issue để xác nhận lần rút đầu tiên bị lệch. Hoàn thành khi chuyển sang dịch bit không dấu, thêm một kiểm thử phân phối trên seed timestamp vào các kiểm thử RNG, và xác nhận các số trong repro khớp với with >>>.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- Rust
- Star
- 25.3k
- Fork
- 1.1k
- Merge trung bình
- 2 ngày 18 giờ
- Pull request đã merge (30 ngày)
- 65
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của clockworklabs/SpacetimeDB
-
Unreal SDK: no way to set confirmed reads on the connection (Rust SDK has with_confirmed_reads)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
clockworklabs/SpacetimeDB#6328 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
clockworklabs/SpacetimeDB#6314 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
clockworklabs/SpacetimeDB#6288 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
clockworklabs/SpacetimeDB#6146 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
CI flake: LintsĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
clockworklabs/SpacetimeDB#5985 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của clockworklabs/SpacetimeDB
Issue tương tự
-
ci: SHA pin 3rd party actionsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
Maintainer thường phản hồi trong vòng 3 ngày
-
Nushell launcher failure.Đang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
-
Zellij doesn't validate hex colors correctly and crashesCó thể đã có người làm @ymcx đã nhận hôm nay. Đang mởsuspected bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
zellij-org/zellij#5732 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug(api): E_DOSE_ATTR_NONFINITE for a modeled D/R still says the value "never reaches you as a NaN"Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 63/100