zkSVM

App

Litepaper

Documentation

Links

DocsHow it works

Privacy model

What each operation shows, and what it does not.

7 min read

Exactly what each operation puts on the public record, what it keeps off it, and the trade-offs behind the design. Wallet builders: the parts that affect your users belong in your interface.

The anonymity set is the product

Cryptography hides which note was spent. It cannot hide that a pool with three users has three users.

If one address shields 100 and, ten minutes later, one address unshields 100, the link is obvious to anyone reading the chain. No proof system fixes that — the leak is in the timing and the amounts, not in the math.

What actually helps:

  • Many users. The only real defence. A pool nobody uses provides nothing.
  • Time between shield and unshield. Minutes are traceable; days are not.
  • Round amounts. Shield 10, not 10.0347. Unusual amounts identify themselves.
  • Not unshielding to the address you shielded from. Users do this constantly.

A wallet that lets someone shield and immediately unshield the same amount to the same address has given them a false sense of privacy — worse than none, because they will act on it. If you build the interface, warn there.

Public boundaries

Shield and unshield are transparent by design. Real funds cross, so an observer sees an address deposit 10 SOL, and later sees 3 SOL arrive somewhere. What is hidden is the link between them.

Only transfer is fully private, and only in the middle of a chain of them.

Address-owned notes name their owner

A claim is signed by the address that owns the note, in public. Observers learn that this address held shielded value and spent some of it now. They do not learn the amount, the source, or the destination — but "this address is a pool user, active at this slot" is real information. For a DAO treasury that is the point; for a person it may not be. An address that wants to receive without ever being seen should hand out a shielded address instead.

Whoever created the note can also recognize its claim. An address has no secret inside the pool, so the nullifier is keyed by the note's blinding, which the creator picked. A payer learns when their payee claimed. Nobody else does.

Finding these notes takes the address's raw secret key: the note is encrypted to the address's Ed25519 key mapped to X25519, and browser wallet adapters do not expose that operation. Keypair holders — CLIs, services, bots — are fine. For adapter-held keys, and for PDAs, which have no key at all, the note's opening (amount, blinding) has to reach the owner some other way: a channel of the sender's choosing, or an encryption key the owner published. Without the opening the note is unspendable, and the pool cannot tell a lost opening from a note nobody has claimed yet.

Returnable notes publish a date, and trust a clock

A reclaim puts the note's not_before on the public record. Wallets round it to a UTC midnight so that it is shared by every note sent that day; a wallet that does not, or a pool with little traffic, lets an observer guess which transfer a reclaim undoes. A redeem publishes nothing a claim would not.

The deadline is judged by the validator clock (Clock.unix_timestamp), which is stake-weighted and can drift by seconds to minutes. It is a deadline for days, not for races. And on the deadline there is a race by design: from not_before on, claimant and sender can both spend, and the first transaction to land wins.

The claimant's copy of a returnable note exists only as a claim code. The ciphertext slot is used by the sender's memo, so even a keypair holder cannot discover such a note by scanning.

An attestation is a moment, a note, and an address

It shows one unspent note at one moment. Nothing stops its owner from spending the note a block later, so a verifier who needs funds to stay needs an escrow, not an attestation. It speaks for a single note: a balance spread over several has to be merged first. It does not count funds either — the tag is a pseudonym of a key, and value moved to a fresh key attests again under a fresh tag, so it cannot by itself weight a vote.

Without a relayer the fee payer is public, which ties "holds at least X" to an address. And within one scope, tags are linkable by design: a verifier should ask for fresh wording when it wants a fresh proof.

A receipt proves a payment, not a payer

Anyone who knows a note's opening can write its receipt: the payer, the payee, or whoever either of them told. It shows that such a note is in the pool — not who funded it, and not that it is unspent. For a returnable note it does not show that the payment completed: the sender may since have taken it back. The sender's ability to write receipts lives in the browser's storage; the chain does not remember who a note was sent to.

Assets and the stake pool

Notes hold SOL or an SPL token, and transfers hide which. The edges do not: a shield or an unshield shows its asset along with its amount.

A yield-bearing note adds a dependency. Its value is a claim on Jito's stake pool — that program, its validator set, and the market for its token. The pool holds the tokens themselves, in a vault only proofs can spend; it does not hold a promise of them.

An exit to SOL is an unshield like any other — recipient and amount are public — and it shows both figures: the tokens redeemed and the lamports paid. It draws on the stake pool's reserve, which is deep but finite; a withdrawal larger than the reserve has to leave as tokens instead.

A stake shield names an exact token amount, and the rate it was computed from moves once an epoch. A shield caught by that boundary is refused and costs only its network fee.

Timing and metadata

Solana records when each pool transaction landed and who signed it. That is a real side channel:

  • The fee payer is public on every pool transaction. Always. A user who submits their own transfers has attached their public wallet to each one — which shows that the wallet used the pool at that moment. In-circuit fees plus a relayer are the answer: the fee is paid from inside the pool to whoever submits.
  • Slot timing correlates. A shield and an unshield in adjacent slots are linked by observation regardless of proofs.
  • Note counts are public: how many commitments each operation added, how many nullifiers it spent. Aggregate, but not nothing.
  • Whoever serves a wallet's RPC sees which transactions it fetches and when. Syncing the whole pool history, rather than querying for specific transactions, is what keeps that uninformative.

The trusted setup

Groth16 needs a one-time setup per circuit. It is sound as long as at least one contributor destroyed their randomness — so the ceremony is built for many independent contributors and closes with a public beacon, and anyone can check its transcript against the keys compiled into the deployed program. 03-ceremony.md has the procedure, and the development setup used for local builds.

Trust in the upgrade authority

A Solana program is replaceable by its upgrade authority. For this program that means the verifying keys and the release rules are replaceable, and so the vault is takeable, by whoever holds that key. There is no on-chain timelock or veto in this repository.

Put the authority behind a multisig, or burn it. Burning trades this risk for another: an immutable program's bugs are immutable too.

No pause, no admin

initialize configures nothing and the program has no privileged instruction. Nobody can freeze the pool, censor a note, or rescue funds — not the deployer, not in an emergency. If a circuit bug is found in a live pool the only lever is the upgrade authority, if it still exists.

Rent is a cost of spending

Every spent note leaves an 8-byte account behind forever, and the spender pays its rent (~0.00095 SOL). Dust notes below that are not worth spending. The records cannot be closed: closing one would make its note spendable again.

Keys

Notes are spent by their owner's key and by nothing else; that is what makes the pool trustless. In the reference wallet the spending key is derived from a wallet signature, so restoring the Solana wallet restores the shielded balance — there is no second secret to lose. A wallet that generates its own spending key should back it up at creation.

Value sent to an address that never collects is the one case the protocol softens: a returnable note goes back to its sender.

Scanning cost

Finding your notes means trial-decrypting every note event since your last scan. Linear in total pool activity, not in your own. A busy pool means a slow first sync on a new device — and no server-side index can help without undoing the privacy the design exists for.

Proof generation cost

Browser proving takes seconds and a few hundred MB of memory. Low-end mobile devices struggle. The proving key and wasm are multi-megabyte downloads, so first use is heavy even on a fast connection.

Concurrency

Two transfers spending the same note: the first to land wins, the second fails creating its nullifier record — and still pays the network fee. That is correct, and it is also a wallet problem, since a user clicking twice sees a confusing failure. Track pending nullifiers client-side.