返回 Expert 笔记
Expert Day 223

Circom入门 — 语法、template、signal

Circom 2.x 语法、template/signal/component 模型、constraint vs witness、quadratic constraints

2026-12-10
Phase 4 - ZK电路开发实战 (Day 223-243)
ZKCircomsnarkjscircuit-DSLpassword-proof

日期: 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 编译器输出三类产物:
    1. .r1cs — 约束系统二进制
    2. .wasm — witness 计算器(用于 prover 端)
    3. .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 inputprivate 输入(默认)signal input password;
signal input + main pubpublic 输入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 KBR1CS 约束系统二进制(含 215 个约束)
password.wasm~50 KBwitness 计算器,prover 端用
password.sym~10 KBwire → 名字映射,debug 用
witness.wtns~7 KB实际 witness 值
password_final.zkey~80 KBproving key
verification_key.json~3 KBverifying key
proof.json~800 BGroth16 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 size192 bytes (3 × 64)2 × G1 + 1 × G2,bn128
Solidity verifier gas~230,000主要消耗在 pairing() 预编译
witness 计算时间<100 ms215 约束规模
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

  1. Q: Circom 的 <--<== 有什么区别?为什么生产代码必须用 <== A: <-- 仅做 witness 赋值,不会生成 R1CS 约束;<== 既赋值也生成约束。如果用 <-- 而忘记加 ===,prover 可以提交任意值,破坏 soundness。zkSync v1 早期就因此被审计指出漏洞。

  2. 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 约束即可。

  3. 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。