The truth about "Privacy" protocols on Solana like PrivacyCash and Umbra
ZK proofs, Merkle trees, nullifiers: the cryptography is real, the privacy isn't. Here is why.
PrivacyCash, Umbra, and most of the privacy protocols on Solana are shielded pools built on Groth16 zero-knowledge proofs, Merkle commitment trees, and nullifier sets to prevent double-spending. The cryptographic construction is sound, and in both leading cases independently audited.
Despite this, a large fraction of the transactions passing through these pools remains linkable. The failure is not in the proof system. It is in the metadata the proof system does not, and cannot, cover.
This article states the threat model precisely: what the proof guarantees, what it leaves exposed, and how an analyst exploits the gap. The empirical figures below come from an analysis of PrivacyCash's on-chain history (roughly 100,000 program signatures sampled), which recovered 7,140 deposit-to-withdrawal pairs that are uniquely linkable on metadata alone, on the order of half of all withdrawals in the sample.
What the proof actually guarantees
A shielded pool proves a single statement: the withdrawal corresponds to some previously committed deposit, without revealing which one. Formally, the proof establishes membership in the commitment set and validity of the nullifier, and nothing beyond that.
This is a statement about the internal state of the pool. It says nothing about the observable variables attached to each transaction on the public ledger. Three of those variables sit entirely outside the proof, and any one of them, if distinctive, re-establishes the link the proof was meant to sever.
The unit of measurement throughout is the anonymity set: for a given withdrawal, the number of candidate deposits that could plausibly correspond to it once observable constraints are applied. A perfect proof guarantees that an analyst cannot distinguish within the anonymity set. It does nothing to keep that set large.
Each public variable acts as a filter that shrinks the set:
- Baseline (proof only): every deposit in the pool is a candidate. The anonymity set is the entire deposit history.
- After amount matching: candidates are restricted to deposits of the same value. The set typically falls to the order of ten.
- After the timing window: candidates are further restricted to deposits preceding the withdrawal within a plausible delay. The set typically falls to a few.
- After volume analysis: in a low-traffic window the intersection collapses to one. Linkage is then complete, and the proof was never touched.
The three sections below treat each filter in isolation, then combine them.
The three side-channels
1. Timing
Every transaction is finalized in a specific slot, slots are spaced roughly 400 ms apart, and the full ledger is public and permanent. Deposit and withdrawal timestamps are therefore exact, ordered, and non-repudiable.
Consider a deposit of 1 SOL at slot 417,283,001 and a withdrawal of 1 SOL at slot 417,283,047, a separation of about 18 seconds. The proof certifies that no cryptographic link can be derived. The analyst does not need one: an 18-second interval with a matching value, in a pool with low concurrent traffic, is already a near-unique correspondence. The proof constrains the cryptography; it says nothing about the timing.
The mitigation is well understood. Tornado Cash documentation explicitly instructed users to delay withdrawals by days or weeks in order to grow the temporal anonymity set. The instruction was advisory, most users ignored it, and most of those users were subsequently deanonymized. Behavioral guidance is not a security control.
2. Amount
Fixed-denomination designs (Tornado Cash used 0.1, 1, and 10 ETH) force every deposit into a small set of standard values. This is deliberate: when all deposits of a denomination are identical, the amount channel carries no information and cannot be used to partition the anonymity set.
PrivacyCash imposes no such constraint; arbitrary deposit amounts are permitted.
A deposit of 3.7182 SOL is, with high probability, unique in the pool. When 3.7182 SOL is later withdrawn, the match is effectively certain regardless of the proof. The proof hides which commitment maps to which nullifier; it does not hide that a distinctive quantity entered the pool and an identical quantity left it. Value acts as a fingerprint, and non-standard amounts are maximally fingerprintable.
3. Volume
This channel is the most consequential and the least discussed.
The anonymity set is not a design parameter; it is an empirical quantity equal to the number of other deposits sharing the same denomination within the relevant time window. If 500 users deposit 1 SOL within a 24-hour window, the set is 500. If three do, it is three. The proof's guarantee is uniform over the set, so a set of size one renders the proof irrelevant: there is only one candidate to point to.
Median anonymity set by withdrawal window, PrivacyCash
PrivacyCash has aggregate volume, but it concentrates around a few denominations and a few hours of the day. A withdrawal executed at 03:00 UTC on a Sunday may share its denomination and window with a single other deposit. That is not an anonymity set. That is a coin flip.
The channels compound
The three channels are not independent failure modes to be addressed one at a time. They intersect, and the intersection is what defeats the system.
Amount matching alone might leave a candidate set of twelve. Timing alone might leave eight. Their intersection, narrowed further by the observation that the pool was quiet in that window, is frequently one. Deanonymization is the product of three filters, not the failure of any single one.
The analyst never attempts to invert Groth16. The procedure is to apply each observable constraint in turn and report the residual candidate set. This is precisely how the 7,140 linkable pairs cited above were recovered: amount, then time, then traffic. No cryptographic assumption was challenged at any step.
Why there is no complete fix today
Eliminating the amount channel requires confidential transfers: deposit and withdrawal values encrypted on-chain, with conservation of funds verified homomorphically rather than in the clear. Solana ships a primitive designed for exactly this, the ZK ElGamal proof program, which permits arithmetic over encrypted balances so a protocol can prove that inputs equal outputs without observing either.
The primitive is not currently usable. As of this writing, the ZK ElGamal proof program is non-functional on both mainnet and devnet: the CLI emits proofs that the deployed on-chain verifier rejects, which indicates a version mismatch between tooling and program. The failure is reproducible. The configure step returns SigmaProof(PubkeyValidity, AlgebraicRelation). Until this is resolved and integrated into a shielded pool, on-chain amounts cannot be hidden, and the amount channel remains open by construction.
The timing and volume channels are, in principle, addressable through protocol design: enforced withdrawal delays, mandatory fixed denominations, and batched rebalancing to flatten the traffic profile. No protocol on Solana currently enforces all three at once.
Conclusion
The cryptography in PrivacyCash and Umbra is not the weak point. The weak point is that amount, timing, and volume are public on Solana, and no deployed protocol closes all three channels simultaneously.
The proofs deliver exactly what they claim: a withdrawal cannot be mathematically linked to a deposit. But mathematical linkage is not what chain-analysis firms rely on. They rely on metadata, and the empirical result is that roughly half of PrivacyCash withdrawals are recoverable from metadata alone. Used without accounting for the three channels above, these protocols do not provide privacy. They provide a mixer with extra steps.
Real privacy on Solana requires hidden amounts. Hidden amounts require a working ZK ElGamal implementation. That precondition is not yet met.