BUILDER SDK — SHIP A GAME ON RAIN CHANNELS
ChannelManager V3 is game-agnostic. A game is three artifacts: (1) an on-chain IRulesVerifier — a pure referee for disputes, (2) a client-side engine mirror inside channels-sdk.js, and (3) house-node support so an operator can serve it. Blackjack and roulette are the two live modules; roulette (296 lines of Solidity, shipped in one release) is the worked example below.
Reality check: today the mux and verifier set are wired by protocol governance (mgr.setVerifier is owner-gated) and the only house node is foundation-run. Third-party games and bonded external operators are the designed path, not yet a permissionless one. Talk to us before building for production.
1THE IRulesVerifier INTERFACE
A rules verifier makes channel disputes objective: given two consecutive dual-signed states (plus the game payloads that hash to their gameStateHash fields, and any reveals), it decides deterministically whether the transition was legal.
interface IRulesVerifier {
struct ChannelCtx {
bytes32 channelId;
bytes32 sessionSeed; // Pyth Entropy seed fixed at open
bytes32 playerChainRoot; // h_0 of the player's hash chain
bytes32 houseChainRoot; // h_0 of the house's hash chain
}
struct FinFields {
uint96 playerBal; uint96 houseBal; uint96 feeAccrued;
uint96 lostGross; uint96 grossAtRisk;
uint8 phase; // 0 = settled/idle (closable), 1 = hand in progress
}
function verifyTransition(
ChannelCtx calldata ctx,
bytes calldata prevGame, // abi-encoded state, keccak == prev ChannelState.gameStateHash
bytes calldata nextGame,
uint8 action, // game-specific action id
bytes32[] calldata reveals // (playerReveal, houseReveal) pairs consumed
) external view returns (bool valid, FinFields memory prevFin, FinFields memory nextFin);
function finOf(bytes calldata game) external view returns (FinFields memory);
}
Design rules for your verifier:
- Pure and deterministic. No storage, no oracle, no block data. The same inputs must verify identically forever — it's a fraud-proof court, not a game server.
- Recompute financials, never trust them.
FinFieldsis derived from the game payload; the manager cross-checks it against the signedChannelState. Conservation must hold across every legal transition. - Randomness comes only from reveals. Derive outcomes as
keccak(pRev, hRev, sessionSeed, channelId, k) % N— roulette uses% 37, blackjack% 52. Unbiased if either party is honest. - Enforce solvency at bet time. Roulette's
_betrejects any batch whose worst-case payout + max fee exceeds gross + houseBal, so_spincan never underflow. Your game needs the equivalent bound, and the house node must map your worst case into the manager's8 × maxUnitStake ≤ allocationbudget. - A legal transition must never be punishable.
proveBadTransitionslashes the signer of an illegal state — false positives are a critical bug. Differential-fuzz your Solidity verifier against your TS engine mirror (roulette's hash-parity is proven in tests: identicalkeccak256(abi.encode(RGame))both sides).
Worked example — roulette: two actions. A_BET (mux 32): IDLE→SPINNING, the bet batch (≤40 bets) is dual-signed before any randomness exists, gross debited from playerBal, well-formedness + solvency checked. A_SPIN (mux 33): SPINNING→IDLE, one mutual reveal pair, number = keccak(pRev,hRev,sessionSeed,channelId,k) % 37, exact payouts 35/17/11/8/5/2/1:1, losing stakes to house minus the 1.25% fee. 17 dedicated forge tests cover every bet type's win and loss path, mapping parity, conservation fuzz and fraud proofs in both directions.
Registration: games plug in through GameMuxVerifier, a 45-line action-range dispatcher (0–31 blackjack, 32–63 roulette; a new game takes the next range) registered as the manager's single verifier. Dispatch isolation is tested: one game's payload can never validate under another's rules.
2CLIENT SDK — channels-sdk.js
The SDK (RainChannels) manages the whole session lifecycle. Real API from the live code:
const session = new RainChannels.ChannelSession({
wsUrl: "wss://api.rainsandbox.xyz",
chainId: 42161,
manager: "0xe13DC056111906aA4d3659880EBDC67138c76DA7",
gameType: "blackjack", // or "roulette"
});
// OPEN — one wallet signature authorizes the burner session key;
// funds can only ever pay out to the wallet, never the session key.
await session.open({ playerWallet, deposit: 2_000_000_000n /* 2,000 RUSD */,
maxUnitStake, openNonce });
// PLAY — every action is an off-chain dual-signed state (~300ms, no gas)
await session.act(A.DEAL, 500_000_000n); // bet 500 RUSD
await session.act(A.HIT);
await session.act(A.STAND);
// roulette: session.placeRouletteBets(bets); await session.spinRoulette();
// PERSIST — survive refreshes and tab sleep
const snap = session.snapshot(); // → localStorage
await session.restore(snap); // resume from last dual-signed state
// CLOSE — cooperative close settles on-chain at the final state
await session.close();
What it does under the hood — all verifiable in the source:
- Hash chain: generates a 4,096-deep keccak chain from a WebCrypto secret; publishes only the root at open; reveals preimages one per card/spin, verifying the house's chain the same way (
keccak(h_k) == h_(k-1)). - Local rules engine mirror: a full TS/JS copy of the Solidity verifier validates every house-proposed state before countersigning. You never sign a state your own engine disagrees with.
- EIP-712 signing: domain
"RAIN ChannelManager" v1, bound to chainId 42161 and the manager address — states can't be replayed cross-channel or cross-chain (fuzz-tested). - Reconnect & self-healing: resume via
helloreplay;unknown_channelerrors trigger on-chain state checks andrecover_closefrom the local snapshot rather than zombie retry loops. - Dispute path: if the house vanishes,
forceClosefrom the last dual-signed state; the challenge window and HouseBond compensation do the rest.
3HOUSE-NODE OPERATOR GUIDE
RPC_URL=... CHAIN_ID=42161 \
CHANNEL_MANAGER=0xe13DC056111906aA4d3659880EBDC67138c76DA7 \
RUSD=0x260c7019E760763988843Bc5b873d749b5937469 \
OPERATOR_PRIVATE_KEY=... HOUSE_SESSION_PRIVATE_KEY=... \
DATA_DIR=./data DEFAULT_ALLOCATION=5000000000 HOUSE_NODE_PORT=8787 \
node dist/house-node.cjs
Keys via env only. The OPERATOR key funds allocations and sends txs; the HOUSE_SESSION key signs channel states only. Durability guarantees the reference node implements (and any operator must match):
- Write-ahead rule: every state is persisted (fsync + atomic rename) before any countersignature leaves the node — failover can never regress below a signature the player holds.
- Per-hand on-chain checkpoints (~130k gas ≈ $0.01–0.02 on Arbitrum, default ON), checkpoint-on-disconnect, SIGTERM flush — three layers so a redeploy mid-session never destroys the only copy of a signed state.
- Grace timers: 180 s player grace → forfeit path; reconciliation sweep cooperatively closes abandoned channels;
recover_closesettles from client snapshots. - House-reveals-first ordering: the node never sees a card before the player does — its stall option degenerates to refusable service, which is slashable.
4HOUSEBOND ONBOARDING — BOND → APPROVAL → RUN
- Bond. Deposit RUSD stake into HouseBond (0x37E41E1d1EE2F96cE0203af06d639448547ade84). Your bond is the ceiling on the limits you can offer: table limit ≤ slashable/32. The two live operators bond 100k RUSD each → 3,125 RUSD max bets. Withdrawals take 7 days and remain slashable while pending.
- Approval. Operator registration is governance-gated in v1 (single foundation-run node; HA/watchtower/HSM explicitly out of scope — see accepted risks). The bonded-external-operator program is the designed v2 path.
- Run the node. Deploy the house node with your keys, wire your allocation budget, and serve sessions. Liveness faults cost you
max(2× open gross, 5 RUSD)compensation to the player plus an equal amount burned — per fault, automatically, from your bond.
