# RAIN Channels — Technical Specification v1

**State-channel blackjack for the RAIN protocol on Arbitrum One**
**Status:** Draft for review · **Date:** 2026-07-28 · **Author:** Dario (spec subagent)
**Scope:** design/architecture specification only. No code changes to the live V3 system are implied by this document.

**Companion documents:**
- `research/randomness-landscape-2026-07.md` (landscape research; esp. FunFair Fate Channels, Q4/Q5)
- `protocol/src/BlackjackV3.sol` (current live per-action Pyth Entropy game — the fallback path)
- `DEPLOYMENT.md` (current deployment & economics)

---

## 0. Summary

RAIN Channels moves blackjack gameplay off-chain into a two-party state channel between the **player client** and a **house node** operated as protocol infrastructure and bankrolled by the **RiskVault**. Every card is produced by an **instant mutual commit-reveal** inside the channel (no oracle, no block confirmation), giving:

- **< 1 s per action** (typically < 200 ms round trip),
- **~zero marginal cost per action** (no gas, no oracle fee — only two on-chain txs per session),
- **two-party trust**: neither player nor house can forge or predict a card alone,
- **enforced worst case**: on any abort, either party can settle on-chain at the **last mutually signed state**, with explicit penalty rules that make stalling strictly unprofitable for both sides.

Economics are unchanged from Blackjack V3: win pays 2× gross, push refunds, natural pays 2.5×, 1.25% fee on **lost gross only**, waterfall burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 ppm, and worst-case exposure capped at ≤ 2% of vault free float. The existing V3 Pyth-Entropy game remains deployed as the fallback path.

---

## 1. Goals & Guarantees

### 1.1 Hard goals

| # | Goal | Target |
|---|---|---|
| G1 | Per-action card latency | < 1 s (design target < 200 ms over WebSocket) |
| G2 | Marginal cost per action | ≈ $0 (0 gas, 0 oracle fees; 2 on-chain txs per *session*) |
| G3 | Randomness trust | Card is unpredictable & unforgeable unless BOTH parties collude with themselves (impossible) — i.e., random if **either** party is honest |
| G4 | Abort safety | Worst case for an honest party on any disconnect/stall/abort = settlement at last mutually signed state, plus penalty flows in the honest party's favor |
| G5 | Foundation blindness | No RAIN foundation key or server secret can reveal a future card. House node secrets only reveal the house's *half* of each card; player half is unknown to it |
| G6 | Economics parity | Identical hand economics, fee, waterfall, and exposure cap as Blackjack V3 |

### 1.2 What IS and is NOT trustless — stated plainly

**Trustless (cryptographically enforced):**
- No card can be *forged*: a card only exists as `keccak256(playerReveal ‖ houseReveal)` against pre-committed hashes; either party detects a wrong reveal immediately and can prove it on-chain.
- No card can be *predicted unilaterally*: each party's reveal is committed before the other's is known; predicting the card requires both secrets.
- Funds: final balances are enforced by the ChannelManager contract from the highest-nonce mutually signed state. Neither party can steal escrowed funds, replay old states (challenge mechanism), or settle a state the other never signed.

**NOT trustless (economically enforced instead):**
- **Selective stalling.** In any mutual commit-reveal, the party that reveals *second* learns the outcome an instant before countersigning. A malicious party could refuse to sign/reveal whenever the pending card is unfavorable. This cannot be cryptographically prevented in a 2-party channel; it is *punished*:
  - Player stalls → after grace period, open hand is settled as a **forfeit** (player loses the hand's gross stake). See §4.2.
  - House stalls → player force-closes; house is **slashed** from its bond and the player receives a **compensation payment** on top of the last-signed balance. See §4.1.
  - §4.3 proves that under these rules stalling never has higher EV than playing on, for either party.
- **Liveness of the house node.** Sub-second UX requires the house node to be online. Downtime never costs the player money (G4), but it costs the player *time* (an on-chain force-close and challenge window). Mitigated operationally (§4.1) and economically (slashing).
- **Privacy.** The house node sees the player's actions and IP like any web casino. This spec does not attempt gameplay privacy.
- **Arbitrum itself.** Settlement and disputes inherit Arbitrum One's trust model (centralized sequencer FCFS ordering, Ethereum DA). Force-close timers are sized so brief sequencer outages cannot expire an honest party's window (§4.4).

**Honest marketing line:** "Neither we nor anyone else can know or choose your next card — and if anyone stops playing, the chain pays you what you're owed, automatically." That claim is true under this design. "Fully trustless" is not; do not claim it.

---

## 2. Architecture

### 2.1 Components

```
                        ARBITRUM ONE (on-chain)
  ┌─────────────────────────────────────────────────────────────────┐
  │  ┌──────────────────┐   funds    ┌────────────┐  fee waterfall  │
  │  │  ChannelManager  │◄──────────►│ RiskVault  │──► burnSink     │
  │  │  open/close/     │  allocate/ │ (LP float) │──► rebatePool   │
  │  │  checkpoint/     │  return    └────────────┘──► anchorReserve│
  │  │  dispute/slash   │                          └─► vaultSink    │
  │  └───────┬──────────┘   ┌───────────────┐                       │
  │          │              │ HouseBond     │  (slashable stake     │
  │          │              │ (per operator)│   backing liveness)   │
  │          │              └───────────────┘                       │
  │          │  optional: 1× Pyth Entropy request per session open  │
  └──────────┼──────────────────────────────────────────────────────┘
             │ open / close / dispute txs (2 txs per session, happy path)
             │
   ══════════╪══════════════ OFF-CHAIN ══════════════════════════════
             │
  ┌──────────┴─────────┐    WebSocket (wss), signed      ┌───────────────────┐
  │  Player client     │◄───────────────────────────────►│  House node       │
  │  (browser SDK in   │   state transitions:            │  (protocol infra, │
  │   app/blackjack.js)│   COMMIT / ACT / REVEAL /       │   TS or Rust,     │
  │  session key +     │   STATE / SIG messages          │   HA cluster,     │
  │  hash-chain seed   │                                 │   HSM session key)│
  └────────────────────┘                                 └───────────────────┘
        │  watches chain for disputes                          │
        └───────────────► Watchtower (optional, §6.7) ◄────────┘
```

- **ChannelManager.sol** (new): escrow, channel registry, cooperative close, forced close with challenge, fraud proofs (newer-state and bad-reveal), timeout adjudication (forfeit/slash), fee waterfall execution at close. Reads/writes RiskVault allocations; slashes HouseBond.
- **HouseBond.sol** (new, may be a module of ChannelManager): stake posted per house-node operator identity; slashed for liveness faults; funds player compensation.
- **House node service** (new, off-chain): TypeScript (or Rust) service holding the house's per-channel hash-chain seed and session signing key; runs the rules engine; hot-hot redundant behind a health-checked endpoint; persists every signed state to replicated storage **before** countersigning (write-ahead rule).
- **Client SDK** (new, integrated into `app/blackjack.js`): burner session key in browser, hash-chain generation, state signing/verification, local rules engine (mirror of house engine — both sides validate every transition), dispute submission path.
- **RiskVault / FeeSplitter / RUSD**: existing contracts, reused. ChannelManager becomes an authorized allocator of vault float, subject to the same exposure cap discipline as V3.

### 2.2 Signed state — versioned struct

All off-chain states are EIP-712 typed structures signed by **both** session keys. Canonical struct (Solidity view, encoded for hashing):

```solidity
struct ChannelState {
    bytes32 channelId;      // keccak(mgr, player, houseOp, openNonce, chainId)
    uint32  stateFormat;    // version of this struct layout (=1)
    uint64  nonce;          // strictly increasing; highest nonce wins
    // -------- balances (RUSD, 6 decimals) --------
    uint96  playerBal;      // player's free balance inside channel
    uint96  houseBal;       // house allocation remaining
    uint96  feeAccrued;     // 1.25%-of-lost-gross accumulated this session (owed to waterfall)
    // -------- hand state --------
    uint8   phase;          // Idle | Committed | Dealt | ActingH0 | ActingH1 | AwaitFinal | HandSettled
    uint96  unitStake;      // current hand unit stake
    uint8   numHands;       // 1 or 2 (split)
    uint8   activeHand;
    bool    doubled0; bool doubled1; bool splitAces;
    bytes   pCards0; bytes pCards1; bytes dCards;  // card indices 0..51, drawn so far
    uint16  cardCount;      // total cards drawn this SESSION (hash-chain cursor)
    // -------- randomness anchors --------
    bytes32 playerChainRoot;  // head of player's hash chain (set at open, immutable)
    bytes32 houseChainRoot;   // head of house's hash chain (set at open, immutable)
    bytes32 sessionSeed;      // optional Pyth Entropy seed mixed into every card (0 if unused)
    bytes32 pendingCommitP;   // player's commit for the NEXT card(s), 0 if none  (defense-in-depth; see §3.2)
    bytes32 pendingCommitH;   // house's commit for the NEXT card(s), 0 if none
}
```

Signature = EIP-712 over `keccak256(abi.encode(DOMAIN, ChannelState))`; domain separator binds `chainId = 42161` and the ChannelManager address (replay protection across chains/deployments, §6.1).

**Invariant per state:** `playerBal + houseBal + feeAccrued + grossAtRisk(handState) == playerDeposit + houseAllocation` (conservation, §5.6). Both engines assert this before signing; ChannelManager asserts it on any state submitted on-chain.

### 2.3 Transport

**WebSocket (wss) chosen over libp2p.** Rationale: single well-known counterparty (the house endpoint), browser-native, trivial reconnect semantics, no NAT traversal problem, no DHT. libp2p buys peer discovery we don't need and adds bundle weight and failure modes. All messages are already signed at the application layer, so transport security is belt-and-braces (TLS) rather than load-bearing. Message framing: JSON (v1) with canonical field ordering for hashing; migrate to a compact binary encoding later if needed.

---

## 3. Randomness Protocol

### 3.1 Primitives

**Hash chains (one per party per session).** At channel open, each party generates a secret `s` and computes a chain `h_N = keccak(s)`, `h_{i} = keccak(h_{i+1})` down to `h_0` (the **root**, published in the opening state; N = 4,096 covers ~800+ hands). Card *k* of the session consumes preimages: the party's reveal for card *k* is `h_k`, verifiable in O(1) against the previously revealed `h_{k-1}` (`keccak(h_k) == h_{k-1}`). Hash chains make every future reveal *pre-committed at channel open* — a party cannot choose its reveal adaptively per card, only choose whether to reveal at all (the stall case, punished in §4).

**Card derivation.**

```
card_k = uint8( uint256(keccak256(abi.encode(
             playerReveal_k, houseReveal_k, sessionSeed, channelId, k
         ))) % 52 )
```

- `% 52` with replacement = **infinite continuous shuffle**, identical to V3's `cardAt` model (same house edge tables, same rules engine math, no deck-tracking state). Modulo bias over a 256-bit hash is < 2⁻²⁴⁸ — ignorable.
- `sessionSeed`: **optional** single Pyth Entropy request made by ChannelManager at channel open, mixed into every card. Cost: one oracle fee per session (cents). Benefit: even if a player's browser RNG is weak/compromised AND the house colludes with the entropy of that browser, the session still contains oracle entropy neither controlled. Recommended ON for real-money, OFF acceptable for play-money. It adds ~2–4 s to channel open only, never to per-card latency.
- `channelId, k` domain-separate cards across sessions and positions (§6.1).

### 3.2 Why hash chains alone suffice — and why we keep per-batch commits anyway

With hash chains, both parties' entire reveal sequences are fixed at open. Neither can grind reveals. The only remaining freedom is the *order of exposure*: whoever reveals second for card *k* computes the card an instant early. To keep this window symmetric and tight, the protocol uses **house-reveals-first** ordering (below): the house — the party with the slashable bond and the most to lose — always exposes its half first. The player then reveals; the house learns the card only upon receiving the player's reveal, simultaneously with (or after) the player. This means:

- The **player** can see a pending card before countersigning the post-draw state → player stall risk → punished by forfeit (§4.2).
- The **house** never sees a card before the player does → the house's stall option degenerates to refusing service, punished by slashing (§4.1).

`pendingCommitP/H` fields exist for actions that draw **multiple cards in one step** (Deal draws 3; Split draws 2): both parties first exchange a commit to nothing beyond "I will reveal preimages k..k+m" — in v1 these are simply the hash-chain indices, so the fields are informational/defensive (they pin the expected cursor range in the signed pre-draw state, making cursor desync disputes trivially decidable).

### 3.3 Message sequence per action

Notation: `P` player, `H` house. Every `STATE` message is a full `ChannelState` + sender signature; the receiver validates the transition with its local rules engine and returns its counter-signature (`SIG`). A state is **final** when both signatures exist. Reveals for card *k*: `pRev_k = playerChain[k]`, `hRev_k = houseChain[k]`.

**BET / DEAL (3 cards: player c1, c2, dealer up — hole card does NOT exist yet, ENHC as in V3):**

```
1. P → H : STATE{nonce=n,   phase=Committed, unitStake=B, playerBal-=B, cursor pins k..k+2} + sigP
2. H → P : SIG(n)                              // both signed: stake is escrowed in-channel
3. H → P : REVEAL{hRev_k, hRev_k+1, hRev_k+2}  // house half first
4. P → H : REVEAL{pRev_k, pRev_k+1, pRev_k+2}  // both halves known → cards computable by both
5. P → H : STATE{nonce=n+1, phase=Acting(or AwaitFinal if natural), cards appended, cardCount=k+3} + sigP
6. H → P : SIG(n+1)
```

**HIT (1 card):**
```
1. P → H : STATE{nonce=n,   phase=Acting, intent=HIT, pins cursor k} + sigP     // "I hit" — binding
2. H → P : SIG(n)
3. H → P : REVEAL{hRev_k}
4. P → H : REVEAL{pRev_k}
5. P → H : STATE{nonce=n+1, card appended; bust/21 auto-advances hand/phase} + sigP
6. H → P : SIG(n+1)
```
Step 1 is critical: the player signs the *intent to hit* **before** any reveal — the drawn card belongs to that intent. A player cannot "un-hit" a bad card: the last fully signed state (n) already records the hit with cursor pinned, and the timeout adjudication (§4.2, rule T3) resolves a pinned-cursor stall by drawing the card from the on-chain-verifiable reveals.

**STAND:** pure state transition, no cards:
```
1. P → H : STATE{nonce=n, activeHand advances or phase=AwaitFinal} + sigP
2. H → P : SIG(n)
```

**DOUBLE (escrow + 1 card):** identical to HIT but the STATE in step 1 also moves an extra `unitStake` from `playerBal` into the hand's gross and sets `doubledX`. Player must have the balance in-channel; no on-chain tx.

**SPLIT (escrow + 2 cards):** as DOUBLE but pins cursor k..k+1; state splits `pCards0/pCards1`, sets `numHands=2`, `splitAces` if applicable (split aces get one card each and auto-stand, matching V3).

**DEALER FINAL (hole card + dealer draws, then settle):** when phase = AwaitFinal:
```
1. H → P : STATE{nonce=n, phase=AwaitFinal, pins cursor k..k+m_max} + sigH   // m_max = 9 (MAX_CARDS-1) worst case
2. P → H : SIG(n)
3. H → P : REVEAL{hRev_k .. hRev_k+j}   // house reveals sequentially; j determined by S17 stopping rule
4. P → H : REVEAL{pRev_k .. pRev_k+j}
5. Either → other : STATE{nonce=n+1, phase=HandSettled, dealer cards appended,
                          balances updated per payout table, feeAccrued += lostGross*1.25%} + sig
6. Other → : SIG(n+1)
```
The number of dealer cards is a *deterministic function* of the reveals (draw while total < 17, S17, stop at MAX_CARDS=10): both engines compute the same `j`; the reveals are consumed lazily in order, and unconsumed pinned indices roll forward to the next hand's cursor. Any disagreement about `j` is objectively decidable on-chain from the reveals (§5.4).

**Skipped-hole-card optimization** (all player hands busted): as in V3, no dealer cards are drawn; phase goes straight to HandSettled. The cursor does not advance.

### 3.4 Rules engine

**Variant: ENHC (European No-Hole-Card), S17, BJ 3:2, double any two, split once (aces one card each), no insurance/surrender, infinite shoe.** Justification:
1. **Consistency:** identical to live V3 — one audited rules engine spec, one payout table, one house-edge disclosure, and the V3 fallback plays the *same game*.
2. **Protocol fit:** ENHC means the hole card genuinely does not exist until the player finishes acting — in the channel this removes any need for the house to hold a hidden-but-committed card during the acting phase (an American peek design would require an extra committed-hole-card sub-protocol and an information-leak analysis around the peek). ENHC keeps the state machine strictly "cards exist ⇔ cards are public to both parties," which is the property the whole dispute system leans on.
3. The ~0.11% house-edge increase of ENHC vs peek is already priced into V3's disclosed economics.

Payouts inside channel accounting (identical to V3 `_settle`): per-hand gross at risk `g` → win credits `2g` to playerBal, push credits `g`, natural (single un-split hand) credits `2.5g`, loss credits `0` and adds `g` to `lostGross`; `feeAccrued += lostGross × 12,500 ppm`; the remaining `lostGross − fee` accrues to `houseBal`. ENHC natural-beats-all including doubled/split gross; player natural pushes dealer natural.

### 3.5 Latency budget

Steps per HIT: 2 signed-state round trips + 2 reveal messages = 3 RTTs worst case, 1.5 with pipelining (SIG piggybacked on REVEAL). At 50 ms RTT: **< 300 ms**; at 150 ms intercontinental: **< 900 ms**. Signing (secp256k1) and keccak are sub-millisecond. Target G1 met with margin.

---

## 4. Disputes, Timeouts, and the No-Stall-Profit Property

All timers are ChannelManager constants (governance-tunable within bounded ranges):

| Constant | Value | Purpose |
|---|---|---|
| `PLAYER_GRACE` | **180 s** (3 min) | Player disconnect grace before forfeit path opens |
| `CHALLENGE_WINDOW` | **10 min** | Forced-close challenge period (submit newer state) |
| `REVEAL_WINDOW` | **60 s** | On-chain deadline to answer a reveal-demand |
| `HOUSE_RESPONSE_SLA` | **30 s** | Off-chain SLA; breach evidenced by player force-close |
| `HOUSE_COMP` | **max(2 × current hand gross, 20 × entryFee-equivalent, floor 5 RUSD)** | Compensation paid to player from HouseBond on house liveness fault |
| `HOUSE_SLASH` | **HOUSE_COMP + protocol penalty (equal amount to vaultSink)** | Total bond deduction per fault |
| `MAX_SESSION_LEN` | 24 h | Hard cap; forces close/refresh, bounds hash-chain length |

### 4.1 House node liveness (mandated design)

**Engineering requirement.** The house node is protocol infrastructure backed by the RiskVault and MUST be engineered to not disconnect:
- ≥ 2 hot-hot node instances in independent availability zones behind a health-checked anycast/failover endpoint;
- **write-ahead state rule:** a node must persist (replicated, fsynced) every state it signs *before* transmitting the signature — failover never regresses below the last signature the player holds;
- shared session-key custody via HSM/KMS or threshold signing across replicas;
- external watchdog that force-drains new-channel intake when replication lag or chain-connectivity degrade;
- on-chain monitoring bot that auto-responds to challenges (the "house watchtower").

**If it disconnects anyway (protocol rule H1):** the player calls `forceClose(channelId, lastSignedState, sigP, sigH)`. After `CHALLENGE_WINDOW` with no newer state submitted:
- player receives **full in-channel balance per the last signed state**, and any open hand's gross **is returned to the player in full** (house failed to serve; the hand is voided in the player's favor — mirror-asymmetric to the player-stall rule, deliberately), and
- player receives **HOUSE_COMP** from HouseBond, and an equal penalty amount is transferred from HouseBond to vaultSink;
- the event is recorded; repeated faults escalate (operator ejection thresholds are a governance parameter).

House downtime is thus a **slashable liveness fault**: it can never gain the house anything and always costs bond.

**Rule H2 (reveal-demand):** if the house has signed a pinned-cursor state but not delivered reveals, the player (or anyone) calls `demandReveal(channelId, indices)`; the house must post `hRev_k..` on-chain within `REVEAL_WINDOW` (chain roots are in the opening record, so validity is checkable on-chain in O(1) per reveal). Failure = liveness fault → H1 consequences. Note the house has *no informational reason* to stall here — under house-reveals-first ordering it hasn't seen the player half — so H2 exists only to convert crashes into clean, compensated outcomes.

### 4.2 Player disconnect / stall (mandated design)

Default rule: **abandoning an open hand forfeits that hand's gross stake** — but honest users lose Wi-Fi, so a grace period applies.

- **Grace period: 180 seconds (3 minutes).** Justification: p95 consumer network blips, page reloads, and mobile network handoffs recover well under 60 s; 3 min covers a router reboot; anything longer mostly protects deliberate stallers while locking house float and (with sessionSeed) letting a player who has *seen an unfavorable pending reveal* shop for a disconnection excuse. 3 min balances honest-user protection against griefing surface; it is a governance-tunable constant in [120 s, 300 s].
- **Reconnect & resume:** within grace, the client re-authenticates with its session key and resumes from the exact last dual-signed state (the house replays `STATE`+`SIG` history from its store; the client verifies against its own). No penalty, no card re-draws — pinned reveals are re-delivered idempotently.
- **After grace expires (rule T1 — mid-decision stall):** if phase = Acting and no pinned cursor is outstanding, the house calls `timeoutClose(channelId, lastSignedState)`; after `CHALLENGE_WINDOW`, the open hand settles as a **fold/loss**: the hand's full gross goes to the house side, `feeAccrued += gross × 1.25%`, all other balances per the signed state. (Contrast V3's `forceStand`, which plays the hand out — in a channel we cannot draw more cards without the player's reveals, and "abandon = lose" is the mandated economic rule.)
- **Rule T2 — post-intent stall (player saw a pending card and vanished):** if the last signed state pins cursor k..k+m (player already signed the intent — hit/double/deal/final), the house posts its reveals on-chain and calls `demandReveal` against the *player's* chain. Player fails to reveal within `REVEAL_WINDOW` + remaining grace → the hand settles as a **fold/loss of the full gross** (same as T1). The player cannot dodge a bad card by stalling: the bad card outcome is a loss of at most the gross, and the stall outcome is exactly a loss of the gross — never better (see §4.3).
- **Rule T3 — player reveals available on-chain:** if the player *does* answer the demand on-chain (e.g., client dead but watchtower alive), the drawn card is computed on-chain from both reveals and the hand continues or settles deterministically per the rules engine — no forfeit, because no information was withheld.
- **Idle-but-connected:** a connected player who simply doesn't act is nudged at 60 s and treated as T1 at 180 s. Timer basis: last dual-signed state's timestamp as recorded in the house's signed heartbeat; on-chain, the timer starts at `timeoutClose` submission (grace runs off-chain, challenge window on-chain — total player protection ≥ 3 min + 10 min).

### 4.3 No-stall-profit theorem

**Claim.** Under rules H1–H2 and T1–T3, at every protocol state, stalling is weakly dominated by continuing to play, for both parties.

**Player.** Enumerate the player's information states:
1. *No pending reveal (phase Acting, no pinned cursor).* Stall outcome (T1): lose the full gross `g` of the open hand. Playing on: EV ≥ −g always (the worst any blackjack continuation can do is lose the gross; standing immediately guarantees EV > −g strictly, since dealer bust probability > 0). Hence stall EV = −g ≤ playing EV, with strict inequality in every non-degenerate state. ∎
2. *Pending reveal seen (player signed intent, house revealed, player computed an unfavorable card).* This is the interesting case — the player knows the card. Stall outcome (T2): lose full gross `g`. Accepting the card: the hand continues with that card; worst case is bust = lose `g`; every other continuation has EV > −g. So even a *maximally* unfavorable card leaves accepting ≥ stalling, with equality only when the card busts a maximally-committed hand — i.e., the stall option adds exactly zero exploitable value. ∎
3. *Between hands (phase Idle/HandSettled).* No open gross; stalling just triggers a normal close at the signed balances — nothing to gain, session ends. ∎

**House.** Under house-reveals-first ordering the house never possesses card information the player lacks, so a house stall is never *informed* — it can only be a denial of service. Its outcome (H1): the open hand's gross returns to the player, the player keeps the full last-signed balance, and the house pays HOUSE_COMP + penalty from bond. Playing on: house EV per hand = +house-edge × stake > 0. Stalling EV = −(HOUSE_COMP + penalty) < 0 < playing EV. Sized so that even a hypothetical information leak (compromised player client revealing pRev early) cannot make stalling profitable: the most a stall can "save" the house is the hand's worst-case payout ≤ 8 × unitStake gross exposure ≈ current hand gross × small multiple, and HOUSE_COMP ≥ 2 × current hand gross plus an equal protocol penalty — deterrence ratio ≥ 4:1 on the *theoretical* leak case and infinite in the no-leak case. Table limits per channel are capped so that `8 × unitStake ≤ HOUSE_COMP` always holds (enforced at open and on every stake change). ∎

**Corollary (griefing bound).** A malicious player can cost the house at most: locked float for grace + challenge (~13 min) and one `timeoutClose` gas fee, while losing his own open gross — self-punishing. A malicious house can cost the player at most ~13 min of time, at a bond cost ≥ HOUSE_COMP + penalty — self-punishing. Neither party can impose an unbounded or profitable grief.

### 4.4 Sequencer-outage safety

All on-chain windows (`CHALLENGE_WINDOW`, `REVEAL_WINDOW`) are enforced in L2 block timestamps; Arbitrum's force-inclusion path (delayed inbox, ~24 h) means a sequencer censoring a party's challenge tx extends risk. Mitigation: `CHALLENGE_WINDOW` responses may also be force-included; for v1 (play-money and small-stakes), 10 min + monitoring is accepted risk; before large-stake mainnet, raise `CHALLENGE_WINDOW` to ≥ the force-inclusion delay for channels above a value threshold, or gate max channel value. Flagged in §9.

---

## 5. Channel Lifecycle & Funds Flow

### 5.1 Open

```
openChannel(playerSessionKey, playerChainRoot, deposit,
            houseOp, houseSessionKey, houseChainRoot, houseAllocation,
            maxUnitStake, useEntropySeed)  — single tx, either party submits with both sigs
```
1. Player deposits `D` RUSD (transferFrom). Player's *wallet key* signs the open authorization; the *session key* (burner) signs states thereafter (§6.6).
2. ChannelManager pulls `A` RUSD of house allocation from RiskVault float, where `A ≥ worstCasePayoutBuffer` and the **exposure cap** holds (§5.5).
3. Both chain roots and (optionally) a Pyth Entropy request are recorded; when the entropy callback lands, `sessionSeed` is fixed and the channel becomes Active (state nonce 0 = opening balances, implicitly signed by the open tx). Without the seed option the channel is Active immediately.
4. HouseBond solvency check: operator's unslashed bond ≥ HOUSE_COMP+penalty × (open channels + 1) headroom factor.

### 5.2 Incremental checkpoints (optional)

Either party may submit `checkpoint(state, sigP, sigH)` at any time: records the state hash + nonce on-chain (no funds move). Purpose: (a) lets the player crystallize a big win against a later house-side data loss, (b) shrinks the maximum regression any lost-device scenario can suffer, (c) lets the house *release* over-allocated float mid-session (`houseBal` decrease against vault) and top up when the player is on a heater. Checkpointed nonce becomes a floor: disputes cannot settle below it.

### 5.3 Cooperative close (happy path — 1 tx)

Either party submits `cooperativeClose(finalState, sigP, sigH)` where `phase ∈ {Idle, HandSettled}`:
- player receives `playerBal`;
- vault receives `houseBal` back;
- **fee waterfall executes now**: `feeAccrued` splits burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 ppm of each hand's lost gross — arithmetically identical to V3 (`feeAccrued = Σ lostGross × 12,500 ppm`; waterfall shares are computed from the accompanying `Σ lostGross` figure carried in the final state so ppm splits match V3 exactly, rounding to the vault's favor as in V3).

### 5.4 Forced close & fraud proofs

- `forceClose(state, sigs)` starts `CHALLENGE_WINDOW`. During the window anyone may submit:
  - **Newer-state proof:** a dual-signed state with higher nonce (or ≥ checkpoint floor) — replaces the candidate and restarts a shortened window (5 min) once, then finalizes (bounded restart prevents extension griefing).
  - **Bad-reveal proof:** a reveal that fails `keccak(h_k) == h_{k-1}` chained to the recorded root — proves its signer equivocated; the equivocator's *entire in-channel balance for the open hand plus a bond penalty (house) / open gross (player)* is awarded to the honest party. (In practice unreachable for honest software; exists to make forged reveals strictly losing.)
  - **Bad-transition proof:** candidate state's hand fields are inconsistent with the deterministic rules engine applied to a prior dual-signed state + on-chain reveals. ChannelManager includes a pure-function rules verifier (the V3 card/total/settle logic refactored into a library) to adjudicate. This keeps disputes *objective* — no oracle, no arbitration.
- After the window: settle per §5.3 plus the applicable T/H rule (forfeit, void-in-player's-favor, compensation, slash).

### 5.5 Exposure math vs vault free float

Per channel: worst-case house outflow = `A` (its allocation) — the channel *cannot* pay the player more than `A + D` total, and `A` is sized at open as `A = min(requested, headroom)` where per-hand worst case is `8 × maxUnitStake` (split + double both hands, win both — same bound as V3's `worstPayout`) times a session multiplier `M` (recommended M = 25 hands of full-kelly headroom, so a hot streak doesn't force constant top-ups; top-up via checkpoint otherwise).

Vault-level cap (mirrors V3's `MAX_EXPOSURE_PPM = 20,000`): `Σ_openChannels A_i ≤ 2% × freeFloat`, checked at every open and every allocation top-up; additionally `A_single ≤ 0.25% × freeFloat` (concentration sub-cap) so no single session can consume the whole exposure budget. Because `A` is *pre-transferred* into ChannelManager escrow, the vault's accounting is conservative: exposure is fully collateralized, never contingent.

### 5.6 Accounting invariants (must hold in every signed state and enforced on-chain at close)

1. **Conservation:** `playerBal + houseBal + feeAccrued + grossAtRisk = D + A` (grossAtRisk = open hand's escrowed gross; zero between hands).
2. **No negative balances;** stake changes (bet/double/split) only move playerBal → grossAtRisk; settlements only move grossAtRisk → {playerBal, houseBal, feeAccrued} per the payout table.
3. **Monotone nonce;** exactly one open hand per channel (no concurrent hands in v1 — a player wanting parallel hands opens parallel channels, each independently under the exposure cap; eliminates the entire concurrent-hand double-spend class by construction).
4. **Cursor monotonicity:** `cardCount` strictly increases with each drawn card and never exceeds the chain length N; each preimage index is consumed at most once.
5. **Fee correctness:** `feeAccrued = Σ settledHands lostGross × 12,500 / 1e6` — recomputable from the hand log; the close tx recomputes from a compact per-hand `lostGross` accumulator carried in state.

---

## 6. Security Analysis

### 6.1 Replay & domain separation
- EIP-712 domain binds chainId + ChannelManager address + channelId; states from one channel/deployment are meaningless in another.
- `channelId` includes an `openNonce` so a player/house pair reopening a channel can never replay old states.
- Card derivation includes `channelId` and index `k` — reveals cannot be transplanted across sessions or positions.

### 6.2 State regression
- Highest-nonce-wins with challenge window + optional checkpoints. Key lesson from Lightning: the punishment for publishing an old state must be automatic and the honest party's response must not require their key to be online → mitigate with **watchtower** support (§6.7) and, unlike Lightning, *no toxic old states for the player*: because the house is the always-online professional party, the design biases response duties toward the house; the player-side response (submit newer state) is also submittable **by anyone** (states carry both sigs; no secret needed) — so a public watchtower can protect all players at once. This removes Lightning's worst UX failure mode.
- House write-ahead rule (§4.1) prevents the *house itself* from accidentally regressing after failover — the historical cause of real channel-fund losses (cf. Lightning `channel.db` corruption incidents forcing risky SCB recoveries).

### 6.3 Signature & encoding pitfalls
- secp256k1 low-s enforcement and rejection of malleated sigs (use OpenZeppelin ECDSA); never use raw `ecrecover` without zero-address check.
- All hashing over `abi.encode` (not `encodePacked`) to kill ambiguous-concatenation collisions (a classic channel bug class).
- `stateFormat` version field: contract rejects unknown versions; future struct migrations require both-parties re-open, never in-place reinterpretation.

### 6.4 Dispute-window griefing & front-running
- **Dispute spam:** `forceClose` requires the caller to be a channel party (or bonded watchtower); newer-state responses restart a *shortened* window at most once (§5.4) — no infinite extension.
- **Front-running the challenge:** responses are strictly beneficial to the honest party regardless of who lands them (anyone-can-respond), so MEV/front-running a response is harmless; front-running a `timeoutClose` with a newer signed state is exactly the intended defense.
- **Gas-price griefing:** all dispute functions sized to be cheap (< 300k gas typical); on Arbitrum the cost of forcing a dispute is cents — acceptable because the *staller* also self-punishes economically (§4.3 corollary).

### 6.5 Collusion & sybil considerations
- Player↔house collusion is economically pointless: the house's counterparty funds are the vault's; a colluding house operator giving cards away would show up as statistically impossible player winrate → **on-chain statistical audit trail**: every close emits the hand log digest; an off-chain auditor (anyone) can verify session RTP against the binomial expectation and prove misconduct from public data. Operator ejection + bond slash for statistically-proven bias is a governance path (flagged §9).
- A *player who is also the house operator* is excluded by requiring house operators to be registered, bonded identities distinct from the channel's player address.
- Weak player entropy: player chain seed generated via WebCrypto CSPRNG; the optional sessionSeed (Pyth) protects players with backdoored clients from a *colluding house* (house alone still can't predict, since player half is fixed at open — but a house that *knows* the player's seed knows everything; sessionSeed does not fix that: if the client is fully compromised, nothing does. Document honestly.)

### 6.6 Key management
- **Player:** burner **session key** generated in-browser per session, held in memory/IndexedDB; authorized by one wallet signature at open (`openChannel` binds sessionKey → funds recipient = wallet address, so a stolen session key can *play badly* but can never redirect withdrawals — closes always pay the wallet). Scope-limited: session key can sign only ChannelState typed data for its channelId. Loss of session key = player disconnect path (grace → resume impossible → T1 forfeit of open hand only; the *balance* is safe because anyone can submit the last signed state). Recommend persisting the last dual-signed state to localStorage after every SIG.
- **House:** session keys per channel derived in HSM/KMS; chain seeds stored encrypted with the same custody tier as the signing keys; replicas share via threshold custody. The *operator's* bond withdrawal key is cold and separate.

### 6.7 Watchtowers
Optional public service (and a protocol-run instance): stores dual-signed states pushed by clients, auto-responds to `forceClose`/`timeoutClose` with newer states, auto-answers `demandReveal` if given the player's remaining chain segment (players may opt in). Since responses need no secrets, watchtowers are trust-minimized (worst case: they do nothing).

### 6.8 Audit focus list
1. ChannelManager settlement arithmetic vs invariants §5.6 (conservation under every close path incl. forfeits/slashes/voids).
2. Dispute state machine: no path settles below a checkpoint floor; no window can be extended unboundedly; timeout ordering (grace vs challenge vs reveal windows) has no dead states.
3. Rules-verifier library equivalence with the off-chain engines (differential fuzz all three).
4. EIP-712 encoding/versioning; reveal-verification O(1) chain check; modulo mapping.
5. RiskVault allocation/return paths and exposure-cap enforcement under concurrent opens (reentrancy, check-effects).
6. HouseBond slashing math and insolvency handling (bond < owed compensation).
7. Historical bug patterns: Raiden/Lightning old-state broadcasts, encodePacked collisions, ecrecover(0), cross-channel replay, fee-rounding drains, griefing via dust channels (enforce min deposit).

---

## 7. Implementation Plan

### 7.1 Components & effort (1 senior engineer ≈ 1 unit; estimates are engineering-months, excluding audit)

| Component | Contents | Effort |
|---|---|---|
| **ChannelManager.sol + HouseBond.sol + RulesVerifier lib** | open/close/checkpoint/dispute/slash, waterfall, vault integration, Entropy seed hook | 2.0 |
| **House node service (TypeScript v1)** | ws server, rules engine, signer/HSM integration, write-ahead store (Postgres + replication), failover, chain watcher/auto-responder, ops dashboards | 3.0 |
| **Client SDK + `app/blackjack.js` integration** | session keys, hash chains, state machine, reconnect/resume, local verification, state persistence, dispute submission UI | 2.0 |
| **Watchtower service** | subset of house node watcher, public API | 0.5 |
| **Adversarial simulation harness** | scripted malicious player & malicious house drivers exercising every T/H rule on a fork | 1.0 |
| **Total** | | **≈ 8.5 eng-months** (≈ 3–4 calendar months with 2–3 engineers) |

Rust rewrite of the house node is a later optimization, not v1.

### 7.2 Test strategy
- **Unit:** every contract function; rules engine golden vectors shared across Solidity/TS/client.
- **Property/fuzz (Foundry):** invariants §5.6 under random action/dispute sequences; differential fuzz Solidity RulesVerifier vs TS engine vs client engine.
- **Adversarial simulation:** automated agents that stall at every decision point, replay every old state, submit every malformed reveal, kill the house node at every message index — assert the honest party's final balance matches the theorem's floor in all runs.
- **Testnet soak:** 4+ weeks on Arbitrum Sepolia with public play-money, chaos-monkey node kills, and a bug bounty on the dispute paths.
- **Statistical:** million-hand RTP simulation of the channel engine vs V3's engine — must match to numerical noise.

### 7.3 Audit scope
ChannelManager + HouseBond + RulesVerifier + vault-integration diffs (~1,500–2,500 SLoC): one full audit (est. $60k–$150k) + the adversarial harness handed to auditors. House node reviewed for key custody and write-ahead correctness (lighter, ops-focused review).

### 7.4 Phased rollout
1. **Phase 0 (now):** V3 Pyth per-action stays live — it remains the permanent fallback and the game for users who prefer pure on-chain play.
2. **Phase 1:** RAIN Channels on the existing demo site with **play-money RUSD**, seedless option allowed, small caps, public dispute-path bug bounty.
3. **Phase 2:** Entropy sessionSeed ON, raise caps toward the 2%/0.25% exposure structure, enable watchtower, external audit complete.
4. **Phase 3:** real-value RUSD; revisit `CHALLENGE_WINDOW` vs force-inclusion for large channels (§4.4); consider additional games (the channel + commit-reveal core is game-agnostic — roulette/dice are strictly simpler than blackjack).

---

## 8. Comparison

| | **RAIN Channels (this spec)** | **V3 Pyth per-action (live)** | **Stake-style centralized commit-reveal** |
|---|---|---|---|
| Randomness trust | 2-party: random if either party honest; stalls punished on-chain automatically | 2-party (contract ↔ Pyth provider): random if either honest; provider can censor (void+refund path) | Operator knows all outcomes in advance; can't retro-change them; verification after the fact, reputational enforcement only |
| Funds custody | On-chain escrow; last-signed-state enforceable by anyone | Fully on-chain | Fully custodial |
| Abort worst case | Enforced signed state + penalties/compensation | Void + full refund after 1 h timeout | Whatever the operator decides |
| Latency per action | **< 1 s (~0.2–0.9 s)** | ~2–5 s (entropy round trip) | < 0.5 s |
| Marginal cost per action | **≈ $0** (2 txs + ≤ 1 oracle fee per *session*) | oracle fee + gas *per action* (cents × several per hand) | ≈ $0 |
| Cost at scale | Competitive with Web2 | Linear in hands played — structurally uncompetitive at volume | Zero |
| Engineering effort | High (~8.5 eng-months + audit) | Done | Low |
| House-node infra needed | Yes (HA service + bond) | No | Yes (their whole stack) |
| Marketing can truthfully say | "No one — not us, not anyone — can know or pick your next card, and the blockchain enforces your payout even if we vanish." | "No one can know your next card; randomness delivered and verified on-chain per action." | "We can't retroactively change results you can later verify." (They CAN know results in advance.) |

---

## 9. Founder Decisions — RESOLVED 2026-07-28 (implemented in v4.0.0)

1. **Entropy sessionSeed: ON from day one** (one request per session, at channel open).
2. **Grace period: 180 s**; after grace the open hand is forfeited (player loses that hand's gross only, no extra fine). UI copy: "Disconnected? You have 3 minutes to reconnect and resume."
3. **HOUSE_COMP = max(2× open hand gross, floor 5 RUSD)** from HouseBond; **an equal protocol penalty is BURNED to burnSink** (never to vault).
4. **Foundation-run house nodes at launch**, but HouseBond + ChannelManager are generic for future bonded external operators.
5. **Checkpoints automatic** when |session P&L| exceeds min(±500 RUSD, 5% of channel size); transparent to the player.
6. **Tiered challenge windows from day one:** 10 minutes for channels ≤ 10,000 RUSD (deposit+allocation), 24 hours above.
7. **Statistical fairness: transparency only in v1** (public logs + verifiable fairness data); no on-chain statistical slashing.
8. **Fee waterfall executed on-chain at channel close** (computed per hand in channel state).
9. **ChannelManager built game-agnostic** with pluggable IRulesVerifier; blackjack (ENHC/S17, V3 economics) is the first module.

### Original open questions (for the record)

1. **Entropy sessionSeed default:** ON from Phase 1 (adds cents + ~3 s per open, strengthens the story) or Phase 2 only?
2. **Grace period final value:** spec recommends 180 s within a [120 s, 300 s] governance range — confirm, and confirm the "abandon = forfeit gross" framing is acceptable in the UI/ToS.
3. **HOUSE_COMP sizing:** formula proposed (≥ 2 × hand gross, floor 5 RUSD, matched protocol penalty). Confirm floor and whether the penalty leg goes to vaultSink or burnSink.
4. **Who operates house nodes:** foundation-run only at launch, or open a bonded-operator program early (affects HouseBond design depth)?
5. **Checkpoint policy:** client-automatic above a P&L threshold (e.g., every ±100 RUSD swing) or manual only?
6. **Large-channel sequencer risk (§4.4):** accept 10-min windows for v1 caps, or tier windows by channel value now?
7. **Statistical-audit slashing** (§6.5): governance mechanism or off-protocol reputational only for v1?
8. **Waterfall timing:** executed at close per session (spec) vs streamed per checkpoint — any treasury-reporting preference?
9. **Multi-game ambition:** should ChannelManager v1 be built game-agnostic (rules-verifier as a pluggable module) at ~+0.5 eng-month cost?

---

*End of specification v1.*
