Project introduction

Provably fair, single-table Texas Hold'em with on-chain custody and commitments.

uHoldem is a reference implementation for 2-6 players on Ethereum Sepolia. Players keep chips in a contract vault and play through a real-time web client. Betting, ranking, and sequencing run on one Node.js server. The dealing protocol combines a precommitted dealer secret with each player's EIP-712 signature, commits the resulting card slots on-chain, and lets browsers verify every reveal independently.

UsersPlayers, operators, protocol reviewers, maintainers.
Runtime scopeOne cash table, six seats, mock USDC.
On-chainCustody, seats, roots, conservation, recovery.
Off-chainAuth, clocks, betting, dealing, ranking, UI.
Mental modelThe chain is the durable ledger for money, seats, and commitments. The server is the live dealer and rules coordinator. @uholdem/protocol is the byte-level agreement shared by server, browser, tests, and auditors. SQLite stores openings and history, but chain-read roots are the independent reference.
01 / System map

Components and authority boundaries

Dependencies flow toward the pure protocol and contracts. The browser presents and verifies; the server sequences; contracts hold value and commitment state.

Runtime component map
flowchart TB
  Wallet["Player wallet"]
  subgraph Browser["Browser - packages/client"]
    UI["React pages / wallet controls"]
    Hook["useGame socket controller"]
    BV["Browser verifier"]
  end
  subgraph Server["Node server - packages/server"]
    Entry["index.ts + Socket.IO auth"]
    Room["Room hand loop"]
    Engine["betting.ts + pot.ts"]
    API["Express read API"]
    Chain["EthersChainService"]
    DB["Store / SQLite"]
  end
  Protocol["@uholdem/protocol"]
  subgraph EVM["Ethereum Sepolia or Anvil"]
    Token["MockUSDC"]
    Vault["ChipVault"]
    Table["PokerTable"]
  end
  Wallet -->|"sign messages and transactions"| UI
  UI --> Hook
  Hook <-->|"Socket.IO"| Entry
  Entry --> Room
  Room --> Engine
  Room --> Protocol
  Room --> DB
  Room --> Chain
  UI -->|"HTTP config / records"| API
  API --> DB
  API --> Chain
  BV --> Protocol
  BV -->|"read roots via /api/rpc"| Table
  Chain -->|"operator writes / reads"| Table
  UI -->|"player writes"| Token
  UI --> Vault
  UI --> Table
  Table --> Vault
  Vault --> Token
Mermaid source is the offline fallback; with network access it renders as SVG.
ComponentResponsibility / boundaryInputsOutputs / authoritySource
React clientWallet UX, table, actions, proof checks, history, verifier. Does not decide legal actions.Config, sockets, wallet, RPC.Player writes and local view.packages/client/src
Game serverSocket auth, room loop, clocks, private delivery, rules coordination, persistence.Commands, environment, chain.RoomView, events, DB, operator writes.packages/server/src
ProtocolEncoding, entropy, shuffle, commitments, replay, proof and audit logic.Bytes, context, secrets, records.Decks, roots, proofs, reports.packages/protocol/src
PokerTableCanonical seats/stacks, hand lifecycle, conservation, timeout.Player/dealer transactions.Durable seat/hand state.contracts/src/PokerTable.sol
ChipVaultUSDC custody, player balances, isolated table escrow.Transfers and table calls.Durable custody.contracts/src/ChipVault.sol
SQLite StoreOperational history and openings; not chain-anchored.Hand lifecycle writes.HandRecord and retry data.packages/server/src/db.ts
02 / Workflow

One hand from connection to public audit

The ordering is security-sensitive: startHand confirms and the root is reread before any card leaves the server.

End-to-end hand sequence
sequenceDiagram
  autonumber
  actor W as Wallet
  participant C as React client
  participant S as Server Room
  participant P as Protocol
  participant T as PokerTable
  participant D as SQLite
  W->>C: connect
  C->>S: auth request / signed nonce
  S-->>C: memory session token
  S->>T: seats and open-hand check
  S->>P: buildEpoch
  S->>T: commitEpoch and wait
  S-->>C: entropy typed data
  W->>C: signTypedData
  C->>S: entropy:sign
  S->>P: buildEntropy then dealHand
  P-->>S: layout + two roots
  S->>T: startHand and wait
  S->>T: reread handRoots
  S->>D: persist deal metadata
  S-->>C: private hole opening
  C->>T: independently read dealRoot
  C->>P: verifyReveal
  loop Betting streets
    S-->>C: legal actions + deadline
    C->>S: action
    S->>S: applyAction and persist
    S-->>C: street opening
    C->>P: verifyReveal
  end
  S->>P: rank and award pots
  S->>D: finishHand exact settlement
  S->>T: settleHand
  S-->>C: result immediately
  T-->>S: receipt and sync seats
  C->>D: fetch delayed hand record
  C->>T: read roots independently
  C->>P: verifyHand
Raw Mermaid remains readable if the optional renderer is unavailable.

Authenticate

A single-use nonce proves address control; it is not hand entropy.

useGame.authenticate -> auth handlers -> issueChallenge -> consumeNonce -> verifyMessage -> registerSocket

Reconcile

Read canonical seats and resolve a chain hand left open by failed settlement.

playOneHand -> reconcileChain -> liveHand -> settlementFor -> settleHand | abortHand

Precommit dealer input

An epoch root fixes K secrets; SQLite consumes indices in order.

ensureEpoch -> latestEpoch / takeHandIndex -> buildEpoch -> commitEpoch

Collect entropy

Each seat signs chain, table, hand, seat, and address. Missing signers leave before a deck exists.

entropyTypedData -> submitSignature -> entropyDigest -> recoverAddress -> buildEntropy

Compute and anchor

Derive M, shuffle without modulo bias, lay out slots, commit one Merkle root, wait.

dealHand -> handSecret -> masterSeed -> shuffleAndLayout -> commitDeal -> startHand

Reveal and verify

Each slot carries cards, salt, and siblings. The browser reads the root from chain.

revealSlot -> socket event -> useGame.verify -> read handRoots -> verifyReveal

Bet

The pure engine applies poker rules; Room adds ownership, clocks, logging, and streets.

runBettingRounds -> legalActions -> waitForAction -> applyAction -> closeStreet

Settle

Only live holes open; pot awards must equal contributions before contract submission.

settle -> evaluateShowdown -> buildPots -> awardPots -> finishHand -> settleHand

Audit

After delay, release D_hand with epoch proof and replay the whole deal and payout.

getHandRecord -> epochProofFor -> VerifyPage -> readHandRoots -> verifyHand
03 / State

Lifecycle, durability, and recovery

The room is stateful. Safety depends on knowing which copy is authoritative and reconciling asynchronous writes.

Room phase state machine
stateDiagram-v2
  [*] --> waiting
  waiting --> paused: epoch commit or recovery
  paused --> waiting: retry or abort succeeds
  waiting --> collecting_entropy: 2+ eligible seats
  collecting_entropy --> waiting: fewer than 2 signatures
  collecting_entropy --> committing: valid signatures
  committing --> betting: start confirmed + root reread
  betting --> betting: action or street
  betting --> showdown: one live player or river
  showdown --> settling: awards computed
  settling --> waiting: result emitted
  settling --> paused: settlement failure
The Phase union and transitions live in packages/server/src/room.ts.
durabletransientchain enforced
StateOwnerDurabilityConsistency rule
Token, vault balance, table escrowMockUSDC / ChipVaultchainExact transfers; table credit bounded by its escrow.
Seats, stacks, epoch and hand rootsPokerTablechainseatSummary is canonical; root precedes cards.
Index, hand, actions, reveals, payoutsSQLitediskWAL and transactions; evidence, not root authority.
Phase, board, betting, pending inputRoomprocessNo seat resync mid-hand or during pending settlement.
Nonces and sessionsindex.ts mapsprocessFive-minute nonce; 12-hour session; lost on restart.
Master secretServer environmentsecretNever in SQLite; mismatch with existing epoch stops continuation.
SQLite relationships
erDiagram
  EPOCHS ||--o{ HANDS : contains
  HANDS ||--|{ PLAYER_SIGNATURES : has
  HANDS ||--|{ COMMITMENTS : fixes
  HANDS ||--o{ REVEALS : opens
  HANDS ||--o{ HAND_ACTIONS : records
  HANDS ||--o{ PAYOUTS : settles
Concrete columns and constraints are in packages/server/src/db.ts.
Start is synchronous

Wait one receipt and compare the reread root before holes.

Settlement is asynchronous

Show submitted arithmetic immediately; gate the next on-chain start.

Recovery is deterministic

Retry exact stored values; timed abort frees seats without moving chips.

04 / Implementation

Function-level guide by execution stage

Rows connect functions to callers, inputs, effects, invariants, and failure behavior.

05 / Operations

Run, inspect, and recover

Local orchestration is scripts/devstack.sh. Production-like operation supplies the server environment and serves the built client separately.

Startup and checks

pnpm install
./scripts/devstack.sh
node scripts/browser-check.mjs
pnpm build
pnpm test:protocol
pnpm --filter @uholdem/server test
pnpm test:contracts
pnpm sim -- --deals 50000

The dev script starts Anvil on :8545, deploys and wires contracts, seats three public test wallets, recreates .devstack/dev.db, starts the server on :4000, and Vite on :5173. Ctrl-C stops all child processes.

Health, logs, and state

QuestionInspectExpected signal
Server and chain alive?curl -s http://127.0.0.1:4000/api/healthok, chainId, dealer.
Current phase?GET /api/tablephase, hand, street, actor, deadline, seats, roots.
Startup failure?.devstack/{anvil,server,client}.logFirst RPC, deploy, config, or UI error.
Local durable state?.devstack/dev.db or DATABASE_URLEpoch, actions, reveals, payouts, tx hashes.
Open chain hand?currentHandId, hands(id), seatSummaryStart time, mask, settled flag, stacks.
Reproducible?GET /api/hands/:id and browser verifierDelayed proof and passing chain-root checks.
Readiness: assertReady checks chain, bytecode, dealer, limits, and gas. Correct config; disable REQUIRE_CHAIN only in chainless tests.
Entropy stall: inspect entropySigned, sockets, and wallet prompts. Unsigned seats are removed; escrow is unchanged.
Action stall: inspect toAct and deadline. Timeout checks or folds; disconnect does not remove a live hand.
Settlement: Room pauses and retries stored values. After HAND_TIMEOUT, anyone may abort with no chip movement.
Verification: unavailable means RPC trouble and retries; failed means an opening does not match the chain root.
06 / Trust

Enforced, verified, and trusted behavior

The fairness claim covers the deal; the conservation claim covers accounting. V1 is not a trustless operator.

Contract enforced

  • Only authorized table/dealer methods move state.
  • Open-hand seats cannot leave before settle/abort.
  • Contribution equals payout; only participants are touched.
  • Per-table escrow prevents cross-table subsidy.

Cryptographically verified

  • Dealer secrets were fixed before entropy.
  • Signatures bind exact chain/table/hand/seat/address.
  • Each opening matches chain dealRoot.
  • Delayed secret replays deck, winners, payout math.

Operator trusted

  • Server sees all holes and may leak/collude.
  • Ranking is off-chain; error is detectable, not disputable.
  • Action log and record are operator-controlled.
  • Operator controls availability and dealer gas.

Out of scope

  • Mainnet, licensing, KYC/AML, geofencing.
  • Lobby, tournaments, rake, tiers, mobile.
  • On-chain evaluator, anchored actions, auto-challenges.
  • V2 document is future design, not runtime.
07 / Extension

Where new behavior belongs

Keep pure rules in protocol/engine, orchestration in Room, authority in contracts, and presentation in client.

Poker rule

Edit server/src/engine/betting.ts and pure tests.

Adapt Room serialization and ActionBar; keep sockets/clocks out of engine.

Protocol field

Update canonical encoders, records, verifier.

Changed vectors are a versioned protocol change across TS and Solidity.

HTTP route

Add in server/src/api.ts.

Never expose writes through the read-only RPC allowlist.

Socket event

Authenticate in server/src/index.ts; behavior on Room.

Mirror handler/cleanup in useGame with explicit ack errors.

Durable field

Extend DB schema and Store paths.

Update HandRecord/verifier if audit meaning changes.

Contract

Implement under contracts/src; expose ABIs/services.

Add unit, fuzz, access, invariant coverage.

Client page

Add parseRoute, component, i18n keys.

Use bigint units and wagmi/viem writes.

Tool

Use scripts/ or protocol scripts.

Reuse protocol code; document inputs, keys, outputs, cleanup.

08 / Review

Tests follow runtime boundaries

Pinned vectors prevent TypeScript/Solidity byte drift. Simulation measures the distribution claim.

Protocol

Shuffle, Merkle, deal/replay, signatures, verifier, vectors.

Server

Betting, pots, settlement timing/recovery, local-chain E2E.

Contracts

Token, vault, table, fuzz, invariants, vectors.

System

2-6 seat simulation and three-wallet browser walkthrough.

Review invariants

InvariantChecked by
Epoch root precedes entropy.ensureEpoch, contract rules, tests.
No card before confirmed dealRoot.startHand wait and playOneHand reread.
Entropy belongs to exact context.submitSignature, buildEntropy, replay tests.
Uniform shuffle and cross-language bytes.drawReference/draw, simulation, vectors.
All chips returned exactly once.Pot totals, settleHand, fuzz/invariants.
Operator cannot lock seats forever.reconcileChain, HAND_TIMEOUT, abort tests.