Clean + add FrozenFire's circuits

pull/21/head
xdt1 8 months ago
parent bf496b6246
commit 5452365ae0
  1. 69
      circuits/README.md
  2. 15
      circuits/anonymity-mining/README.md
  3. 140
      circuits/anonymity-mining/mining-rewards.md
  4. 124
      circuits/anonymity-mining/tornado-trees.md
  5. 132
      circuits/core-deposit-circuit.md

@ -0,0 +1,69 @@
# Circuits
Behind the Tornado.cash front-end sits a number of [Circom](https://docs.circom.io) circuits, which enable the fundamental privacy guarantees that Tornado.cash users enjoy. These circuits implement the [Zero Knowledge protocol](https://en.wikipedia.org/wiki/Zero-knowledge\_proof) that Tornado.cash's smart contracts interface with to prove claims about a user's deposit, such as that it is valid, that is hasn't already been withdrawn, and in the context of Anonymity Mining, the number of blocks that exist between a note's deposit transaction and its withdrawal.
### How ZK Circuits Work
#### SNARKs and GROTH16
Before trying to understand how Tornado.cash works under the hood, you first need to understand Zero Knowledge circuits, how they're constructed, and how proofs are generated client-side, then verified on-chain. While there are a [few different types](https://en.wikipedia.org/wiki/Zero-knowledge\_proof#Zero\_knowledge\_types) of ZK systems, Tornado.cash relies upon a variant known as "succinct non-interactive arguments of knowledge" (SNARK), specifically a variant called GROTH16.
#### Circom and snarkjs
Because we're not all Vitalik, it's best if we have some simple tools that will abstract away the generation and execution of these complicated polynomial commitments. This is where [Circom](https://docs.circom.io) and [snarkjs](https://github.com/iden3/snarkjs) come in.
Circom is easiest to think of as a compiler for a circuit language which acts very much like the kind of [hardware description language](https://en.wikipedia.org/wiki/Hardware\_description\_language) that electrical engineers would use to describe an electrical circuit. Except instead of an electrical circuit, we're describing an **arithmetic circuit**, which contains components, and the way that they connect together.
When you compile a Circom circuit, the resulting output is an [R1CS constraint system](https://docs.circom.io/1.-an-introduction/background#rank-1-constraint-system) and a [Wasm](https://en.wikipedia.org/wiki/WebAssembly) executable that will be used to generate a [witness](https://docs.circom.io/1.-an-introduction/background#witness).
**R1CS**
To understand R1CS (Rank-1 constraint system), there is of course more math. And where there's important cryptosystem math, there's a [post by Vitalik](https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649#5539).
> An R1CS is a sequence of groups of three vectors $$(a, b, c)$$, and the solution to an R1CS is a vector $$s$$, where $$s$$ must satisfy the equation $$s . a * s . b - s . c = 0$$, where $$.$$ represents the dot product - in simpler terms, if we "zip together" $$a$$ and $$s$$, multiplying the two values in the same positions, and then take the sum of these products, then do the same to $$b$$ and $$s$$ and then $$c$$ and $$s$$, then the third result equals the product of the first two results.
>
> The next step is taking this R1CS and converting it into QAP form, which implements the exact same logic except using polynomials instead of dot products ... instead of checking the constraints in the R1CS individually, we can now check all of the constraints at the same time by doing the dot product check on the polynomials.
>
> If we try to falsify any of the variables in the R1CS solution that we are deriving this QAP solution from - say, set the last one to 31 instead of 30, then we get a $$t$$ polynomial that fails one of the checks.
In short, the R1CS is a set of polynomial constraints which any proof generated by the circuit must satisfy. These constraints are [generated by Circom](https://docs.circom.io/2.-circom-fundamentals/constraints-generation) based on the relationship between various "signals" and operations in your circuit design.
**Witnesses**
Now, depending on what you're using Tornado.cash for, you might not want any witnesses. However, don't worry, if everything is working correctly, all of the witnesses to your interactions with Tornado.cash will be aggressively compacted, and their bodies disposed of as you please.
In the context of a SNARK circuit, a witness is the set of values that need to be generated from the inputs to the circuit, based on the circuit design, to satisfy all of the constraints imposed by the circuit. You can think of the witness generator produced by Circom as a circuit-specific decompression function which runs your inputs through the circuit, and snapshots all of the various intermediate values that are produced along the way.
With this expanded form generated from your inputs, you know which values must be assigned to the constraints specified by the R1CS in order to construct a valid proof.
**Proof**
When you think of a "proof", you probably imagine that it's an incontrovertible guarantee that something is true. However, in the context of a SNARK, a "proof" actually represents an _argument_ that something is _almost certainly_ true. If we were to try to transmit the solution to every single polynomial constraint imposed by a circuit, we would end up with proofs that were orders of magnitude larger than if we simply show that certain sorts of relationships hold true between the intermediate state values within the circuit.
It's possible that for any given circuit, someone with sufficient computing power could generate a proof that satisfies the circuit's constraints in a malformed way, but this would be roughly equivalent in difficulty to [factoring large primes](https://en.wikipedia.org/wiki/RSA\_Factoring\_Challenge).
So, when generating a proof for a SNARK circuit, you're calculating the intermediate states of your circuit for a given input (witness generation), and then calculating the relationships between your inputs, the intermediate states, and the circuit's outputs.
Once you have the proof that you've satisfied the necessary set of constraints, you can then publish that proof and some subset of your inputs and outputs (a.k.a. public signals). Knowing the R1CS, your public signals, your proof, and the circuit's proving key, anyone can then verify that your proof satisfies the R1CS, and that your public signals are what would be expected to correspond to your proof.
### Circuits
With that understanding of ZK proving circuits well-in-hand, let's delve into how Tornado.cash uses some relatively simple circuits to enable you to privately and permissionlessly obscure the relationship between your deposit and withdrawal transactions on a public blockchain network, and then to later prove things _about_ the relationship between your deposit and withdrawal (e.g. how long you waited before withdrawing).
Tornado.cash is best understood as having two separate major components.
#### Core Deposit Circuit
The core deposit circuit is what most users interact with, proving that a user has created a commitment representing the deposit of some corresponding asset denomination, that they haven't yet withdrawn that asset, and that they know the secret that they supplied when generating the initial commitment.
{% content-ref url="core-deposit-circuit.md" %}
[core-deposit-circuit.md](core-deposit-circuit.md)
{% endcontent-ref %}
#### Anonymity Mining
The anonymity mining circuits form the basis for the Anonymity Mining program, which incentivizes users to leave their deposits in the contract for longer periods of time, so as to ensure that the Tornado.cash deposit pools maintain a large number of active deposits (thus increasing [k-anonymity](https://en.wikipedia.org/wiki/K-anonymity) for other users).
{% content-ref url="anonymity-mining/" %}
[anonymity-mining](anonymity-mining/)
{% endcontent-ref %}

@ -0,0 +1,15 @@
# Anonymity Mining
The anonymity mining protocol underpins the [Anonymity Mining Program](../../anonymity-mining.md), which rewards users according to the block duration that they wait before withdrawing their deposits.
Anonymity Mining uses a Tornado Trees contract as a ZK-efficient register of deposit and withdrawal transactions, which enables users to make efficient proofs regarding their usage.
{% content-ref url="tornado-trees.md" %}
[tornado-trees.md](tornado-trees.md)
{% endcontent-ref %}
Once deposits and withdrawals are registered into the Tornado Trees contract, they become eligible for having their rewards claimed into a shielded AP account.
{% content-ref url="mining-rewards.md" %}
[mining-rewards.md](mining-rewards.md)
{% endcontent-ref %}

@ -0,0 +1,140 @@
---
description: >-
Anonymity Mining rewards are claimed using ZK proofs showing the block
duration that notes were left deposited. Anonymity Points are kept in shielded
accounts until swapped for TORN.
---
# Reward Claim
## Account Tree
Anonymity Points claimed as rewards for anonymity mining are stored in "shielded" accounts within a Merkle Tree, using many of the same patterns as in the core deposit contract and Tornado Trees.
### Shielded Balance
The special property that the accounts tree brings to the table is that only the owner of the account knows its balance. Each time that an account's balance is updated by claiming additional rewards, or withdrawing from it, a new account is created, and the old account is nullified.
The balance of an account is stored as a component of the commitment hash that is inserted into the tree. The balance of an account can only by known by having the private key used to produce an encrypted backup of its commitment inputs.
### Account Commitment
An account commitment consists of three components:
1. The 31-byte amount of AP in the account
2. A 31-byte random secret
3. A 31-byte random nullifier
An account commitment hash is the Poseidon hash of these three components
## Claiming a Reward
### Inputs to a Reward Proof
#### The total set of public inputs for a reward proof are:
1. The block reward rate for the claim
2. The fee that you're paying the relayer (or zero)
3. The Tornado instance address associated with the note
4. The reward nullifier (Poseidon hash of the note nullifier)
5. The args hash
6. The root of the account tree pre-claim (input)
7. The nullifier of the existing account being added to
8. The root of the account tree after updating it (output)
9. The path indices to the new account, as an integer, left-padded with zeroes
10. The commitment for the new account
11. The root of the deposit tree
12. The root of the withdrawal tree
#### The total set of private inputs for a reward proof are:
1. The secret of the note being claimed
2. The nullifier of the note being claimed
3. The amount coming from an existing account
4. An array of path elements to the existing account commitment in the tree
5. The path indices to the existing account, as an integer, left-padded with zeroes
6. The balance of the new account
7. The secret of the new account
8. The nullifier of the new account
9. An array of path elements to the new account
10. The block number when the claimed note was deposited
11. The path indices to the deposit in the Tornado Trees deposit tree, as an integer, left-padded with zeroes
12. An array of path elements to the deposit in the Tornado Trees deposit tree
13. The block number when the claimed note was withdrawn
14. The path indices to the withdrawal in the Tornado Trees withdrawal tree, as an integer, left-padded with zeroes
15. An array of path elements to the withdrawal in the Tornado Trees withdrawal tree
### Proven Claims
Rewards are claimed by proving that:
1. You know the path to a deposit and withdrawal event pair
2. The deposit and withdrawal correspond to the same note whose secret and nullifier you know
3. The withdrawal event's block number is a specified duration after the deposit event's block number
4. The amount you're claiming is then a multiple of the agreed-upon reward rate, times the specified block duration
5. The disclosed "reward nullifier" is the Poseidon hash of the original note nullifier
### Sanity Checks
An invariant constraint is first created showing that the amount that is coming from an existing account, plus the block reward rate times the block duration, is equal to the new account's balance plus the relayer fee.
An overflow check is then performed on the input, output, and block duration values to ensure that they fit within 248 bits, 248 bits, and 32 bits, respectively, without overflowing.
### Input Account Validation
The input account balance, secret, and nullifier are hashed to obtain the input account commitment, and then the path elements and path indices are used to verify that such a commitment exists within the specified input root, but in such a way that if the input account balance is 0, the check passes regardless.
This scheme allows for new accounts to be created without publicly disclosing that they are new.
The public input nullifier hash is checked against the input account's nullifier component.
### Output Account Validation
The output account balance, secret, and nullifier are hashed to obtain the output account commitment, and are checked against the public output commitment hash.
The account tree update is validated using the input root, output root, output commitment as a new leaf, and the specified output path elements and indices.
### Tornado Trees Validation
The Tornado Commitment used for the note being claimed is computed using the note secret and nullifier, and then the corresponding commitment in the Tornado Trees deposit tree is computed using the Tornado instance address, note commitment, and deposit block number as components.
The deposit commitment is then verified to exist at the specified deposit path beneath the deposit tree root.
The withdrawal commitment in the Tornado Trees withdrawal tree is then computed using the Tornado instance address, note nullifier hash, and withdrawal block number as components. The withdrawal commitment is verified to exist at the specified withdrawal path beneat the withdrawal tree root.
### Reward Nullifier
The reward nullifier for the provided note is then computed by generating the Poseidon hash of the note's nullifier. This is compared to the reward nullifier specified as a public input.
### Args Hash Square
As a last step, the args hash is squared into a public output, to ensure that the reward transaction parameters cannot be tampered with relative to the proof.
## Completing the Reward Transaction
Using the generated proof, we can now call the `reward` method of the Miner contract.
The reward method takes the proof, and the public inputs to the proof, and verifies:
1. The input account nullifier has not already been used
2. The output account path corresponds to the input root leaf index
3. The deposit and withdrawal roots are the current or previous roots of the Tornado Trees contract (are known roots)
4. The args hash is correct with respect to the extData args
5. The relayer fee is within a valid 248-bit integer range
6. The reward rate equals what is expected for the Tornado instance specified
7. The reward has not already been claimed according to the reward nullifier
8. The proof is valid according to the Reward verifier, using the public inputs provided
If these preconditions are met, then:
1. The input account is nullified
2. The reward is nullified
3. The account tree root is updated to the specified output account root
4. If applicable, the relayer is rewarded TORN using the fee AP at the current AMM rate
5. A New Account event is emitted
#### Batch Rewards
There is an alternate method in the Miner contract which deals with "batch rewards". While most transactions claim rewards individually, batch reward transactions claim multiple rewards at once. This avoids having to wait for the next block for each reward claim, since each claim has to insert a new leaf from the last account root.
A batch reward transaction specifies a sequence of `reward` method parameter sets, which are executed incrementally.

@ -0,0 +1,124 @@
---
description: >-
The Tornado Trees contract and circuit acts as an ZK-efficient register of
deposit and withdrawal transactions that the Anonymity Mining functions can
use to validate claims made by users about their
---
# Tornado Trees
## Tornado Proxy
When accessing the Tornado.cash deposit contracts using the official UI, all transactions execute through a proxy contract called the [Tornado Proxy](https://github.com/tornadocash/tornado-trees/blob/master/contracts/TornadoTrees.sol). Since the deposit contracts themselves are immutable, and many features of Tornado.cash were added well after the original deposit contracts were deployed, the Tornado Proxy provides a way to inject additional functionality without replacing the battle-tested deposit contract instances.
The two most noteworthy functions of the Tornado Proxy are its ability to back up user deposits on-chain using encrypted note accounts, and the function which queues deposits and withdrawals for processing in the Tornado Trees contract.
### Registering Deposits and Withdrawals
When you make a deposit through the Tornado Proxy, and when you later make a withdrawal through the same, the proxy calls corresponding methods on the [Tornado Trees](https://github.com/tornadocash/tornado-trees/blob/master/contracts/TornadoTrees.sol) contract.
Registering a deposit takes the address of the deposit contract `uint160`, the commitment Pedersen hash `bytes32`, and the current block number `uint`, [ABI encodes](https://docs.soliditylang.org/en/v0.8.9/abi-spec.html#abi) them, then produces a `keccak256` (a.k.a. [SHA3-256](https://en.wikipedia.org/wiki/SHA-3)) hash over the resulting message. This hash is inserted into a queue within the contract, to be later batched into the deposit Merkle Tree (not to be confused with the deposit contract).
Registering a withdrawal is the essentially the same as registering a deposit, except instead of using the commitment hash, the nullifier hash of the withdrawal is used instead. The resulting `keccak256` hash is inserted into the withdrawal queue, to be later batched into the withdrawal Merkle Tree.
The `registerDeposit` and `registerWithdrawal` contract methods of the Tornado Trees contract each emit a corresponding event, `DepositData` and `WithdrawalData` containing the same values as were included in the computed hash, plus an additional event field indication the order in which they entered into the queue.
## Chunked Merkle Tree Update
Standard Merkle Trees are fairly expensive to store and update, especially if you want to commit to a large number of leaves. Depositing a note into the Tornado.cash deposit contracts can cost upwards of 1.2M gas, which can be hundreds of dollars worth of ETH if depositing on Ethereum mainnet. Most of this gas cost results from simply inserting a commitment into the deposit contract Merkle Tree.
What if, instead of spending all of that gas, we could instead simply propose a new Merkle Root that we computed off-chain, and prove that it's valid using a Zero Knowledge proof?
However, verifying Zero Knowledge proofs is itself quite expensive. So, instead of updating the Merkle Tree for every change, we can batch insertions together into aggregate commitments which can be verified as a whole.
### Chunked Tree Structure
The deposit and withdrawal trees are both fixed-size Merkle Trees 20 levels deep, but with a notable feature. The "chunk size" of the tree determines a level at which updates are computed in aggregate, instead of as individual insertions.
In the case of Tornado Trees, the chunk size is 256 (2^8), so each chunk is 8 levels high. The complete tree is still limited to 2^20 leaves, but those leaves are divided into 256-leaf chunks, with a total of 2^12 chunks.
The hash function used to produce node labels is [Poseidon](https://www.poseidon-hash.info), which is similar to the [Pedersen](https://iden3-docs.readthedocs.io/en/latest/iden3\_repos/research/publications/zkproof-standards-workshop-2/pedersen-hash/pedersen.html) hash function used in the core deposit contract, in that it's an elliptic curve hashing algorithm. The major difference between the two is that Poseidon operates over the BN128 elliptic curve instead of Baby Jubjub, and where Pedersen uses 1.7 constraints per bit in a ZK proof, Poseidon only uses between 0.2 and 0.45 constraints per bit.
### Collecting the Events
In order to compute an update to the tree, it's necessary to know the existing structure of the tree. To obtain this, you query from the contract logs the `DepositData` or `WithdrawalData` events emitted earlier, depending on which tree you're updating.
Using the index indicated by `lastProcessedDepositLeaf` or `lastProcessedWithdrawalLeaf`, you can then split the events into two sets of leaves - "committed" and "pending". As the names would imply, the former set of leaves are the ones that are already committed within the Merkle Tree, and the latter are all of the leaves which still need to be inserted.
### Computing a Tree Update
Using the committed events, we're able to reconstruct the current state of Merkle Tree by first computing the Poseidon hash for each of the existing leaves, using the Tornado instance address, commitment/nullifier hash, and block number as the inputs to the hashing algorithm.
The empty state of the Merkle Tree starts with every leaf node labelled using a "zero value" of `keccak256("tornado") % BN254_FIELD_SIZE`, similarly to how zero nodes work in the core deposit circuit, except using the BN254 elliptic curve. This ensures that all paths within the tree are invalid until a valid commitment is inserted on a leaf, and gives a constant, predictable label for each node whose children are zeroes.
The leaves of the tree are then populated from left to right with the leaf hashes for the set committed events, and then the non-leaf nodes are updated up to the root. If everything is done right, the resulting root should be equal to what's currently stored in `depositRoot` / `withdrawalRoot` of the Tornado Trees contract.
Now that we have the "old root", we can proceed to take a chunk of pending events (256 of them), compute their Poseidon hashes, and insert them into the tree. After updating the non-leaf nodes up to the root, we will have the "new root".
Next, we need to collect a list of path elements starting from the subtree leaf, as well as an array of `0/1` bits indicating whether each path element is to the left or right of its parent node.
### Computing the Args Hash
The last thing that we need before we can compute a proof is a hashed list of arguments that we'll be passing into our proving circuit, with a very particular structure.
Construct a message that is the concatenation of these fields:
1. The old root label (32 bytes)
2. The new root label (32 bytes)
3. The path indices as an integer, left-padded with zeroes (4 bytes)
4. For each event
1. The commitment/nullifier hash (32 bytes)
2. The Tornado instance address (20 bytes)
3. The block number (4 bytes)
Compute the SHA-256 hash of this message, and then modulus the hash against the BN128 group modulus found in the `SNARK_FIELD` constant of the Tornado Trees contract.
## Generating a Merkle Tree Update Witness
### Inputs to a Tree Update Witness
The [Batch Tree Update](https://github.com/tornadocash/tornado-trees/blob/master/circuits/BatchTreeUpdate.circom) circuit takes a single public input, which is the resulting SHA-256 args hash in the BN128 field.
The additional private inputs for a Tree Update witness are:
1. The old root
2. The new root
3. The path indices as an integer, left-padded with zeroes
4. An array of path elements
5. For the pending events being inserted
1. An array of commitment/nullifier hashes
2. An array of Tornado instance addresses
3. An array of block numbers
### Proven Claim
To prove that we have updated the tree correctly, we don't have to provide a proof that spans the entire tree. Instead, we can prove just that a subtree of the 8-level chunk size, with a list of specified leaves, was added in place of the left-most zero leaf of a 12-level tree.
### Proving the Args Hash
Instead of specifying all of the inputs publicly, we can take the Args Hash that we computed earlier and compare it to the result of computing the same hash within the ZK circuit, using the private inputs. This makes for a much more efficient proof verification execution.
### Build the Subtree
Taking the three fields of each pending event in the order (instance, commitment/nullifier, block number), we compute the Poseidon hash of each leaf. We then construct the Merkle Tree just for the subtree that we're updating. Since the subtree is full, we don't need to worry about any zero leaves.
### Verify the Subtree Insertion
Lastly, we verify that inserting the subtree root at the proposed position results in the old root transforming to the new root. This works essentially the same way as for the [Merkle Tree Check](../core-deposit-circuit.md#computing-the-witness) in the core deposit circuit, except using Poseidon instead of MiMC.
The [Merkle Tree Updater](https://github.com/tornadocash/tornado-trees/blob/master/circuits/MerkleTreeUpdater.circom) first verifies that the specified path contains a zero leaf by computing what the root would be given the path elements, path indices, and a zero leaf. It compares this against the specified old root, and then repeats that process again with the proposed subtree leaf, comparing the resulting root to the new root.
## Completing the Tree Update
With the witness generated, and the proof generated from it, we can now call the corresponding `updateDepositTree` or `updateWithdrawalTree` method on the Tornado Trees contract.
We pass the update method the proof, the args hash, the old root, the new root, path indices, and a list of events that we're batch inserting. The update method then verifies that:
1. The old root specified is the current root of the corresponding tree
2. The specified merkle path points to the first available subtree zero leaf
3. The specified args hash corresponds to the supplied inputs and proposed events
4. The proof is valid according to the circuit verifier, using the args hash as public input
If these preconditions are met, then each inserted event is deleted from the queue. The corresponding contract fields indicating the current and previous roots are updated, as well as a pointer to the last event leaf.
There is a special branch of logic in this method which also handles migrating events from the Tornado Trees V1 contract. After the initial migration, these paths are never visited, and can be safely ignored.

@ -0,0 +1,132 @@
# Core Deposit Circuit
The core deposit circuit is what most users interact with, proving that a user has created a commitment representing the deposit of some corresponding asset denomination, that they haven't yet withdrawn that asset, and that they know the secret that they supplied when generating the initial commitment.
## Making a Deposit
A deposit into Tornado.cash is a very simple operation, which doesn't actually involve any ZK proofs. At least not yet. To make a deposit, you invoke the `deposit` method of a [Tornado contract](https://github.com/tornadocash/tornado-core/blob/master/contracts/Tornado.sol) instance, supplying a [Pedersen Commitment](https://crypto.stackexchange.com/questions/64437/what-is-a-pedersen-commitment), along with the asset denomination that you're depositing. This commitment is inserted into a specialized [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree), where the structure of the Merkle Tree is aligned to an elliptic curve associated with a prime in the order of the BN128 elliptic curve, and the labels of the tree are computed using MiMC hashing.
### Commitment Scheme
When you make a "commitment" in the context of cryptography, what you're doing is taking a secret value - often large and random - and running it through some cryptographic function (e.g. a hash function), then disclosing the result. Later, when you need to make good on the commitment, you prove that you know the original secret value.
This is known as a [commitment scheme](https://en.wikipedia.org/wiki/Commitment_scheme).
### Pedersen Hash
A [Pedersen Hash](https://iden3-docs.readthedocs.io/en/latest/iden3\_repos/research/publications/zkproof-standards-workshop-2/pedersen-hash/pedersen.html) is an extremely specialized hashing function that is particularly well-suited for use in applications leveraging Zero Knowledge proving circuits. Where other hashing functions like SHA-256 are designed to exhibit properties such as producing very different outputs for even slightly different inputs (the [avalanche effect](https://en.wikipedia.org/wiki/Avalanche_effect)), Pedersen hashing instead prioritizes the ability to compute the hash extremely efficiently in Zero Knowledge circuits.
Hashing a message with Pedersen compresses the bits of the message down to a point along an [elliptic curve](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) called [Baby Jubjub](https://github.com/barryWhiteHat/baby_jubjub). Baby Jubjub is in the order of the BN128 elliptic curve that is supported by precompiled operations on the Ethereum network which were added in [EIP-196](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-196.md). This means that operations that use the Baby Jubjub curve, such as Pedersen Hashing, are highly gas-efficient.
When you compute the Pedersen hash of a message, the resulting point along the its elliptic curve is very efficient to verify, but infeasible to reverse back into the original message.
### Tornado Commitment
To generate a commitment for a Tornado.cash deposit, you first generate two large random integers, each 31 bytes in length. The first value is a nullifier that you will later disclose in order to withdraw your deposit, and the second is a secret that secures the confidential relationship between your deposit and withdrawal.
The preimage of your deposit note is the concatenation of these two values (`nullifier` + `secret`), resulting in a message 62 bytes in length. This message is Pedersen hashed, resulting in an output representing an element of the Baby Jubjub elliptic curve encoded as a 32-byte big-endian integer.
If you want to see this in code form, you can reference the [tornado-cli deposit function](https://github.com/tornadocash/tornado-cli/blob/master/cli.js#L53-L112).
### MiMC Merkle Tree
The [Tornado contract](https://github.com/tornadocash/tornado-core/blob/master/contracts/Tornado.sol) is a specialized [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) which labels its nodes using MiMC hashes.
For those not familiar with Merkle Trees, they are binary trees where each non-leaf node is labelled with the hash of the labels of its child nodes, and the leaf nodes are labelled with the hash of their data. Ordinarily, Merkle Trees use a one-way cryptographic hashing function like SHA-2, but in this case, we're using MiMC, which has some useful properties.
One of the useful properties of MiMC is that it's well-suited to operating over prime fields, which is important to us because Zero Knowledge proofs are fundamentally based on prime fields, and Pedersen Hashes are points within a prime field defined by the Baby Jubjub elliptic curve - which is in turn within the order of the BN128 curve supported natively on Ethereum. Because Zero Knowledge proofs are operationally expensive, and each operation in an Ethereum transaction has a corresponding gas cost, the specific types of operations we design around need to be as gas-efficient as possible.
The other particularly useful properties of MiMC are that it's non-parallelizable, and difficult to compute but easy to verify. These properties add to the security of the contract by making it computationally infeasible to calculate a forged "commitment" which has a colliding path within the merkle tree.
### Zero Nodes
During the initialization of the Tornado Merkle Tree, a single path spanning the height of the tree is preallocated starting with a "zero leaf" node with a label of `keccak256("tornado") % FIELD_SIZE`. Each subsequent non-leaf node toward the root is then labelled as if the entire bottom of the tree were populated by that same same leaf node.
The purpose of these "zero nodes" is to ensure that all paths within the merkle tree are invalid until they terminate in a valid commitment.
### Inserting a Commitment
When you insert a commitment into the Tornado contract's merkle tree, you are replacing a "zero leaf" with a new leaf whose label is the MiMC hash of your Pedersen commitment, and then traversing up the tree updating each subsequent parent node with a new label based on the label updates that your new leaf introduces below.
Commitments are inserted from left to right within the tree, with every two commitment insertions filling a "subtree". Each insertion increments the "index" of the tree, determining whether the next commitment will be inserted on the left or right side of the entry to its merkle path.
Once your deposit has updated the tree, the label of the top-most node becomes the tree's new "root", and is added to a rolling history containing the labels of the last 100 roots, for later use in processing withdrawal transactions.
The Tornado.cash deposit contracts are deployed with 20 "levels", with each level increasing the number of potential leaves by a power of 2. That means that the contract's merkle tree supports up to 2^20 leaves, allowing for up to 1,048,576 deposits to be made into the contract before it needs to be replaced.
The reason behind this seemingly-low number of levels is that every deposit has to perform as many updates to the tree as there are levels. A tree with more levels would require more gas per deposit, as well as correspondingly larger proof sizes when withdrawing notes.
## Making a Withdrawal
Having made a deposit, you now have a set of truth claims that you can generate a proof based upon. Generally speaking, Zero Knowledge proofs are anchored to some value(s) known by both the prover and the verifier, to which a relationship is going to be proven to a set of values known only by the prover. The circuit verifier can confirm that the prover has used the value(s) that are known, and that the proof that they computed satisfies the constraints imposed by the circuit.
### Inputs to a Withdrawal Proof
In the case of Tornado.cash deposits, the prover (the person submitting a withdrawal transaction), and the verifier (the deposit contract's withdrawal method) both know a recent merkle root. The prover also supplies a set of other public inputs that they used for the generation of their proof.
#### The total set of public inputs for a withdrawal proof are:
1. A recent merkle root
2. The Pedersen hash of the nullifier component from their deposit commitment
3. The address of the recipient of their withdrawal
4. The address of the relayer that they've selected (or their own address)
5. The fee that they're paying the relayer (or zero)
6. The refund that they're paying the relayer (or zero)
#### The additional private inputs for a withdrawal proof are:
1. The nullifier component from their deposit commitment
2. The secret component from their deposit commitment
3. The set of node labels that exist in the path between the root and the leaf nodes of the merkle tree
4. An array of `0/1` values indicating whether each specified path element is on the left or right side of its parent node
### Proven Claims
It would be easy to miss the clever new piece of knowledge we created when we constructed and inserted our commitment into the merkle tree. You might be inclined to think that to make a withdrawal, we're simply going to prove that we know the components of the Pedersen commitment, and that the merkle tree is just an efficient way to store those commitment hashes.
What's special about this construction is that it enables us to prove not just that we know the components of a deposited commitment, but rather it enables us to prove simply that we **know the path to a commitment within the tree**, and **how to get there** starting with a commitment preimage.
If we were only to prove that we knew the preimage to a deposited hash, we would risk revealing which commitment is ours. Instead, we're not disclosing the commitment preimage, but instead we're simply proving that we have knowledge of a preimage to a commitment within the tree. Which commitment is ours remains completely indistinguishable on the withdrawal side of the circuit protocol.
### Computing the Witness
**Nullifier Hash Check**
In order to compute the witness for the withdrawal proof, our circuit first takes the private deposit commitment inputs (`nullifier` + `secret`), and runs them through a circuit component which simultaneously computes the Pedersen hash of the full commitment message, and the Pedersen hash of the nullifier alone. The circuit then compares the resulting nullifier hash to the one you supplied as a public input, and asserts their equality.
**This proves that the nullifier hash that you supplied publicly is in fact a component of your original commitment.**
**Merkle Tree Check**
Next, the circuit takes the commitment hash it has computed, the merkle root you have specified publicly, and the path elements and left/right selectors that you specified privately, as inputs to a component which checks your merkle tree path claim.
The Merkle Tree Checker starts from the bottom of the path, inputting your commitment hash and the first element of your proposed path into a Muxer. The Muxer takes a third input, which is an element from your supplied left/right directions. The Muxer component uses these directions to inform an MiMC hashing component as to the order of its inputs. If the supplied direction is 0, then the supplied path element is on the left, and your commitment hash is on the right. If the direction is 1, then the order is reversed.
The MiMC hasher outputs the resulting hash, and the Merkle Tree Checker proceeds to the next level. It repeats the last process, except this time, instead of using your commitment hash, it uses the hash of the last level. It continues to run through each level of the proposed path, until it ends up with a final hash output.
The Merkle Tree Checker compares the hash that it has computed to the public merkle root input that you supplied, and asserts their equality.
**This proves that your commitment exists within some path beneath the specified merkle root.**
**Extra Withdrawal Parameter Check**
Before finishing, the circuit takes each of the remaining four public inputs, and squares them into a public output. While this isn't strictly necessary, it creates a set of constraints within your proof that ensure that your transaction parameters can't be tampered with before your withdrawal transaction is processed. If any of those parameters were to change, your proof would no longer be valid.
### Computing the Proof
Now that we have a witness for our proof, we take those witnessed state values and input them into the R1CS corresponding to the Withdrawal circuit, and run the prover over it. Out of the prover comes two proof artifacts. The first is the proof itself, according to the SNARK protocol we're using, and the second is the set of public inputs and outputs corresponding to that proof.
### Completing a Withdrawal Transaction
With the withdrawal proof now generated, you supply that proof, along with its public inputs, to the `withdraw` method of the deposit contract. This method verifies that:
1. The specified relayer fee does not exceed the value of the denomination of asset being withdrawn
2. The supplied nullifier hash has not been spent before
3. The supplied merkle root is known, using the 100-root historical record
4. The supplied proof is valid
One of the artifacts deployed as a dependency of the deposit contract is a Solidity contract that is generated using the proving key of the Withdrawal circuit as an input. This Verifier contract is an optimized proof verifier with a single public view function, which accepts a proof and the array of six public inputs as `uint256` values.
This function returns `TRUE` if the proof is valid according to the public inputs.
If the above preconditions are met, the supplied nullifier hash is inserted into the set of spent nullifiers, and then the value of the deposit is distributed amongst the recipient and relayer, according to the specified fee parameters.
Loading…
Cancel
Save