RAIN RNG v2.1 · operator node

Randomness neither side
can bias. Every round
with a public proof.

A two-party commit-reveal RNG for casino and game operators. Your server and the RAIN node each lock in a secret before either sees the other’s, so neither side can bias the outcome. Every spin ships with a /verify link any player can check.

slot-server.mjs
import { NodeClient } from "@rain/rng-node";
import { drbg } from "@rain/rng-core";

const rng = new NodeClient({
  baseUrl: "https://api.rainrng.xyz",
  apiKey:  process.env.RAIN_API_KEY,   // server-side only
  operatorPubKey: NODE_PUBKEY,         // pin it
});

await rng.open("player-42");
const round = await rng.round(JSON.stringify({ bet: 1 }));

const reels = drbg(round.gameSeed("slot-v1"), "slot-v1").cursor();
[reels.intBelow(20), reels.intBelow(20), reels.intBelow(20)];
// → [7, 13, 2]   proof: await rng.verifyUrl(round.k)
live

Node status

Read live from api.rainrng.xyz when this page loads. These endpoints are public and need no key.

—
—
—
—
—
—
—
—
—
protocol

How a round works

Each side commits to its secret before it can see the other’s. The result of a round is fixed only when both reveals meet. The house can’t pick a seed that favours it, and your server can’t either.

01 · terms

Node commits

The node publishes houseSeedCommit and the root of a hash chain of 4,096 rounds, all signed with its ed25519 key.

02 · open

Operator commits

After seeing the terms, your server sends its own seed and chain root. The session seed mixes both: keccak(houseSeed, playerSeed, sessionId).

03 · reveal

Round k

Your server reveals its chain link for round k. The node checks it, persists to Postgres before replying, then reveals its own link.

04 · settle

Draw & prove

The round value r_k seeds an SP 800-90A HMAC-DRBG (or ChaCha20). Outcomes use unbiased rejection sampling. Every round gets a public proof.

# one round, as seen on the wire operator ──POST /v2/terms─────────────▶ node houseSeedCommit, houseChainRoot, sig operator ──POST /v2/open/:termsId──────▶ node playerSeed, playerChainRoot operator ──POST /v2/reveal {k, pRev}──▶ node check link → WAL write → reply operator ◀─────────── {k, hRev} ────────── node r_k = keccak256(pRev_k, hRev_k, sessionSeed, sessionId, k) outcome = drbg(r_k, gameId).intBelow(N) player ──GET /verify/:sid/:k────────▶ node public proof, CORS *
integration

Quickstart

You generate the API key yourself and send us only its SHA-256 fingerprint. The node stores fingerprints only, so the key itself never leaves your servers.

# generate on YOUR server — keep $KEY in your secret store
KEY=$(openssl rand -hex 32)

# send RAIN only the fingerprint (64 hex chars)
echo -n "$KEY" | sha256sum
# → f2e9…5fb3   (cannot be reversed into the key)
# once RAIN confirms your fingerprint is loaded:
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST https://api.rainrng.xyz/v2/terms \
  -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{}'
# 200 → connected      401 → key/fingerprint mismatch
import { NodeClient } from "@rain/rng-node";

const c = new NodeClient({
  baseUrl: "https://api.rainrng.xyz",
  transport: "ws",            // or "http"
  apiKey: process.env.RAIN_API_KEY,
  operatorPubKey: process.env.RAIN_NODE_PUBKEY,
});
const sid = await c.open("session-label");
// pipelining: commit k+1 while the reels of k animate
# every protected call carries the same header
Authorization: Bearer <RAIN_API_KEY>

# WebSocket: header on upgrade, or first frame
wss://api.rainrng.xyz/v2/ws
{"type":"auth","key":"<RAIN_API_KEY>"}

# errors
401 unauthorized   403 other operator's session
429 rate limited (honor Retry-After)   503 self-test failed

Integration rules

  • Keep the key on your server only. Never put it in a browser or mobile build. Players don’t need it, because /verify is public.
  • Pin the node key. Your client rejects terms not signed by the published ed25519 key.
  • Refuse bets if the node is down. Never fall back to local randomness.
  • Chain rollover. After 4,096 rounds, open new terms with one call. No gas, no checkpoint.
  • Pipelining. Send commit k+1 during the animation of k. With a reel animation of 1 s or more, the network drops out of the player’s wait entirely.
  • Show proofs. Link every result to /verify/:sid/:k under a “Powered by RAIN RNG” badge.
reference

API

JSON over HTTPS, with the same envelopes over WebSocket. Base URL https://api.rainrng.xyz.

methodpathaccessdescription
POST/v2/termsapi keyNode-signed terms: houseSeedCommit, houseChainRoot, chain length.
POST/v2/open/:termsIdapi keyOperator seed and chain root; opens the session and binds it to your operator id.
POST/v2/revealapi keyCommit for round k. The node persists, then returns hRev_k. Idempotent on replay.
GET/v2/reveal/:sid/:kapi keyRe-fetch an already-revealed round, e.g. after a client crash.
WS/v2/wsapi keyThe same operations as JSON envelopes over one connection, for the lowest latency.
GET/verify/:sid/:kpublicProof for one round: both reveals, the session seed and r_k. CORS *.
GET/healthzpublicLiveness, store, auth mode, rate limit, self-test state.
GET/selftestpublicRuns known-answer tests now. Any failure switches output off (503) until fixed.
GET/metricspublicPrometheus text: rounds, latency, 401/403/429 per operator.
GET/publicNode identity: operator id, ed25519 public key, chain length, endpoint list.
for players

Verify a round

Paste a proof link, or a session id and round number. This page fetches the proof straight from the node; nothing passes through us.

proof
design

Security model

Built so that a bug, a crash or a bad actor on either side shows up in the proof and can’t pass silently.

Two-party entropy

Each side draws 256 bits from the OS CSPRNG and commits to them first. A seed is never taken from the clock or a fixed value, or set by one side alone.

Persist before reveal

Every reveal is written to Postgres before the node answers. After a restart or a client crash, each round re-fetches identically and can never be redrawn.

Signed terms

Terms are signed with the node’s ed25519 key. Clients pin the public key, so a spoofed node is rejected.

Per-operator keys

Keys are stored as SHA-256 hashes only and compared in constant time. A session belongs to the operator that opened it. Any other operator gets 403.

Rate limiting

Each operator gets its own token bucket, 429 with Retry-After, and its own metrics.

Self-tests

Known-answer tests for keccak, SHA-256, HMAC-DRBG and ChaCha20 run at startup, every 24 h and on demand. If one fails, the node stops issuing rounds.

evidence

Statistical & latency results

Our own pre-testing, run on the same code the node runs, and organised to the GLI-19 v3.0 structure.

0 failed

Dieharder 3.31.1 full battery, stream input. ChaCha20 90 passed / 5 weak; HMAC-DRBG 83 passed / 3 weak.

188/188

NIST SP 800-22 result lines in range, for each mechanism (1,000 × 10⁶ bits).

15/15

TestU01 SmallCrush statistics passed, for both mechanisms.

10⁸

Scaled game outcomes per game (dice, roulette, coin, reels) checked with χ², KS, serial correlation and runs tests.

88 ms p50

One spin over WAN, sequential, coast-to-coast path (~95 ms RTT). 0 errors in 4,000 rounds.

23 ms p50

Same path with WebSocket and pipelining, at a harsh 60 ms animation.

≈ 0 ms network

With a real reel animation of 1 s or more, the reveal is already waiting when the player looks.

Status, stated plainly: RAIN RNG is pre-certification. The numbers above come from our own test run, not from a lab certificate. External audit and lab certification are planned. This node is the Stage 1 test environment; real-money traffic waits for the production stage.