Tornado cash

Tornado cash is governed by its deposit note from funding to withdrawal

Tornado cash is controlled by a deposit note that a Classic client generates before the deposit transaction. The note encodes a 62-byte private preimage used to prove control of one fixed-denomination commitment. After funding confirms, the source wallet no longer authorizes withdrawal; the unspent note does. Losing every readable copy removes the private data required to close that position.

The lifecycle has four distinct states: generated locally, committed on-chain, verifiably unspent and permanently spent. Each state exposes different evidence, so the correct artifact determines the next action.

Choosing the instance fixes the entire exit

One contract instance fixes one chain, asset and denomination, so that choice defines the note's eventual withdrawal value.

On Ethereum mainnet, the Classic ETH instances accept exactly 0.1, 1, 10 and 100 ETH. DAI instances use 100, 1 000, 10 000 and 100 000 DAI, while separate instances exist for cDAI, USDC, USDT and WBTC. Every instance maintains its own commitments, roots and spent-nullifier set. A note from chain ID 1 therefore belongs to one amount on one contract; it can't migrate into another denomination or token pool after funding. Pool activity also belongs to that exact instance, so nearby denominations don't enlarge its anonymity set.

The choice occurs before any wallet signature. Select the intended network, token and denomination, then confirm that the funding amount matches the instance exactly. ERC-20 tokens also need enough approved allowance for that contract. Note generation comes next, while all choices remain reversible.

Note generation and backup precede funding

One 62-byte preimage must exist before funding, because that value supplies the complete private withdrawal witness.

The Classic note joins two independently generated 31-byte values: a nullifier and a secret. Their concatenation produces 62 bytes, rendered as 124 hexadecimal characters after the 0x marker. The surrounding text records the asset, denomination and numeric chain ID, so Ethereum mainnet appears as chain ID 1. A Pedersen hash over both values creates a 32-byte commitment, while a separate Pedersen hash of the nullifier becomes the spent-status key. Neither hash lets a client reconstruct the original pair.

Before the deposit signature

Tornado CLI supports creating the note and its deposit invoice offline. The invoice exposes the commitment needed for funding without exposing the 62-byte preimage. MetaMask or WalletConnect then signs the deposit from an online wallet. At this point, several backups should already reproduce the entire note exactly, including asset, amount, chain ID and payload.

After the deposit signature

Once Ethereum confirms the deposit, wallet access and note control separate. The wallet proves who funded the commitment, but the withdrawal circuit asks for the note's nullifier, secret and Merkle path. Moving the note between storage locations doesn't alter on-chain state. Deleting the last readable copy removes the only data from which the client generates the private witness. The next verifiable artifact is the Deposit event.

Funding creates one immutable commitment

One deposit call inserts one 32-byte commitment into the chosen instance and assigns it a permanent leaf index.

An ETH instance requires the transaction's native value to equal its denomination. An ERC-20 instance receives zero native value and pulls the fixed token amount with SafeERC20 after allowance exists. This applies to DAI, USDC, USDT, cDAI and WBTC pools. The successful call emits a Deposit event carrying the commitment, a 32-bit leaf index and a timestamp. The contract rejects duplicate commitments, so each generated note must enter only once.

Funding also produces a 32-byte Ethereum transaction hash. Clients render that hash as 64 hexadecimal digits after 0x. It locates the receipt and its logs; it isn't a second withdrawal credential. Record it beside the note as public evidence, then read the commitment's status rather than treating the wallet balance as the pool balance.


How do you tell whether a deposit note is still unspent?

Two checks reveal Tornado cash note status: locate its Deposit event and query the derived nullifier hash.

Unspent commitment

An unspent note has a matching commitment in the selected contract and a false result from isSpent. The pool holds exactly one denomination against that commitment. A client also counts later deposits in the same instance, although that activity metric changes neither the note's amount nor its spend authority.

Spent nullifier

A spent note returns true for its 32-byte nullifier hash. The corresponding Withdrawal event records the recipient, nullifier hash, relayer and fee. The original commitment remains in the Merkle tree, so its presence alone never establishes an available balance. The nullifier mapping provides the conclusive status, and every later status check derives the same key from the note.

No matching deposit

No matching Deposit event means the client is reading the wrong chain or instance, parsing the note incorrectly or using an incomplete event index. The note's asset, denomination and chain ID narrow the search to one contract. Resolve that mapping before generating a proof, because the witness path must come from the same tree.


Routine adjustments stop at the note boundary

Zero top-ups, partial withdrawals or internal transfers exist for a Classic note after its commitment enters the pool. The fixed denomination can't be resized, split or merged on-chain. Routine control instead means preserving the note, monitoring its unspent flag and choosing exit parameters. The recipient, relayer, fee and refund remain selectable until proof generation binds them. A changed destination then requires another proof. The next operational choice is how to retain all 124 hexadecimal characters without changing one.

Backup forms change custody, not withdrawal authority

Seven storage options preserve the same 124-character payload, while each places recovery control inside a different boundary.

KeePassXC and VeraCrypt keep encrypted material in user-controlled local files. Bitwarden and 1Password add synchronized vault workflows, while an encrypted archive remains software-neutral at the format level. A plain text file and paper copy remove account dependencies but leave confidentiality to physical or filesystem controls. None changes the commitment or adds protocol recovery; the readable note remains the bearer of withdrawal authority.

Overview: Backup forms change custody, not withdrawal authority
Backup option Stored form Required for recovery Custody or control
Plain text file Readable text One intact file Reader of any copy
Encrypted archive Encrypted container Archive, software and passphrase Holder of file and passphrase
KeePassXC secure entry Local encrypted database Database and unlock credential Local database holder
Bitwarden secure note Synchronized vault item Vault access and unlock method Vault account holder
1Password secure note Synchronized vault item Vault access and unlock credentials Vault account holder
VeraCrypt volume Encrypted volume Volume and unlock material Volume holder with credentials
Printed paper copy Physical text One complete legible copy Physical holder

Multiple copies should use independent failure boundaries, because duplicating one file onto the same device preserves one point of loss. After confirming that every backup restores the exact string, the next task is rebuilding current Merkle data.

A fresh Merkle path reconnects the note to current state

Twenty binary path positions connect the note's 32-byte commitment to a root recognized by its Classic contract.

Classic instances use a 20-level Merkle tree with 1 048 576 leaf positions and retain 30 recent roots on-chain. The 30-root ring buffer limits accepted roots, not note age. A client scans Deposit events, recreates the MiMC tree and derives 20 sibling nodes plus 20 left-or-right selectors for the commitment's current path.

Proof generation uses a recent reconstructed root from the correct instance. If cached data names an older root outside the ring, the contract rejects that root even when the note is valid. Updating the event set and recomputing the path changes only the witness data; it alters neither the commitment, amount nor unspent status. With the path current, the proof can bind the chosen exit.


Overlapping gold and silver cryptocurrency coins

Proof generation binds the exit parameters

Six public inputs bind every Groth16 withdrawal proof to one root, nullifier hash, recipient, relayer, fee and refund.

Private witness

The private side contains four categories: the 31-byte nullifier, 31-byte secret, 20 Merkle siblings and 20 binary path selectors. Circom evaluates the Pedersen commitment and nullifier hash, then checks the MiMC path against the supplied root. Websnark builds the Groth16 proof without placing the note preimage or exact leaf position among the public inputs. The circuit therefore proves membership and control while keeping the selected commitment private, as described in Using Tornado cash.

Public statement

The six public values define what the contract authorizes. Both recipient and relayer are 20-byte Ethereum addresses, conventionally shown as 40 hexadecimal digits after 0x. The fee can't exceed the instance denomination. For an ETH pool, the refund is exactly zero; ERC-20 pools pair a refund with native ETH supplied by the transaction sender.

Contract verification

The Solidity verifier checks the proof against those six values. The pool also requires an unspent nullifier and a root in its 30-entry history. Editing the recipient, relayer, fee or refund invalidates the existing proof because those fields belong to the proved statement. The note itself remains unchanged throughout this computation. Once the parameters are final, submission comes directly from a wallet or through a relayer.

What changes when a relayer submits the withdrawal?

One Tornado cash relayer replaces the recipient as transaction sender, while the proof still fixes destination and compensation.

The relayer broadcasts the transaction and pays its Ethereum gas, then receives the fee encoded among the six public inputs. The recipient doesn't need ETH beforehand for that submission path. A direct withdrawal instead uses a wallet-funded transaction, with zero relayer fee in the standard flow. Relayer service fees and gas prices move with the provider and network, so the fixed contract rule matters most: the fee must remain at or below the pool denomination.

For ETH instances, both message value and refund are zero during withdrawal. ERC-20 instances accept message value equal to the chosen refund, sending that native ETH toward the recipient when execution succeeds. The control decision stays with the note holder until proof details are shared for submission.


Withdrawal confirmation converts the note into a spent record

One successful transaction permanently marks the nullifier hash and closes the note's only spendable state.

During execution, the contract checks that the fee doesn't exceed the denomination, the nullifier is false, the root is known and the Groth16 proof verifies. It then sets the nullifier mapping to true before processing transfers. The recipient receives the denomination minus the relayer fee, and the relayer receives that fee when it exceeds zero. A Withdrawal event records four items: recipient, nullifier hash, relayer and fee. Ethereum applies these steps atomically, so a reverted transaction leaves the note unspent and transfers nothing.

After confirmation, the Deposit event still exists and the commitment remains a tree leaf. The changed artifact is the public spent-nullifier mapping. Restore any backup, derive the same nullifier hash and the contract will now return true. At that point, archive the note as a closed record rather than an available balance.


The immutable Classic design keeps this lifecycle stable

Four ETH denominations have persisted because each deployed Classic instance fixes its asset and amount during construction.

The core contracts declare a Solidity ^0.7.0 pragma and store commitments, Merkle roots and nullifier hashes on Ethereum rather than in an operator database. Archiving the original interface repositories doesn't rewrite that bytecode or its state. MetaMask, WalletConnect, Tornado CLI and later community interfaces are clients around the same contract rules. Their presentation and data access differ, but no interface recovers a missing 62-byte preimage.

Even so, Tornado cash Nova introduced a different variable-amount model with shielded transfers on Gnosis Chain, so its balance lifecycle shouldn't be applied to Classic notes. For Classic positions, the durable sequence remains precise: choose one instance, generate one note, fund one commitment, reconstruct a current witness, bind six public inputs and confirm one nullifier.

Before you start with Tornado cash

Must the funding wallet remain connected until withdrawal?

No. The funding wallet only signs and broadcasts the deposit transaction; after confirmation, the 62-byte note preimage supplies withdrawal authority. Closing the browser, disconnecting MetaMask or using a different recipient doesn't alter the commitment. The later client derives the commitment and nullifier hash from the note, reconstructs a Merkle path and builds the proof. The funding address's private key can't substitute for a missing note.

When does an ERC-20 allowance matter in this lifecycle?

An ERC-20 allowance matters before the deposit transaction, when the selected Classic contract pulls its exact denomination from the funding address. DAI, USDC, USDT, cDAI and WBTC pools use the token contract's allowance mechanism, while ETH pools receive native value directly. Once the deposit confirms, that allowance doesn't grant withdrawal authority. The note controls the later proof, and any unused allowance remains a separate wallet setting.

What happens if a withdrawal transaction reverts before confirmation?

The note remains unspent when a withdrawal transaction reverts, because Ethereum rolls back every state change in that transaction. The nullifier mapping isn't set, no token transfer completes and no final Withdrawal event remains. A new proof is needed if the recipient, relayer, fee, refund or chosen Merkle root changes. Reusing unchanged proof data only makes sense while all six public inputs still match the next submission.

Will generating a compliance report consume the note?

Generating a compliance report doesn't consume the note by itself. The report tool derives deposit information from the note and supports selective disclosure of the related transaction history, but only a successful contract withdrawal marks the nullifier hash as spent. The note still carries withdrawal authority afterward, so handling the report and its underlying secret requires the same control as any other readable backup. Report generation also doesn't change the recipient.

Who pays Ethereum gas when the destination address has no ETH?

A relayer pays the Ethereum transaction gas when it submits the withdrawal, then receives the fee bound into the proof's public inputs. The recipient receives the fixed denomination minus that fee. A direct withdrawal works differently: the submitting wallet pays gas and the relayer fields use the direct path. For ERC-20 pools, the refund field supports sending native ETH to the recipient; Classic ETH withdrawals require a zero refund.

Are the deposit transaction hash and note interchangeable backups?

No. A 32-byte transaction hash identifies the public deposit transaction, while the note contains the private 62-byte preimage needed for the withdrawal witness. The transaction receipt helps locate the commitment, leaf index and timestamp, but it doesn't reveal the nullifier and secret. A note recreates its commitment and locates the event; a transaction hash alone can't recreate the note. Keep those artifacts labeled separately throughout the position lifecycle.