SCALING INVENTORY — every reduction of RNG output to a game range in sdk-v2
GLI-19 §3.2.1 requires source-code review of "any and all core randomness algorithms, scaling algorithms, shuffling algorithms". This is the complete inventory of every %, integer division, bit-mask or table walk that maps RNG output to a game outcome in this repository, produced by grep -n '%' … over packages/rng-core/src, packages/rng-session/src, packages/rng-node/src and every vendored engine in packages/games/vendor (the vendored files are NOT minified: 300–560-column lines but readable, commented JavaScript; sizes 10–60 kB; sha256 in packages/games/vendor/MANIFEST.json). Repo commit: see lab/FINGERPRINTS.json.
Classification:
- CERTIFIED-PATH — the v2.1 draw layer submitted for certification:
drbg(r, gameId) → intBelow (rejection) / shuffle (Fisher–Yates) / float.
- LEGACY — the pre-v2.1 modulo reductions used by the on-chain quick games (byte-identical to the Solidity verifiers); bias bound
< N/2^256 (Appendix B of lab/RNG-DESCRIPTION.md); out of the certification scope for the v2.1 RNG, listed so the lab sees them and so a future submission of the legacy games can reference them.
- NOT-RNG — arithmetic that happens to use
% but does not reduce random output (index wrapping, payout math, hash internals).
1. @rain/rng-core (packages/rng-core/src)
| File:line |
Code |
Class |
Notes |
drbg.ts:166-168 |
limit = 2^32 − (2^32 mod n); loop { v = uint32(i+used); used++; if (v < limit) return v % n } |
CERTIFIED-PATH |
intBelowEx — rejection sampling; the only % on the certified path is applied to a word already known to be < limit, a multiple of n, so v % n is exactly uniform. Proof: Appendix B.1. |
drbg.ts:170-173 |
Fisher–Yates: for j = len−1..1: swap(a[j], a[intBelow(j+1)]) |
CERTIFIED-PATH |
shuffle — standard Durstenfeld variant; each of the len! permutations has probability ∏ 1/(j+1) = 1/len! given uniform intBelow. Consumes a variable number of words (rejections); used is returned so cursors advance deterministically. |
drbg.ts:161 |
uint32 = (b0<<24 | b1<<16 | b2<<8 | b3) >>> 0 |
CERTIFIED-PATH |
Big-endian word extraction from draw(i); >>> 0 is a sign fix, not a reduction. |
drbg.ts:162 |
uint53 = uint32·2^21 + (b4<<16|b5<<8|b6) >> 3; float = uint53 / 2^53 |
CERTIFIED-PATH |
53 uniformly distributed bits → [0,1) with every representable multiple of 2^-53 equally likely. No %. |
drbg.ts:113 |
b[j] = v % 256 in uint64be(i) |
NOT-RNG |
Encodes the draw index as 8 big-endian bytes (additional_input); not applied to random output. |
drbg.ts:68-78 |
rotl, le32, counter >>> 0 |
NOT-RNG |
ChaCha20 block-function internals (RFC 8439). |
drbg.ts:203 |
seed32Compat(r) = last 4 bytes of r |
DEPRECATED — not reachable in production paths; prints a warning; listed under "undisclosed switches" in RNG-DESCRIPTION §9. |
Development-only; a lab should confirm no production engine imports it (grep -rn seed32Compat packages/games → none). |
rng.ts:117 |
outcomeUint(...) % n in outcomeMod |
LEGACY |
uint256(r) % N; dice N=100, roulette N=37. Bias < N/2^256 (Appendix B.2). Byte-identical to QuickGamesVerifier.sol. |
rng.ts:120 |
modN(r, N) = BigInt(r) % N |
LEGACY |
Same reduction on an already derived r. |
keccak.ts, sha256.ts |
internal % 5, % 64, etc. |
NOT-RNG |
FIPS 202 / FIPS 180-4 internals (rho/pi offsets, message padding). KAT-verified. |
2. @rain/rng-session and @rain/rng-node
| File:line |
Code |
Class |
Notes |
rng-session/src/index.ts:334 |
string "value = uint256(r) % N" in fairness() output |
NOT-RNG |
Documentation string in the fairness panel object; references the LEGACY formula. |
rng-node/src/house.ts:131 |
same string in /verify response |
NOT-RNG |
Documentation string. /verify recomputes r, not a scaled value. |
rng-node/src/metrics.ts:11 |
this.i = (this.i+1) % this.cap |
NOT-RNG |
Latency ring buffer index. |
rng-session (all) |
— |
— |
No reduction anywhere: the session layer produces r_k (256 bits) and hands it to drbg() or the game. |
3. Vendored game engines (packages/games/vendor) — readable sources
These are the live Playmarket/MAY engines (vendor/rain/*.umd.js) and the Slot Engine V2 bundles (vendor/slot-engine-v2/*.mjs, built by esbuild from TypeScript, not minified: identifiers, comments and structure preserved). They are shipped so the lab can review every scaling path of the live games; none of them uses the v2.1 drbg() layer yet — they are the LEGACY per-game derivations and are out of scope of the v2.1 RNG certificate. They are listed exhaustively.
3.1 rain/quick-games-engine.umd.js (mirror of QuickGamesVerifier.sol)
| Line |
Code |
Class |
Notes |
| 142 |
roll = Number(r % 100n) + 1 |
LEGACY |
Dice 1..100. r = outcome() (256-bit). Bias < 100/2^256. |
| 144 |
n = Number(r % 37n) |
LEGACY |
Roulette 0..36. |
| 40 |
slotsReels: for i<3: pick(theme, keccak256(abi.encode(r, i)) % 100n) |
LEGACY |
Sakura-777 quick slots: three independent 256-bit hashes (r‖i) each reduced mod 100, then a weighted table walk (slotsWeightedPick, weights sum to 100 per theme — e.g. [42,23,10,9,6,4,3,3]). Weighted selection is exact given uniform 0..99. |
| 103 |
roll = r % 1000000n; f = (r / 1000000n) % 100n |
LEGACY |
Basketball: shot success ppm (BB_P[spot], six-figure ppm) and a secondary 0..99 for animation variant f. Two fields from one r: r mod 10^6 and ⌊r/10^6⌋ mod 100 — independent to within < 10^8/2^256. |
| 70, 73 |
n % 3 |
NOT-RNG |
Roulette street/corner index geometry (table layout), applied to bet positions, not RNG. |
3.2 rain/craps-engine.umd.js
| Line |
Code |
Class |
Notes |
| 67 |
d1 = r % 6 + 1; d2 = (r / 6) % 6 + 1 |
LEGACY |
Two dice from one 256-bit r: base-6 digits. Exactly independent uniform pairs to within < 36/2^256 (documented inline as "bias < 2^-250"). |
| 13 |
+11.1%, 1.41% |
NOT-RNG |
Comment text (house edge). |
3.3 rain/step-games-engine.umd.js (Dribble / Penalty step games)
| Line |
Code |
Class |
Notes |
| 42 |
rollFor(..., runId, idx) = keccak256(abi.encode(pRev,hRev,sessionSeed,channelId,uint32 runId,uint8 idx)) % BPS |
LEGACY |
Per-step 256-bit hash reduced to basis points (BPS = 10000); compared against DRIBBLE_BPS[step] / SHOT_BPS[step]. |
| 142 |
rDive = (r & M64) % PPM; rAcc = ((r>>64) & M64) % PPM; rSave = ((r>>128) & M64) % PPM |
LEGACY |
Penalty: three 64-bit fields of one hash each reduced mod 10^6 (ppm). Bias < 10^6/2^64 ≈ 5.4e-14 per field — larger than the 256-bit cases but still far below any statistical detectability at 10^12 samples; documented in Appendix B.2. |
| 131 |
zoneOf(t) = ((t−1) % 3) + 1 |
NOT-RNG |
Maps a target index to a zone. |
3.4 rain/crash-live-engine.umd.js
| Line |
Code |
Class |
Notes |
| 35-38 |
r = keccak256(abi.encode(pRev,hRev,sessionSeed,channelId,uint32 roundId,uint32 k)); crash ⇔ (r & (2^64−1)) < thr |
LEGACY |
Per-tick Bernoulli trial: low 64 bits of the hash compared with a threshold thr (ppm-derived, firstThreshold adds the house edge on tick 1). A comparison of a uniform 64-bit integer with a fixed threshold is exactly Bernoulli(thr/2^64); no modulo. |
| 26-40 |
(… << 64n) / 1000000n, (r*base)/WAD |
NOT-RNG |
Fixed-point multiplier math (payout curve), not applied to RNG output. |
3.5 rain/crash-live-client.umd.js, rain/engine-v2-channel.umd.js
No reductions. engine-v2-channel derives seed = keccak256(abi.encode(pRev, hRev, sessionSeed, channelId, k, keccak256(gameId))) (line 50) and hands it to the Slot Engine V2 KeccakRng; crash-live-client only verifies chains (chainOk, line 41) and hash-chain construction (lines 42-50).
3.6 slot-engine-v2/engine.mjs (+ engine-book.mjs, engine-nudge.mjs, engine-orbs.mjs, which share the same RNG classes at the same relative positions)
| Line |
Code |
Class |
Notes |
| 374-431 |
class KeccakRng { next32(): hash = keccak256(seed ‖ gameIdHash ‖ uint256 round ‖ uint256 hashIdx); return 4-byte slice } |
LEGACY generator |
Counter-mode keccak-256 stream keyed by the 32-byte seed from engine-v2-channel: 8 words per hash, hashIdx increments. Cryptographically equivalent in structure to the v2.1 ChaCha mechanism (PRF of (key, counter)) but not an SP 800-90A mechanism — this is why v2.1 introduces drbg(). |
| 423-425 |
nextInt(n) = this.next32() % n |
LEGACY — modulo on a 32-bit word |
Bias up to n/2^32. For the largest n used (reel-strip length ≤ 4096; weight totals ≤ 100 000 per cell per VENDOR.txt) the bias is ≤ 2.3e-5 relative per cell — the only reduction in the repository where modulo bias is not astronomically small. It is measurable only with > 10^10 draws per cell; it does not affect RTP at the calibrated 4 decimals (VENDOR.txt records stratified 96.0x % on 160–480 M spins). Migration to drbg().intBelow is the recommended remediation for these engines before they are submitted; not part of the v2.1 RNG scope. |
| 426-430 |
nextFloat = ((next32>>>5)·2^26 + (next32>>>6)) / 2^53 |
LEGACY |
53-bit float from two words; exact. Used for non-outcome cosmetics only (grep: no nextFloat call sites in spin logic). |
| 432-491 |
class FastRng (xoshiro128**, nextInt = next32 % n) |
NOT REACHABLE in production |
Development/simulation generator; @rain/games src/index.ts:129 constructs only KeccakRng. A lab should confirm via the bundle's export list (line 1535) that the FE does not instantiate it for real spins. |
| 798-799, 1080, 1150, 1169, 1226, 1257, 1516 |
rng.nextInt(c.reels), nextInt(c.rows[reel]), nextInt(totalW) < coinWeight, nextInt(L) (strip stop), nextInt(reels.total[reel]) then cumulative-weight walk, nextInt(strip.length), nextInt(blastTotal), pickValue: nextInt(cv.total) + cumulative walk |
LEGACY (call sites) |
Every consumer of the modulo nextInt. Weighted picks (cum[k] <= x) are exact given a uniform x. |
| 1155, 1201 |
strip[(stop + r) % L], (stop − 1 + L) % L |
NOT-RNG |
Wrapping a reel position around the strip; r here is the row offset, not RNG. |
| 130-135, 163-164, 250-254 |
% 5, % 64, % _256n, % 10, % 4 |
NOT-RNG |
@noble/hashes keccak internals (rho/pi), and byte-alignment checks. |
3.7 Solidity verifiers (rain-risk-markets contracts, referenced, not vendored here)
QuickGamesVerifier.sol (r % 100, r % 37, slots keccak(r,i) % 100), CrapsRulesVerifier.sol (r % 6, (r/6) % 6), EngineV2RulesVerifier.seedOf (no reduction — emits the 256-bit seed). All LEGACY / byte-identical with §3.1–3.2 by construction (npm run test:live re-checks the vectors against Arbitrum One). solc is not installed in this build environment, so bytecode fingerprints are not in FINGERPRINTS.json; the deployed addresses and Sourcify status are listed in docs/RNG.md §0.
4. Summary for the lab
| Class |
Count of distinct reductions |
Where |
| CERTIFIED-PATH |
3 (intBelow rejection, shuffle Fisher–Yates, float 53-bit) |
rng-core/src/drbg.ts — ~15 lines, zero dependencies |
| LEGACY, 256-bit modulo (bias < N/2^256) |
8 (dice, roulette, quick-slots ×3 reels, basketball ×2, craps ×2, step BPS) |
rng-core/src/rng.ts, vendor/rain/*.umd.js |
| LEGACY, 64-bit modulo (bias < 10^6/2^64) |
3 (penalty dive/acc/save) |
vendor/rain/step-games-engine.umd.js:142 |
| LEGACY, threshold compare (exact) |
1 (crash tick) |
vendor/rain/crash-live-engine.umd.js:38 |
| LEGACY, 32-bit modulo (bias ≤ n/2^32) |
1 method, 7 call sites |
vendor/slot-engine-v2/*.mjs KeccakRng.nextInt |
| NOT-RNG |
~20 |
hash internals, index wrapping, payout math |
The only scaling with non-negligible (though still RTP-invisible) bias is KeccakRng.nextInt in Slot Engine V2. The certified v2.1 layer replaces it; the migration is one line (rng.nextInt(n) → cursor.intBelow(n)), and @rain/games already exposes drbg through @rain/rng-core for this purpose.