0/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.
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