v2.8 · RNG PRE-SUBMISSION PACKAGE v1.2
RAIN RNG · PRE-SUBMISSION PACKAGE v1.2 · DOCUMENT

DOCS/INTEGRATION-OPERATOR — OPERATOR INTEGRATION

Source sdk-v2/docs/INTEGRATION-OPERATOR.md · commit 96368332 · GLI CSR §2.2: Integration · ← package index · raw on GitHub ↗

RAIN RNG v2.1 — operator integration (slot / table server)

Two pages for the builder who has a working game server and wants provably fair, lab-describable randomness without changing the game math or the UX. Packages: @rain/rng-core (draw layer, zero deps), @rain/rng-session (ceremony + transports), rain_rng (Python mirror).

1. Roles — who is A, who is B, who verifies

Party Runs Holds Cannot
A Operator game server RainSpinClient (@rain/rng-session) its hash chain pRev_1..n, its seed half choose or predict r_k: it needs B's hRev_k, which is pinned to B's pre-committed chain
B RAIN node (rain-rng-node, or RainRngHouse embedded) RainRngHouse its hash chain hRev_1..n, its seed half choose or predict r_k: A's pRev_k (and A's bet) is persisted before B reveals
— Player / lab / regulator verifyRound() (TS, Python, or any keccak tool) the public proof: sessionId, sessionSeed, k, pRev, hRev, pAnchor, hAnchor be lied to: every value is recomputable from public commitments

Neither side alone can bias: the outcome needs one secret from each, each secret is the only value that hashes to the previously published anchor, and B's secret leaves B only after A's commitment (decision + pRev_k) is durably persisted. The player does not have to trust the operator or RAIN — only keccak256 and HMAC-SHA-256.

2. The five calls

import { RainSpinClient, httpTransport /* or wsTransport, inProcessTransport */ } from "@rain/rng-session";
import { drbg, verifyRound } from "@rain/rng-core";

const spin = new RainSpinClient(httpTransport("https://rng.your-node.example/rng"), { chainLen: 65536, maxInFlight: 2 });
await spin.open();                                            // 1. session: both sides commit (chain roots + seed halves), B reveals its seed half, both derive sessionSeed
const { r, k, proof } = await spin.spinSeed(undefined, betJson); // 2. round k: commit (bet + pRev_k) → B persists → hRev_k → r = keccak(pRev, hRev, sessionSeed, sessionId, k), derived by A
spin.prefetch();                                              // 3. pipelining: send the commit for k+1 now, while the reels of k animate
const c = drbg(r, "my-slot-v3").cursor();                     // 4. draws: one RAIN seed per spin → HMAC-DRBG → intBelow / float / shuffle for YOUR math
verifyRound({ ...proof, channelId: proof.sessionId, expected: proof.r }).ok;  // 5. audit: anyone, any time, from the proof alone

Python (pip install -e packages/rng-py): rain_rng.drbg(r, "my-slot-v3").cursor().int_below(n) — byte-identical (packages/rng-py/tests asserts vectors generated by the TS reference).

3. Per-spin flow

player            operator server (A)                          RAIN node (B)
  │ bet ─────────►│                                                  │
  │               │ decision = bet json; pRev_k = chain_A[k]          │
  │               │ Commit{sessionId,k,decision,pRev_k} ────────────►│ check keccak(pRev_k)==pAnchor
  │               │                                                  │ PERSIST (fsync / COMMIT)  ← the rule
  │               │◄─────────────────── Reveal{k,hRev_k} ────────────│ hRev_k = chain_B[k]
  │               │ check keccak(hRev_k)==hAnchor                     │
  │               │ r = keccak(pRev_k,hRev_k,sessionSeed,sessionId,k) │
  │               │ reels = math(drbg(r,gameId))                      │
  │◄─ outcome+proof                                                  │
  │  (animation)  │ prefetch: Commit for k+1 ───────────────────────►│  ← overlaps the animation

Measured (examples/slot-spin-endpoint/loadtest.mjs, 1,000 spins): RNG work on the /spin critical path p50 0.4 ms / p95 0.5 ms with pipelining, both with an in-process node and with a simulated remote node at 40 ms RTT (without pipelining the remote case costs the full RTT: p95 41 ms). DRBG + 3 reels ≈ 0.2 ms.

4. Replay / audit procedure

Given a proof { sessionId, sessionSeed, k, pRev, hRev, pAnchor, hAnchor, r, gameId }:

  1. keccak256(pRev) == pAnchor and keccak256(hRev) == hAnchor — both reveals are the committed ones (anchors = previous round's reveals; chain roots at k = 1; roots are in the session's Terms/Open, published at open or anchored via RngAnchor.sol).
  2. r == keccak256(pRev ‖ hRev ‖ sessionSeed ‖ sessionId ‖ uint256(k)).
  3. key = HMAC-SHA-256(r, "RAIN-RNG-v2.1|" + gameId); draw(i) = HMAC_DRBG(key).Generate(32, uint64be(i)); feed the operator's published math the same cursor → the same reels. In code: verifyRound(...) then drbg(r, gameId).
  4. Session-level: sessionSeed == keccak256(houseSeed ‖ playerSeed ‖ sessionId) and keccak256(houseSeed) == houseSeedCommit (from Terms), i.e. B did not swap its seed after seeing A's. Both sides keep the transcript (house.transcript(), client.rotations); the reference node exposes GET /verify/:sessionId/:k.

5. What to hand a test lab

  • Source of r: two-party commit-reveal (this doc §1–§3, docs/RNG.md §2–§3), 256 bits per round, never reused (k strictly increasing, chains consumed once).
  • Draw derivation: docs/RNG.md §2.5 — HMAC_DRBG (SP 800-90A Rev.1 §10.1.2, HMAC-SHA-256) default; ChaCha20 (RFC 8439) selectable; intBelow by rejection (no modulo bias); float = 53-bit.
  • Vectors: packages/rng-core/test/cavp-hmac-drbg-sha256.json (NIST CAVP HMAC_DRBG, 32 cases), FIPS 180-4 / RFC 4231 / RFC 8439 KATs in test/drbg.test.mjs, cross-language drbg-vectors.json (TS ↔ Python).
  • Implementations: packages/rng-core/src/{sha256,drbg,keccak,rng}.ts, packages/rng-py/rain_rng/*.py — no third-party crypto.
  • Statistical: the smoke test (chi-square, monobit, runs) is a regression guard, not a certification; the lab runs its own suite on drbg(r, gameId) streams (any r), see docs/LAB-READINESS.md.
  • Out of scope for RAIN: the game's math/RTP/paytable and its use of the cursor (that is the operator's engine, certified per game).

6. Deprecation: 32-bit compat

seed32Compat(r) (r & 0xFFFFFFFF → xoshiro/mulberry/PCG) exists only for migrating legacy 32-bit-seeded engines and prints a warning: 32 bits of state is not production-secure (a few observed draws reconstruct the spin). Development only. Production = drbg(r, gameId). outcomeMod (uint256(r) % N) remains for byte-identity with the on-chain verifiers (bias < N/2^256); new games should use intBelow.

7. Chain rotation (long sessions)

Default chain length is 65,536 (was 4,096): ≈18 h at one spin per second. At 90 % consumed, needsRotation flips on both sides; RainSpinClient.spinSeed() performs the rotation itself when nothing is in flight: B issues fresh Terms (new houseSeedCommit, new chain root), A answers with a fresh Open (new playerSeed, new chain root), B reveals, both derive the new sessionSeed; k restarts at 1 under a new sessionId. No chain transaction — unless you wire RngAnchorAdapter into onRotate. Every rotation is recorded (rotations[], history[]) with the link to the previous session for audit; old proofs keep verifying against the old session's public data. Building a 65,536 chain costs ≈1.3 s of keccak (do it ahead of time; rotateTerms() may be called early).

8. HA note

The operator can run its own RAIN node (party B) — the protocol does not require RAIN-the-company in the loop; what makes it fair is the two-party structure, the persist-before-reveal rule, and public commitments. packages/rng-node (WAL, HTTP+WS, /verify, HA behind shared Postgres) is the operator-runnable node; see README § Gaps for its current state versus the Playmarket-internal production node. If A and B are the same legal entity, publish the commitments (RngAnchor.sol or a transparency log) so the player's verification is against something the operator cannot rewrite.

9. Attribution requirement

MIT with an attribution condition (/LICENSE): any player-facing product using this RNG must display "Powered by RAIN RNG" linking to https://www.playmarkets.bet/fairness (fairness panel, footer, about page). badge() returns HTML/Markdown/SVG. This is a licence term.

← Back to the package index · Rendered 2026-09-14 09:34 UTC from the repository copy; the markdown in the zip / repo is the document of record.