5/5
This video provides a detailed explanation of Tornado Cash, a privacy tool designed to break the on-chain link between senders and recipients of crypto transfers. It emphasizes that the content is for educational purposes to understand cryptographic techniques like Zero-Knowledge Proofs (ZKPs), Merkle trees, and commitments, and not a guide to building similar tools. **The Problem Tornado Cash Solves:** Blockchains are inherently transparent. If you receive funds (e.g., a 10 ETH hackathon prize), anyone can see this transaction on a block explorer, link your address to your identity (if known from other contexts), and track your financial activity. This compromises privacy and can make users vulnerable to targeting. Tornado Cash aims to restore this privacy by "mixing" funds. **How Tornado Cash Works (High-Level):** Tornado Cash operates in three main phases: 1. **Deposit Phase:** * A user deposits a **fixed amount** (denomination, e.g., 0.1, 1, 10 ETH) of cryptocurrency into a Tornado Cash smart contract. * When depositing, the user generates a **secret note** containing secret deposit information (a `secret` and a `nullifier`, both random values). This note is stored locally by the user and is crucial for withdrawal. It's a non-custodial design; losing the note means losing access to the funds. * A **commitment** (a hash of the secret and nullifier) is sent to the smart contract along with the funds. 2. **"Mixing" Funds:** * The smart contract pools deposits from many users. Since all deposits within a specific pool are of the same fixed denomination, they become indistinguishable from one another. * The more deposits in the pool (the larger the "anonymity set"), the harder it is to link a specific deposit to a specific withdrawal. 3. **Withdrawal Phase:** * The user, using their secret note, generates a **Zero-Knowledge Proof (ZK-SNARK)** off-chain. This proof cryptographically demonstrates that they made a valid deposit into the pool without revealing *which* specific deposit was theirs. * The user submits this ZK-proof and some public inputs (like the recipient address and Merkle root) to the `withdraw` function of the smart contract. * The funds are then sent to a **new, different address** specified by the user, effectively breaking the on-chain link to the original depositing address. **Key Cryptographic Concepts and Their Application:** * **Fixed Denominations (3:32):** * Tornado Cash uses separate smart contracts for different fixed amounts of ETH (e.g., 0.1 ETH, 1 ETH, 10 ETH) or ERC20 tokens. * **Reason:** If variable amounts were allowed, linking deposits and withdrawals would be trivial based on the unique transaction amounts, defeating the privacy goal. Fixed denominations ensure all transactions within a pool are identical in value, increasing the anonymity set's effectiveness. * **Anonymity Set (4:37):** * Refers to the number of depositors in a given pool. The larger the anonymity set, the greater the privacy. * If only one person deposits and withdraws, there's no privacy. With 1000 depositors, there's a 0.1% chance of linking a specific withdrawal to a deposit based purely on participation. * **Commitments (6:08):** * A cryptographic technique allowing a user to commit to a value (or values) while keeping it hidden, with the ability to reveal it later. * **Properties:** * **Binding:** The committer cannot change the committed value after the fact. * **Hiding:** The commitment itself reveals nothing about the original value. * Tornado Cash uses **Pedersen Commitments**. The formula is `Commitment = value*G + randomness*H`. * In Tornado Cash: `Commitment = SECRET*G + NULLIFIER*H`. * `SECRET` and `NULLIFIER` are two random 31-byte values generated by the user and form part of their private note. * Pedersen commitments offer *information-theoretic hiding* (even with unlimited computation, the original value isn't revealed from the commitment) and *computational binding* (secure as long as certain math problems like the discrete logarithm problem are hard). * The Cyfrin Updraft course (mentioned at 8:20) uses **Poseidon Commitments**, which are newer and more efficient for ZK-SNARKs. * **The Secret Note (8:45):** * Generated during deposit, contains the `secret` and `nullifier`. * Format example: `tornado-[network]-[denomination]-[nullifier]-[secret]`. * Stored locally by the user. It's non-custodial; if lost, funds are irrecoverable. * **Nullifier and Nullifier Hash (14:08, 15:28):** * The `nullifier` is a private random value. Its hash, the `nullifierHash`, is a public input to the withdrawal circuit and smart contract. * **Purpose:** To prevent double-spending. When a note is withdrawn, its `nullifierHash` is recorded on-chain. The `withdraw` function checks if a `nullifierHash` has already been spent. Since only the legitimate owner knows the `nullifier` (and thus can generate the correct `nullifierHash` for the ZK-proof), they can only spend the note once. The raw nullifier is kept private to maintain the link break. * **Merkle Trees (9:43, 10:07):** * Commitments are inserted into an on-chain Merkle tree. * Tornado Cash uses an **Incremental Merkle Tree**, which can be updated efficiently on-chain when new deposits are made. * **Purpose:** To prove that a specific commitment (and thus deposit) exists in the set of all deposits without revealing the commitment itself during withdrawal. The ZK-proof includes a Merkle path (`pathElements` and `pathIndices`) to a known Merkle `root`. * The `isKnownRoot` check (15:45) ensures the proof is against a recent and valid Merkle root (one of the last 30) to handle cases where the tree updates between proof generation and submission. * Hash Function: Tornado Cash uses **MiMC Sponge** for its Merkle tree. The Cyfrin Updraft course uses **Poseidon Hash**. * **Zero-Knowledge Proofs (ZK-SNARKs) (11:40):** * Generated off-chain by the user for withdrawal. * The proof generation process involves: 1. The user provides their secure note (containing `secret`, `nullifier`). 2. Off-chain JavaScript (in the frontend or CLI) uses the events emitted by the `Deposit` function to reconstruct the history of all commitments and build the current Merkle tree. 3. It calculates the Merkle proof (`pathElements`, `pathIndices`) for the user's commitment. 4. These private inputs (`secret`, `nullifier`, `pathElements`, `pathIndices`) and public inputs (`root`, `nullifierHash`, `recipient`, `relayer`, `fee`, `refund`) are fed into the **circuit**. 5. The circuit logic (written in a language like Circom) defines the rules/constraints. 6. If the inputs satisfy the constraints, a **witness** is generated. 7. The witness is used to create the ZK-SNARK proof (a compact byte string). * **On-chain Verification (16:26):** * The `withdraw` function calls `verifier.verifyProof()`. * The `verifier` is a separate smart contract automatically generated from the compiled circuit. It's circuit-specific. * `verifyProof` checks if the submitted ZK-proof is valid for the given public inputs. * **Circuits (12:10, 12:52):** * Essentially code that defines the rules and computations the inputs to a ZK-proof must satisfy. * The withdrawal circuit in Tornado Cash verifies: 1. The commitment derived from the private `secret` and `nullifier` is part of the Merkle tree whose root is the public `root` input (proves valid deposit). 2. The public `nullifierHash` correctly corresponds to the private `nullifier` (links the nullifier to be spent). 3. Dummy calculations involving `recipient`, `relayer`, `fee`, and `refund` are performed to ensure these values are part of the proof and prevent front-running. * **Front-Running Prevention (17:37):** * If the `recipient` address wasn't part of the ZK-proof's public inputs and constraints, an attacker could observe a valid withdrawal transaction in the mempool, copy the proof, and replace the recipient address with their own, effectively stealing the funds. * By including the `recipient` (and other details like `fee` and `relayer`) in the circuit and as public inputs to `verifyProof`, the ZK-proof becomes cryptographically tied to these specific values. Any change to these values by an attacker would make the proof invalid. * **Relayers (18:45):** * Enable "gasless" withdrawals. A user can generate a proof specifying a relayer's address and a fee. * The relayer submits the withdrawal transaction, pays the gas fees, and collects the specified fee from the withdrawn amount. The remaining funds go to the user's intended recipient address. * This is trustless because the ZK-proof ensures the funds (minus the fee) can only go to the recipient address embedded in the proof. This allows users to withdraw to a fresh address that has no ETH to pay for gas. **Code Functions Discussed:** * **`deposit(bytes32 _commitment) external payable nonReentrant` (5:35, 9:08):** * Takes the `_commitment` (Pedersen hash of nullifier + secret) as input. * Requires that the `_commitment` has not been submitted before. * Marks the `_commitment` as submitted in a mapping (`commitments[_commitment] = true;`). * Calls an internal function `_insert(_commitment)` to add the commitment to the on-chain Merkle tree, returning the `insertedIndex`. * Calls `_processDeposit()` which checks if `msg.value` (ETH sent with the call) equals the fixed `denomination` for that contract instance. * Emits a `Deposit(_commitment, insertedIndex, block.timestamp)` event. * **`withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant` (11:40, 15:22):** * Takes the ZK-SNARK `_proof` and various public inputs. * Checks: * Relayer `_fee` is not greater than the `denomination`. * The `_nullifierHash` has not been spent yet (checked against the `nullifierHashes` mapping). * The `_root` is a known, recent Merkle root (using `isKnownRoot(_root)`). * Calls `verifier.verifyProof(_proof, publicInputsArray)` to verify the ZK-SNARK. The `verifier` is a separate contract generated from the circuit. * If verification passes: * Marks the `_nullifierHash` as spent (`nullifierHashes[_nullifierHash] = true;`). * Calls `_processWithdraw(...)` to transfer funds to the `_recipient` and `_relayer`. * Emits a `Withdrawal` event. **Important Notes:** * The video is a simplified overview. The actual implementation involves more complex cryptographic primitives and engineering. * The disclaimer (0:00, 0:23) stresses the educational nature of the video. * The Cyfrin Updraft course is mentioned (8:20, 11:24, 16:14, 19:28) as a resource for learning to build ZK applications, where they use newer techniques like Poseidon commitments/hashes and the Noir language. A link to the course is provided in the description. * An external video explaining incremental Merkle trees is also mentioned (10:26). This summary covers the core concepts, mechanisms, and flow of Tornado Cash as explained in the video, including the cryptographic techniques that enable its privacy features.
This video provides a detailed explanation of Tornado Cash, a privacy tool designed to break the on-chain link between senders and recipients of crypto transfers. It emphasizes that the content is for educational purposes to understand cryptographic techniques like Zero-Knowledge Proofs (ZKPs), Merkle trees, and commitments, and not a guide to building similar tools.
The Problem Tornado Cash Solves:
Blockchains are inherently transparent. If you receive funds (e.g., a 10 ETH hackathon prize), anyone can see this transaction on a block explorer, link your address to your identity (if known from other contexts), and track your financial activity. This compromises privacy and can make users vulnerable to targeting. Tornado Cash aims to restore this privacy by "mixing" funds.
How Tornado Cash Works (High-Level):
Tornado Cash operates in three main phases:
Deposit Phase:
A user deposits a fixed amount (denomination, e.g., 0.1, 1, 10 ETH) of cryptocurrency into a Tornado Cash smart contract.
When depositing, the user generates a secret note containing secret deposit information (a secret
and a nullifier
, both random values). This note is stored locally by the user and is crucial for withdrawal. It's a non-custodial design; losing the note means losing access to the funds.
A commitment (a hash of the secret and nullifier) is sent to the smart contract along with the funds.
"Mixing" Funds:
The smart contract pools deposits from many users. Since all deposits within a specific pool are of the same fixed denomination, they become indistinguishable from one another.
The more deposits in the pool (the larger the "anonymity set"), the harder it is to link a specific deposit to a specific withdrawal.
Withdrawal Phase:
The user, using their secret note, generates a Zero-Knowledge Proof (ZK-SNARK) off-chain. This proof cryptographically demonstrates that they made a valid deposit into the pool without revealing which specific deposit was theirs.
The user submits this ZK-proof and some public inputs (like the recipient address and Merkle root) to the withdraw
function of the smart contract.
The funds are then sent to a new, different address specified by the user, effectively breaking the on-chain link to the original depositing address.
Key Cryptographic Concepts and Their Application:
Fixed Denominations (3:32):
Tornado Cash uses separate smart contracts for different fixed amounts of ETH (e.g., 0.1 ETH, 1 ETH, 10 ETH) or ERC20 tokens.
Reason: If variable amounts were allowed, linking deposits and withdrawals would be trivial based on the unique transaction amounts, defeating the privacy goal. Fixed denominations ensure all transactions within a pool are identical in value, increasing the anonymity set's effectiveness.
Anonymity Set (4:37):
Refers to the number of depositors in a given pool. The larger the anonymity set, the greater the privacy.
If only one person deposits and withdraws, there's no privacy. With 1000 depositors, there's a 0.1% chance of linking a specific withdrawal to a deposit based purely on participation.
Commitments (6:08):
A cryptographic technique allowing a user to commit to a value (or values) while keeping it hidden, with the ability to reveal it later.
Properties:
Binding: The committer cannot change the committed value after the fact.
Hiding: The commitment itself reveals nothing about the original value.
Tornado Cash uses Pedersen Commitments. The formula is Commitment = value*G + randomness*H
.
In Tornado Cash: Commitment = SECRET*G + NULLIFIER*H
.
SECRET
and NULLIFIER
are two random 31-byte values generated by the user and form part of their private note.
Pedersen commitments offer information-theoretic hiding (even with unlimited computation, the original value isn't revealed from the commitment) and computational binding (secure as long as certain math problems like the discrete logarithm problem are hard).
The Cyfrin Updraft course (mentioned at 8:20) uses Poseidon Commitments, which are newer and more efficient for ZK-SNARKs.
The Secret Note (8:45):
Generated during deposit, contains the secret
and nullifier
.
Format example: tornado-[network]-[denomination]-[nullifier]-[secret]
.
Stored locally by the user. It's non-custodial; if lost, funds are irrecoverable.
Nullifier and Nullifier Hash (14:08, 15:28):
The nullifier
is a private random value. Its hash, the nullifierHash
, is a public input to the withdrawal circuit and smart contract.
Purpose: To prevent double-spending. When a note is withdrawn, its nullifierHash
is recorded on-chain. The withdraw
function checks if a nullifierHash
has already been spent. Since only the legitimate owner knows the nullifier
(and thus can generate the correct nullifierHash
for the ZK-proof), they can only spend the note once. The raw nullifier is kept private to maintain the link break.
Merkle Trees (9:43, 10:07):
Commitments are inserted into an on-chain Merkle tree.
Tornado Cash uses an Incremental Merkle Tree, which can be updated efficiently on-chain when new deposits are made.
Purpose: To prove that a specific commitment (and thus deposit) exists in the set of all deposits without revealing the commitment itself during withdrawal. The ZK-proof includes a Merkle path (pathElements
and pathIndices
) to a known Merkle root
.
The isKnownRoot
check (15:45) ensures the proof is against a recent and valid Merkle root (one of the last 30) to handle cases where the tree updates between proof generation and submission.
Hash Function: Tornado Cash uses MiMC Sponge for its Merkle tree. The Cyfrin Updraft course uses Poseidon Hash.
Zero-Knowledge Proofs (ZK-SNARKs) (11:40):
Generated off-chain by the user for withdrawal.
The proof generation process involves:
The user provides their secure note (containing secret
, nullifier
).
Off-chain JavaScript (in the frontend or CLI) uses the events emitted by the Deposit
function to reconstruct the history of all commitments and build the current Merkle tree.
It calculates the Merkle proof (pathElements
, pathIndices
) for the user's commitment.
These private inputs (secret
, nullifier
, pathElements
, pathIndices
) and public inputs (root
, nullifierHash
, recipient
, relayer
, fee
, refund
) are fed into the circuit.
The circuit logic (written in a language like Circom) defines the rules/constraints.
If the inputs satisfy the constraints, a witness is generated.
The witness is used to create the ZK-SNARK proof (a compact byte string).
On-chain Verification (16:26):
The withdraw
function calls verifier.verifyProof()
.
The verifier
is a separate smart contract automatically generated from the compiled circuit. It's circuit-specific.
verifyProof
checks if the submitted ZK-proof is valid for the given public inputs.
Circuits (12:10, 12:52):
Essentially code that defines the rules and computations the inputs to a ZK-proof must satisfy.
The withdrawal circuit in Tornado Cash verifies:
The commitment derived from the private secret
and nullifier
is part of the Merkle tree whose root is the public root
input (proves valid deposit).
The public nullifierHash
correctly corresponds to the private nullifier
(links the nullifier to be spent).
Dummy calculations involving recipient
, relayer
, fee
, and refund
are performed to ensure these values are part of the proof and prevent front-running.
Front-Running Prevention (17:37):
If the recipient
address wasn't part of the ZK-proof's public inputs and constraints, an attacker could observe a valid withdrawal transaction in the mempool, copy the proof, and replace the recipient address with their own, effectively stealing the funds.
By including the recipient
(and other details like fee
and relayer
) in the circuit and as public inputs to verifyProof
, the ZK-proof becomes cryptographically tied to these specific values. Any change to these values by an attacker would make the proof invalid.
Relayers (18:45):
Enable "gasless" withdrawals. A user can generate a proof specifying a relayer's address and a fee.
The relayer submits the withdrawal transaction, pays the gas fees, and collects the specified fee from the withdrawn amount. The remaining funds go to the user's intended recipient address.
This is trustless because the ZK-proof ensures the funds (minus the fee) can only go to the recipient address embedded in the proof. This allows users to withdraw to a fresh address that has no ETH to pay for gas.
Code Functions Discussed:
deposit(bytes32 _commitment) external payable nonReentrant
(5:35, 9:08):
Takes the _commitment
(Pedersen hash of nullifier + secret) as input.
Requires that the _commitment
has not been submitted before.
Marks the _commitment
as submitted in a mapping (commitments[_commitment] = true;
).
Calls an internal function _insert(_commitment)
to add the commitment to the on-chain Merkle tree, returning the insertedIndex
.
Calls _processDeposit()
which checks if msg.value
(ETH sent with the call) equals the fixed denomination
for that contract instance.
Emits a Deposit(_commitment, insertedIndex, block.timestamp)
event.
withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant
(11:40, 15:22):
Takes the ZK-SNARK _proof
and various public inputs.
Checks:
Relayer _fee
is not greater than the denomination
.
The _nullifierHash
has not been spent yet (checked against the nullifierHashes
mapping).
The _root
is a known, recent Merkle root (using isKnownRoot(_root)
).
Calls verifier.verifyProof(_proof, publicInputsArray)
to verify the ZK-SNARK. The verifier
is a separate contract generated from the circuit.
If verification passes:
Marks the _nullifierHash
as spent (nullifierHashes[_nullifierHash] = true;
).
Calls _processWithdraw(...)
to transfer funds to the _recipient
and _relayer
.
Emits a Withdrawal
event.
Important Notes:
The video is a simplified overview. The actual implementation involves more complex cryptographic primitives and engineering.
The disclaimer (0:00, 0:23) stresses the educational nature of the video.
The Cyfrin Updraft course is mentioned (8:20, 11:24, 16:14, 19:28) as a resource for learning to build ZK applications, where they use newer techniques like Poseidon commitments/hashes and the Noir language. A link to the course is provided in the description.
An external video explaining incremental Merkle trees is also mentioned (10:26).
This summary covers the core concepts, mechanisms, and flow of Tornado Cash as explained in the video, including the cryptographic techniques that enable its privacy features.
A cryptographic key to Unveiling Tornado Cash: Enhancing Privacy on Transparent Blockchains - Unlock the workings of Tornado Cash, a protocol for private transactions on public ledgers. Discover its operational flow from deposit to withdrawal, and the ZKPs, Merkle trees, and nullifiers that safeguard anonymity.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025