Privacy Patterns in ZK
Zero KnowledgePrivacy Patterns in ZK
Zero-knowledge proofs are often equated with “privacy,” but the relationship is nuanced. ZK is a proof-system property — it guarantees the verifier learns nothing beyond the statement’s truth. Privacy is an application-level goal that requires careful protocol design on top of ZK. This article catalogs the major design patterns, their cryptographic building blocks, real-world deployments, and the traps that leak metadata despite mathematically perfect proofs.
Pattern 1: Commitment-Nullifier (Private Transfers)
The most battle-tested privacy pattern in crypto. Used for private value transfers where a user proves “I own something in a set” without revealing which one.
How It Works
┌─────────────────────────────────────────────────────────┐
│ DEPOSIT │
│ │
│ 1. User generates secret s and nullifier n │
│ 2. Computes commitment C = Hash(s, n, value) │
│ 3. Sends C to contract → inserted into Merkle tree │
│ │
│ Merkle Root │
│ / \ │
│ H₁₂ H₃₄ │
│ / \ / \ │
│ C₁ C₂ [C₃] C₄ ← C₃ is our commitment │
│ │
├─────────────────────────────────────────────────────────┤
│ WITHDRAW │
│ │
│ 1. User computes nullifier_hash = Hash(n) │
│ 2. Generates ZK proof that: │
│ a) They know s, n such that C = Hash(s, n, value) │
│ b) C is in the Merkle tree (with valid path) │
│ c) nullifier_hash = Hash(n) │
│ 3. Contract checks: │
│ - Proof verifies ✓ │
│ - nullifier_hash not in spent-set ✓ │
│ - Adds nullifier_hash to spent-set │
│ - Sends value to recipient │
└─────────────────────────────────────────────────────────┘
Key Cryptographic Components
Commitment scheme: The commitment C hides both the depositor’s identity and the value. It must be:
- Hiding: Given C, you can’t recover s, n, or value
- Binding: The depositor can’t open C to a different value later
Typical choice: Pedersen hash (algebraically friendly for SNARKs) or Poseidon hash (for STARKs).
Nullifiers: The nullifier is a deterministic value derived from the secret. It serves as a “serial number” for the commitment — revealed at withdrawal to prevent double-spending, but unlinkable to the original commitment without knowing the secret.
Critical property: the nullifier must be deterministic — given the same commitment secret, the same nullifier is always produced. Otherwise, a user could withdraw twice from the same commitment by generating different nullifiers.
Merkle tree: Stores all commitments. The ZK proof includes a Merkle path proving membership without revealing the position (leaf index). Typical depth: 20-32 levels (supporting 2²⁰ to 2³² deposits).
Real-World Deployments
Zcash (2016-): The original. Zcash’s “shielded pool” uses this pattern with Groth16 SNARKs. Commitments go into a Merkle tree of “note commitments.” Spending requires revealing a nullifier and proving knowledge of the spending key. The Sapling upgrade (2018) improved performance with Jubjub curve. Orchard (2022) moved to Halo 2 (no trusted setup). Privacy is optional — most Zcash transactions are transparent, which creates a smaller anonymity set for shielded users.
Tornado Cash (2019-2022): Applied the pattern to Ethereum. Users deposit fixed denominations (0.1, 1, 10, 100 ETH) into separate pools. The fixed denomination is crucial — if amounts varied, deposit/withdrawal amount correlation would break privacy. Used Groth16 SNARKs with a trusted setup ceremony. ~$7B in deposits before OFAC sanctions in August 2022.
Aztec (2020-): Built a full privacy-preserving L2 on Ethereum using the commitment-nullifier pattern generalized to arbitrary state (not just transfers). Notes replace UTXO-like commitments. Uses PLONK with a universal trusted setup.
Limitations
- Anonymity set size: Privacy is only as good as the number of users. A pool with 10 deposits provides much less privacy than one with 10,000.
- Fixed denominations: Tornado Cash requires fixed amounts; variable amounts leak information through correlation.
- Timing analysis: If you deposit 1 ETH and someone withdraws 1 ETH 30 seconds later, the linkage is obvious despite the ZK proof.
- Compliance tension: The pattern makes it fundamentally impossible to distinguish legitimate privacy from illicit mixing, leading to regulatory action.
Pattern 2: Private Input (ZK-KYC / Selective Disclosure)
The user proves a property about private data without revealing the data itself. Unlike the commitment-nullifier pattern, there’s no “pool” — the proof is about the user’s own credentials.
How It Works
┌─────────────────────────────────────────────────────────┐
│ ISSUANCE │
│ │
│ 1. Trusted issuer (government, bank) creates a signed │
│ credential: Sig(issuer_sk, {name, DOB, country, ID}) │
│ 2. User stores credential locally │
│ │
├─────────────────────────────────────────────────────────┤
│ VERIFICATION │
│ │
│ 1. Verifier asks: "Are you over 18 and from the EU?" │
│ 2. User generates ZK proof: │
│ Public inputs: issuer's public key, today's date │
│ Private inputs: full credential, issuer's signature │
│ Proves: │
│ a) Signature is valid under issuer's public key │
│ b) DOB field implies age ≥ 18 │
│ c) Country field is in EU list │
│ WITHOUT revealing: name, exact DOB, ID number, etc. │
│ 3. Verifier checks proof against issuer's public key │
└─────────────────────────────────────────────────────────┘
Technical Details
The ZK circuit verifies a digital signature inside the proof. This is computationally expensive — ECDSA verification in a ZK circuit requires hundreds of thousands of constraints. Projects optimize by:
- Using ZK-friendly signature schemes (EdDSA on BabyJubJub curve)
- Using BBS+ signatures (designed for selective disclosure)
- Replacing signatures with Merkle tree membership proofs (issuer publishes a Merkle root of valid credentials)
Real-World Deployments
Polygon ID (2022-): Full implementation of ZK-KYC using Iden3’s protocol. Credentials are issued as Merkle tree entries. Claims follow the W3C Verifiable Credentials standard. The user holds their credential in a mobile wallet and generates Groth16 proofs on-device. Verification happens on-chain or off-chain.
Worldcoin / World ID (2023-): Proves “this person is a unique human” without revealing identity. Uses iris biometrics to generate a unique hash, inserted into a Merkle tree. Users prove membership (they’ve been scanned) and uniqueness (via a nullifier tied to the application, preventing double-signaling). Uses Semaphore under the hood.
Sismo (2022-2023): Proved Ethereum account properties (e.g., “I hold >1 ETH” or “I’m a Gitcoin donor”) without revealing which account. Used Hydra-S1 proof system. Shut down in 2023 but the pattern influenced later projects.
zk-email (2023-): Proves properties about emails you’ve received (e.g., “I got a confirmation email from X”) by verifying DKIM signatures inside a ZK circuit. Enables proof of identity, account ownership, etc. without revealing email contents.
The “De Facto” vs “Formally ZK” Distinction
An important subtlety: many ZK-KYC systems are not truly zero-knowledge in the cryptographic sense. They are “verifiable computation with hidden inputs.”
True ZK requires that the proof reveals nothing beyond the statement. But many selective disclosure proofs leak:
- That you have a credential from issuer X (which narrows the set)
- The type of credential
- Timing information (when you generated the proof)
For most practical purposes, this “de facto” privacy is sufficient — the verifier can’t learn your name, DOB, or ID number. But it falls short of the theoretical ZK definition where the verifier learns nothing beyond “the statement is true.”
Pattern 3: Anonymous Group Signaling (Voting, Reputation)
The user proves membership in a group and takes an action (vote, signal, attest) exactly once, without revealing their identity within the group.
How It Works
┌─────────────────────────────────────────────────────────┐
│ SETUP │
│ │
│ 1. Each member generates identity: (sk, pk) │
│ 2. All public keys are inserted into a Merkle tree │
│ 3. Merkle root = group identity │
│ │
│ SIGNAL (vote, attest, etc.) │
│ │
│ 1. Member computes: │
│ - external_nullifier = Hash(topic) (e.g., "vote#7")│
│ - internal_nullifier = Hash(sk, external_nullifier) │
│ 2. Generates ZK proof: │
│ - "I know sk such that pk is in the Merkle tree" │
│ - "internal_nullifier = Hash(sk, external_nullifier)"│
│ - "signal = <my vote>" │
│ 3. Contract/verifier checks: │
│ - Proof valid │
│ - internal_nullifier not seen before for this topic │
│ │
│ Result: valid anonymous vote, no double-voting │
└─────────────────────────────────────────────────────────┘
The External Nullifier Trick
The external nullifier is the key innovation. It scopes the nullifier to a specific action:
Topic: "Election 2024" → external_null = Hash("Election 2024")
Alice's nullifier: Hash(alice_sk, external_null) = 0xabc...
Alice can vote exactly once in this election.
Topic: "Proposal #42" → external_null = Hash("Proposal #42")
Alice's nullifier: Hash(alice_sk, external_null) = 0xdef...
Different nullifier! Alice can also vote here without linkage.
Within one topic, a member gets exactly one nullifier (preventing double-voting). Across topics, nullifiers are unlinkable (preserving anonymity).
Real-World Deployments
Semaphore (2019-): The foundational protocol for anonymous group signaling. Developed by the Ethereum Foundation’s Privacy & Scaling Explorations (PSE) team. Uses Groth16 proofs. Membership Merkle tree of depth 20 (supports ~1M members). Widely used as a building block by other projects.
MACI (Minimal Anti-Collusion Infrastructure, 2020-): Designed for on-chain voting that resists bribery and coercion. Voters encrypt their votes to a coordinator’s public key. The coordinator decrypts, tallies, and publishes a ZK proof of correct tallying — without revealing individual votes. Even if a voter shows their “receipt” to a briber, they could have submitted a key-change message that invalidated it. The ZK proof ensures the coordinator can’t cheat the tally.
Zupass (2023-): Used at Zuzalu and Devconnect events. Issues “PCDs” (Proof-Carrying Data) — ZK proofs of event attendance, identity, etc. Built on Semaphore. Attendees could prove “I attended Zuzalu” without revealing which specific attendee they are.
Voting-Specific Considerations
Anonymous voting has unique requirements beyond basic group signaling:
| Requirement | How ZK Addresses It |
|---|---|
| Eligibility | Merkle tree membership proof |
| One-person-one-vote | Nullifier per election |
| Ballot secrecy | ZK hides which member voted which way |
| Coercion resistance | MACI’s encrypted votes + key changes |
| Verifiability | Anyone can verify the ZK proof of tally |
| Censorship resistance | On-chain submission (harder to censor) |
Pattern 4: Hidden State Games (Dark Forest Pattern)
The game state (or part of it) is hidden from other players. Each move is accompanied by a ZK proof of validity, but the actual state remains private.
How It Works
In the Dark Forest game (2020-2022), the universe is a grid of planets. Each player knows the coordinates of their planets, but other players don’t. The game map is too large to store on-chain — instead, planet locations are commitments.
┌─────────────────────────────────────────────────────────┐
│ PLANET DISCOVERY │
│ │
│ 1. Player finds planet at coordinates (x, y) │
│ 2. Computes: planet_hash = MiMC_Hash(x, y) │
│ 3. Submits ZK proof to contract: │
│ Public: planet_hash, planet_level │
│ Private: x, y │
│ Proves: │
│ a) planet_hash = Hash(x, y) │
│ b) planet_level = derive_level(x, y) │
│ c) (x, y) is within valid bounds │
│ 4. Contract records planet_hash → owner │
│ │
│ MOVEMENT │
│ │
│ 1. Player moves from planet A(x₁,y₁) to B(x₂,y₂) │
│ 2. ZK proof: │
│ Public: hash_A, hash_B, distance_bound │
│ Private: x₁, y₁, x₂, y₂ │
│ Proves: │
│ a) Hashes match coordinates │
│ b) Distance(A, B) ≤ range │
│ 3. Contract updates ownership without learning coords │
└─────────────────────────────────────────────────────────┘
Why This Pattern Is Unique
Unlike the other patterns, the hidden state pattern creates incomplete information games on a public blockchain. Without ZK, all on-chain state is visible, making strategic games trivial (every opponent sees your moves in the mempool). With ZK:
- Fog of war: Players explore a shared universe but only see their neighborhood
- Hidden strategy: Build-up and movements are private until they interact with other players
- Provable fairness: Despite the fog, the game rules are enforced by ZK proofs — no one can cheat
Real-World Deployments
Dark Forest (2020-2022): The original and most famous ZK game. Built on xDai/Gnosis Chain. Used circom circuits with Groth16 proofs. Ran multiple community “rounds.” Proved the concept but suffered from slow proof generation on user devices (~10-30s per move).
Manta Network Staking (2022-): Uses a similar pattern for private staking — stake amounts and delegation choices are hidden behind commitments.
Potential applications: Supply chain verification (prove goods moved through valid routes without revealing routes), private auctions (bid commitment + ZK proof of sufficient funds), private DeFi positions.
Metadata Leakage: The Elephant in the Room
Even with perfect ZK proofs, real-world systems leak metadata at multiple layers:
Network Layer
┌─ Transaction submitted ──────────────────────────┐
│ │
│ IP address → links to identity │
│ Timing → correlates deposit/withdraw │
│ Gas payment source → links to funded address │
│ Transaction ordering→ MEV bots observe mempool │
│ │
│ Mitigations: │
│ - Tor/mixnet for submission │
│ - Relayers (third party submits tx) │
│ - Account abstraction (no direct gas payment) │
└────────────────────────────────────────────────────┘
On-Chain Layer
- Deposit/withdrawal amounts: Even with fixed denominations, the choice of denomination (0.1 vs 100 ETH) reveals information.
- Timing patterns: Depositing before a deadline and withdrawing right after is linkable.
- Unique behavior: If only 3 people deposited 100 ETH this week and one withdrew, the anonymity set is 3.
- Graph analysis: Long-term patterns across multiple deposits/withdrawals can be correlated.
Application Layer
- Anonymity set size: The fundamental limit. A pool with 10 users provides ~3.3 bits of anonymity. A pool with 1,000,000 provides ~20 bits.
- Interaction patterns: If a “private” user consistently interacts with the same public address, the linkage is inferrable.
- Side channels: Screen sharing, browser fingerprinting, and social engineering bypass all cryptographic protections.
The Metadata Hierarchy
Privacy strength (weakest to strongest):
1. No privacy → everything public (standard blockchain)
2. Amount hiding → values hidden, graph visible
3. Sender hiding → sender anonymous, recipient known (ring signatures)
4. Full transaction → sender, recipient, amount all hidden (Zcash shielded)
5. Full network → above + IP protection + timing obfuscation
6. Full operational → above + no metadata leakage at any layer
Most ZK systems today operate at level 3-4.
Level 5-6 requires additional infrastructure beyond ZK.
Starknet Context: Phase 1 vs Future
Starknet currently operates as a validity rollup (ZK-rollup) where STARKs prove correct execution, but privacy is not a design goal in Phase 1.
Current State (Phase 1)
- All transaction data is visible on L1 (calldata/blobs) and L2
- STARKs are used for scalability, not privacy
- The “ZK” in “ZK-rollup” refers to the proof system, not a privacy property
- Account abstraction and native AA wallets are standard
Future Privacy Possibilities
Starknet’s architecture could support privacy through:
-
Account-level privacy: Private balances using commitment-nullifier patterns, with STARK proofs of valid transitions. Cairo is well-suited for this since Poseidon hash (used in commitments) has efficient AIR constraints.
-
Application-level privacy: Individual dApps adding privacy features (private voting, confidential DeFi) using Cairo programs that generate proofs over private inputs.
-
Volition mode: Users choose between on-chain data availability (transparent) and off-chain data availability (private but with different trust assumptions).
-
Recursive proving: A user could generate a local STARK proof of a private computation, then Starknet’s prover wraps it in the block proof. The private inputs never leave the user’s device.
Technical Feasibility
Cairo/Starknet’s architecture is particularly amenable to privacy because:
- Poseidon hash is native and cheap (vs. SHA-256 or Keccak in EVM-based systems)
- The AIR framework naturally supports witness-based (private input) proving
- Recursive STARK verification in Cairo is already deployed (SHARP)
The barriers are primarily product and regulatory, not technical.
Decision Matrix: Which Pattern to Use
| Use Case | Pattern | Key Building Blocks | Example Projects |
|---|---|---|---|
| Private token transfers | Commitment-Nullifier | Merkle tree, nullifier, Pedersen/Poseidon hash | Zcash, Tornado Cash, Aztec |
| Age/identity verification | Private Input (ZK-KYC) | Signature verification in ZK, selective disclosure | Polygon ID, Worldcoin, zk-email |
| Anonymous voting | Group Signaling | Merkle membership, external nullifier | Semaphore, MACI, Zupass |
| Anti-collusion voting | Group Signaling + encryption | MACI coordinator, encrypted ballots | MACI |
| Hidden-state games | Hidden State | On-chain commitments, per-action proofs | Dark Forest |
| Private DeFi (AMM, lending) | Commitment-Nullifier + encrypted order flow | Note-based state, encrypted memos | Aztec, Penumbra |
| Proof of solvency | Private Input | Balance commitments, range proofs | Various CEX attestations |
| Anonymous credentials | Private Input | BBS+ signatures or Merkle credentials | Polygon ID, AnonAadhaar |
Choosing the Right Pattern
Do you need to prevent double-spending/double-action?
YES → Use nullifiers (Pattern 1 or 3)
Is there a fixed group of participants?
YES → Anonymous Group Signaling (Pattern 3)
NO → Commitment-Nullifier (Pattern 1)
NO → Private Input pattern (Pattern 2) may suffice
Is the data issued by a trusted authority?
YES → ZK-KYC / Selective Disclosure
NO → Self-attested proof (weaker trust model)
Is the hidden state interactive (multi-player)?
YES → Hidden State (Pattern 4)
Cross-Cutting Concerns
Proof System Choice
| Pattern | Typical Proof System | Why |
|---|---|---|
| Commitment-Nullifier | Groth16 / PLONK | Small on-chain proof, fast verification |
| ZK-KYC | Groth16 (mobile) / Halo 2 | Must run on user device; small proof for on-chain |
| Group Signaling | Groth16 (Semaphore) | Established circuits, small proof |
| Hidden State Games | Groth16 / STARK | Depends on proof generation speed requirements |
STARKs are increasingly viable for all patterns as prover speed improves (Stwo, Plonky3), especially when privacy proofs can be batched or recursively aggregated.
Composability vs Privacy
A fundamental tension: privacy breaks composability. In transparent DeFi, contracts call other contracts and read each other’s state. With privacy:
- Contracts can’t read hidden balances
- Atomic composability requires all components to understand the privacy model
- Cross-protocol interactions need compatible commitment schemes
Aztec’s approach: build a full private execution environment where “notes” replace public state, and contracts interact through private function calls with ZK proofs at each step.
References
- Hopwood, D., et al. “Zcash Protocol Specification.” zips.z.cash/protocol
- Pertsev, A., Semenov, R., & Storm, R. (2019). “Tornado Cash Privacy Solution.” Whitepaper
- Koh, W., et al. (2020). “Semaphore: Zero-Knowledge Signaling on Ethereum.” semaphore.pse.dev
- Buterin, V. (2019). “Minimal Anti-Collusion Infrastructure.” ethresear.ch
- Polygon ID Documentation. devs.polygonid.com
- Worldcoin. “World ID Protocol.” whitepaper
- Dark Forest. “The Dark Forest Community.” blog.zkga.me
- Buterin, V. (2022). “Some ways to use ZK-SNARKs for privacy.” Blog post
- Aztec Protocol. “Aztec Documentation.” docs.aztec.network
See also: Commitment Schemes, Nullifiers, Merkle Trees, STARKs vs SNARKs, FRI Protocol, Execution Trace & AIR