Zero-knowledge and post-quantum signature components for the poker application SkullCard. The application is live on the web at skullcard.com. The rust/README.md contains schema information and detailed architecture of the API.
This project migrated away from a Circom implementation to Halo 2. A whitepaper Version 2 is in progress to explain this update in detail. Version 1 of the whitepaper gives a conceptual overview of the motivation and implementation of zero-knowledge proofs in the SkullCard poker application.
SkullCard uses a Halo2 KZG zero-knowledge proof to guarantee a the shuffle contains the expected cards and players received cards from this shuffle without requiring players to trust the server. As the server uses a RNG to shuffle the cards, users still have to trust that the server did not arrange the cards maliciously. Before any cards are dealt, the server:
- Generates a cryptographically random permutation of 52 cards, each paired with a random 31-byte BN256 Fr salt.
- Commits to the entire deck by building a depth-6 Poseidon Merkle tree (BN256 Fr, R_F=8 R_P=56 x^5 S-box) over 64 leaves (52 cards padded to the next power of two).
- Proves in zero knowledge, using a Halo2 KZG/SHPLONK circuit over the BN256 curve, that all 52 card indices are distinct values in
[0, 51]and that the Merkle root correctly commits to their leaf hashes. - Returns the proof bundle. The Merkle root is the public input to the proof: it is embedded in the first 32 bytes of the bundle and is never sent as a separate field.
- Signs the response with an ML-DSA-65 post-quantum signature so clients can verify the response originated from this server.
| Path | Description |
|---|---|
rust/ |
Axum HTTP service: shuffle, Merkle tree, Halo2 KZG prover, and REST endpoint. See rust/README.md. |
rust/circuit/ |
Halo2 circuit crate: permutation proof, Poseidon Merkle commitment, KZG trusted setup, WASM verifier exports. See rust/circuit/README.md. |
circom_circuit/ |
Deprecated. Original Circom/Groth16 implementation, superseded by the Halo2 KZG circuit. Kept for reference only. |
The shuffle service is an Axum HTTP server (rust/) written in Rust. It is called once per round to generate the shuffle and produce the proof. Full API documentation, deployment instructions, and Docker/Cloud Run examples are in rust/README.md.
A client validates a shuffle in three independent steps.
Step 1: Verify the proof. Pass the raw proof bundle to verify_deck. This checks the KZG/SHPLONK transcript and confirms the embedded Merkle root is the one the prover actually used. No trust in the server is required.
const valid = verify_deck(bundleBytes); // true if the permutation proof is soundStep 2: Verify dealt cards against the committed root. Each dealt card comes with a Merkle path (a sequence of sibling hashes and directions). The client:
- Computes the leaf hash for the card:
poseidon(card_index, salt). - Walks the path, hashing the current node with each sibling in order.
- Compares the resulting root against the one extracted from the proof bundle.
If the roots match, the card was provably included in the committed shuffle. The server cannot substitute a card that was not part of the original permutation without invalidating either the proof or the path check.
// Extract root from bundle (bytes 0-31, little-endian)
let root = 0n;
for (let i = 31; i >= 0; i--) root = (root << 8n) | BigInt(bundle[i]);
// Verify a single card's path
let current = poseidon(card_index, salt);
for (const { sibling, direction } of merklePath) {
current = direction === 0
? poseidon(current, sibling) // current is left child
: poseidon(sibling, current); // current is right child
}
const cardValid = current === root;Step 3: Verify the ML-DSA-65 signature. The mlDsaSignature field proves the response came from the legitimate server. The signed payload is the raw proof bytes followed by the 8-byte little-endian Unix timestamp. Use the @noble/post-quantum library and the public verification key from rust/README.md.
A local backend service must be running for these integration tests (integration.test.js).
npm install
npm test