Skip to content

A simple tornado-cash style coin mixer implemented using RISC Zero

Notifications You must be signed in to change notification settings

willemolding/simple-mixer

 
 

Repository files navigation

Simple Mixer 💸♻️💸

A tornado-cash style coin mixer implemented using RISC Zero

About

Implements a protocol very similar to the original Tornado-cash with the following changes:

  • Uses sha256 hashing for the nullifier and commitment tree instead of Pederson and MiMC hashes
  • Removes the withdrawal fee functionality. Mostly to keep the demo simple.

Why rewrite tornado cash with RISC Zero? Aside from being a nice example it opens up the possibility to compose additional proofs with the withdrawal proofs. For example it would be straightforward to add compliance checking to ensure that the withdrawer is a member of a whitelisted set without linking this identity to their account.

The protocol works as follows:

Deposit

The depositor generates a nullifier, $k$, and secret value, $r$, locally. These are hashed together to produce a note commitment

$C = H(k || r)$.

This is submitted to the contract along with a pre-determined amount of eth (this example uses 1 Eth sized notes). The tuple ($k$, $r$) makes up the spending key for this note.

Upon receiving the deposit the contract appends the note commitment to its internal incremental Merkle tree and stores the tree root, $R$. It also saves the commitment to ensure it cannot be used again.

Withdrawal

To spend the withdrawer needs to construct a proof with the following form:

I know $k$, $r$, $l$, $O(l)$ such that:

  • $h = H(k)$
  • O(l) is a valid merkle proof for leaf $C = H(k || r)$ rooted at $R$

where $l$ is the leaf position of the note commitment they are attempting to spend and $h$ is the nullifier hash.

In this case $h$ and $R$ are the public inputs and $k$, $r$, $l$, and $O(l)$ are private inputs. The proof also needs to commit to the receiving address $A$ so that the withdrawal transaction is non-malleable.

To construct a valid merkle proof the withdrawer has to reconstruct the contract merkle tree locally. It does this by querying an RPC node for all deposit events and builds a tree locally with extracted the note commitments.

The contract verifies this proof, checks that the nullifier hash, tree root and receiving address match the journal, and checks this nullifier hash has not been used already. If satisfied it allows the withdrawer to transfer 1 note worth of Eth out of the contract to the receiver. It then stores the nullifier hash in contract state so it cannot be used again.

Repo Contents

Note

The contracts Mixer.sol, EthMixer.sol and MerkleTreeWithHistory.sol are modified versions of the original Tornado cash contracts and should not be considered original code for this submission. The main modification is the integration of the RISC Zero verifier.

.
├── apps
│   ├── Cargo.toml
│   ├── src
│   └── bin
│       └── client                  // CLI client for interacting with the mixer
│           ├── abi.rs              // defines ABI types used when interacting with the chain
│           ├── deposit.rs          // deposit logic (spending key generation, tx submission)
│           ├── main.rs             // CLI entry point and arg parsing
│           └── withdraw.rs         // withdrawal logic (merkle tree reconstruction, proof generation, tx submission)
├── core                            // Crate with common functionality between the client and the guest program
│   ├── Cargo.toml
│   └── src
│       └── lib.rs                  // exports the `ProofInput` type and encoding helpers
├── contracts
│   ├── Mixer.sol                   // Mixer implementation, this checks the proofs and stores the merkle tree and nullifiers
│   ├── EthMixer.sol                // Mixer impl specific to using Eth (rather than an erc20 token)
│   ├── MerkleTreeWithHistory.sol   // Incremental merkle tree implementation. Modified to use sha2
│   └── ImageID.sol                 // Generated contract with the image ID for your zkVM program
├── methods
│   ├── Cargo.toml
│   ├── guest
│   │   ├── Cargo.toml
│   │   └── src
│   │       └── bin
│   │           └── can_spend.rs     // Guest program for performing a note spend check
│   └── src
│       └── lib.rs                   // Compiled image IDs and tests for the guest program
├── tests
│    └── MerkleTree.t.sol            // Tests ensuring compatibility between on-chain and off-chain merkle tree
└── justfile                          // commands for the repo (similar to a makefile)

Getting this to work also required some modifications to the incremental merkle tree crate to make it generic over the hash function and to allow verifying proofs without reconstructing the whole tree. See the diff.

Running the Demo!

First, install Rust and Foundry, and then restart your terminal.

# Install Rust
curl https://sh.rustup.rs -sSf | sh
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash

To install rzup, run the following command and follow the instructions:

curl -L https://risczero.com/install | bash
rzup

This repo uses the just command runner. Install it with:

cargo install just

Build the Code

  • Update git submodules.

    git submodule update --init
  • Builds for zkVM program and the client app

    cargo build
  • Build your Solidity smart contracts.

    NOTE: cargo build needs to run first to generate the ImageID.sol contract.

    forge build

Run Locally

The easiest way to demo the mixer is using a local anvil devnet. Start an anvil instance and keep it running:

just start-devnet

In another shell deploy the mixer contract with:

just deploy

and get the contract address from the deploy output

== Logs ==
  You are deploying on ChainID 31337
  Deployed RiscZeroGroth16Verifier to 0x9A676e781A523b5d0C0e43731313A708CB607508
  Deployed EthMixer to 0x0B306BF915C4d645ff596e518fAf3F9669b97016 <-----------THIS ONE

Set the contract and Bonsai API (optional, required for non-x86 arch) values in the .env.anvil file

export CONTRACT=""
...
export BONSAI_API_KEY="YOUR_API_KEY"

Depositing Eth to the mixer

Once the above is set up you can deposit 1 eth from the anvil test account by running:

just deposit

This will perform the secret generation and send a commitment along with 1 Eth to the mixer. The spending key hex will be written to std-out. Copy this for the next step.

Withdrawing from the mixer

To withdraw run with the spending key from above

just withdraw <spending-key-hex>

This needs to generate a spending proof so may take a minute or so. Once it has generated a proof it will submit it on-chain to be verified and if successful will trigger the withdrawal 1 note worth of Eth. Attempting to withdraw with the same spending key more than once will fail.

Run the Tests

  • Tests the zkVM program and client

    cargo test
  • Test the Solidity contracts

    forge test -vvv

References

About

A simple tornado-cash style coin mixer implemented using RISC Zero

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Solidity 50.2%
  • Rust 48.9%
  • Just 0.9%