Skip to content

dpinones/mental-poker

Repository files navigation

Mental Poker

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.

Mental Poker Gameplay


Live Demo

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.


Quick Start

Sepolia (testnet)

# Prerequisites: Node.js v20+, nargo 1.0.0-beta.17
cd circuits && nargo compile    # compile circuits locally (required)
make ui-sepolia                 # https://localhost:5173

Connect with Cartridge Controller — browser wallet, no extension needed.

Local devnet

# 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

Play

  1. Tab 1: Connect wallet → Create Table
  2. Tab 2: Connect wallet → Join Game (enter the table ID)
  3. Game runs automatically: key registration → shuffle → deal → play poker

Each player's browser generates ZK proofs locally. All proofs are verified on-chain.


What We Built

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

Deployed Contracts (Sepolia)

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

ZK Circuits (Noir)

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.


How It Works

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)  |                            |

Protocol Steps

  1. Register — Each player proves ownership of their public key (pk = sk * G) with a ZK proof
  2. Aggregate key — Contract computes APK = pk_1 + pk_2 + ... + pk_n (n-out-of-n threshold)
  3. Encrypt deck — Cards encoded as (i+1) * G, encrypted with ElGamal using APK
  4. Shuffle & remask — Each player permutes + re-encrypts the entire deck, submits a ZK proof that the shuffle is valid without revealing the permutation
  5. Deal — Contract assigns encrypted cards to players
  6. Reveal — Other players provide reveal_token = sk * C1 with ZK proof for each card
  7. 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.


Architecture

┌─────────────────────────────────────────────────────────────────────┐
│  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) │ │
│  └──────────────┘ └──────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Cryptography

  • 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 u256 in Cairo (Grumpkin field is ~254 bits, exceeds felt252's ~251 bits)

Why Grumpkin?

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.


Anti-Cheating Guarantees

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

Tech Stack

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)

Project Structure

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

Interactive Notebook

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.ipynb

See NOTEBOOK_SETUP.md for detailed instructions.


All Commands

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

Documentation


Creators ✨

Caravana Studio
Caravana Studio
dub_zn
@dub_zn
dpinoness
@dpinoness

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors