Pools take one exact amount and nothing else. That sameness is the point: if everyone sends a different figure, the figures alone give the matches away. Take the busiest pool you can afford, because the number of deposits sitting in it is your privacy. Your note is minted here, in this tab, before a single lamport moves — it is the only thing that can ever pull the deposit back out, and it is never uploaded.
Your browser rolls two random numbers and hashes them into one commitment. The commitment is the only half the chain ever sees. The two numbers stay on this page — they are not sent to our server, not to the relayer, not to anyone, and there is no copy of them anywhere but here.
-
-
-
-
The commitment is a one-way hash. Nobody can run it backwards to recover the two numbers, and nobody can tell two commitments apart - which is exactly why your deposit blends into every other one.
This is the only thing that can withdraw your money. It is not stored anywhere. If you lose it, the funds are gone permanently - there is no reset, no support, no recovery. Nobody can help you, including us.
Copy it into a password manager, or write it down. This page deliberately offers no file download: a tool that trains you to save files it hands you is a tool that teaches a habit worth exploiting.
Your wallet sends the amount plus the commitment. The contract appends the commitment to the Merkle tree and pools your funds with everyone else's.
Wait as long as you can before doing this. Withdrawing minutes after depositing links the two by timing alone, no matter how good the cryptography is.
Your withdrawal path is built from the pool's leaves. They are public chain data, so any indexer for this program can serve them - and whatever is served is checked against the on-chain root in your browser before it is used. If this front-end is down or you do not trust it, point somewhere else.
Use an address with no history. Sending to a wallet that already holds your funds undoes everything you just paid for.
Your browser builds a zero-knowledge proof of one statement: “I know the secrets behind some leaf in this tree.” Not which leaf. That word is what breaks the link. This takes a few seconds and runs entirely on your machine.
------The pool hides which deposit is yours. It cannot hide when: a withdrawal moments after a deposit is matched to it by the clock alone. Waiting is what breaks that, and it costs nothing but time.
A withdrawal is indistinguishable from any other deposit in the same pool, so the anonymity set is simply the number of deposits. A pool with a handful of deposits provides essentially no privacy - timing alone deanonymises it.
Money does not drop straight into the pool here. It lands first in a holding wallet this service controls, where the contract cannot touch it, and waits there for a decision.
The reason is blunt: a deposit is final. The contract keeps no record of who sent what and has no instruction for giving anything back — and writing one would be a cure worse than the disease, because code that can return your deposit is code that can send anyone's deposit anywhere. Holding first is the last moment at which any decision is still possible.
An approval breaks the amount into whole denominations and deposits them: 1.5 SOL goes in as one 1 SOL note plus four 0.1 SOL notes, and any remainder too small for the smallest pool stays behind, because a pool takes exact multiples or nothing at all. A refusal sends the money back where it came from — or to a different address you name, if it arrived from an exchange withdrawal that cannot receive anything.
This holding step is a property of how TornadoSol is operated, not of the contract. The program neither knows nor cares that it happened; everything below this point is the pool itself.
Two random field elements — a nullifier and a secret — are drawn in your browser and hashed together: commitment = Poseidon(nullifier, secret). You hand the contract a fixed amount of SOL and that one hash. It writes the hash in as a leaf of a Merkle tree twenty levels deep — room for roughly a million deposits — and drops your SOL into a shared vault.
Those two numbers are your note. Nothing transmits them, and nothing can recover them for you.
Every deposit in a pool looks the same: one amount, and a hash that says nothing about who made it. Each new deposit that lands after yours is one more figure in the crowd you are standing in. The privacy here comes from the crowd; the cryptography only guarantees the crowd is genuine.
Withdrawing means producing a Groth16 zero-knowledge proof of one sentence: I know the nullifier and secret behind some leaf of this tree. Your secrets and the Merkle path to your leaf go in as private inputs. Exactly three values come out in public — the tree root, a nullifierHash, and an extDataHash.
Everything hangs on the word some. The proof establishes that a valid leaf exists and that you hold its secrets, while saying nothing about which leaf it is — so there is no thread running from the withdrawal back to your deposit.
On chain the contract runs the pairing check itself — about 110,000 compute units through Solana's BN254 syscall — confirms the root you proved against is one of the last hundred it has seen, and then opens a new account keyed to your nullifierHash.
That account is the double-spend guard. Present the same note twice and the second attempt cannot create an account that already exists. The nullifier hash is derived from the nullifier on its own and never from the commitment, so it marks a note spent without revealing which leaf was spent.
Wallet A put 1 SOL in.
Someone who knew a valid secret took 1 SOL out.
Nothing is shared between those two statements — no address, no amount trail, no timestamp that has to match. What is left is arithmetic: one of 400 is strong, one of 2 is a name tag. No cryptography rescues a thin pool.
"How anonymous am I" has two honest answers, and quoting only one of them flatters the tool at your expense:
You are one of every deposit in the pool. Deposits the operator placed themselves count in full here, because from outside they are indistinguishable from yours.
You are one of the deposits whose notes they do not already hold. They can recognise their own withdrawals and reach everyone else's by elimination.
The pool picker prints both figures instead of the kinder one. On a young pool, where most of the deposits belong to the operator, assume the second.
A proof that said nothing but "I know a leaf" could be lifted off the wire and pointed at a thief's wallet. So the recipient, the relayer and the fee are hashed into extDataHash and welded into the proof. Alter any one of them and the proof stops verifying. That is why a complete stranger can submit your withdrawal for you and still be unable to redirect it or pad the fee.
A fresh wallet holds nothing, so it cannot pay its own transaction fee — and topping it up from your old wallet would rebuild the exact link you just paid to cut. The relayer broadcasts instead and takes its fee out of the withdrawal itself. You sign nothing, and the destination ends up with a single transaction in its history and no visible funding source.
These are measured rather than guessed — scripts/measure-costs.js reads the lamport deltas of a real deposit and a real withdrawal:
| step | lamports | SOL |
|---|---|---|
| deposit transaction fee | 4,992 | 0.000005 |
| nullifier record (permanent) | 1,002,240 | 0.001002 |
| withdrawal transaction fee | 5,000 | 0.000005 |
| relayer margin at its floor | 15,000 | 0.000015 |
| total per note | 1,027,232 | 0.001027 |
Almost all of that — 97% — is the nullifier record, and nobody chooses to charge it. The entry that marks a note spent has to outlive the note permanently, or the note becomes spendable a second time. It is rent paid to the chain, not revenue for an operator.
Because the cost is flat, the denomination decides whether it feels cheap: the same ~0.001 SOL covers a 0.1 SOL mix and a 10 SOL one. That is 1% of the first and 0.01% of the second.
The contract does not even keep the leaves — only the root and the rightmost node at each level. That is why an indexer replays deposit events to rebuild the tree, so your browser has a Merkle path to prove against.
Your note was never held here. It is two random numbers that have only ever existed in your browser. What this site supplies is the leaf set your browser needs to build a path, and that is public chain data — any indexer for this program can serve it, and the Leaf source control on the withdraw tab lets you aim at a different one.
Whatever answers gets checked against the on-chain root inside your browser before any proof is built, so a wrong or hostile leaf set is thrown out rather than believed. That is what makes pointing at a stranger's indexer safe: you are not trusting it, you are checking it.
The honest limit: TornadoSol ships no fallback list, so today there is one indexer. The mechanism exists; standing a second one up elsewhere is a deployment decision, not a code change.
No equations. Just the shape of the thing.
At this particular cloakroom you do not march straight to the rail. You pass your coat over a desk first, and it waits there while someone decides whether to take it in.
Accepted, it goes on the rail and your ticket is yours. Declined, the coat comes back — and if you turned up in a taxi that cannot take it home again, they will hand it to a different address you name instead.
The desk exists for one reason: once a coat is on the rail, nothing but a ticket gets it off again. Nobody back there knows whose coat is whose — that is the entire design — so there is no mechanism for handing one back later. The desk is the last place a decision can still be made.
Hand in a coat, take a numbered ticket. Come back later, show the ticket, collect a coat. The attendant never needs to remember your face — the ticket does the remembering.
Now suppose every coat handed in is the same coat. You come back and collect a coat rather than your coat — and since they are identical, that costs you nothing. What it costs anyone watching the door is everything: there is no longer a way to say which coat went to whom.
That is why the amounts are fixed. A pool where one person puts in 7.3 SOL and everyone else puts in 1 SOL contains exactly one person worth watching.
Here is where it stops being ordinary. You never hand the ticket over, because a ticket could be walked back to the moment it was issued. What you hand over is a statement:
“I am holding a valid ticket for this coat check.”
You demonstrate that the sentence is true without producing the ticket and without indicating which ticket it is. The attendant is satisfied, hands over a coat, and has learned nothing whatsoever about which one was yours. That is a zero-knowledge proof, and there is no sleight of hand in it — it is arithmetic.
If the ticket never appears, what stops you collecting ten coats? On collection you give up one unforgeable scrap — a serial number derived from your ticket — and the attendant writes it in a ledger. The scrap leads nowhere near the coat you handed in, but if it ever shows up a second time, the second claim is refused.
That ledger is permanent, and paying for one line of it to exist forever is where almost the whole of your fee goes.
Collecting costs a small handling charge. Pay it from your own pocket and the attendant sees your wallet again — which was the one thing the whole exercise was meant to prevent.
So a courier collects on your behalf and takes the charge out of the coat itself. The instructions you signed name that courier and that exact charge, so they cannot help themselves to more or drop the coat somewhere else.
If you are the only person using the cloakroom, everyone knows whose coat it is, and the finest proof ever written will not help you. If a thousand people are using it, you are one of a thousand.
The crowd is the privacy. The mathematics only guarantees that the crowd is real. And if you walk in and straight back out again, the timing hands you over regardless of how many people are inside.
Put a few names in, the way you might label a coat. Then mix them and look at what is left for anyone watching from outside.
Round numbers: about 0.001 SOL, near enough ten cents, to go in and come out again. Nearly all of it buys one permanent line in the attendant's ledger — the serial number that stops a second claim. It has to sit there forever, so it is bought once, forever.
The charge does not scale. Ten cents against a small amount is noticeable; against a large one it disappears. That is one good reason to use the largest pool you are comfortable with.
Nearly every coat on this rail was hung there by the person who runs the place. They know which ones are theirs — so when a coat they do not recognise walks out, they know whose it was.
To a stranger at the door you are one of many. To the operator you may be one of one. Both numbers are printed on the pool picker, because quoting only the flattering one would be a lie.
The cloakroom needs a lock, and it has to be forged before the first coat goes on the rail. Rather than one person building it — who could keep a quiet copy of the master-key shape — the lock is passed between several unrelated strangers. The first casts a rough shape and scores it with their own secret pattern, then passes it on. The next scores it again. To cut a master key afterwards you would need every one of their secret patterns.
That is what puts the lock beyond everyone, including the people who made it. No single contributor can open it on their own. Not a stranger, not a contributor, not whoever runs TornadoSol.
The last score is made with something nobody could have arranged beforehand: the fingerprint of a Bitcoin block that had not been mined yet. Unpredictable means unriggable — including by the last person to take a turn.
And every step is yours to check. Each contributor publishes a fingerprint of the lock as it stood after their turn, somewhere this service has no power to edit later. Compare it with the chain on the Proof & trust tab.
Your ticket was never kept on the premises. It is two numbers that have only ever existed on your own machine — which is exactly why losing them is fatal and why nobody else is in a position to lose them for you.
What the building provides is the list of which coats are on the rail, and that list is public. Anyone can keep a copy, and the withdraw tab lets you point at someone else's. Your browser measures any list it is handed against the rail itself before trusting a word of it, so a forged list is caught rather than swallowed.
A mixer asks you to accept a pile of claims you have no way of seeing for yourself. This page separates the ones you can check from the ones you cannot.
Check the deployed bytes yourself: node scripts/verify-deployment.js pulls the on-chain program down, compares it byte for byte against a local build, and confirms the ceremony key is the one baked into it.
This is the single part of a mixer that no amount of reading the code will settle, which is why it deserves the space. It is also the part that has to be right once, before any money moves — there is no fixing it later.
The problem. A zero-knowledge proof system runs on public parameters — the proving key — and producing them requires secret randomness. That randomness gets called toxic waste, because the moment the key exists it has to be destroyed. Anyone who kept a copy can manufacture a valid proof for a deposit that never happened. The pool pays out, the proof verifies, and nothing on the blockchain looks the least bit wrong. Auditing the contract will never find it.
The fix. Instead of one person producing the key, several take turns. Each receives the previous file, folds in their own fresh randomness, and hands it on. Forging a proof afterwards would require every contributor's secret. So the guarantee reads:
One honest contributor is enough. If a single person destroyed their randomness, the setup holds — whatever the others did.
Which is why both the number of contributors and their independence matter. A single contributor who is also the operator reduces the whole thing to "trust the operator" — the assumption a mixer exists to delete.
The beacon. The final step folds in a value nobody could have known ahead of time — here, the hash of a Bitcoin block from the future. To rig a contribution you would have had to predict that block. It shuts the door on the last participant picking randomness that cancels everyone else out.
Why publishing matters. Every contributor's file has a SHA-256 fingerprint. Having contributed, they post it somewhere this service cannot go back and edit, and anyone can then confirm it turns up in the chain below. Without that step, three contributions from three people are indistinguishable from one operator contributing three times under three names — the file history looks identical either way. An unpublished contribution is the operator's word, and the operator's word is the thing this is supposed to replace.
Read it downwards. Each row is one contribution, and the hash is the state of the proving key once that person's randomness went in. Reproduce any row with snarkjs zkey verify against the file the API hands you.
Every row below is an automated test, and the build fails if the attack ever gets through. See scripts/test-attacks.js, test-spl.js, test-relayer.js.
| attack | stopped by |
|---|---|
| Spend the same note twice | nullifier account already exists |
Resubmit with nullifierHash + r - same field element, different account | canonicality check |
| Relayer swaps in its own recipient | extDataHash mismatch |
| Use a 5 SOL proof against the 10 SOL pool | pool bound into extDataHash |
| Charge a fee larger than the deposit | FeeTooHigh |
| Flip a single byte of the proof | pairing check fails |
| Prove against a made-up root | UnknownRoot |
| Reclaim a spent nullifier to replay it | no close instruction exists |
| Withdraw a mint-B note from the mint-A pool | pool address bound into the proof |
| Feed the relayer junk to burn its funds | simulated before broadcast |
Proving happens locally, in WebAssembly. The server is handed a proof and a destination and nothing else — never a nullifier, never a secret. Open your browser's network tab and watch it: not one request carries them.
The recipient and the exact fee are welded into the proof. A relayer that changes either one produces a proof that fails to verify. It can decline to submit; it cannot point the money somewhere else.
The indexer rebuilds the tree from chain events and holds its computed root up against the root the contract reports. If the two disagree it returns an error rather than a leaf set that would quietly produce an unusable proof.