ZK Circuits
Technical specification of the Groth16 circuits used for privacy-preserving withdrawals.
Withdraw Circuit
The withdraw.circom circuit proves knowledge of a valid deposit secret without revealing it. It uses Poseidon hashing for ZK-friendly cryptographic operations.
pragma circom 2.1.5;
include "../node_modules/circomlib/circuits/poseidon.circom";
template Withdraw() {
// Private inputs (only prover knows)
signal input secret;
signal input nullifier;
// Public inputs (on-chain verifiable)
signal input commitment; // Poseidon(secret, nullifier)
signal input nullifierHash; // Hash of nullifier (prevents reuse)
signal input recipient; // Withdrawal recipient address
signal input amount; // Amount to withdraw
// Compute the commitment from secret and nullifier
component poseidon = Poseidon(2);
poseidon.inputs[0] <== secret;
poseidon.inputs[1] <== nullifier;
// Verify computed commitment matches public commitment
commitment === poseidon.out;
// Compute nullifier hash
component nullifierHasher = Poseidon(1);
nullifierHasher.inputs[0] <== nullifier;
// Verify nullifier hash matches
nullifierHash === nullifierHasher.out;
}
component main {public [commitment, nullifierHash, recipient, amount]} = Withdraw();Circuit Metrics
| Metric | Value | Description |
|---|---|---|
| Constraints | ~1,600 | Total R1CS constraints |
| Private Inputs | 2 | secret, nullifier |
| Public Inputs | 4 | commitment, nullifierHash, recipient, amount |
| Gas Cost | ~200,000 | On-chain verification cost |
| Proof Time | ~5 seconds | Browser generation time |
Poseidon Hash Function
Poseidon is a ZK-friendly hash function designed for efficient circuit implementation.
Why Poseidon?
- ‣~8x fewer constraints than SHA256 in ZK circuits
- ‣Native field arithmetic (BN254 curve)
- ‣Security level: 128-bit
- ‣Standardized in circomlib
Parameters
- ‣Curve: BN254
- ‣State width (t): 3 (for 2 inputs)
- ‣Rounds: 8 full + 57 partial
- ‣S-box: x^5
Trusted Setup
Groth16 requires a trusted setup ceremony. The keys are generated in two phases:
Circuit to R1CS
Universal setup
Circuit-specific
Verification key
Circuit to R1CS
Universal setup
Circuit-specific
Verification key
# 1. Compile circuit to R1CS
circom withdraw.circom --r1cs --wasm --sym -o build/
# 2. Powers of Tau (universal setup)
snarkjs powersoftau new bn128 14 pot14_0000.ptau
snarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau
# 3. Phase 2 (circuit-specific)
snarkjs groth16 setup withdraw.r1cs pot14_final.ptau withdraw_0000.zkey
snarkjs zkey contribute withdraw_0000.zkey withdraw_final.zkey
# 4. Export verification key
snarkjs zkey export verificationkey withdraw_final.zkey verification_key.json
# 5. Generate Solidity verifier
snarkjs zkey export solidityverifier withdraw_final.zkey Groth16Verifier.solFile Locations
The circuit files are organized in the following structure:
public/circuits/
├── withdraw_js/
│ └── withdraw.wasm # WASM for proof generation (browser)
├── keys/
│ ├── withdraw_final.zkey # Proving key (3.5 MB)
│ └── verification_key.json # Verification key
└── withdraw.r1cs # Circuit constraints
contracts/src/
├── Groth16Verifier.sol # On-chain verifier (auto-generated)
└── Groth16VerifierAdapter.sol # Interface adapterProof Generation Flow
Proofs are generated client-side in the browser using snarkjs:
WASM + zkey
Hash nullifier
~5 seconds
Solidity calldata
WASM + zkey
Hash nullifier
~5 seconds
Solidity calldata
import * as snarkjs from 'snarkjs'
async function generateWithdrawProof(
secret: bigint,
nullifier: bigint,
commitment: bigint,
recipient: string,
amount: bigint
) {
// Load circuit files
const wasmPath = '/circuits/withdraw_js/withdraw.wasm'
const zkeyPath = '/circuits/keys/withdraw_final.zkey'
// Compute nullifier hash
const nullifierHash = poseidon([nullifier])
// Generate proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
{
secret,
nullifier,
commitment,
nullifierHash,
recipient: BigInt(recipient),
amount
},
wasmPath,
zkeyPath
)
return { proof, publicSignals }
}On-Chain Verification
The Groth16Verifier contract uses BN254 elliptic curve pairings to verify proofs:
| Component | Description | Gas Cost |
|---|---|---|
| Proof Points | A (G1), B (G2), C (G1) | ~10,000 |
| Verification Key | Embedded in contract | 0 (storage) |
| Pairing Check | e(A,B) = e(α,β) * e(vk,γ) * e(C,δ) | ~180,000 |
| Public Signals | Validate 4 public inputs | ~10,000 |