[Tornadocash Circuit Breakdown] Part 1

[Tornadocash Circuit Breakdown] Part 1
none 0.0 0

Introduction

Tornado Cash is a decentralized privacy protocol designed to anonymize cryptocurrency transactions on the Ethereum blockchain. By using advanced cryptographic techniques like zk-SNARKs, it severs the on-chain link between deposit and withdrawal addresses, allowing users to protect their financial privacy. Through its non-custodial and decentralized design,

Tornado Cash enables individuals to conduct private transactions while retaining full control of their funds The goal of this circuit is to allow a prover to prove membership in a group of public keys without revealing which specific key (or secret key) they possess. In this tutorial, we will breakdown the tornado cash circuit to better understand the concept behind the tornado cash platform.

How Tornado cash works

  1. Deposit: A user selects a specific amount of cryptocurrency to deposit into Tornado Cash. Upon initiating the deposit, the protocol generates a unique secret, often referred to as a “commitment” or “note.” This secret is a hash that serves as proof of the deposit without revealing the user’s identity. The user is advised to securely store this note, as it will be required for future withdrawals.

  2. Mixing: The deposited funds are pooled together with those of other users within Tornado Cash’s smart contract. This aggregation makes it challenging to trace the original source of any specific withdrawal, thereby enhancing privacy.

  3. Withdrawal: When the user decides to withdraw their funds, they must provide the unique note generated during the deposit. This note serves as a zero-knowledge proof, allowing the user to demonstrate ownership of the deposited funds without revealing their identity. The user can withdraw the funds to a new address, further obscuring the link between the deposit and withdrawal addresses.

Key Features:

  • Zero-Knowledge Proofs: Tornado Cash employs zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge) to enable users to prove they have made a deposit without revealing any additional information. This cryptographic technique ensures that the withdrawal cannot be linked back to the deposit, maintaining user anonymity.

  • Non-Custodial: Users retain full control over their funds throughout the process. Tornado Cash’s smart contracts manage the mixing process without ever taking custody of the assets, reducing the risk of theft or loss.

Building the Circuit

For installation and project setup, check out Part1 of this section in the series.

Complete Circuit code

you can check out the tornadocash repo here

include "../node_modules/circomlib/circuits/mimcsponge.circom";

// Computes MiMC([left, right])
template HashLeftRight() {
    signal input left;
    signal input right;
    signal output hash;

    component hasher = MiMCSponge(2, 1);
    hasher.ins[0] <== left;
    hasher.ins[1] <== right;
    hasher.k <== 0;
    hash <== hasher.outs[0];
}

// if s == 0 returns [in[0], in[1]]
// if s == 1 returns [in[1], in[0]]
template DualMux() {
    signal input in[2];
    signal input s;
    signal output out[2];

    s * (1 - s) === 0
    out[0] <== (in[1] - in[0])*s + in[0];
    out[1] <== (in[0] - in[1])*s + in[1];
}

// Verifies that merkle proof is correct for given merkle root and a leaf
// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
template MerkleTreeChecker(levels) {
    signal input leaf;
    signal input root;
    signal input pathElements[levels];
    signal input pathIndices[levels];

    component selectors[levels];
    component hashers[levels];

    for (var i = 0; i < levels; i++) {
        selectors[i] = DualMux();
        selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
        selectors[i].in[1] <== pathElements[i];
        selectors[i].s <== pathIndices[i];

        hashers[i] = HashLeftRight();
        hashers[i].left <== selectors[i].out[0];
        hashers[i].right <== selectors[i].out[1];
    }

    root === hashers[levels - 1].hash;
}

Code Walkthrough

1. HashLeftRight Template

template HashLeftRight() {
    signal input left;
    signal input right;
    signal output hash;

    component hasher = MiMCSponge(2, 1);
    hasher.ins[0] <== left;
    hasher.ins[1] <== right;
    hasher.k <== 0;
    hash <== hasher.outs[0];
}

The HashLeftRight template computes the MiMC sponge hash of two inputs (left and right) and produces a single output (hash).

Concepts:

  1. MiMC Sponge:
  • MiMC is a cryptographic hash function designed for zero-knowledge proofs (ZKPs). It is lightweight and zk-SNARK-friendly.
  • Sponge functions process inputs of variable length to produce outputs of fixed size.
  1. How it Works:
  • The MiMCSponge component takes two inputs (left and right).
  • k (key) is set to 0 since no additional secret keying is required.
  • The result is stored in hash.

Purpose:

This template will hash two nodes of a Merkle tree (a parent node in the Merkle path) to calculate the next level’s hash.


2. DualMux Template

template DualMux() {
    signal input in[2];
    signal input s;
    signal output out[2];

    s * (1 - s) === 0
    out[0] <== (in[1] - in[0])*s + in[0];
    out[1] <== (in[0] - in[1])*s + in[1];
}

The DualMux template selects one of two values (in[0] or in[1]) based on the selector signal (s) and swaps them accordingly.

Concepts:

  1. Signal s:
  • s is a binary signal (either 0 or 1).
  • Constraint: s * (1 - s) === 0 ensures that s is always binary.
  1. Output Computation:
  • If s = 0: out[0] = in[0], out[1] = in[1] (no swap).
  • If s = 1: out[0] = in[1], out[1] = in[0] (values swapped).

Purpose:

This template determines whether a Merkle proof element is the left or right sibling of the current node. It reorders the inputs to correctly compute the hash for the next level.


3. MerkleTreeChecker Template

template MerkleTreeChecker(levels) {
    signal input leaf;
    signal input root;
    signal input pathElements[levels];
    signal input pathIndices[levels];

    component selectors[levels];
    component hashers[levels];

    for (var i = 0; i < levels; i++) {
        selectors[i] = DualMux();
        selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
        selectors[i].in[1] <== pathElements[i];
        selectors[i].s <== pathIndices[i];

        hashers[i] = HashLeftRight();
        hashers[i].left <== selectors[i].out[0];
        hashers[i].right <== selectors[i].out[1];
    }

    root === hashers[levels - 1].hash;
}

This template verifies a Merkle proof for a given leaf node. The proof consists of pathElements (siblings along the path) and pathIndices (indicating left/right positions). The circuit ensures that the computed root matches the provided root.

  1. Inputs:
  • leaf: The data node to verify.
  • root: The claimed Merkle root.
  • pathElements: Array of sibling nodes at each level of the tree.
  • pathIndices: Array of binary values (0 or 1) indicating whether the sibling is on the left or right.
  1. Selectors and Hashers:
  • For each tree level:
    • Use DualMux to order the leaf/hash and sibling node correctly based on pathIndices[i].
    • Use HashLeftRight to compute the hash for the next level.
  1. Root Verification:
  • After iterating through all levels, the hash at the last level (hashers[levels - 1].hash) is compared to the claimed root.

Purpose:

The circuit guarantees that:

  1. The provided leaf and path produce the specified root.
  2. The constraints are satisfied, ensuring cryptographic integrity.

How it works:

  1. Initialization:
  • Start with the leaf as the initial hash.
  • Traverse up the Merkle tree using pathElements and pathIndices.
  1. For Each Level:
  • Use DualMux to select the correct order of sibling nodes.
  • Hash the current node and sibling to compute the parent hash.
  1. Final Check:
  • Verify that the final computed hash matches the provided root.

Conclusion

Congratulations! You’ve completed a hands-on implementation of zero-knowledge proofs. This process may seem complex initially, but as you create more advanced circuits, you’ll see how useful ZKPs can be for privacy-preserving computation.

We encourage you to continue your exploration of topics on zk proofs. If you’re interested in diving deeper, you can follow up on the pathway here Zero-Knowledge Proofs on the Celo Blockchain: A Comprehensive Tutorial Series - Pathways - Celo Academy

Resources

Circom docs
SnarkJS
0xparc Circom Workshop

About the author

I’m Jonathan Iheme, A full stack block-chain Developer from Nigeria. With a great passion for Zero Knowledge Technology.

linkedIn
Twitter