All posts
2026-05-26

Anonymity sets on Solana: what actually makes a shielded pool private

Privacy on a public chain is not a property of one transaction — it is a property of a crowd. When you deposit into SolMask's shielded pool and later withdraw, what protects you is not the zero-knowledge proof on its own. The proof says, mathematically, "I own one of the unspent notes in this Merkle tree." It does not say which note. The set of notes the proof could plausibly refer to — the anonymity set — is what hides you. If that set is small, the math is excellent but the privacy is thin. If the set is large and well-mixed, the same math becomes meaningful. Everything we do in protocol design after the circuits are written is aimed at growing and shaping that set.

What exactly is the anonymity set?

The anonymity set for your withdraw is every other unspent note in the same pool whose unlock_slot has elapsed by the time your withdraw lands. That's the operative definition — not "every deposit ever made" and not "every note still in the Merkle tree." The two filters matter.

The first filter, "unspent," excludes notes whose owners have already withdrawn. SolMask enforces single-use by recording a nullifier PDA for each spent note; the on-chain check is binary and irreversible. Once a peer has withdrawn, their note is no longer a candidate sender for your withdraw, because the verifier would catch the double-spend. From an observer's standpoint, spent notes drop out of the suspect set.

The second filter, unlock_slot elapsed, exists because every deposit picks a privacy delay at insertion time. The delay is encoded as a slot number written into the note's commitment; the withdraw circuit enforces that the chain's current slot is past that value. A peer who deposited five minutes ago with a one-week delay is not yet a valid sender — their note exists in the tree, but no proof that references it can verify yet. To an outside observer trying to deanonymise you, they remove that peer from your candidate set because they can see the deposit timestamp and the minimum delay any pool member chose.

Put concretely: if the pool contains 4,000 unspent notes, 600 of which are still inside their delay window, your anonymity set at withdraw time is 3,400. That is the right number to reason about — not the deposit count, not the tree size, not the TVL.

How does the math of one-in-N actually behave?

A pool with 10 viable peer notes gives any observer roughly 1-in-10 deniability for who is behind a given withdraw. With 10,000, it's 1-in-10,000. The marginal value of one additional peer falls off quickly — going from 10 to 100 peers is a ten-fold improvement; going from 10,000 to 10,100 is barely a rounding error on the observer's posterior. But the function never reaches zero, and the floor matters more than the ceiling.

The right intuition is not "more is better forever." It's "below a certain threshold, the math is informally broken; above it, you are paying diminishing returns to push a number that's already protective." For most realistic threats — block-explorer correlation, casual chain analysis, a counterparty trying to map your treasury — a few hundred peers in the right amount band is already strong. For nation-state-grade adversaries with auxiliary data, no public anonymity set is enough on its own; you need operational hygiene around timing and recipients as well, which we'll come to.

The number to watch is not pool TVL or total deposit count. It is the count of unspent peers within your amount band whose delays have elapsed. SolMask's UI exposes this number on the withdraw screen because it is the single most useful piece of information you can have before signing a proof.

Why does the privacy delay matter so much?

The privacy delay is the most underrated control on a shielded pool. It directly determines how many other deposits arrive between yours and your withdraw, which is exactly how the set grows.

SolMask's default delay is 10 minutes. You can choose longer — up to one week, in increments — and the longer you wait, the more peer deposits get queued behind yours. A pool that accepts five deposits per hour gives a 10-minute window almost no growth headroom; the same pool gives a one-day window 120 fresh peers. For depositors who can tolerate latency, longer is unambiguously stronger.

The default exists because most depositors will not choose a setting that costs them latency, even when the privacy gain is meaningful. Ten minutes is the floor we picked as a reasonable balance: long enough that an observer watching the mempool cannot trivially correlate your deposit and withdraw inside the same minute, short enough that the UX is not painful. If you care about privacy more than latency — and for treasury operations, payroll, or anything not personally urgent, you should — pick a longer delay. The unlock_slot field on your note is set at deposit time and is enforced by the circuit at withdraw, so the delay is non-negotiable once you've committed.

One subtle point: the delay does not retroactively grow your set. A 10-minute delay you chose at deposit time is binding regardless of how many peers arrive after the unlock slot. If you want to benefit from a peer who deposits two hours after you, your own withdraw must happen after both their deposit and after your own unlock — but your unlock has already elapsed by hour two, so what you actually need is patience between the unlock moment and the withdraw moment. Many users unlock and immediately withdraw. That throws away the largest source of free anonymity growth: the time after you are eligible to withdraw but choose not to yet.

How does the amount shape your set?

If your withdraw is an outlier amount, the amount itself partially deanonymises you. A pool where every note is 0.05 SOL and you withdraw 1 SOL is, from a chain-analysis standpoint, telling an observer almost everything they need to know: only one deposit could have funded a 1 SOL note, and that deposit is yours. The proof is still valid, the nullifier still hides which specific note was spent, but the conditional distribution over candidate notes collapses to one.

The defensive posture is to deposit in amounts that match what others deposit. SolMask does not enforce fixed denominations — the pool accepts arbitrary amounts because rigid denominations create UX friction and split liquidity across many under-populated pools — but you should think of your effective anonymity set as "peers in roughly the same amount band as you." If the peer median is 0.5 SOL and you deposit 47 SOL, your set is functionally peers in the 10+ SOL band, which is much smaller than the total peer count.

The same applies to withdraws. If you deposit 100 USDC and withdraw 47 USDC, the partial amount makes you slightly more distinctive than if you withdrew the full 100 or a round denomination. Round amounts compose well with peer behaviour; oddly precise amounts do not. If you can split a large withdraw into rounded sub-withdraws across several days, you gain on both axes.

What does timing leak that the proof does not hide?

The proof hides which note was spent. It does not hide that a deposit happened at slot X and a withdraw happened at slot Y. If those two events are close in time and no peer activity occurred between them, an observer with no special privilege — just block explorer access — can rule out almost every other candidate sender by elimination. The proof's denominator is anonymity-set-sized; the timing correlation is what shrinks the numerator.

Consider a worked example. Three depositors — A, B, C — each deposit 5 SOL into the pool within the same minute. Ten minutes later, three withdraws of 5 SOL each leave the pool to three fresh addresses within the same minute. From the chain's view, anybody can see three deposits in, three withdraws out, all the same amount, all clustered. The anonymity set within this micro-window is the three peers themselves: A is one of for sure, but the observer's prior collapses to a uniform distribution over those three, not over the thousands of peers in the broader pool. The three depositors did almost nothing for one another because they acted simultaneously. The proof technology was fully engaged and the privacy outcome was a 1-in-3 set.

The fix is decorrelation in time. Deposit, then forget. Come back in hours or days, ideally during a period of pool activity, and withdraw. The longer your deposit has been sitting in the pool with other deposits flowing in around it, the larger the peer set you blend into and the harder the timing correlation gets for the observer.

A second timing leak worth naming: same-epoch behaviour by the same off-chain identity. If you deposit from a wallet linked to your Twitter, and you withdraw to a wallet that immediately purchases an NFT also linked to that Twitter within the same hour, the on-chain proof is fine and the off-chain inference is fatal. Privacy is a pipeline; the chain is one stage. Keep the off-chain hygiene in mind, or none of this work matters.

What does SolMask not fix?

The threat model is honest about its limits, and so should this post be. Three things SolMask cannot do for you:

Network-layer observation. If you submit your withdraw proof from an IP address that an adversary can correlate with your deposit's IP address, you are linked at the transport layer regardless of the chain. The relayer mitigates this — your withdraw is broadcast from the relayer's network identity, not yours — but the relayer is one entity, and a sufficiently resourced adversary could correlate even relayer-submitted traffic if they have a vantage point. Use a different network for deposit and withdraw if your threat model includes network observers.

Recipient-side address reuse. If the address you withdraw to has any prior on-chain history that ties it to you — a previous SOL transfer from a known wallet, an NFT mint, an exchange withdrawal — the destination identifies you regardless of how clean the SolMask leg was. The mitigation is to use a fresh address for each withdraw. We have a page on this, /learn/choosing-a-recipient-address; it's the single most important hygiene step after the deposit itself.

Adversaries with auxiliary data. If a counterparty knows you sent exactly 47.231 SOL into the pool at a specific time because you transferred it from an exchange they can subpoena, no shielded pool fixes that. The anonymity set is large but the conditional set, given the auxiliary observation, may be one. Privacy is conditional on what your adversary already knows.

The protocol gives you the structural primitives — the proof, the nullifier, the Merkle tree, the delay. The operational discipline around them is yours. We document the circuit details at /docs/circuits and the full threat model at /docs/threat-model; the /glossary/nullifier and /glossary/shielded-pool entries are the short versions if you just want the vocabulary.

FAQ

Q. What is the anonymity set of SolMask's SOL pool right now? A. The withdraw UI shows it live — the count of unspent peer notes whose unlock slots have elapsed. The number changes with every deposit, withdraw, and unlock. We do not publish a TVL-equivalent privacy metric because TVL conflates spent and unspent notes; the live UI number is the one that matters.

Q. Does a larger pool guarantee better privacy for my withdraw? A. Larger is better all else equal, but composition matters more at the margin. A 10,000-note pool where 9,900 notes are in a different amount band than yours gives you a 100-note effective set. Match peer amounts and choose a non-zero delay; raw pool size is the floor, not the ceiling, of your privacy.

Q. If I deposit and withdraw with a 10-minute delay, is that enough? A. It's the default, not the recommendation. Ten minutes is fine for casual privacy. For meaningful privacy against a motivated observer, choose hours or days, and let your unlock_slot elapse well before you actually withdraw. The marginal cost of patience is zero; the marginal gain in set size is everything.

Q. Do fixed denominations protect me better than arbitrary amounts? A. Fixed denominations give a perfectly uniform amount distribution, which is the strongest case. SolMask chose arbitrary amounts for UX and liquidity reasons, and the practical loss is small as long as you stick to round denominations close to peer behaviour. Watch the pool's withdraw history and don't be an obvious outlier.

Q. Can the relayer see which note I'm spending? A. No. The relayer sees the proof, the nullifier, and the public inputs (Merkle root, withdraw amount, recipient). The proof reveals nothing about which leaf of the tree it commits to. The nullifier is a one-way function of your nullifier_secret, which the relayer never learns. The relayer's view is roughly what any block explorer sees post-broadcast.

Q. If I deposit into the SOL pool and the SOL pool's USDC twin separately, do I get a combined anonymity set? A. No. Each mint has its own pool, its own Merkle tree, its own anonymity set. A SOL deposit cannot be referenced by a USDC withdraw's proof. If you want privacy in both assets, you build the set in both pools independently.

Q. What's the minimum delay I can choose? A. The current default and floor is 10 minutes. We considered shorter; the privacy gain from lowering it further was negligible and the correlation risk was high. If your use case truly cannot tolerate ten minutes, the right answer is probably not a shielded pool — it's a different tool entirely.

Anonymity sets on Solana: what actually makes a shielded pool private · SolMask