Privacy Patterns in ZK

Zero Knowledge

Privacy 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:

RequirementHow ZK Addresses It
EligibilityMerkle tree membership proof
One-person-one-voteNullifier per election
Ballot secrecyZK hides which member voted which way
Coercion resistanceMACI’s encrypted votes + key changes
VerifiabilityAnyone can verify the ZK proof of tally
Censorship resistanceOn-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:

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

  2. Application-level privacy: Individual dApps adding privacy features (private voting, confidential DeFi) using Cairo programs that generate proofs over private inputs.

  3. Volition mode: Users choose between on-chain data availability (transparent) and off-chain data availability (private but with different trust assumptions).

  4. 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 CasePatternKey Building BlocksExample Projects
Private token transfersCommitment-NullifierMerkle tree, nullifier, Pedersen/Poseidon hashZcash, Tornado Cash, Aztec
Age/identity verificationPrivate Input (ZK-KYC)Signature verification in ZK, selective disclosurePolygon ID, Worldcoin, zk-email
Anonymous votingGroup SignalingMerkle membership, external nullifierSemaphore, MACI, Zupass
Anti-collusion votingGroup Signaling + encryptionMACI coordinator, encrypted ballotsMACI
Hidden-state gamesHidden StateOn-chain commitments, per-action proofsDark Forest
Private DeFi (AMM, lending)Commitment-Nullifier + encrypted order flowNote-based state, encrypted memosAztec, Penumbra
Proof of solvencyPrivate InputBalance commitments, range proofsVarious CEX attestations
Anonymous credentialsPrivate InputBBS+ signatures or Merkle credentialsPolygon 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

PatternTypical Proof SystemWhy
Commitment-NullifierGroth16 / PLONKSmall on-chain proof, fast verification
ZK-KYCGroth16 (mobile) / Halo 2Must run on user device; small proof for on-chain
Group SignalingGroth16 (Semaphore)Established circuits, small proof
Hidden State GamesGroth16 / STARKDepends 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

  1. Hopwood, D., et al. “Zcash Protocol Specification.” zips.z.cash/protocol
  2. Pertsev, A., Semenov, R., & Storm, R. (2019). “Tornado Cash Privacy Solution.” Whitepaper
  3. Koh, W., et al. (2020). “Semaphore: Zero-Knowledge Signaling on Ethereum.” semaphore.pse.dev
  4. Buterin, V. (2019). “Minimal Anti-Collusion Infrastructure.” ethresear.ch
  5. Polygon ID Documentation. devs.polygonid.com
  6. Worldcoin. “World ID Protocol.” whitepaper
  7. Dark Forest. “The Dark Forest Community.” blog.zkga.me
  8. Buterin, V. (2022). “Some ways to use ZK-SNARKs for privacy.” Blog post
  9. Aztec Protocol. “Aztec Documentation.” docs.aztec.network

See also: Commitment Schemes, Nullifiers, Merkle Trees, STARKs vs SNARKs, FRI Protocol, Execution Trace & AIR