name: circom-dev description: Zero-knowledge proof circuit development using circom. Use when implementing arithmetic circuits for zkSNARKs, including circuit design, constraint verification, witness generation, and testing. Triggers on tasks like "create a circom circuit", "implement ZK proof", "write Merkle tree circuit", "add range proof", or any zero-knowledge cryptography implementation requiring circom.
Circom Development
Expert guidance for designing, implementing, and testing zero-knowledge proof circuits using circom.
Overview
This skill provides comprehensive support for circom circuit development:
- Circuit Implementation: Design and write optimized arithmetic circuits
- Constraint Verification: Ensure circuits are properly constrained and secure
- Witness Generation: Create and test witness calculators
- Testing: Comprehensive testing patterns for circuits
- Best Practices: Security guidelines and optimization techniques
Quick Start
1. Project Setup
Initialize a new circom project with required dependencies:
# Copy package.json template
cp assets/package.json ./package.json
# Install dependencies
npm install
# Create project structure
mkdir -p circuits build scripts
2. Create Circuit
Use the circuit template as a starting point:
cp assets/template_circuit.circom circuits/your_circuit.circom
Edit the circuit with your logic:
pragma circom 2.0.0;
include "node_modules/circomlib/circuits/poseidon.circom";
template YourCircuit() {
signal input in;
signal output out;
component hasher = Poseidon(1);
hasher.inputs[0] <== in;
out <== hasher.out;
}
component main = YourCircuit();
3. Compile Circuit
Use the compilation script:
bash scripts/compile_circuit.sh circuits/your_circuit.circom
This generates:
build/your_circuit_js/your_circuit.wasm- Witness calculatorbuild/your_circuit.r1cs- Constraint systembuild/your_circuit.sym- Symbol mapping
4. Setup Proving Keys
Generate zkey and verification key:
bash scripts/setup_keys.sh build/your_circuit.r1cs
This generates:
build/zkey/your_circuit.zkey- Proving keybuild/zkey/verification_key.json- Verification keybuild/zkey/your_circuit_verifier.sol- Solidity verifier
5. Create Test
Use the test template:
cp assets/template_test.js test.js
Update test inputs and run:
node test.js
Workflow Decision Tree
┌─────────────────────────────────────┐
│ What do you need to do? │
└──────────────┬──────────────────────┘
│
┌───────┴────────┐
│ │
New Circuit Modify Existing
│ │
▼ ▼
Start from Read existing
template circuit first
│ │
▼ ▼
Implement Understand
constraints constraints
│ │
▼ ▼
Compile Make changes
│ │
▼ ▼
Setup keys Recompile
│ │
▼ ▼
Write tests Update tests
│ │
└────────┬───────┘
▼
Run & verify
│
┌───────┴────────┐
│ │
Success Failure
│ │
▼ ▼
Complete Debug & fix
│
└──> Repeat
Circuit Design Guidelines
1. Define Requirements
Before writing code, clarify:
- Private inputs: What information must remain secret?
- Public inputs: What can be revealed to the verifier?
- Outputs: What statement are you proving?
- Constraints: What rules must be enforced?
Example: Password authentication
- Private: password
- Public: passwordHash
- Statement: "I know a password that hashes to passwordHash"
2. Choose Components
Consult circomlib_components.md for standard components:
- Hashing: Poseidon, MiMC
- Comparisons: IsZero, LessThan, IsEqual
- Merkle Trees: SMTVerifier
- Signatures: EdDSA
3. Write Constraints
Follow these principles:
Use <== for most operations (assigns AND constrains):
output <== input1 * input2;
Use === for explicit constraints:
component.out === expectedValue;
NEVER use <-- alone (no constraint):
// DANGEROUS - prover can cheat!
temp <-- unconstrained_value;
4. Validate Inputs
Always constrain input ranges:
// Ensure value is less than maximum
component check = LessThan(32);
check.in[0] <== value;
check.in[1] <== maxValue;
check.out === 1;
See best_practices.md for security guidelines.
Common Circuit Patterns
Consult circuit_patterns.md for complete implementations:
Authentication
- Password proof: Prove knowledge of password without revealing it
- Credential verification: Prove possession of valid credentials
Merkle Trees
- Membership proof: Prove element is in a set without revealing which
- Tree update: Prove correct update of Merkle tree
Range Proofs
- Value in range: Prove value is within bounds (e.g., age > 18)
- Balance sufficiency: Prove sufficient balance without revealing amount
Voting
- Anonymous voting: Vote without revealing identity
- Weighted voting: Vote with weight based on holdings
Privacy
- Private transfer: Transfer funds without revealing sender/receiver/amount
- Nullifier pattern: Prevent double-spending
Testing Workflow
1. Unit Test Components
Test individual templates in isolation:
const circuit = await wasm_tester("circuits/component.circom");
// Test valid input
const input = { in: 10 };
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
// Test expected output
await circuit.assertOut(witness, { out: 100 });
2. Test Edge Cases
Always test:
- Zero values:
{ in: 0 } - Maximum values: Near field prime
- Boundary conditions: Min/max range values
- Invalid inputs: Should fail constraint checks
3. Integration Testing
Test full proof generation and verification:
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
wasmPath,
zkeyPath
);
const vKey = JSON.parse(fs.readFileSync(vkeyPath));
const isValid = await snarkjs.groth16.verify(vKey, publicSignals, proof);
assert(isValid);
Use scripts/verify_proof.js for standalone verification:
node scripts/verify_proof.js -p proof.json -s public.json -v verification_key.json
Optimization Techniques
1. Minimize Constraints
Fewer constraints = faster proving time.
Check constraint count:
npx snarkjs r1cs info build/circuit.r1cs
Optimization tips:
- Reuse components when possible
- Minimize hash operations (expensive)
- Use
assertfor compile-time checks (no runtime cost) - Combine operations where possible
2. Choose Efficient Components
- Poseidon > MiMC: Poseidon is optimized for zkSNARKs
- Bit operations: Use Num2Bits/Bits2Num for bit manipulation
- Range checks: Use SafeLessThan for range-checked comparisons
3. Profile Circuit
Monitor statistics:
npx snarkjs r1cs info circuit.r1cs
Output shows:
- Number of constraints
- Number of public inputs/outputs
- Number of private inputs
- Number of wires
Security Checklist
Before deploying, verify:
- All signals properly constrained (no
<--without verification) - Input ranges validated
- Division operations properly constrained (with remainder check)
- Public/private signals correctly specified
- Edge cases tested (0, max values, boundaries)
- No under-constrained circuits (verify constraint count)
- Code reviewed
- Static analysis tools run (Circomspect, PICUS if available)
See best_practices.md for detailed security guidelines.
Troubleshooting
Common Issues
"Constraint doesn't match"
- Check all signals are properly constrained with
<==or=== - Verify arithmetic is correct
- Look for signals using
<--without corresponding constraints
"Not enough values"
- Ensure all inputs are provided in test
- Check array sizes match template parameters
"Scalar size exceeds field size"
- Input value is too large for the field
- Use range constraints to validate inputs
High constraint count
- Review circuit for optimization opportunities
- Consider refactoring complex logic
- Check for unnecessary component instantiations
Debugging Tips
- Add debug signals: Create intermediate signals to inspect values in witness
- Use circom logger: Add
log()statements in circuit - Test incrementally: Build circuit piece by piece, testing each addition
- Verify constraint count: Monitor constraint growth as you add logic
Scripts Reference
compile_circuit.sh
Compile circom circuits to WASM and R1CS.
./scripts/compile_circuit.sh <circuit_file> [options]
Options:
-o, --output DIR Output directory (default: build)
-h, --help Show help
setup_keys.sh
Generate proving and verification keys.
./scripts/setup_keys.sh <r1cs_file> [options]
Options:
-s, --size N Circuit size (default: 12)
-o, --output DIR Output directory (default: build/zkey)
-p, --ptau DIR ptau directory (default: ptau)
-h, --help Show help
verify_proof.js
Verify a zero-knowledge proof.
node scripts/verify_proof.js [options]
Options:
-p, --proof FILE Proof JSON file
-s, --signals FILE Public signals JSON file
-v, --vkey FILE Verification key file
-h, --help Show help
References
This skill includes detailed reference documentation:
circomlib_components.md
Standard library component reference:
- Hash functions (Poseidon, MiMC)
- Comparators (IsZero, LessThan, IsEqual)
- Multiplexers (Mux1, Mux3)
- Bitwise operations (Num2Bits, Bits2Num)
- Merkle trees (SMTVerifier)
- Signatures (EdDSA)
best_practices.md
Security and optimization guidelines:
- Constraint writing best practices
- Security considerations
- Optimization techniques
- Testing and debugging
- Common pitfalls and how to avoid them
circuit_patterns.md
Implementation patterns for common use cases:
- Authentication patterns
- Merkle tree patterns
- Range proof patterns
- Voting patterns
- Privacy-preserving patterns
Example: Complete Workflow
Here's a complete example of implementing a password authentication circuit:
// 1. Create circuit: circuits/password_auth.circom
pragma circom 2.0.0;
include "node_modules/circomlib/circuits/poseidon.circom";
template PasswordAuth() {
signal input password;
signal input passwordHash;
component hasher = Poseidon(1);
hasher.inputs[0] <== password;
passwordHash === hasher.out;
}
component main {public [passwordHash]} = PasswordAuth();
# 2. Compile
bash scripts/compile_circuit.sh circuits/password_auth.circom
# 3. Setup keys
bash scripts/setup_keys.sh build/password_auth.r1cs
// 4. Create test: test_password.js
const snarkjs = require("snarkjs");
const circomlibjs = require("circomlibjs");
async function test() {
// Calculate expected hash
const password = 12345;
const poseidon = await circomlibjs.buildPoseidon();
const passwordHash = poseidon.F.toString(poseidon([password]));
// Generate proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
{ password, passwordHash },
"build/password_auth_js/password_auth.wasm",
"build/zkey/password_auth.zkey"
);
// Verify
const vKey = require("./build/zkey/verification_key.json");
const isValid = await snarkjs.groth16.verify(vKey, publicSignals, proof);
console.log("Valid:", isValid);
}
test();
# 5. Run test
node test_password.js
Additional Resources
- Circom Documentation: https://docs.circom.io/
- circomlib Repository: https://github.com/iden3/circomlib
- snarkjs: https://github.com/iden3/snarkjs
- 0xPARC Learning: https://learn.0xparc.org/
- ZK Whiteboard Sessions: https://zkhack.dev/whiteboard/