TornadoCash is a well-known project for anonymous transactions. When a hacking incident occurs, hackers often transfer their funds to TornadoCash to ensure that their money can be moved without being traced. However, in recent years, there has been an increase in the frequency and scale of hacking attacks, which has caught the attention of regulatory authorities in the United States. As a result, a series of regulatory measures have been implemented, such as website shutdowns, arrests of developers, and removal of the source code from GitHub.
You may be curious about how TornadoCash achieves anonymity. Recently, I conducted some research to understand its operational mechanism. Here, I will do my best to explain in a straightforward manner how TornadoCash maintains anonymity and why they need to do so. I hope this knowledge can help you better understand TornadoCash.
In each transaction, there are typically two parties involved: a sender and a receiver. TornadoCash aims to conceal the identity of the sender in order to prevent third parties from identifying who initiated the transaction through transaction records. This way, even if the transaction is recorded on the blockchain, the identity of the sender cannot be traced.
TornadoCash utilizes a straightforward and widely adopted method called a mixer to achieve anonymous transactions. A mixer is a tool that combines funds from different senders to achieve the goal of anonymity.
Simply put, a mixer is like a box where senders deposit money, and recipients withdraw money from the box. Because the money is mixed together, it is impossible to determine which specific sender's money the recipient receives. This method makes the transactions anonymous and prevents the identification of senders from transaction records.
The mixer model needs to address an important question: Who can claim the money? Without a proper mechanism to verify the identity of the recipient, anyone could claim the funds from the box, leading to security risks.
To solve this problem, TornadoCash employs a straightforward solution, which is the use of receipts. A receipt is a document that proves the identity of the recipient, similar to a luggage claim ticket given to hotel guests. Unlike the luggage storage model in real life, TornadoCash's receipts are not generated by a smart contract but provided by the user upon depositing funds. This is because the logic of the smart contract is stored on the blockchain, visible to anyone. Malicious individuals could replicate the same receipt and attempt to claim funds that do not belong to them.
The receipt in TornadoCash is provided by the user, and it is generated by hashing the secret
and nullifier
together. The secret
and nullifier
are both chosen and generated by the user. In the TornadoCash Smart Contract, this receipt is referred to as a commitment
. However, for the purpose of understanding, we will continue using the term "receipt" here. The nullifier
has a special purpose, which we will explain later.
Implementing the function of receipts on the blockchain is more challenging than imagined. On the blockchain, all information is public, including the receipts generated by Alice. If Bob uses Alice's receipt to claim funds from the smart contract, it becomes easy for others to know that Bob received the funds originally deposited by Alice, thus failing to achieve the goal of hiding the sender's identity.
Therefore, the existing receipt mechanism needs improvement. Before discussing the improvements, let's first understand the purposes of receipts in the entire system. In fact, receipts serve two main purposes:
Proof of previous sender's deposit: The receipt serves as proof that the sender has deposited funds previously into the system. It demonstrates that the sender has the authority to claim the funds at a later stage.
Ensuring each recipient can claim funds only once: The receipt ensures that each recipient can claim the funds only once, preventing double spending or multiple claims.
To illustrate this using a real-life example of luggage storage, when you have a receipt, it means you have previously deposited your luggage, and you can only retrieve the luggage once. In other words, if we can find a new way to prove that someone has deposited funds into the system and ensure that each recipient can claim the funds only once throughout the system, we can achieve the same effect as receipts.
Before explaining how TornadoCash achieves these two objectives, let's review why we need to do so. If Bob directly uses Alice's receipt to withdraw money, others can learn from the blockchain that the transaction was initiated by Alice, thus failing to achieve the goal of hiding the sender.
Therefore, we need to design a method that allows Bob to withdraw money without directly using Alice's receipt. Instead, he can use some form of proof to claim the funds. This ensures that someone has deposited funds before the withdrawal, and Bob's proof can only be used once to receive the money. Furthermore, this proof should not reveal any information related to Alice's receipt. In this way, even if Bob withdraws the money, we cannot determine who sent it to him, thus achieving the goal of hiding the sender. This is the design concept behind TornadoCash.
TornadoCash employs two techniques:
Merkle Tree
Zero-Knowledge Proof
When users want to deposit money into the Smart Contract, they need to provide a receipt, which is the hash value of the (secret, nullifier)
pair mentioned earlier. This receipt, known as a commitment
, is considered as a leaf in the Merkle Tree by the TornadoCash Smart Contract and added to the Merkle Tree (as shown in the diagram below, assuming the commitment
is placed at leaf node r6
in the Merkle Tree).
The Merkle Tree is a data structure that allows for the calculation of parent hash
values by hashing the leaf data at the bottom layer pairwise. In the provided diagram, for example, the hash value of r5
and r6
is computed to obtain the parent hash
value at the next layer, denoted as H(r5, r6)
. This process continues as the parent hash values are hashed pairwise to generate the hash value at the next higher layer. For instance, H(r5, r6)
and H(r7, r8)
are hashed to obtain H(H(r5, r6)
, H(r7, r8))
. This process repeats until the Merkle Tree's root is obtained, which is referred to as the Merkle Root
.
The Merkle Tree data structure possesses a unique characteristic wherein to prove that a particular data is one of the leaves in the Merkle Tree, it is sufficient to provide the series of parent hash values traversed from the leaf to the root. In the given diagram, for instance, to prove that r6
is one of the leaves in this Merkle Tree, it is only necessary to provide the data r5
, r6
, H(r7, r8)
, and H(H(r1, r2), H(r3, r4))
. These pieces of data are the intermediate parent hash values that establish the inclusion proof for the leaf data in the Merkle Tree.
Steps:
Calculate the hash value of r5
and r6
to obtain H(r5, r6)
.
Calculate the hash value of H(r5, r6)
and H(r7, r8)
to obtain H(H(r5, r6)
, H(r7, r8))
.
Calculate the hash value of H(H(r1, r2), H(r3, r4))
and H(H(r5, r6), H(r7, r8))
to obtain the Root.
If the calculated Root is the same as the Merkle Root, it proves that r6
is one of the leaves in the Merkle Tree.
The basic concept of Zero-Knowledge Proofs (ZKPs) is to provide a method where users can prove knowledge of certain information without revealing the actual content of that information. For example (the definition may not be precise, but the idea is the same), if I claim to possess the private key for a specific address, I can prove this by sending any amount of cryptocurrency to another address without actually disclosing the private key itself.
In TornadoCash, ZKP technology is utilized to hide the deposit source. Suppose we want to prove that we made a deposit on TornadoCash (assuming it is at position r6
). We can provide the following proof:
Knowledge: r6, r5, H(r7, r8), and H(H(r1, r2), H(r3, r4))
Proof: Generate a proof using ZKP technology.
Verify: In the verification contract of TornadoCash, calculate the Root based on the Proof data and check if it matches the Merkle Root on the blockchain. If they match, it indicates that r6 is one of the leaves in the TornadoCash Merkle Tree, implying that someone previously deposited funds in TornadoCash.
Using ZKP technology, we can prove that we possess certain specific knowledge without revealing the actual information. In this example, we have demonstrated knowledge of r6
, r5
, H(r7, r8)
, and H(H(r1, r2), H(r3, r4))
without disclosing the actual content of these pieces of information. Through such proof, we can hide the source of the deposit, thereby achieving anonymous transactions.
To prevent the scenario where assets are withdrawn multiple times or more within a single transaction, TornadoCash introduces the concept of a nullifier
. This is a common attack method that could potentially cause losses to TornadoCash. To mitigate this risk, TornadoCash incorporates the use of nullifiers.
During the deposit process, the nullifier
is generated by hashing the secret
along with other parameters to create a commitment
. When a user wants to make a withdrawal, they must provide the hash value of the nullifier
(hash(nullifier)
). If this value already exists in the TornadoCash contract, it indicates that the funds associated with this deposit have already been withdrawn, and the withdrawal attempt will fail. However, if the hash(nullifier)
has not been recorded, the withdrawal will be considered valid. TornadoCash then records the hash(nullifier)
in the contract to ensure that the same deposit cannot be withdrawn again.
In summary, TornadoCash uses nullifiers
to prevent double withdrawal attacks, ensuring the security and reliability of transactions. This mechanism safeguards against the unauthorized duplication of withdrawals and enhances the overall integrity of the system.
Let's summarize the components included in the final TornadoCash proof:
Nullifier: The nullifier
used during deposit generation to prevent double withdrawal.
Secret: The secret
used during deposit generation.
Path Elements: The hash values traversed from the deposit commitment
leaf to the Merkle Tree root.
Path Indices: Indicates whether each element in the pathElements
is a left or right child.
These components will be used to construct a Zero-Knowledge Proof (ZKP) that allows Bob to prove his entitlement to claim the deposit without revealing his nullifier
and secret
. When verifying this ZKP, the following public data is required:
Root: The Merkle Tree root of TornadoCash.
NullifierHash: The hash value of Bob's nullifier
, used to check for the risk of duplicate withdrawals.
Based on the provided example, when TornadoCash receives the proof
and nullifierHash
(assuming Alice's deposit is at position r6
), the following actions are executed:
Using the secret
and nullifier
from the proof, calculate r6
.
Calculate the root using r6
, r5
, H(r7, r8)
, and H(H(r1, r2), H(r3, r4))
.
Compare the calculated root
with the Merkle Root
stored in TornadoCash. If they match, it indicates a previous deposit. TornadoCash keeps a record of the latest 30 Merkle Roots
to reduce the chance of transaction failures since the Merkle Root changes with each deposit. The comparison is made against the 30 most recent Merkle Roots.
Set the map entry with the key as hash(nullifier)
to true to prevent double withdrawal.
Transfer 1 ether to Bob.
Steps 1 to 3, which involve validating the proof, are executed in the Zero-Knowledge (ZK) verification process, ensuring that other parties cannot obtain the secret
, nullifier
, or related information.
By following these steps, TornadoCash ensures the security and integrity of the withdrawal process, preventing unauthorized withdrawals and maintaining privacy for participants.
TornadoCash is an anonymous transfer project that uses a mixer to hide the identity of transaction senders.
TornadoCash utilizes receipts (commitments
) to control access rights. Receipts are generated from the secret
and nullifierHash
, and each receipt can only be used once.
It uses a Merkle Tree to record deposit information, with receipts serving as leaf nodes. The Merkle Root is calculated, and users only need to provide the intermediate data from the leaf to the root to prove that their data is one of the leaves in the Merkle Tree.
Zero-Knowledge Proofs are employed to hide the deposit source. Nullifiers
are used to prevent Double Withdrawal attacks. TornadoCash proofs require verification by comparing them with public data, such as the Merkle Root
and hash(nullifier)
.
Lastly, I would like to express my gratitude to Martine and Anton for reviewing the article and providing valuable feedback! Thank you very much for their assistance and contributions!