Introduction
Tornado Cash is an open-source decentralized coin mixer on the Ethereum network, developed by Roman Semenov, Alexey Pertsev, and Roman Storm. It was launched in December 2019, and as of March 2024, 4.00M ETH are deposited with 125K ETH in the pool (Tornado Cash Analysis↗). The feature provided by Tornado Cash is a kind of double-edged sword: it provides strong anonymity so Ethereum co-founder Vitalik Buterin could send anonymous funds to aid Ukraine↗, but it allows coins earned from criminal activity to be laundered. For example, both North Korean government hacking group Lazarus↗ and the Euler finance hacker mixed their coins using Tornado Cash.
In this post, we will talk about the mathematical principles behind Tornado Cash. Let’s deep dive into Tornado Cash!
As of August 2022, the U.S. Department of the Treasury’s Office of Foreign Assets has announced that any transactions within the United States or U.S. persons that involve Tornado Cash are prohibited. This post is for educational purposes only. We are not responsible for any misuse of the information.
Background
Hash Function
A hash function is a mathematical function that takes in input messages of arbitrary size and returns a fixed-size string of characters called a hash value. For example, (remainder of dividing by ) is a hash function for integer input. The purpose of a hash function is to generate a unique digital fingerprint of the input data, which can be used for various purposes, such as data storage, data retrieval, data integrity checking, digital signatures, and password verification.
We may define a special type of hash function named cryptographic hash function. A hash function is called a cryptographic hash function if it satisfies the below three secure properties:
- Pre-image resistance: Given hash value , it is hard to find any input message that .
- Second-preimage resistance: Given input message , it is hard to find message that .
- Collision resistance: It is hard to find any two different input messages that .
There are many different hash functions available, each hashes with its own characteristics, strengths, and weaknesses. Common examples of hash functions include MD5, SHA-1, SHA-2, and SHA-3. (Note: MD5 and SHA-1 are now deprecated and generally should not be used for cryptographic purposes.)
Merkle Tree
A Merkle tree is a tree data structure used in cryptography and computer science to efficiently verify the integrity and authenticity of large sets of data. The tree structure is based on cryptographic hash functions. To construct a Merkle tree, the elements to be stored are first hashed and the resulting hash values are arranged as the leaf nodes of the tree. Then, pairs of adjacent leaf nodes are hashed together to create a new set of hash values, which are used as the parents of the previous leaf nodes. This process is repeated recursively until a single hash value, known as the root hash, is generated. Here is an example of storing elements “Alice”, “Bob”, “Charles”, “Daniel”, “Emma”, “Fiona”, “Grace”, and “Henry” in a Merkle tree.
Example of storing elements “Alice”, “Bob”, “Charles”, “Daniel”, “Emma”, “Fiona”, “Grace”, and “Henry” in a Merkle tree; is a cryptographic hash function.
After storing elements in this Merkle tree, the root is opened. To prove “Alice” is in the Merkle tree, it is enough to provide . Then verifier can recompute by . This additional value required to recompute Merkle root is called Merkle proof (also known as Merkle path).
Example of proving the existence of element “Alice” in a Merkle tree. Green-colored boxes denote the Merkle proof.
Due to the preimage resistance of , Merkle proof does not reveal the other stored elements and the total number of elements in Merkle proof is log scale of the total number of elements stored in Merkle tree.
Commitment Scheme
A commitment scheme is a cryptographic protocol that allows a party to commit to a message or a value, without revealing the value itself, and later reveal it in a verifiable manner. This is useful in scenarios where a party needs to prove that they made a commitment to a particular message at an earlier time without revealing the message until a later time. Check out this scenario:
Alice has the foreknowledge of tomorrow’s Nasdaq closing price . Alice wants to convince her foreknowledge without revealing the value itself. Then Alice can commit price by choosing sufficiently long random string and going public with where is a cryptographic hash function and is a concatenation operation. Due to preimage resistance property, it is impossible to recover from . After tomorrow, is known to everyone. Then Alice opens . Now Alice’s foreknowledge is verified by checking .
Question: Is essential? What if defining instead of ?
Answer: can be revealed by checking until hash value is the same as . The secret acts as a salt that prevents brute-forcing the hash to reveal the secret.
(Non-interactive) Zero-Knowledge Proof
A zero-knowledge proof is a cryptographic protocol that allows a prover to demonstrate to the verifier that a given statement is true without revealing any additional information beyond the truth of the statement itself. In other words, it allows a prover to convince a verifier that they know a secret value without revealing that value or any other information about it. Zero-knowledge proof must satisfy the below three properties:
- Completeness: If the statement is true, then an honest prover should be able to convince the verifier of its truth.
- Soundness: If the statement is false, then no cheating prover should be able to convince the verifier of its truth.
- Zero-knowledgeness: The proof should not reveal any additional information beyond the truth of the statement.
Completeness and soundness are clear but zero-knowledgeness can be confusing for a beginner. Can we formally define “any additional information is not revealed to verifier”? The answer is yes. To prove zero-knowledgeness of a given protocol, we show every message communicated between prover and verifier can simulated by someone who does not know the statement. For me, zero-knowledge proof for a graph coloring problem is really helpful to understand the concept of zero-knowledge proof. It is recommended to check out this article↗.
Zero-knowledge proofs are classified into two types, interactive or non-interactive, depending on the communications between the prover and verifier. If the prover and verifier interact by exchanging messages, similar to an oral test, the protocol is called interactive. On the other hand, if the prover creates a proof that cann be verified without requiring any further interaction between the prover and verifier, the protocol is called non-interactive.
There are several examples of zero-knowledge proof protocols such as zk-SNARK, zk-STARK, Bulletproofs, and Ligero. While we will only cover details of the zk-SNARK, which is used in Tornado Cash later, the most important thing to note is that for any function and given output , it is possible to prove the knowledge of an input satisfies without revealing . Moreover, any interactive zero-knowledge proof can be transformed into non-interactive zero-knowledge proof. Finally, for any function and given output , a prover can prove their knowledge of an input satisfies without revealing by sending only proof to a prover with no interaction.
Protocol Overview
Tornado Cash aims to provide users with complete anonymity while using Ethereum by allowing them to make private transactions. So the main features of Tornado Cash are simply deposits and withdrawals. This can be done in a single transaction with a predetermined amount of Ether. The amount is not specified in the white paper↗, but it is either 0.1 or 1 or 10 or 100 ETH in the current implementations. The reason for fixing the amount is to prevent tracking the transaction based on its value. For example, there are 10 deposits from and 10 withdraws from . If the amount is different for each deposit, linking deposit address and withdrawal address is trivial.
Setup
First, a Pedersen hash function↗ and MiMC hash function↗ are used in Tornado Cash. We denote each function as and , respectively. The contract has a Merkle tree of height , where each non-leaf node is the hash of its two children with . Since the height is , there are leaves. The leaves are initialized as and will be replaced by some value from left to right when a deposit is made. Since each leaf corresponds to a single deposit, the contract supports for only deposits. After deposits, no more deposits are allowed.
When the contract maintains a Merkle tree, it
does not have to store all the leaves in a tree. It is enough to store
only elements for each newly-used leaf, which correspond to the
nodes in the route from the recently inserted leaf to the root. For
further details, please check the
code↗.
The filledSubtrees
is the array representing a Merkle tree as above.
Although not necessary, the contract stores the most recent (as mentioned in the white paper) or (in the implementation) root values for user convenience, not just the most recent one. Finally, the contract also has a list of nullifier hashes to prevent double spending, which we will introduce later.
Deposit
The process for a deposit is as follows:
- A user generates two random numbers (nullifier), (secret) and computes commitment . This two numbers must be memorized. This is a commitment scheme that we introduced earlier in this blog post.
- Send ETH transaction to contract with data . If the tree is not full, the contract accepts the transaction and replaces the leftmost zero leaf to . This changes the elements in the tree, including the root.
Note that are hidden but commitment is opened to everyone, and the order is also preserved. For example, from the beginning, if addresses and make deposits, then it is publicly known that the first leaf belongs to , the second leaf belongs to , and the third leaf belongs to . However, this means almost nothing because, during withdrawal, the leaf index is not revealed.
Withdrawal
The process for withdrawing the -th leaf is as follows:
- Compute the Merkle proof , which proves an existence of in Merkle tree with root . Root must be one of the recent root values.
- Compute the nullifier hash .
- Compute the proof , which proves the knowledge of such that and is a valid Merkle proof for -th leaf .
- Send ETH transaction to contract with . The contract accepts the request if proof is valid and is not in the list of nullifier hashes.
- The contract adds to the list of nullifier hashes.
To understand this withdrawal procedure, we recall non-interactive zero-knowledge proof (NIZK). In NIZK, a prover can prove their knowledge of an input satisfies without revealing by sending only proof to prover with no interaction. Once we define as
and the target output , then proof represents that prover is a legitimate withdrawer without revealing , so the anonymity is guaranteed. We discuss proof more in the Zero-Knowledge Proof in Tornado Cash section.
The withdrawal procedure seems sound. But what about the fee? If a user wants to withdraw a coin to a fresh wallet, the wallet cannot pay a fee because it has no balance. It is also impossible to pay from the original wallet because it allows linking the fresh wallet and original wallet, which violates anonymity. To resolve this issue, users can optionally use a relay. The user chooses coin recipient address and fee and includes them into the proof . Then a user sends the proof to relay and relay transmits it into the contract. A relayer takes a fee as compensation, but they cannot change any withdrawal data, including recipient address.
Zero-Knowledge Proofs in Tornado Cash
Now we have a bird’s-eye view of Tornado Cash. We will take a more in-depth look at the zero-knowledge proofs used in Tornado Cash in this section.
zk-SNARKs
The mathematics behind zk-SNARKs, such as group theory, elliptic curve, pairing, and sigma protocol is quite challenging. While we will provide a brief introduction to zk-SNARK in this section, it may not be sufficient for those encountering the concept for the first time. Fortunately, there are numerous articles and slides available online that introduce zk-SNARKs and their basic concepts. We recommend reading these resources first to gain a better understanding if you are a newcomer.
zk-SNARK is an acronym that stands for Zero-Knowledge Succinct Non-interactive Argument of Knowledge.
- Zero-Knowledge: Verifier learns nothing other than the statement is true.
- Succinct: Proof size and verifier time is sublinear.
- Non-interactive: Need no interaction.
- Argument of Knowledge: Not only for existence of input but also prover’s knowledge of .
Not only zero-knowledge but succinct is also mysterious. For example, if a prover wants to prove their knowledge of input such that to a verifier, aside from zero-knowledgeness, the easiest way is just to send to verifier. Of course, the proof size is and verifier time is exactly the same as computing , so it is not succinct. Now we face the question, Is it even possible to prove something more cheaply than directly calculating it? Before we start introducing details of zk-SNARKs, we will show a simple example that allows a verifier to accept a proof with less computation than by directly calculating it. Here is the example:
There are two by matrices and . The prover wants to prove that . If the verifier directly calculates and compares it with , the time complexity is — or by using a state-of-the-art algorithm↗. However, if the verifier selects random vector size of and checks whether , its time complexity is reduced to . It is possible such that but so the verifier might accept wrong , but the probability of this occurring is dramatically reduced when the verifier repeats this verification for different values of .
Groth16
Let’s move on to the zero-knowledge proof system actually used in Tornado Cash. Tornado Cash uses Groth16↗, which is an improved version of Pinocchio↗. In Groth16, a prover transforms their statement into a QAP (quadratic arithmetic program) form. We will show a transformation step by step using a simple example:
def f(x, y):
return x**2 + 3 * y**2 + 10
assert f(x,y) == 17 # f(2, 1) = 17
Flattening
Prover wants to prove their knowledge of satisfy .
To do this, the prover first converts the given statement into a
sequence of special forms where x = y
and x = y (op) z
, where
y, z
can be variables or numbers, and op
can be + or *. This is
similar to the concept of three address code from compilers. The
transformation of the equation is as follows:
x2 = x * x
y2 = y * y
t1 = 3 * y2
t2 = x2 + t1
out = t2 + 10
R1CS
After flattening, these conditions are transformed into a R1CS
(Rank-1 Constraint System). R1CS is a system of equations, where each
equation is defined by a triplet of vectors
such that
. In our case, we will build a system such that the solution vector
. For example, flattened
equation x2 = x * x
is transformed into
As you can see, each of the elements in vectors represents a coefficient for a variable (or constant) used in the system of equations. And the solution vector represents all of the variables used in the system.
Continuing with our example, y2 = y * y
is transformed into
t1 = 3 * y2
is transformed into
and t2 = x2 + t1, out = t2 + 10
is transformed into
After representing all the flattened equations in this format, the only prover who knows the correct can find solution vector of given constraint system. So the prover’s ultimate goal now is to prove their knowledge of .
QAP
The purpose of QAP is to prove same system but decrease communication cost using polynomials instead of vector dot products. From the vectors in R1CS, we can derive polynomials. For example, polynomial (note: this is NOT , which is a vector in the R1CS) is defined by the points where corresponds to the terms for the elements corresponding to in the solution vector in the coefficient vectors respectively. This polynomial is degree and can be generated by Lagrange interpolation↗. Polynomials are also generated in the same way. And the same goes for .
Let (yes, vector of polynomials!) and the same goes for . It can observed that for . Let and the same for . Then, there exists a polynomial such that if is properly chosen. In this example, , so . This is called satisfying assignment.
From this long journey, we transformed the statement into multiplications of polynomials. If a prover can convince that
- and are derived from same , and
- exists such that where ,
then the verifier accepts the prover’s knowledge. To guarantee succinctness, rather than directly handling polynomials, for some random point is checked.
Homomorphic Hiding & Pairing
The equation can hold for some when . This probability is negligible, but if is known, then an attacker can easily construct satisfying . Therefore should be hidden but allowing the prover to calculate . It seems contradictory, but homomorphic hiding allows this. From the hardness of elliptic curve discrete logarithm problem, we define an encryption function for a generator . Then satisfies the following:
- It is hard to find from .
- then .
- .
- .
Rather than directly giving to the prover, the prover is given . Then, since and are linear combinations of , the prover can calculate . Moreover, the fact that is really derived form can be proven by the knowledge of coefficient assumption test. We omit information of the coefficient assumption test in this article.
Now the prover generates , which will be given to the verifier, and the verifier can generate since is known. However, the verifier cannot check because is not homomorphic in multiplication. We need another mapping called pairings↗. A pairing is a map that satisfies where are elliptic curves with the same equation but different generators having order and is a cyclic group of order . We slightly extend to and , where .
From now on, we can multiply hiding values. Finally, is checked from Since only are required to verify, the proof size is fixed to elements of and element of .
Trusted Setup
In this procedure, the prover needs generated from a random . One possible scenario is that the verifier chooses random , generates and sends these values to the prover. However, this method is very costly for prover and only enables an interactive model. In the non-interactive model, the verifier cannot send something to the prover. Therefore, instead of verifier, a third party generates common reference string (CRS) **** and opens it to everyone. This is called a trusted setup. The requirement of trusted setup is a significant weakness of zk-SNARK.
Originally, CRS contained some circuit-specific values other than , and the exact procedure is little more complex because it requires checking whether and are derived from same using knowledge of coefficient assumption. You may refer to the Groth16 paper↗ to learn more about this.
Powers-of-Tau
In Tornado Cash, the users act as provers while the contract serves as the verifier. In the beta version of Tornado Cash, the Tornado Cash team generated CRS and made it public (see here↗, proving key and verification key are derived from CRS). If all users trust the Tornado Cash team, they can generate proof using CRS provided by the team. However, if the Tornado Cash team become malicious, they can generate forged proof and potentially steal all the coins. To resolve this issue, the Tornado Cash team replaced CRS generated from the crypto community, called power-of-tau. User to first hold their own random . User generates . Note that Then user generates from , without knowing . After user generates the list, the CRS from secret is generated. In the case of Tornado Cash, 1,114 users participated↗ and CRS is successfully replaced↗.
Implementations in Tornado Cash
Now we get back to statements in Tornado Cash. Our goal is to prove and verify the knowledge of such that for a for
This needs to be transformed into a QAP. Tornado Cash uses circuit
compilers Circom↗ and
snarkjs↗. Circuit compiler Circom has
their own language called circom
language↗. After coding
in a circom
language↗,
the code is transformed into the R1CS using circom compiler. After that,
snarkjs
takes care of almost everything, such as generating CRS (via
the trusted setup ceremony in case of Tornado Cash), proof, and a smart
contract for the
verification↗.
Security Concerns
In this section, we will discuss several security concerns for protocol and user levels. It is important to note that addressing these issues does not necessarily mean that Tornado Cash is vulnerable.
Hash Functions
Using different hash functions and seems unnecessary, and the reasons for this choice were not given by the authors. ABDK Consulting, who audited Tornado Cash, also pointed this out (see here, section 5.1)↗. Moreover, the Pedersen hash function has a homomorphic property, so it is not a cryptographic hash function. Instead, defining both and as MiMC (or any other SNARK-friendly hash function) and applying domain separation might be better.
Dependencies
The contract source code is not very long and well-analyzed. However, as is often the case, small flaws can cause an apocalypse in this field. In fact, in October 2019, the Tornado Cash team discovered a vulnerability where an attacker could make arbitrary deposits due to issues with the external dependency of the zk-SNARK implementation of the MiMC (see here)↗. Therefore, all the dependencies must be also carefully audited.
User Mistakes
The user must choose a high entropy and . There are also possibilities of losing anonymity from user mistakes. We recommend reading this article↗.
Conclusion
Tornado Cash is a popular, decentralized coin mixer on the Ethereum network that offers strong anonymity to its users through the use of cryptographic techniques. As a main feature of Tornado Cash is anonymity in making private transactions, we looked at the math behind their deposits and withdrawals. Taking a deeper dive into their zero-knowledge proof system, Groth16, we exemplified how a transformation of a prover’s statement into quadratic arithmetic program form may occur with reference to flattening, homomorphic hiding and pairing, and the R1CS – and followed with a discussion of procedures such as trusted setup and powers-of-tau. There were also several security concerns to note (including using different hash functions, dependencies needing auditing, and user mistakes). Since the anonymity and privacy in Tornado Cash is one of its strongest features, it is interesting to take a look behind the curtain and see the math behind it.
It is worth recognizing that while the anonymity provided by Tornado Cash can be beneficial, it can also be used for illegal or prohibited activities. Additionally, it is important to note once again that using Tornado Cash for any transactions within the United States or by U.S. persons is prohibited. We recommend you do your own research and consult with qualified legal counsel before interacting with Tornado Cash.