Workshop Tasks
-
Task 1: CommitmentHasher
- Building secure commitments
- Understanding Pedersen hash
-
Task 2: HashLeftRight
- MiMC hash implementation
- Binary tree basics
-
Task 3: MerkleTreeChecker
- Merkle tree verification
- Path validation
-
Task 4: Withdraw Circuit
- Combining all components
- Final implementation
- Tornado Cash Whitepaper - The foundational paper we're implementing
- Circomlib Documentation - Library of circuits we'll use
- Hardhat-ZKit - Our testing framework
You can choose between two setup methods:
- Node.js >= 18
- Circom
- Basic familiarity with terminal
- Basic understanding of:
- Cryptographic hash functions
- Smart contracts (basic)
# Clone the repository
git clone https://github.com/vaniiiii/zk-workshop
cd zk-workshop
# Install dependencies
npm install
# Verify installation
npx hardhat help
npx hardhat zkit
npx hardhat compile
npx hardhat zkit compile
npx hardhat clean
# Verify Circom installation
circom --help
circom --version- Docker installed on your system
- Git
- Basic familiarity with terminal
# Clone the repository
git clone https://github.com/vaniiiii/zk-workshop
cd zk-workshopStart Docker container:
For macOS/Linux:
docker run -it --rm -v $(pwd):/workspace vani0xff/zk-workshop bashFor Windows PowerShell:
docker run -it --rm -v ${PWD}:/workspace vani0xff/zk-workshop bashIf Windows path issues occur, use full path:
docker run -it --rm -v C:\full\path\to\workshop:/workspace vani0xff/zk-workshop bash- Edit files locally in your preferred editor
- Run all commands in the Docker terminal
- To exit Docker: type
exit - To restart: run the docker run command again
- Verify setup inside Docker container:
circom --help circom --version
A mixer is a privacy protocol that enables users to break the on-chain link between source and destination addresses. It works like a pool where many users deposit fixed amounts of tokens, and later withdraw them to different addresses, making it impossible to trace which deposit corresponds to which withdrawal.
- User generates two random values:
nullifierandsecret - Creates a commitment (hash of both values)
- Deposits tokens along with the commitment
- Commitment gets added to a Merkle tree
- User provides a zero-knowledge proof showing:
- They know a secret corresponding to a commitment in the tree
- They haven't withdrawn these tokens before (using nullifier)
- If proof verifies, tokens are sent to a new address
- Commitments hide the actual secret values
- Merkle tree proves membership without revealing which leaf
- Nullifiers prevent double-spending without linking to deposit
Our implementation consists of four main components:
- CommitmentHasher: Creates commitment and nullifier hashes
- HashLeftRight: Hash two values using MiMc hash function
- MerkleTreeChecker: Verifies membership proofs
- Withdraw: Main circuit combining all components
circuits/
βββ lib/ # Template implementations (edit these)
β βββ commitment_hasher.circom
β βββ hash_left_right.circom
β βββ merkletree_checker.circom
β βββ withdraw.circom
βββ commitment_hasher.circom # Entry points (do not edit)
βββ hash_left_right.circom
βββ merkletree_checker.circom
βββ withdraw.circom
All workshop tasks are completed in the circuits/lib/ directory. The entry point files in circuits/ handle circuit instantiation.
The workshop consists of two parts:
In this part, we'll implement the core zero-knowledge circuits that power the mixer. You'll build each component step by step, with tests to verify your implementation.
After completing the circuits, we'll explore the smart contract side. This includes:
- Deposit and withdrawal mechanics
- On-chain verification
- Complete system integration
The full implementation can be found in scripts/withdraw.ts.
π circuits/lib/commitment_hasher.circom
A circuit that creates two different hashes:
- A commitment that securely combines nullifier and secret
- A nullifier hash that serves as a public identifier
- Uses Pedersen Hash, a specialized commitment scheme that:
- Maps inputs to points on an elliptic curve
- Combines points using curve arithmetic
- Provides:
- Perfect hiding: Output reveals nothing about inputs
- Computational binding: Practically impossible to find different inputs with same output
- Efficient for ZK circuits
π‘ Need a hint for CommitmentHasher?
- Structure:
component nullifierBits = Num2Bits(248);
component secretBits = Num2Bits(248);
component commitmentHasher = Pedersen(496); // Why 496?
component nullifierHasher = Pedersen(248);- Think about:
- How to connect bits between components
- Why nullifierHash only needs nullifier bits
- Order of bits in commitment
npx hardhat zkit compile
npx hardhat test test/commitmentHasher.t.tsπ circuits/lib/hash_left_right.circom
A circuit that combines two inputs into one hash, used as the building block for our Merkle tree.
- Uses MiMCSponge hash function:
- Optimized for ZK circuits
- Fewer constraints than Pedersen
- Perfect for binary tree structures
π‘ Need a hint for HashLeftRight?
- Structure:
component hasher = MiMCSponge(2, 220, 1);
// Think about:
// - How to connect inputs
// - Which output to use
// - What the k parameter meansnpx hardhat zkit compile
npx hardhat test test/hashLeftRight.t.tsπ circuits/lib/merkletree_checker.circom
A circuit that verifies a value exists in a Merkle tree without revealing which leaf it is.
- Uses path verification
- Implements selective hash combination
- Handles dynamic positioning with DualMux
π‘ Need a hint for MerkleTreeChecker?
- Structure:
component selectors[levels];
component hashers[levels];
// Think about:
// - How to initialize components
// - How to connect between levels
// - Final root comparisonnpx hardhat zkit compile
npx hardhat test test/merkleTreeChecker.t.tsπ circuits/lib/withdraw.circom
The main circuit that ties everything together to enable private withdrawals.
- Combines commitment verification
- Implements membership proof
- Ensures single-use through nullifiers
- Maintains zero-knowledge properties
π‘ Need a hint for Withdraw?
- Structure:
component hasher = CommitmentHasher();
component tree = MerkleTreeChecker(levels);
// Think about:
// - How to verify nullifierHash
// - How to connect commitment to tree
// - Why we need recipient constraintnpx hardhat zkit compile
npx hardhat zkit verifiers
npx hardhat test test/withdraw.t.tsUse logging for debugging:
// In circuits
log("Value of signal:", signal);
log("Debug point reached");// In tests
console.log("Witness output:", witness);
console.log("Circuit inputs:", input);Clear cache and compile again:
npx hardhat clean
npx hardhat zkit compileTo test your circuit with the smart contract implementation:
# Generate Solidity verifier
npx hardhat zkit verifiers
# Deploy and test the contract
npx hardhat run scripts/withdraw.ts