Commit-Reveal Lifecycle
Every game is a small on-chain state machine. Guesses are locked in as SHA-256 commitments before anything is revealed, and the matchup type itself is hidden behind a second commitment made by the matchmaker — neither player can change their guess based on privileged knowledge.
State machine
Section titled “State machine”The lifecycle in order:
- Match — the off-chain matchmaker pairs players and generates a hidden matchup commitment.
- Create — P1 calls
create_gamewith the matchup commitment. The matchmaker co-signs to attest the commitment is legitimate (verified againstGlobalConfig.matchmaker), preventing a player from substituting their own commitment. P1 pays gas; the matchmaker pays nothing. - Join — P2 calls
join_game; the game becomes Active and chat opens. - Chat — anonymous conversation over the WebSocket relay (see Agent Protocol).
- Commit — each player submits a SHA-256 commitment of their guess. The on-chain commit order is recorded (
first_committer, value1= P1 or2= P2) — it decides the tiebreak when both players guess correctly in a different-teams match. - Reveal — the first revealer provides their guess preimage plus the matchup preimage; the second revealer provides their guess preimage only, and the game resolves per the payoff matrix.
Guess commitment scheme
Section titled “Guess commitment scheme”The guess never exists on-chain (or on any server) in plaintext until reveal:
- Commit: the player generates 32 random bytes (preimage
R). The last bit encodes the guess:R[31] & 1—0= “same team”,1= “different team”. The player submitsSHA-256(R)as the commitment. - Reveal: the player submits
R. The program verifiesSHA-256(R) == commitmentand extractsguess = R[31] & 1.
Matchup commitment scheme
Section titled “Matchup commitment scheme”The matchup type (same team vs. different teams) uses the identical construction, committed by the matchmaker:
- Commit (at creation): the backend generates 32 random bytes
R_matchupwithR_matchup[31] & 1encoding the matchup type, andSHA-256(R_matchup)is stored on the Game account asmatchup_commitment.R_matchupstays server-side. - Reveal (at first guess reveal): the first player to reveal also submits
R_matchup(delivered to them via thereveal_dataWebSocket message). The program verifies it against the stored commitment and extracts the matchup type — after both guesses are committed, so neither player can exploit the knowledge.
Timeout payoffs deliberately do not depend on the matchup type (both forfeit, or the sole committer/revealer wins), so a stalled game never needs the matchup reveal.
Timeouts
Section titled “Timeouts”| Window | Anchor | Elapsed outcome |
|---|---|---|
| Active timeout — ~1 hour (7,200 slots) | game enters Active | Neither committed: both stakes forfeit to the pool/treasury |
| Commit timeout — ~1 hour (7,200 slots) | game enters Active | One committed: committer wins the full pot; non-committer slashed |
| Reveal timeout — ~2 hours (14,400 slots) | both commits in | One revealed: revealer wins the full pot; non-revealer forfeited |
The revealer receiving the full pot (not just a refund) is what removes timeout griefing — otherwise a losing player could refuse to reveal at zero cost while halving the winner’s payout.
Who can crank what
Section titled “Who can crank what”| Instruction | Caller | Notes |
|---|---|---|
create_game | P1 + matchmaker cosign | Matchmaker attests the matchup commitment; on the frontend this runs session-signed (see Session Keys) |
join_game | P2 | |
commit_guess / reveal_guess | Each player | Also available as *_session variants |
resolve_timeout | Anyone | Permissionless crank once a window elapses |
refund_pending | Player | Recovers a stake from an abandoned pairing (EVM mirror: cancelPending) |
close_game | Anyone | Permissionless; reclaims Solana rent after resolution (no EVM analogue) |
The same lifecycle runs on the EVM contracts with per-phase timeout anchors (resolveTimeout) — see EVM & Cross-Chain for the differences that the chains force.