Authenticate
A single-use nonce proves address control; it is not hand entropy.
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.
@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.Dependencies flow toward the pure protocol and contracts. The browser presents and verifies; the server sequences; contracts hold value and commitment state.
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
| Component | Responsibility / boundary | Inputs | Outputs / authority | Source |
|---|---|---|---|---|
| React client | Wallet 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 server | Socket auth, room loop, clocks, private delivery, rules coordination, persistence. | Commands, environment, chain. | RoomView, events, DB, operator writes. | packages/server/src |
| Protocol | Encoding, entropy, shuffle, commitments, replay, proof and audit logic. | Bytes, context, secrets, records. | Decks, roots, proofs, reports. | packages/protocol/src |
| PokerTable | Canonical seats/stacks, hand lifecycle, conservation, timeout. | Player/dealer transactions. | Durable seat/hand state. | contracts/src/PokerTable.sol |
| ChipVault | USDC custody, player balances, isolated table escrow. | Transfers and table calls. | Durable custody. | contracts/src/ChipVault.sol |
| SQLite Store | Operational history and openings; not chain-anchored. | Hand lifecycle writes. | HandRecord and retry data. | packages/server/src/db.ts |
The ordering is security-sensitive: startHand confirms and the root is reread before any card leaves the server.
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
A single-use nonce proves address control; it is not hand entropy.
Read canonical seats and resolve a chain hand left open by failed settlement.
An epoch root fixes K secrets; SQLite consumes indices in order.
Each seat signs chain, table, hand, seat, and address. Missing signers leave before a deck exists.
Derive M, shuffle without modulo bias, lay out slots, commit one Merkle root, wait.
Each slot carries cards, salt, and siblings. The browser reads the root from chain.
The pure engine applies poker rules; Room adds ownership, clocks, logging, and streets.
Only live holes open; pot awards must equal contributions before contract submission.
After delay, release D_hand with epoch proof and replay the whole deal and payout.
The room is stateful. Safety depends on knowing which copy is authoritative and reconciling asynchronous writes.
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
| State | Owner | Durability | Consistency rule |
|---|---|---|---|
| Token, vault balance, table escrow | MockUSDC / ChipVault | chain | Exact transfers; table credit bounded by its escrow. |
| Seats, stacks, epoch and hand roots | PokerTable | chain | seatSummary is canonical; root precedes cards. |
| Index, hand, actions, reveals, payouts | SQLite | disk | WAL and transactions; evidence, not root authority. |
| Phase, board, betting, pending input | Room | process | No seat resync mid-hand or during pending settlement. |
| Nonces and sessions | index.ts maps | process | Five-minute nonce; 12-hour session; lost on restart. |
| Master secret | Server environment | secret | Never in SQLite; mismatch with existing epoch stops continuation. |
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
Wait one receipt and compare the reread root before holes.
Show submitted arithmetic immediately; gate the next on-chain start.
Retry exact stored values; timed abort frees seats without moving chips.
Rows connect functions to callers, inputs, effects, invariants, and failure behavior.
Local orchestration is scripts/devstack.sh. Production-like operation supplies the server environment and serves the built client separately.
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.
| Question | Inspect | Expected signal |
|---|---|---|
| Server and chain alive? | curl -s http://127.0.0.1:4000/api/health | ok, chainId, dealer. |
| Current phase? | GET /api/table | phase, hand, street, actor, deadline, seats, roots. |
| Startup failure? | .devstack/{anvil,server,client}.log | First RPC, deploy, config, or UI error. |
| Local durable state? | .devstack/dev.db or DATABASE_URL | Epoch, actions, reveals, payouts, tx hashes. |
| Open chain hand? | currentHandId, hands(id), seatSummary | Start time, mask, settled flag, stacks. |
| Reproducible? | GET /api/hands/:id and browser verifier | Delayed proof and passing chain-root checks. |
assertReady checks chain, bytecode, dealer, limits, and gas. Correct config; disable REQUIRE_CHAIN only in chainless tests.entropySigned, sockets, and wallet prompts. Unsigned seats are removed; escrow is unchanged.toAct and deadline. Timeout checks or folds; disconnect does not remove a live hand.HAND_TIMEOUT, anyone may abort with no chip movement.The fairness claim covers the deal; the conservation claim covers accounting. V1 is not a trustless operator.
Keep pure rules in protocol/engine, orchestration in Room, authority in contracts, and presentation in client.
Edit server/src/engine/betting.ts and pure tests.
Adapt Room serialization and ActionBar; keep sockets/clocks out of engine.
Update canonical encoders, records, verifier.
Changed vectors are a versioned protocol change across TS and Solidity.
Add in server/src/api.ts.
Never expose writes through the read-only RPC allowlist.
Authenticate in server/src/index.ts; behavior on Room.
Mirror handler/cleanup in useGame with explicit ack errors.
Extend DB schema and Store paths.
Update HandRecord/verifier if audit meaning changes.
Implement under contracts/src; expose ABIs/services.
Add unit, fuzz, access, invariant coverage.
Add parseRoute, component, i18n keys.
Use bigint units and wagmi/viem writes.
Use scripts/ or protocol scripts.
Reuse protocol code; document inputs, keys, outputs, cleanup.
Pinned vectors prevent TypeScript/Solidity byte drift. Simulation measures the distribution claim.
Shuffle, Merkle, deal/replay, signatures, verifier, vectors.
Betting, pots, settlement timing/recovery, local-chain E2E.
Token, vault, table, fuzz, invariants, vectors.
2-6 seat simulation and three-wallet browser walkthrough.
| Invariant | Checked 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. |