Skip to content

Zero-Knowledge Proofs

MIST.cash uses Groth16 zero-knowledge proofs to enable private transactions on Starknet. This guide explains the proof system architecture and how to work with it.

A zero-knowledge proof allows you to prove a statement is true without revealing any information beyond the statement itself. In MIST.cash, the statement is:

“I know a valid commitment in the Merkle tree, and I know its secret key.”

The proof convinces the contract to release tokens without revealing which commitment you’re claiming.

┌─────────────────────────────────────────────────────┐
│ Client (Browser) │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Witness │───▶│ Gnark │───▶│ Garaga │ │
│ │ Builder │ │ (WASM) │ │ (Calldata) │ │
│ └──────────┘ └────────────┘ └──────────────┘ │
│ │ │ │
│ Groth16 Proof Starknet │
│ Calldata │
└─────────────────────────────────┬───────────────────┘
┌─────────────────────────┐
│ Starknet (On-Chain) │
│ │
│ Chamber Contract │
│ └── Garaga Verifier │
│ └── Groth16 Check │
└─────────────────────────┘
ComponentRoleTechnology
GnarkCircuit definition and proof generationGo, compiled to WASM
GaragaProof formatting for Starknet verificationTypeScript
ChamberOn-chain verification and token releaseCairo (Starknet)

The witness is the private input to the proof. It contains everything the prover knows but doesn’t want to reveal:

interface Witness {
ClaimingKey: string; // The secret key
Owner: string; // Recipient address
TxAsset: string; // Token address
Withdraw: {
Amount: string; // Withdrawal amount
};
MerkleRoot: string; // Current tree root (public)
MerkleProof: string[]; // Path from leaf to root
Tx1Secret: string; // Derived: txSecret(key, owner)
}

The circuit verifies:

  1. hash(ClaimingKey, Owner) produces a valid TxSecret
  2. hash(TxSecret, TxAsset, Amount) produces a valid leaf
  3. The leaf exists in the Merkle tree (verified via MerkleProof against MerkleRoot)
  4. The nullifier derived from this proof hasn’t been spent

Use prove_groth16 for just the proof, or full_prove for proof + Starknet calldata:

import { prove_groth16, full_prove } from "@mistcash/sdk";
// Just the proof
const result = await prove_groth16(witness);
if (result.status === "ok") {
console.log("Raw proof:", result.proof);
}
// Proof + formatted calldata for Starknet
const calldata = await full_prove(witness);
// calldata is bigint[] ready for chamber.handle_zkp()

The SDK includes pre-built fixtures for testing without generating real proofs:

import { FIXTURES } from "@mistcash/sdk";
// Use in tests
const testWitness = FIXTURES.witness;
const testProof = FIXTURES.proof;

Proof generation runs entirely client-side in the browser via WASM. Performance varies by device:

Proofs are verified on-chain by the Garaga verifier integrated into the Chamber contract. The verification checks:

  1. The proof is mathematically valid (Groth16 pairing check)
  2. The Merkle root matches the current contract state
  3. The nullifier hasn’t been used before

If all checks pass, the contract releases the tokens to the specified recipient.

PropertyGuarantee
SoundnessCannot forge a proof without knowing the claiming key
Zero-knowledgeThe proof reveals nothing about which commitment is being claimed
Non-malleabilityProofs cannot be modified or replayed (nullifier prevents double-spend)
CompletenessA valid witness always produces a valid proof