DOCS/INTEGRATION-OPERATOR — OPERATOR INTEGRATION
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 }:
keccak256(pRev) == pAnchorandkeccak256(hRev) == hAnchor— both reveals are the committed ones (anchors = previous round's reveals; chain roots at k = 1; roots are in the session'sTerms/Open, published at open or anchored viaRngAnchor.sol).r == keccak256(pRev ‖ hRev ‖ sessionSeed ‖ sessionId ‖ uint256(k)).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(...)thendrbg(r, gameId).- Session-level:
sessionSeed == keccak256(houseSeed ‖ playerSeed ‖ sessionId)andkeccak256(houseSeed) == houseSeedCommit(fromTerms), i.e. B did not swap its seed after seeing A's. Both sides keep the transcript (house.transcript(),client.rotations); the reference node exposesGET /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 (kstrictly 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;intBelowby 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 intest/drbg.test.mjs, cross-languagedrbg-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 ondrbg(r, gameId)streams (anyr), seedocs/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.