Skip to content

Repository files navigation

Showdown

SHOWDOWN

ZK Texas Hold'em โ€” fully on-chain, with zero-knowledge card shuffling

No server holds the deck. No operator can see your cards. Every shuffle is a zero-knowledge proof.

Status โ€ข How It Works โ€ข Security Model โ€ข Roadmap โ€ข Get Started


๐Ÿ“ Status

Showdown is an EVM poker engine โ€” standard Solidity, portable across EVM chains. A working prototype is deployed and playable on Lisk Sepolia testnet.

Live demo texas-seven.vercel.app
Demo video YouTube
Deployments Lisk Sepolia (original prototype) ยท Avalanche Fuji (live โ€” full hand played on-chain)
Next target Avalanche C-Chain โ€” see Roadmap
Stage Prototype. Testnet funds only, not audited, not for real-money play.

Deployed Contracts โ€” Lisk Sepolia

Deployed Contracts โ€” Avalanche Fuji

An earlier Fuji deployment (factory 0xBf3c326Cโ€ฆ, verifier 0xAE5d214eโ€ฆ) is superseded. It deployed the raw RevealVerifier rather than the ZgRevealVerifier wrapper that Game.sol calls, so card reveals reverted. Games created against it cannot decrypt their cards.

Point the frontend at Fuji with NEXT_PUBLIC_CHAIN_ID=43113. To redeploy:

cd packages/contracts
forge script script/Deploy.s.sol --rpc-url fuji --broadcast   # reads PRIVATE_KEY from .env

The script deploys a fresh RevealVerifier on any chain other than Lisk Sepolia, then GameFactory, linking the QuickSort and TexasPoker libraries automatically.


๐ŸŽฏ Features

  • Trustless card shuffling โ€” ZK-SNARK proofs (Zypher Network's ZK Shuffle SDK) prove each shuffle is a valid permutation without revealing card order
  • Real stakes โ€” smart contracts hold and distribute the pot; no IOU accounting
  • Strategic showdown โ€” choose 3 of 5 community cards, so hand construction is a skill decision rather than automatic
  • Anti-griefing โ€” 120-second action timeout with permissionless forceFold()
  • Auto-decryption โ€” cards reveal as soon as all reveal tokens are submitted
  • Readable errors โ€” raw revert data translated to plain language

๐Ÿ” Security Model

Showdown is honest about which parts are trustless today and which are not.

Trustless (verified on-chain):

  • Card revealing via ZK proofs
  • Hand evaluation (TexasPoker.sol)
  • Pot custody and distribution (immutable contract logic)

Not yet trustless:

  • Shuffle verification runs client-side. Players generate valid ZK shuffle proofs, but those proofs are not currently checked on-chain.

Why, precisely

ZgShuffleVerifier โ€” the contract that would verify shuffle proofs on-chain โ€” compiles to 27,002 bytes of runtime bytecode. EIP-170 caps deployed contracts at 24,576 bytes. It overshoots by 2,426 bytes:

$ forge build --sizes
| Contract             | Runtime Size (B) | Runtime Margin (B) |
| ZgShuffleVerifier    |           27,002 |             -2,426 |
| VerifierKeyExtra1_52 |           16,454 |              8,122 |
| VerifierKeyExtra2_52 |           16,450 |              8,126 |
| GameFactory          |           15,369 |              9,207 |
| Game                 |           14,037 |             10,539 |
| RevealVerifier       |            6,964 |             17,612 |
Error: some contracts exceed the runtime size limit (EIP-170: 24576 bytes)

Two things worth stating plainly, because both are commonly gotten wrong:

  1. This is not a Lisk limitation. EIP-170 is enforced identically by every major EVM chain โ€” Base, Optimism, Arbitrum, and Avalanche C-Chain included. Changing chains does not fix it.
  2. Compiler settings do not fix it either. Rebuilding with optimizer_runs = 1 yields 26,974 bytes โ€” still 2,398 over.

Where the bytes actually go

Profiling the vendored verifier turned up two specific causes, both addressable:

Change ZgShuffleVerifier runtime size Margin
As shipped 27,002 โˆ’2,426
Externalize the inlined VerifierKey_52 table 24,735 โˆ’159
Drop the unused verifyGenericProof entry point 20,755 +3,821 โœ…

PlonkVerifier.verifyProof is private and reached from two public entry points that differ only by a constant bool. The optimizer specializes the ~6KB verifier body for each, so the contract carries two near-identical copies โ€” and the generic one is never called by this project, which only verifies shuffle proofs. Removing it saves 6,247 bytes and brings the verifier comfortably under EIP-170 on its own.

The VerifierKey_52 externalization is independent and worth 2,267 bytes. The mechanism already exists here: VerifierKeyExtra1_52 and VerifierKeyExtra2_52 are deployed as standalone contracts and staticcall'd directly into memory.

This is an upstream limitation, not a local one

The verifier sources are byte-identical to zypher-game/uzkge. Upstream's own reference deployment contract, ShuffleService, inlines both the 20- and 52-card key tables and compiles to 31,072 bytes โ€” 6,496 over EIP-170. Upstream ships no real deployment script (scripts/deploy.js is the stock Hardhat sample) and configures no live network; the Solidity verifier is exercised only against an in-memory Hardhat node. Zypher's production answer is zytron-precompiles, which provides PLONK and shuffle verification as native precompiles on their own L2 โ€” going under EIP-170 rather than fitting inside it.

So the client-side shuffle here was never a shortcut. On any EIP-170 chain it is the only option the SDK offers as shipped.

Verified

test/ShuffleVerify52.t.sol deploys the size-reduced verifier and runs it against upstream's own 52-card proof vector, lifted verbatim from uzkge/contracts/solidity/test/plonk_52.js โ€” Zypher's proof, not one generated to match this implementation.

[PASS] test_verifierFitsUnderEip170()        ZgShuffleVerifier runtime size: 20,755 / 24,576
[PASS] test_verifiesUpstream52CardProof()    valid proof accepted
[PASS] test_rejectsTamperedProof()           corrupted proof rejected
[PASS] test_reportVerificationGas()          2,383,293 gas

test/ShuffleVerifyLive.t.sol closes the loop from the other side, running the verifier against a proof captured from a live run of this app's own /api/get-masked-cards โ†’ /api/first-shuffle pipeline:

[PASS] test_verifiesProofGeneratedByThisApp()   2,382,928 gas

The shuffle Showdown produces today is one the contract can verify on-chain. The remaining work is wiring Game.sol to call verifyShuffle.

Proof generation latency

Proving is the slowest part of the system by a wide margin, measured against the dev server with routes warm:

Operation Warm
/api/generate-key 0.01s
/api/get-masked-cards 14.3s
/api/first-shuffle 46.7s

Every player shuffles, so a heads-up hand spends roughly 110 seconds generating proofs before the first bet. This dominates the ~2.38M gas of on-chain verification in practical terms and is the main obstacle to the game feeling playable. Reducing it โ€” proving in parallel, moving generation to the client, or a faster proving backend โ€” is ahead of any gas concern.

On-chain shuffle verification costs ~2.38M gas per shuffle. Every player shuffles, so an n-player hand pays that n times.

Against Avalanche C-Chain fees that is affordable. At a spot base fee of ~0.061 gwei (60,946,345 wei, sampled from api.avax.network in July 2026), one verification costs roughly 0.000145 AVAX โ€” a fraction of a cent, so a heads-up hand adds well under a cent of verification cost. Congestion raises this, and the figure is a point sample rather than a benchmark; producing proper numbers across real gameplay is part of milestone 1.

Until this lands in the deployed game: testnet play only. Treat the shuffle as fair-by-convention, not fair-by-proof.


๐Ÿ—บ Roadmap: Avalanche C-Chain

The engine is chain-agnostic Solidity, so the migration is mostly mechanical โ€” with one genuine piece of engineering.

1. Turn on on-chain shuffle verification Sizing and correctness are already proven (see Security Model): the verifier fits at 20,755 bytes and accepts upstream's real 52-card proof, at ~2.38M gas. What remains is integration and economics โ€” deploy the verifier, wire Game.sol to call verifyShuffle at shuffle submission, benchmark real cost per hand at C-Chain fees, and decide where verification stays affordable. This removes the last trust assumption in the protocol.

1b. Upstream the fix The EIP-170 fix applies to every game built on Zypher's SDK, not just this one. Worth a PR to zypher-game/uzkge.

2. Deploy to Avalanche โ€” Fuji done, C-Chain remaining Fuji is live: the contracts are deployed (see above) and a full hand has been played and settled on-chain โ€” create, join, shuffle with ZK proofs, bet, reveal, showdown, payout. What remains is promoting the same deployment to C-Chain mainnet and publishing gas benchmarks for a full hand at mainnet fees.

3. Session-key "buy-in" UX (the wallet-popup fix) A full hand currently makes each player sign ~15 wallet transactions โ€” join, shuffle, a bet per round, a reveal-token submission per round, chooseCards, settlement. That's the single biggest thing standing between this and something that feels like a game. The fix is a per-user in-app wallet:

  • On first connect, the user signs one message; that signature deterministically derives a dedicated game wallet (non-custodial, recoverable on any device by re-signing, nothing stored server-side).
  • The user buys in once โ€” funding that wallet with chips in a single transaction from their EOA.
  • Every subsequent game action is signed silently by the game wallet: the deterministic housekeeping (shuffle, reveal tokens, chooseCards) automatically, and each bet the moment the user clicks it. Signing loses the popup, not the decision, and the blast radius is capped to the chips deposited โ€” never the user's main wallet.

Net effect: per-hand wallet popups drop from ~15 to zero. The only wallet interactions left are buying in when chips run low and cashing out when leaving โ€” front-loaded, not per-action.

Game.sol already supports this with essentially no access-control change: every write function gates only on msg.sender matching the recorded player address, with no EOA-vs-contract assumption anywhere, so the game wallet simply registers as the player. The one contract change is placeBet, which today requires msg.value in native AVAX โ€” moving bets onto an internal chip balance is what lets the buy-in happen once instead of per bet. Testnet uses faucet chips so onboarding costs nothing. Production graduates the derived wallet to an ERC-4337 smart account with scoped session keys and a paymaster, making housekeeping gasless.

4. Tournament mode Sponsor-funded prize-pool contracts, Elo ratings, scheduled multi-table events.

5. Mainnet & first players Mainnet launch, plus a community game night with the Team1 Nigeria and Web3Bridge builders โ€” a skill-based tournament with a sponsored prize pool (free entry, no player wagering, so it's an esports-style prize, not gambling) โ€” driving 100+ unique players onto Avalanche.


๐ŸŽฎ How It Works

The game runs an 8-stage flow from shuffle to payout:

Shuffle โ†’ Ante โ†’ Pre-Flop โ†’ Flop โ†’ Turn โ†’ River โ†’ End (Choose Cards) โ†’ Winner

Stage 1: Shuffle

Every player shuffles the deck before betting opens.

  1. Player 1 generates a masked deck + public key commitment + SNARK proof, submits on-chain
  2. Players 2โ€“N fetch the on-chain deck, shuffle, generate a SNARK proof, submit
  3. Play begins only when all players have shuffled

Each shuffle is cryptographically proven to be a valid permutation without revealing card order. (See Security Model for where that proof is currently checked.)

Stages 2โ€“6: Betting Rounds

Round Cards Revealed Action Required
1. Ante None Initial pot contribution
2. Pre-Flop 2 hole cards per player Bet + submit reveal tokens for opponents' cards
3. Flop 3 community cards Bet + submit reveal tokens
4. Turn 4th community card Bet + submit reveal tokens
5. River 5th community card Bet + submit reveal tokens

Bets are transfers of the chain's native token, sent with the transaction. Players must call or raise the current high bet, or fold and forfeit their stake. Each action carries a 120-second timeout; any player may call forceFold() once it expires.

Card Reveal System

  1. Submit reveal tokens โ€” derived from your secret key, these let others decrypt specific cards without ever exposing the key itself
  2. Auto-decryption โ€” once all tokens for a card are in, it decrypts automatically (2-second refresh); no further action needed
  3. Privacy โ€” only you can decrypt your hole cards until the End round; community cards decrypt once all tokens are submitted

Stage 7: End Round โ€” Strategic Choice

After River betting, each player picks 3 of the 5 community cards and submits via chooseCards().

[Hole Card 1] [Hole Card 2] [Community] [Community] [Community]
     โ†‘             โ†‘             โ†‘           โ†‘           โ†‘
   fixed         fixed      your choice  your choice  your choice
Your hole: [Aโ™  Kโ™ ]
Community: [Qโ™  Jโ™  10โ™  2โ™ฆ 7โ™ฃ]
            1   2   3   4  5

Pick 1, 2, 3 โ†’ [Aโ™  Kโ™  Qโ™  Jโ™  10โ™ ] = Royal Flush ๐Ÿ†

Standard Hold'em picks your best five automatically. Showdown makes you do it โ€” hand reading becomes a skill the player exercises, not a function the client calls.

Stage 8: Winner Determination

  1. TexasPoker.sol evaluates each player's 5-card hand
  2. Each hand receives a weight โ€” Royal Flush 9000+, High Card 0โ€“999
  3. Highest weight wins; the pot transfers immediately
  4. claimWinnings() remains available as a fallback if the automatic transfer fails

Hand rankings (see TexasPoker.sol): Royal Flush 9000+ ยท Straight Flush 8000+ ยท Four of a Kind 7000+ ยท Full House 6000+ ยท Flush 5000+ ยท Straight 4000+ ยท Three of a Kind 3000+ ยท Two Pair 2000+ ยท Pair 1000+ ยท High Card 0โ€“999


๐Ÿง‘๐Ÿผโ€๐Ÿ’ป Tech Stack

  • Smart contracts โ€” Solidity, Foundry
  • ZK โ€” @zypher-game/secret-engine, Groth16 / Plonk over BN254
  • Frontend โ€” Next.js, Tailwind CSS, shadcn/ui
  • Web3 โ€” wagmi, viem, web3modal
  • Backend โ€” Hono

๐Ÿš€ Get Started

Turborepo monorepo. apps/www is the Next.js application; packages/contracts is the Foundry project.

# Clone with submodules (forge-std, openzeppelin-contracts)
git clone --recurse-submodules https://github.com/oderahub/showdown.git
cd showdown

pnpm install
pnpm dev

Contracts:

cd packages/contracts
forge build --sizes    # note: ZgShuffleVerifier exceeds EIP-170, see Security Model
forge test

๐Ÿ“ธ Screenshots


๐ŸŽฅ Demo

Demo Video


Built with Zypher Network's ZK Shuffle SDK.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages