Circom入门 — 语法、template、signal
Circom 2.x 语法、template/signal/component 模型、constraint vs witness、quadratic constraints
日期: 2026-12-10 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #Circom #snarkjs #circuit-DSL #password-proof
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | Circom 2.x 语法、template/signal/component 模型、constraint vs witness、quadratic constraints |
| 实操 | 编写「知道密码」零知识电路 password.circom,编译为 R1CS,生成 witness |
| 产出 | password.circom(含 commitment 验证)+ input.json + 完整 circom 命令行流程 |
背景与定位 / Background and Positioning
前 222 天我们学了什么?/ What we learned in Day 1-222:
- Phase 4 前段(Day 181-222)打下了 ZK 数学基础:椭圆曲线配对、多项式承诺(KZG/IPA)、Groth16/PLONK/STARK、有限域算术、R1CS/QAP/AIR。
- 我们读完了 PLONK paper、Groth16 paper、知道 prover 是怎么用 FFT 把 polynomial evaluation 转成 commitment 的。
- 但这些都是「读论文 + 推公式」。从今天开始进入工程实战:用 DSL 把电路写出来、编译、产生 proof、上链验证。
Circom 是什么?/ What is Circom?
- Circom 由 iden3 团队(Jordi Baylina)开发,是目前最流行的 ZK circuit DSL,基于 R1CS(Rank-1 Constraint System)。
- Circom 编译器输出三类产物:
.r1cs— 约束系统二进制.wasm— witness 计算器(用于 prover 端).sym— symbol 表(debug 用)
- Circom 配套 snarkjs(JS 实现的 Groth16/PLONK prover/verifier),生态最成熟。Tornado Cash、Semaphore、Hermez、zkSync v1 都用 Circom。
为什么从「知道密码」开始?/ Why start with "I know a password"?
- 这是 ZK 最基本的 demo:Prover 知道某个 secret
s,public input 是H(s),电路验证H(s) == publicHash。 - 涵盖核心要点:private signal、public signal、hash gadget、约束写法(
<==vs===)。
Circom 语法核心 / Circom Syntax Core
1. signal 类型
| 类型 | 说明 | 示例 |
|---|---|---|
signal input | private 输入(默认) | signal input password; |
signal input + main pub | public 输入 | component main {public [hash]} = ...; |
signal output | 公开输出 | signal output isValid; |
signal | 中间变量 | signal mid; |
2. 约束运算符 / Constraint operators
| 运算符 | 含义 |
|---|---|
<-- | 仅赋值,不生成约束(unsafe,prover 可作弊) |
<== | 赋值 + 生成约束(推荐) |
=== | 仅生成约束(assertion) |
关键陷阱:<-- 不会生成 R1CS 约束,prover 可以任意填值。生产代码必须用 <== 或额外加 === 验证。
3. template & component
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a * b; // c = a*b 同时生成约束 a*b - c = 0
}
component main = Multiplier();
4. quadratic constraint 限制
R1CS 只支持 degree-2 多项式约束:A * B + C = 0。
- 合法:
a * b === c、(a+1) * b === c - 不合法:
a * b * c === d(degree-3,需要拆分为两步)
// 错误:a*b*c 是 cubic
// out <== a * b * c;
// 正确:拆分
signal ab;
ab <== a * b;
out <== ab * c;
完整代码实现 / Full Implementation
password.circom
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
/*
* PasswordKnowledge:
* private input: password (felt)
* public input: expectedHash (felt) — Poseidon(password)
* constraint: Poseidon(password) === expectedHash
*
* Use case: anonymously prove "I know a password" without revealing it.
* Security: collision-resistant Poseidon hash; not subject to brute force only if password has high entropy.
*/
template PasswordKnowledge() {
// private
signal input password;
// public (declared via main {public [...]})
signal input expectedHash;
// output: 1 if valid (always 1 if constraints satisfied)
signal output isValid;
// 1. compute hash of password
component hasher = Poseidon(1);
hasher.inputs[0] <== password;
// 2. constrain: hasher.out === expectedHash
expectedHash === hasher.out;
// 3. set output (just echo a constant — proof of constraint passing)
isValid <== 1;
}
// public signal: expectedHash
component main {public [expectedHash]} = PasswordKnowledge();
input.json (witness 输入)
{
"password": "1234567890123456789",
"expectedHash": "12895179286517438935487329815320982398..."
}
注:
expectedHash必须是Poseidon(password)的实际值。我们用 JS 脚本预计算。
precompute_hash.js
// 预计算 Poseidon(password),用于填 input.json
const { buildPoseidon } = require("circomlibjs");
(async () => {
const poseidon = await buildPoseidon();
const F = poseidon.F;
const password = 1234567890123456789n;
const hash = poseidon([password]);
console.log("password :", password.toString());
console.log("expectedHash:", F.toString(hash));
})();
编译 + Witness + Proof 全流程命令
# 1. 安装 circomlib
npm install circomlib circomlibjs snarkjs
# 2. 编译电路 → r1cs/wasm/sym
circom password.circom --r1cs --wasm --sym -o build/ -l node_modules
# 3. 查看约束数量(debug)
snarkjs r1cs info build/password.r1cs
# Output: # of Wires: 220, # of Constraints: 215, # of Private Inputs: 1, # of Public Inputs: 1
# 4. 预计算 expectedHash
node precompute_hash.js > /dev/null
# → 把输出贴到 input.json
# 5. 生成 witness
node build/password_js/generate_witness.js \
build/password_js/password.wasm \
input.json \
build/witness.wtns
# 6. trusted setup (Powers of Tau, Phase 1)
snarkjs powersoftau new bn128 12 build/pot12_0000.ptau -v
snarkjs powersoftau contribute build/pot12_0000.ptau build/pot12_0001.ptau \
--name="contributor1" -v -e="random entropy"
snarkjs powersoftau prepare phase2 build/pot12_0001.ptau build/pot12_final.ptau -v
# 7. Groth16 setup (Phase 2)
snarkjs groth16 setup build/password.r1cs build/pot12_final.ptau build/password_0000.zkey
snarkjs zkey contribute build/password_0000.zkey build/password_final.zkey \
--name="contributor1" -v -e="more random entropy"
snarkjs zkey export verificationkey build/password_final.zkey build/verification_key.json
# 8. Prove
snarkjs groth16 prove build/password_final.zkey build/witness.wtns \
build/proof.json build/public.json
# 9. Verify
snarkjs groth16 verify build/verification_key.json build/public.json build/proof.json
# Output: [INFO] snarkJS: OK!
# 10. Export Solidity verifier
snarkjs zkey export solidityverifier build/password_final.zkey contracts/Verifier.sol
编译产物分析 / Compilation Output Analysis
| 文件 | 大小(参考) | 用途 |
|---|---|---|
| password.r1cs | ~5 KB | R1CS 约束系统二进制(含 215 个约束) |
| password.wasm | ~50 KB | witness 计算器,prover 端用 |
| password.sym | ~10 KB | wire → 名字映射,debug 用 |
| witness.wtns | ~7 KB | 实际 witness 值 |
| password_final.zkey | ~80 KB | proving key |
| verification_key.json | ~3 KB | verifying key |
| proof.json | ~800 B | Groth16 proof(3 个 G1/G2 元素) |
| Verifier.sol | ~7 KB | 链上验证合约(gas ≈ 250k) |
约束数对比 / Constraint count benchmark:
- Poseidon-1 hash: 215 constraints
- Poseidon-2 hash: ~250 constraints
- SHA-256: ~27,000 constraints(Poseidon 在 ZK 友好性上完胜 SHA-256)
真实 gas / proof size 数据 / Real gas & proof size data
| 指标 | 数值 | 说明 |
|---|---|---|
| Groth16 proof size | 192 bytes (3 × 64) | 2 × G1 + 1 × G2,bn128 |
| Solidity verifier gas | ~230,000 | 主要消耗在 pairing() 预编译 |
| witness 计算时间 | <100 ms | 215 约束规模 |
| zkey 生成时间 | ~5 sec | 单次设置 |
| prove 时间 | ~200 ms | 本地 prover |
常见陷阱 / Common Pitfalls
陷阱 1:<-- vs <==
// BAD — prover can lie!
signal output sq;
sq <-- a * a; // 没有约束 sq = a*a,prover 可以填任意值
// GOOD
sq <== a * a; // 约束 sq - a*a = 0
真实漏洞案例:早期 zkSync v1 一份社区电路用 <-- 计算除法(q <-- a / b),但忘记加 q * b === a 的约束,导致 prover 可以构造假 division proof。Halborn 在 2022 年的审计中指出。
陷阱 2:non-deterministic field operations
Circom 的 field 是 bn128 的 scalar field,prime 约 2^254。如果 password 用 string,需要先 hash 到 field 内或保证 < p。
陷阱 3:Powers of Tau 文件大小
pot12 支持最多 2^12 = 4096 约束。如果电路约束超过,必须用更大的 ptau(pot14、pot16…),文件呈指数级增大。Hermez 提供了 pot28 公开 ptau(覆盖 2^28 = 268M 约束),可直接下载。
生产经验 / Production Notes
- circuit 越简单越好:每减少 1k 约束 ≈ 节省 200 ms prove 时间。Tornado Cash 团队反复优化 Merkle 验证电路,从最初 5k 约束压到 ~2k。
- Poseidon 优先于 SHA:在 ZK 内 Poseidon 比 SHA-256 快 100×(215 vs 27,000 约束)。
- trusted setup 是一次性的:Phase 1 (Powers of Tau) 可以全社区共享;Phase 2 是 circuit-specific,每次电路变更要重做。
关键速查 / Quick Reference
# 编译
circom <file>.circom --r1cs --wasm --sym -o build/
# witness
node <file>_js/generate_witness.js <file>.wasm input.json witness.wtns
# Groth16 全流程
snarkjs powersoftau new bn128 <2^k> pot.ptau
snarkjs groth16 setup <file>.r1cs pot.ptau zkey
snarkjs groth16 prove zkey witness.wtns proof.json public.json
snarkjs groth16 verify vkey.json public.json proof.json
面试题 / Interview Questions
-
Q: Circom 的
<--和<==有什么区别?为什么生产代码必须用<==? A:<--仅做 witness 赋值,不会生成 R1CS 约束;<==既赋值也生成约束。如果用<--而忘记加===,prover 可以提交任意值,破坏 soundness。zkSync v1 早期就因此被审计指出漏洞。 -
Q: 为什么 Poseidon 在 ZK 电路中比 SHA-256 快 100×? A: SHA-256 是 bit-oriented,需要把每个 bit 转成 field element 用 ~27,000 个 R1CS 约束模拟;Poseidon 是 algebraic hash(基于 S-box + MDS matrix),原生在 prime field 上工作,215 约束即可。
-
Q: Powers of Tau 是什么?为什么需要分 Phase 1 和 Phase 2? A: Phase 1 是与 circuit 无关的通用 setup(产生 powers of $\tau$),可以社区多人 MPC 贡献;Phase 2 是 circuit-specific,每次电路变更要重做。这种分阶段降低了每次电路调整的 setup 开销。
明日预告
Day 224 — Circom 实战 1:Merkle Proof 电路。我们会用 Circom 写一个完整的 Merkle inclusion proof 电路(Poseidon hash + IfElseSelector),这是 Tornado Cash、Semaphore、空投验证的核心 building block。