Skip to main content

Overview

Privacy Cash enables private transactions on Solana by breaking the on-chain link between deposits and withdrawals. Users deposit SOL or SPL tokens into a shared pool, creating commitments that are stored in a Merkle tree. When withdrawing, they prove ownership of a commitment using zero-knowledge proofs without revealing which specific commitment they own.

Transaction Flow

1

Deposit (Shielding)

Users deposit funds into the privacy pool, generating a secret commitment that represents their deposit.When you deposit:
  1. Generate a random private key and blinding factor
  2. Create a UTXO (Unspent Transaction Output) containing:
    • Amount
    • Public key (derived from private key)
    • Blinding factor
    • Mint address (token type)
  3. Compute commitment: hash(amount, publicKey, blinding, mintAddress)
  4. Submit transaction with the commitment
  5. Commitment is added to the on-chain Merkle tree
The commitment is publicly visible, but the underlying details (amount, owner) remain encrypted.
2

Privacy Pool Storage

All commitments are stored in a Merkle tree on-chain, creating a growing anonymity set.The Merkle tree:
  • Height: 26 levels
  • Maximum capacity: 67,108,864 leaves (2^26)
  • Root history: 100 most recent roots
  • Hash function: Poseidon (ZK-friendly)
Each new commitment increases the anonymity set, making it harder to link deposits to withdrawals.
3

Withdrawal (Unshielding)

Users withdraw funds by proving they own a commitment in the tree without revealing which one.When you withdraw:
  1. Select input commitments to spend
  2. Generate a Merkle proof showing the commitment exists in the tree
  3. Create a nullifier to prevent double-spending
  4. Generate output commitments for change or recipients
  5. Create a zero-knowledge proof that proves:
    • You know the private key for the input commitment
    • The commitment exists in the Merkle tree
    • The nullifier is correctly computed
    • Input amounts equal output amounts (plus fees)
  6. Submit the proof and nullifiers to the program
The recipient address can be different from the depositor, breaking the on-chain link.
4

Nullifier Check

The program verifies the proof and checks that nullifiers haven’t been used before.Verification steps:
  1. Verify the Merkle root exists in root history (confirms commitment was in tree at some point)
  2. Check that nullifiers haven’t been used (prevents double-spending)
  3. Verify the zero-knowledge proof using Groth16
  4. Verify the external data hash matches (recipient, amount, fees)
  5. Execute the transfer if all checks pass
Once a nullifier is used, the commitment is “spent” and cannot be reused.

Universal JoinSplit Transactions

Privacy Cash implements universal JoinSplit transactions, supporting flexible transaction types:

Deposits

Shield funds by creating new commitments
  • Transfer SOL/tokens from wallet to privacy pool
  • Generate 2 output commitments (for amount splitting)
  • External amount is positive
  • Subject to deposit limits (configurable per token)

Withdrawals

Unshield funds by spending commitments
  • Transfer from privacy pool to any recipient
  • Consume up to 2 input commitments
  • External amount is negative
  • No withdrawal limits

Transfers

Internal transfers between commitments
  • Spend old commitments, create new ones
  • External amount is zero
  • Stay within the privacy pool
  • Refresh commitments for enhanced privacy

Splitting/Merging

Reorganize amounts within the pool
  • Split large commitment into smaller ones
  • Merge multiple commitments into one
  • Useful for making exact-amount withdrawals

UTXO Model

Privacy Cash uses a UTXO (Unspent Transaction Output) model similar to Bitcoin, adapted for zero-knowledge proofs:
UTXO Structure
{
  amount: u64,           // Amount of SOL or tokens
  pubkey: Field,         // Public key (derived from private key)
  blinding: Field,       // Random blinding factor for uniqueness
  mintAddress: Pubkey    // Token type (SOL or SPL token mint)
}
Each transaction:
  • Consumes up to 2 input UTXOs (must be in Merkle tree)
  • Creates exactly 2 output UTXOs (added to Merkle tree)
  • Input amounts must equal output amounts (accounting for external transfers and fees)
The circuit enforces the amount invariant: sum(inputs) + publicAmount = sum(outputs)Where publicAmount = extAmount - fee

Multi-Token Support

Privacy Cash maintains separate Merkle trees for different token types:
Native SOL uses a dedicated Merkle tree with PDA seeds ["merkle_tree"].
  • Mint address: 11111111111111111111111111111112
  • Default deposit limit: 1,000 SOL
  • Deposit fee: 0% (free)
  • Withdrawal fee: 0.25%

Fee Structure

Privacy Cash uses a transparent fee model:
OperationFee RateAdjustable
Deposits0% (free)✅ Yes
Withdrawals0.25% (25 basis points)✅ Yes
Internal transfers0%-
Fees include a 5% error margin (configurable). Relayers can charge slightly more to cover gas costs and slippage.
Fee validation formula:
minimum_acceptable_fee = expected_fee × (1 - fee_error_margin)
Where:
  • expected_fee = amount × fee_rate / 10000 (for deposits/withdrawals)
  • fee_error_margin = 500 (5% tolerance)

Security Properties

Deposits and withdrawals cannot be linked unless:
  • The anonymity set is too small (< 10 users)
  • Timing correlation is possible (only user in time window)
  • Amount correlation exists (unique amount)
Best practices:
  • Wait between deposit and withdrawal
  • Use common amounts (0.1, 1, 10 SOL)
  • Split large amounts across multiple commitments
Nullifiers prevent the same commitment from being spent twice.The program:
  1. Creates nullifier account on first spend (PDA: ["nullifier0", nullifier])
  2. Transaction fails if nullifier account already exists
  3. Checks both nullifiers in 2-input transactions
  4. Prevents nullifier collisions between input slots
This is enforced at the Solana runtime level before proof verification.
The zero-knowledge circuit enforces that:
  • Input amounts equal output amounts (conservation of value)
  • Output amounts fit in 248 bits (prevents overflow)
  • Public amount correctly accounts for external transfers and fees
  • All commitments are computed correctly
The program additionally verifies:
  • External amounts don’t exceed deposit limits
  • Fees meet minimum requirements
  • Pool has sufficient balance for withdrawals
Groth16 proofs ensure computational integrity:
  • Prover cannot forge proofs for invalid statements
  • Proof verification is deterministic
  • Verifying key is hardcoded in program (no substitution attacks)
  • Public inputs are bound to the proof
The circuit has been audited by:
  • Accretion
  • HashCloak
  • Zigtur
  • Kriko

Implementation Reference

Key files in the source code:

Transaction Circuit

circuits/transaction.circom:22Universal JoinSplit circuit with 2 inputs and 2 outputs

Transact Instruction

lib.rs:213Main instruction for SOL deposits and withdrawals

Merkle Tree

merkle_tree.rsOn-chain Merkle tree implementation

Proof Verification

utils.rs:214Groth16 proof verification logic

Next Steps

Privacy Model

Learn about the privacy guarantees and threat model

Zero-Knowledge Proofs

Understand the cryptographic proofs that power Privacy Cash

Commitments & Nullifiers

Deep dive into the core cryptographic primitives

Quick Start

Start building with Privacy Cash SDK

Build docs developers (and LLMs) love