Todas las entradas
2026-05-26

The privacy delay, explained: why SolMask makes you wait

What is the SolMask delay, mechanically?

When you deposit into the SolMask shielded pool, the UI asks you to pick a delay. The default is 10 minutes; the dropdown offers everything from "Instant" up to 1 week. That choice is not cosmetic. The frontend takes your chosen delay, converts it from wall-clock time to Solana slots — currently 1,500 slots for 10 minutes, 216,000 for 1 day, 1,512,000 for a week, computed against the rolling average slot time — and bakes that slot count into the note. Specifically, the deposit instruction reads Clock::get()?.slot, adds your delay-in-slots, and stores the result as the note's unlock_slot field inside the Poseidon commitment. The commitment, with unlock_slot already inside, is what lands on-chain as a leaf in the Merkle tree.

From that moment on, your unlock_slot is part of the note's mathematical identity. You cannot edit it. You cannot lower it. The on-chain withdraw verifier receives the current slot as a public input to the proof, and the circuit asserts current_slot ≥ unlock_slot. If you try to withdraw early, the proof generation itself either fails (snarkjs detects the constraint violation client-side) or, if you manually constructed a proof claiming an inflated current slot, the on-chain verifier rejects it because the slot in the public input is checked against the actual Clock sysvar. There is no admin override. There is no fast-forward. The delay is a contract you sign with the protocol at deposit time, and the protocol enforces it without exception.

This applies per note. If you make three deposits with different delays, each note has its own unlock_slot. A 10-minute deposit unlocks ten minutes after it lands; a 1-week deposit unlocks a week after it lands; they do not interfere with each other. You can interleave them freely and withdraw each on its own schedule. The pool does not track delays per user — it cannot; users are not a concept the protocol observes. It tracks them per note, encoded inside each individual commitment.

What does the delay actually buy you?

The delay buys time for other deposits to accumulate. Privacy in a shielded pool is statistical: when you withdraw, your withdraw is mathematically indistinguishable from any of the other valid notes you could have been withdrawing. The set of all such notes is your anonymity set. The longer you wait between deposit and withdraw, the more notes have landed in the pool that match your mint and amount-band, and the larger your anonymity set is when you finally pull funds out.

Consider a chain analyst watching the pool. They see deposit transactions land. They see withdraw transactions land. They cannot read the proofs to figure out which deposit corresponds to which withdraw — the Groth16 proof reveals nothing about the leaf. What they can do is filter by timing. If wallet A deposits 1 SOL at 14:02:00 and a fresh wallet B receives 0.9947 SOL out of the pool at 14:02:15, the analyst can shortlist the candidate set to deposits that landed before 14:02:15 and have not yet been withdrawn. If only wallet A's deposit fits, the analysis is trivial. The delay is what defeats this. With a 10-minute delay, any deposit made in the same 10-minute window is a candidate; with a 1-day delay, any deposit in the same day is a candidate; with a 1-week delay, any deposit in the same week is a candidate. The bigger the window, the more deposits, the more candidates, the less the analyst learns.

Note that anonymity-set size is not literally "all deposits in the window." It is the set of unspent notes of the same mint with the same unlock_slot ≤ current_slot bound. Notes that have already been withdrawn are removed from the candidate set, because their nullifiers are now in the on-chain nullifier set and the analyst can see that. So a deposit made an hour ago, withdrawn forty minutes ago, no longer contributes to your anonymity set. This is why a high-throughput period is more valuable than a low-throughput one: lots of currently unspent notes is what matters, not lots of historical activity.

The delay is the lever you have over this. You cannot control how many other people deposit; you can only control how long you are willing to wait, in exchange for whatever growth the pool happens to produce in that window. On a typical day the SolMask pool sees enough activity that 10 minutes covers a non-trivial set of candidates for sub-1-SOL deposits; for larger amounts you want longer, because the amount-band slice of the pool is thinner.

Why is the default 10 minutes, and not zero or an hour?

The default exists to protect users who pick the default. That is most of them. So the default has to be defensible without any thought from the user.

Zero — or "Instant" — is a foot-gun for someone who actually needs privacy. The naive timing-correlation attack is so easy that any analyst with a CSV of deposits and withdraws can run it in seconds. Same-amount, same-asset, same-second matches reduce to a one-line filter. If we made "Instant" the default we would be selling users a privacy product that does not deliver privacy. We refuse to do that.

An hour, on the other hand, is too punishing for the most common case: a first-time user testing a 0.1 SOL send. If the test takes an hour to complete, the user leaves. Worse, an hour-long default trains users to think SolMask is slow when in fact it does not have to be — the underlying proving and on-chain settlement take seconds.

Ten minutes is the smallest delay that defeats the naive timing attack while still letting a user complete a deposit-and-withdraw cycle in under twenty minutes end-to-end. It is what we recommend as the floor for any send where you actually care about the link being broken. Below that, you are essentially saying "I want the deposit and the withdraw to look related to anyone watching, but I want the proof anyway" — which is occasionally a real use case (testing, internal accounting) and which we support via the "Instant" option, but it is not what most users want.

When should you use a longer delay?

Three categories of sends should never use the default.

Payroll. If you are paying employees from a treasury wallet, the source side of every transfer is identifiable — your treasury wallet has a public history. The whole point of routing payroll through SolMask is to sever the link between "this treasury paid out 4.2 SOL" and "this contractor received 4.197 SOL." With a 10-minute delay, an analyst correlating the two is looking at a window of, optimistically, dozens of deposits. With a 1-day delay, that window contains thousands. Always pick at least a 1-day delay for payroll. 3 days is better if you can absorb the latency.

Donations from public-facing wallets. Same logic. If your wallet is identified — as a project treasury, a founder address, a known philanthropist — and you want the recipient not to be link-identified to you, the source side is the high-value secret. Use a delay long enough that the destination cannot be timing-correlated back. A day at minimum; a week for high-stakes donations where the link is the actual sensitive thing.

OTC settlement and treasury operations. Anything an investigator might look at retroactively, asking "where did this counterparty's funds come from?" deserves the maximum delay. The cost is calendar time, not money. Three days or a week. The expense feels worse than it is — you are not blocked from doing anything else with the originating wallet during the delay, and the destination wallet does not need to exist yet at deposit time.

A common mistake is to underweight the delay because the wait feels like the cost. It is not. The cost is the calendar time, not your attention. You set the delay, you close the tab, you come back later, the funds are withdrawable. Treat the delay as a free resource you happen to be denominating in hours instead of dollars.

When does "Instant" actually make sense?

It makes sense when you do not need privacy and you know that you do not need privacy. Concretely: testing the protocol on devnet or mainnet with a tiny amount; debugging a relayer integration; moving funds between two wallets that you already publicly control and where the link being visible is a non-issue. We support Instant because being honest about what the product does requires being honest about what it does not do — sometimes a user does not need privacy, and forcing them to wait 10 minutes to confirm a click is hostile. Use Instant when the privacy is not the point. Do not use it when it is.

We have considered, and rejected, the idea of hiding the Instant option behind a "developer mode" toggle. The rationale: someone who wants Instant can find it; someone who does not want it would not pick it; and surfacing it explicitly is more honest than burying it. See /docs/threat-model for the broader articulation of what assumptions a SolMask deposit makes about the user.

What does the delay not do?

The delay does not change anything about the network layer. Your IP is visible to whatever RPC you sent the deposit through, and to the relayer (if you used one) at withdraw time. A patient observer who controls or subpoenas the relayer's logs can correlate IPs across deposit and withdraw regardless of how long the delay was. If the network layer is part of your threat model, route through Tor, a trusted VPN, or your own infrastructure — the delay does not help with this and was never intended to.

The delay does not change the recipient address. You picked a recipient at withdraw time, not at deposit time. The note does not commit to a destination — only to an amount, a secret, a nullifier_secret, an unlock_slot, and a mint. The recipient is a public input to the withdraw proof, chosen freely at that moment. This is a deliberate design: locking the recipient in at deposit time would leak the relationship if someone observed the deposit and the recipient's later receipt; leaving it free lets you decide who gets the funds long after the deposit has gone cold. See /learn/choosing-a-recipient-address for the guidance on what makes a good recipient address.

The delay does not influence anonymity-set composition beyond timing. It does not, for instance, force the pool to wait until enough other deposits have landed before yours becomes withdrawable. The slot bound is a wall-clock bound, not an activity bound. If the pool is dead for the entire week of your 1-week delay, your withdraw will be lonely; the delay does not detect or compensate for that. In practice this is rarely an issue — the pool sees enough activity at common amount-bands — but it is the correct mental model: the delay buys you a window of opportunity, not a guaranteed crowd.

A practical recommendation matrix

The mapping we recommend, in plain English:

Instant. Use for testing, debugging, and intra-account moves where you genuinely do not need privacy. Treat as a developer affordance, not a privacy mode.

10 minutes (default). Use for casual sends, drop-off transfers, and anything where the recipient is short-lived and not high-value. The default exists because it is good enough for the median case; if you do not have a specific reason to pick differently, pick this.

1 hour. Use for sensitive personal sends where the link matters but the latency does not — paying someone whose receipt you do not want correlated with your name, for instance. The hour buys you a meaningfully larger candidate set than 10 minutes without costing you a workday.

6 hours to 1 day. Use for payroll, donations from public-facing wallets, and most treasury operations. This is the right floor for any send where the source side is publicly identifiable and the destination side is meant to be unlinked. A day is rarely too long; an hour is usually too short.

3 days to 1 week. Use for high-stakes treasury, OTC settlement, and any send that a sophisticated investigator might examine retroactively. The cost is wall-clock time you would not be using anyway; the benefit is a candidate set thick enough to make timing analysis hopeless.

The slot-conversion constants and unlock-slot enforcement live in the on-chain program's deposit and withdraw paths. See /glossary/unlock-slot and /glossary/shielded-pool for the canonical definitions, and /docs/threat-model for what assumptions each delay setting implicitly makes about your adversary.

FAQ

Can I lower my delay after depositing? No. The unlock_slot is committed inside the Poseidon hash that became your note's on-chain leaf. Changing it would require regenerating the leaf, which would require knowing the secret to construct a new commitment, which would be a different note — not the one you deposited. The chain enforces the original.

What happens if I try to withdraw early? The proof generation fails client-side because the constraint current_slot ≥ unlock_slot is violated. If you somehow constructed a proof with a forged current slot, the on-chain verifier rejects it: the public-input slot is checked against Clock::get()?.slot, which the program reads directly.

Does the delay affect the withdraw fee? No. Deposits are free, and the withdraw fees (the 23 bps percentage fee and the 0.003 SOL flat fee) are independent of delay. Picking a longer delay does not cost more or less. See /docs/fees.

If I pick 1 week, can I withdraw any time after a week, or only at exactly 1 week? Any time after. The constraint is current_slot ≥ unlock_slot, not equality. Once the unlock slot is reached, the note stays withdrawable indefinitely — there is no expiry.

Does the pool keep my note safe during the delay? Yes. The note's commitment is a permanent leaf in the on-chain Merkle tree; the funds are held by the program vault. No one can withdraw your note without knowing your secret and nullifier_secret. The delay only restricts when you can withdraw, not the safety of the funds in between.

Can a longer delay actually hurt me if the pool goes quiet? In theory yes — if no other deposits land during your entire delay window, your withdraw is correlatable to the deposits that did land. In practice the active windows for common amount-bands are dense enough that this is not a real concern at 10 minutes or above. If you are depositing an unusual amount (e.g., 47.3 SOL), pick a longer delay specifically to give the amount-band time to fill out.

Is the delay visible on-chain? Indirectly. The unlock_slot is inside the commitment, not exposed. But an observer can see when you deposited and when you withdrew, and infer a lower bound on your chosen delay from the gap. The choice itself is not separately published. You cannot, however, hide that the gap was at least that long.

The privacy delay, explained: why SolMask makes you wait · SolMask