Blockchain Poker Game Development
Poker on blockchain is a task that looks simpler than it actually is. A smart contract guarantees transparency and fair payouts. But cards cannot be revealed on-chain until showdown — this destroys the game. So we need to hide information in a public environment where everything is visible to everyone. This is the mental poker task, formally described in 1979 by Shamir, Rivest, and Adleman. Practical implementation requires a choice: full cryptography (complex and expensive) or a trusted off-chain component (simpler, but requires trust).
Fundamental Problem: Information Hiding On-Chain
In traditional poker, the dealer physically deals cards face down. In public blockchain, all transaction data is visible to all nodes. If you write cards in contract state — any node reads them directly. If you encrypt them — who holds the key?
Approach 1: Mental Poker with Commit-Reveal
Classic cryptographic approach:
- Each player contributes a random seed for shuffling (commit phase)
- Deck is shuffled deterministically from combination of all seeds
- Each card is encrypted with key of each player (layered encryption)
- Card is opened only when all players reveal their part of the key
Problem: with 6 players each card is encrypted 6 times. Opening requires 6 on-chain transactions. With 5 streets of poker — dozens of transactions per hand. Gas and latency unacceptable for mainnet.
Approach 2: ZK Proof for Hidden Cards
ZK-SNARK allows you to prove "player holds card of needed value for payout" without revealing the card itself until showdown. Projects like ZK-Holdem (based on Circom) try this path.
Complexity: ZK schemes for poker rules (full hand evaluation) — non-trivial task. Proof generation on mobile — slow (seconds). On desktop — acceptable.
Approach 3: Trusted Execution Environment (TEE)
Dealer service runs in Intel SGX or AWS Nitro Enclave — isolated environment where even server operator doesn't see data. Cards are dealt inside TEE, players see only their cards through encrypted channel. Only round results and commitments go on-chain.
Compromise: trust in TEE manufacturer (Intel). For most gaming applications this is acceptable — not worse than trusting casino dealer.
Approach 4: Off-chain Game Server + On-Chain Settlement
Most practical for production: game server maintains game state off-chain, players sign moves (bet, fold, raise) via state channel, final result recorded on-chain for payout.
Players → [Game Server] → manages hidden cards, game state
↕ signed moves (state channel)
↓ final result + signatures
[Settlement Contract] → pays out winners
State Channel Architecture
State channel is ideal model for poker. Players open channel, deposit funds, all moves are signed off-chain messages. On-chain transaction only for channel open and close.
contract PokerStateChannel {
struct Channel {
address[6] players;
uint256[6] deposits;
uint256 totalPot;
bytes32 stateHash; // hash of current game state
uint256 nonce; // move counter
ChannelStatus status;
}
struct PlayerMove {
uint8 playerId;
MoveType moveType; // BET, RAISE, CALL, FOLD, CHECK
uint256 amount;
uint256 channelNonce; // must match channel nonce
bytes signature; // player signature
}
function openChannel(address[6] calldata players, uint256[6] calldata deposits)
external payable returns (bytes32 channelId) {
// Verify all deposits
uint256 totalDeposit = 0;
for (uint i = 0; i < 6; i++) totalDeposit += deposits[i];
require(msg.value == totalDeposit, "Incorrect deposit");
channelId = keccak256(abi.encode(players, block.timestamp, block.prevrandao));
channels[channelId] = Channel({
players: players,
deposits: deposits,
totalPot: totalDeposit,
stateHash: bytes32(0),
nonce: 0,
status: ChannelStatus.OPEN
});
}
function closeChannel(
bytes32 channelId,
uint256[6] calldata finalBalances,
bytes[6] calldata playerSignatures
) external {
Channel storage ch = channels[channelId];
require(ch.status == ChannelStatus.OPEN, "Channel not open");
// Verify all player signatures on final state
bytes32 finalStateHash = keccak256(abi.encode(channelId, finalBalances, ch.nonce));
for (uint i = 0; i < 6; i++) {
require(
ECDSA.recover(ECDSA.toEthSignedMessageHash(finalStateHash), playerSignatures[i])
== ch.players[i],
"Invalid player signature"
);
}
// Pay out
for (uint i = 0; i < 6; i++) {
if (finalBalances[i] > 0) {
payable(ch.players[i]).transfer(finalBalances[i]);
}
}
ch.status = ChannelStatus.CLOSED;
}
}
Dispute Mechanism
What if game server disappears or tries to cheat? State channel must have dispute resolution:
Timeout-based: if player doesn't get response within N blocks, he can initiate dispute, presenting last signed state. Counter-party must respond with newer state. If no response — timeout player wins.
Forced reveal: at showdown all active players must reveal cards on-chain within timeout. If not revealed — counts as fold. Protection against strategy of "disconnect when losing showdown".
function initiateDispute(bytes32 channelId, GameState calldata lastKnownState, bytes calldata sig)
external {
Channel storage ch = channels[channelId];
require(ch.players[_getPlayerId(channelId, msg.sender)] == msg.sender, "Not player");
disputes[channelId] = Dispute({
challenger: msg.sender,
challengeState: lastKnownState,
challengeTime: block.timestamp,
resolved: false
});
ch.status = ChannelStatus.DISPUTED;
emit DisputeInitiated(channelId, msg.sender, lastKnownState.nonce);
}
Fairness: Verifiable Shuffling
Game server shuffles deck. How to prove it doesn't cheat, knowing player cards?
Commit-Reveal Shuffling:
- Before dealing, server publishes seed hash:
commitment = hash(seed + salt) - Players contribute entropy
- Final deck = shuffle(seed XOR player_entropy_1 XOR ... XOR player_entropy_N)
- After game, server reveals seed — everyone can verify shuffle
// Server side
const serverSeed = crypto.randomBytes(32)
const commitment = keccak256(concat([serverSeed, salt]))
await contract.publishCommitment(channelId, commitment)
// After all player entropy received:
const finalSeed = xorAll([serverSeed, ...playerEntropyContributions])
const deck = shuffleDeck(standardDeck, finalSeed) // deterministic Fisher-Yates
// After game:
await contract.revealSeed(channelId, serverSeed, salt)
// Anyone can verify: hash(serverSeed + salt) == commitment
// And: shuffle(standardDeck, serverSeed XOR playerEntropy) == used deck
Game Logic Off-Chain
Poker logic (Texas Hold'em hand evaluation, betting rounds, pot management) — completely off-chain on server. Contract only handles: deposit, state commitments, dispute, payout.
// Hand evaluator
import { Hand } from 'pokersolver'
function evaluateHand(holeCards: Card[], communityCards: Card[]): HandResult {
const hand = Hand.solve([...holeCards, ...communityCards].map(c => c.toString()))
return {
rank: hand.rank,
name: hand.name, // 'Royal Flush', 'Full House', etc.
value: hand.value,
cards: hand.cards
}
}
function determineWinner(players: ActivePlayer[], communityCards: Card[]): Winner[] {
const hands = players.map(p => ({
player: p,
hand: Hand.solve([...p.holeCards, ...communityCards].map(c => c.toString()))
}))
const winners = Hand.winners(hands.map(h => h.hand))
return winners.map(w => hands.find(h => h.hand === w)!.player)
}
NFTs and Game Assets
Player avatars / profile NFT. Cosmetic NFTs don't affect gameplay, but give identity and secondary market. ERC-721 with dynamic metadata (win rate, games played) via tokenURI with on-chain or off-chain data.
Poker table NFT. Private table as NFT: NFT owner manages table settings (rake %, blinds structure, invite-only), receives part of rake. Passive income for NFT holders.
Chip sets and card deck skins. Pure cosmetic, but significant for retention. ERC-1155 for fungible cosmetics.
Token Economics and Rake
Rake — commission from each pot, like casino. 2-5% of pot — standard. In on-chain poker rake goes to protocol treasury. Distribution:
Pot rake (3%) → 50% burn / buyback game token
→ 30% staking rewards (stakers = liquidity providers)
→ 20% development fund
Rakeback NFT. Players with certain NFT get partial rake return. Incentive to hold NFT, sink for token (NFT bought with tokens).
Stack
| Component | Technology |
|---|---|
| Smart contracts | Solidity + Foundry |
| State channels | Nitro Protocol / custom |
| Game server | Node.js + TypeScript |
| Real-time | WebSocket (Socket.io) |
| Frontend | React + Three.js / Pixi.js for table |
| Wallet | wagmi + WalletConnect v2 |
| ZK (if chosen) | Circom + snarkjs |
| TEE (if chosen) | AWS Nitro Enclaves |
Development Process
Architecture decision (1 week). Choose approach to card hiding: off-chain trusted server, TEE, or ZK. This determines everything.
Smart contracts (3-4 weeks). State channel contract, dispute mechanism, NFT contracts, settlement logic.
Game server (3-5 weeks). Poker logic, state management, WebSocket multiplayer, commit-reveal shuffling, anti-cheat.
Frontend (4-6 weeks). 3D or 2D table, dealing animations, betting UI, real-time moves of other players, wallet integration.
Security and Audit. Audit of state channel contract and dispute mechanism — critical. Economic testing of rake model.
MVP with off-chain server and basic state channel — 3-4 months. Production with ZK or TEE, full NFT ecosystem, lobby system — 6-9 months.







