A tornado-cash style coin mixer implemented using RISC Zero
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:
The depositor generates a nullifier,
This is submitted to the contract along with a pre-determined amount of eth (this example uses 1 Eth sized notes). The tuple (
Upon receiving the deposit the contract appends the note commitment to its internal incremental Merkle tree and stores the tree root,
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
In this case
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.
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.
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
-
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 theImageID.sol
contract.forge build
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"
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.
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.
-
Tests the zkVM program and client
cargo test
-
Test the Solidity contracts
forge test -vvv