Trustless multiplayer card games with provable privacy.
No dealer, no server, no trust — just math.
ZK proofs generated in the browser (Noir), verified on-chain (Cairo via Garaga). A reusable protocol for any card game.
Built for Starknet Re{define} Hackathon — Privacy Track.
Play now: mental-poker.vercel.app (deployed on Starknet Sepolia testnet)
Source code at examples/poker-multiplayer/ — full Texas Hold'em game built with Vite + React + TypeScript.
# Prerequisites: Node.js v20+, nargo 1.0.0-beta.17
cd circuits && nargo compile # compile circuits locally (required)
make ui-sepolia # https://localhost:5173Connect with Cartridge Controller — browser wallet, no extension needed.
# Prerequisites: nargo, scarb 2.15.0 + 2.14.0, starknet-devnet 0.7.2, sncast 0.53.0, Node.js
make setup && make ui # http://localhost:5173- Tab 1: Connect wallet → Create Table
- Tab 2: Connect wallet → Join Game (enter the table ID)
- Game runs automatically: key registration → shuffle → deal → play poker
Each player's browser generates ZK proofs locally. All proofs are verified on-chain.
A complete mental poker protocol running end-to-end on Starknet:
- 3 Noir ZK circuits — key ownership, card decryption, 52-card shuffle
- Cairo smart contracts — game-agnostic protocol layer + Texas Hold'em implementation with on-chain hand evaluation
- Garaga verifiers — all proofs verified on-chain via generated Cairo contracts
- TypeScript SDK — browser-based proof generation with NoirJS + Barretenberg WASM
- Multiplayer UI — playable Texas Hold'em with local devnet and Sepolia testnet support
- Interactive notebook — step-by-step walkthrough of the cryptography in Python
All contracts are live on Starknet Sepolia testnet:
| Contract | Address | Explorer |
|---|---|---|
| Texas Hold'em | 0x028fc...1cbd9 |
View on Voyager |
| Verifier: Key Ownership | 0x06e24...9c885 |
View on Voyager |
| Verifier: Decrypt Card | 0x01b5e...9c6c6 |
View on Voyager |
| Verifier: Shuffle Encrypt | 0x01aa3...2781a4 |
View on Voyager |
Three circuits in the circuits/ workspace, all compiled with Noir 1.0.0-beta.17:
| Circuit | ACIR Opcodes | What It Proves |
|---|---|---|
key_ownership |
63 | Player knows the secret key behind their public key (pk == sk * G). Submitted when joining a game. |
decrypt_card |
63 | Reveal token is correctly computed (reveal_token == sk * C1) and player owns the corresponding public key. Submitted for each card reveal. |
shuffle_encrypt |
928 | Player applied a valid permutation to the 52-card deck and re-encrypted every card with fresh randomness — without revealing the permutation. 418 public inputs (deck in + deck out + aggregate public key). |
The shared/ library contains the core cryptographic primitives: ElGamal encryption, remask, reveal token computation, and permutation validation — all operating on Grumpkin's native embedded curve.
Players shuffle, deal, and reveal cards without trusting each other. Every action is backed by a zero-knowledge proof verified on Starknet.
Player A Starknet Player B
| | |
|-- join(pk_A, ZK proof) --->|<--- join(pk_B, ZK proof) --|
| | |
| APK = pk_A + pk_B | (aggregate public key) |
| | |
|-- shuffle + ZK proof ----->| |
| |<---- shuffle + ZK proof ---|
| | |
| (deck is now doubly shuffled — nobody knows the order)
| | |
| |<-- reveal token + proof ---|
|-- decrypt locally | |
| (only A sees A's cards) | |
- Register — Each player proves ownership of their public key (
pk = sk * G) with a ZK proof - Aggregate key — Contract computes
APK = pk_1 + pk_2 + ... + pk_n(n-out-of-n threshold) - Encrypt deck — Cards encoded as
(i+1) * G, encrypted with ElGamal using APK - Shuffle & remask — Each player permutes + re-encrypts the entire deck, submits a ZK proof that the shuffle is valid without revealing the permutation
- Deal — Contract assigns encrypted cards to players
- Reveal — Other players provide
reveal_token = sk * C1with ZK proof for each card - Unmask — Recipient computes
C2 - sum(reveal_tokens), looks up card value. Contract verifies cryptographically on-chain
After all players shuffle, nobody knows the card order — not even the last shuffler, because previous shuffles already randomized the deck.
┌─────────────────────────────────────────────────────────────────────┐
│ Browser (TypeScript SDK) │
│ ┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌────────────────┐ │
│ │ Grumpkin │ │ NoirJS + │ │ Garaga │ │ starknet.js │ │
│ │ EC ops │ │ Barretenberg │ │ WASM │ │ v8 │ │
│ │ (ElGamal) │ │ (ZK prover) │ │ (proof │ │ (transactions) │ │
│ │ │ │ │ │ → calldata)│ │ │ │
│ └──────────┘ └──────────────┘ └─────────┘ └────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────┘
│ transactions
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Starknet │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ TexasHoldem.cairo │ │
│ │ (betting, showdown, hand evaluation) │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ MentalPokerComponent.cairo │ │ │
│ │ │ (game-agnostic: shuffle, deal, reveal, unmask) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ Garaga │ │ Garaga │ │ Garaga │ │
│ │ verifier: │ │ verifier: │ │ verifier: │ │
│ │ key_ownership│ │ decrypt_card │ │ shuffle_encrypt (52 cards) │ │
│ └──────────────┘ └──────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
- Curve: Grumpkin (
y^2 = x^3 - 17), Noir's native embedded curve — makes EC operations extremely cheap in ZK circuits - Encryption: Exponential ElGamal, implemented from scratch using
std::embedded_curve_ops - Card encoding:
card_i = (i+1) * G— lookup table for decryption (52 precomputed points) - Coordinates: Stored as
u256in Cairo (Grumpkin field is ~254 bits, exceeds felt252's ~251 bits)
We originally planned to use Baby JubJub with noir-elgamal, but std::ec was removed in Noir 1.0. Grumpkin is the native curve of Noir's proving system, so EC operations (point addition, scalar multiplication) are native instructions rather than simulated arithmetic. This is why our 52-card shuffle circuit is only 928 opcodes — implementations on non-native curves would need tens of thousands.
Every action in the protocol is verified either by a ZK proof or by on-chain cryptographic checks:
| Attack | Prevention |
|---|---|
| Fake public key | ZK proof of key ownership verified on-chain |
| Invalid shuffle | ZK proof verifies permutation + re-encryption (418 public inputs checked) |
| False reveal token | ZK proof verifies token == sk * C1 |
| Lie about card value | On-chain verification: C2 - sum(tokens) == (value+1) * G |
| Duplicate cards | card_value_used bitmap prevents same card appearing twice |
| Peek at others' cards | Requires all reveal tokens — impossible without all secret keys |
| Stall the game | Timeout mechanism forces progress or ends game |
| Component | Technology | Purpose |
|---|---|---|
| ZK circuits | Noir 1.0.0-beta.17 | Define what to prove |
| Proving backend | Barretenberg (bb.js) | Generate proofs in browser (UltraHonk) |
| On-chain verification | Garaga 1.0.1 | Generate Cairo verifiers from Noir circuits |
| Smart contracts | Cairo 2.15.0 on Starknet | Game state + proof verification |
| Elliptic curve | Grumpkin (y^2 = x^3 - 17) |
All cryptographic operations |
| Encryption | Exponential ElGamal (custom) | Encrypt/re-encrypt/decrypt cards |
| SDK | TypeScript + NoirJS + starknet.js v8 | Browser integration |
| Frontend | Vite + React | Poker UI |
| Wallet | Cartridge Controller | Starknet wallet (Sepolia) |
mental-poker/
├── circuits/ # Noir ZK circuits
│ ├── shared/ # Library: ElGamal, remask, permutation checks
│ ├── key_ownership/ # Prove pk == sk * G (63 opcodes)
│ ├── decrypt_card/ # Prove reveal_token == sk * C1 (63 opcodes)
│ └── shuffle_encrypt/ # Prove valid shuffle of 52 cards (928 opcodes)
│
├── contracts/ # Cairo smart contracts
│ ├── src/
│ │ ├── mental_poker_component.cairo # Game-agnostic protocol layer
│ │ ├── texas_holdem.cairo # Poker: betting, showdown, hand eval
│ │ ├── verifiers.cairo # Cross-contract calls to Garaga
│ │ ├── grumpkin.cairo # On-chain EC operations
│ │ └── hand_evaluator.cairo # Poker hand ranking
│ ├── verifier_key_ownership/ # Garaga-generated verifier
│ ├── verifier_decrypt_card/ # Garaga-generated verifier
│ └── verifier_shuffle_encrypt/ # Garaga-generated verifier
│
├── sdk/ # TypeScript SDK
│ └── src/
│ ├── crypto/ # Grumpkin EC ops + ElGamal
│ ├── proof/ # NoirJS prover + Garaga calldata
│ ├── contract/ # starknet.js contract interaction
│ └── poker/ # Card mapping + game logic
│
├── examples/
│ ├── poker-multiplayer/ # Playable poker UI (Vite + React)
│ └── poker-bots/ # Headless bot players
│
├── architecture.md # Detailed technical architecture
├── mental_poker_interactivo.ipynb # Interactive Python notebook
└── Makefile # Build orchestration
mental_poker_interactivo.ipynb lets you execute the entire protocol step by step in Python — no external dependencies needed (pure Python arithmetic).
Sections: Grumpkin curve implementation, key generation, card encoding, ElGamal encryption, aggregate keys, deck encryption, shuffle & remask, reveal tokens, unmasking, full 2-player simulation, and 6 anti-cheating experiments.
# Setup
python3 -m venv notebook-venv && source notebook-venv/bin/activate
pip install jupyter ipykernel
python -m ipykernel install --user --name mental-poker-notebook
jupyter notebook mental_poker_interactivo.ipynbSee NOTEBOOK_SETUP.md for detailed instructions.
make setup # Full local setup: devnet + build + deploy + copy artifacts
make ui # Start frontend (local devnet, http://localhost:5173)
make ui-sepolia # Start frontend (Sepolia testnet, https://localhost:5173)
make deploy-sepolia # Deploy contracts to Sepolia- architecture.md — Full technical architecture: protocol flow, circuit details, contract storage, Garaga integration
- mental_poker_interactivo.ipynb — Interactive Python notebook: execute the protocol step by step
![]() Caravana Studio |
![]() @dub_zn |
![]() @dpinoness |




