零知识证明(Zero-Knowledge Proof, ZKP)被誉为密码学的"圣杯":证明者可以不泄露任何信息地让验证者相信某个陈述为真。它既是隐私币(Zcash、Tornado Cash)的基石,也是 zkRollup 扩容方案与新型身份体系的引擎。本文将从数学直觉出发,拆解 zk-SNARK/zk-STARK 的异同,并带你在 Circom 中编写第一个电路。
一、零知识证明的核心思想
什么是零知识?
一个零知识证明系统包含两个角色:
证明者 Prover ──► 证明 π(不含任何秘密信息) ──► 验证者 Verifier
│ │
│ 知道秘密 w(如哈希原像) │ 只看到公开输入 x 与证明 π
│ 声称:"我知道 x 对应的秘密 w" │ 验证 π,接受或拒绝
一个安全的证明系统必须满足三条性质:
| 性质 | 英文 | 含义 |
|---|---|---|
| 完备性 | Completeness | 若陈述为真且证明者诚实,验证者必然接受 |
| 可靠性 | Soundness | 若陈述为假,恶意证明者几乎不可能伪造可接受的证明 |
| 零知识 | Zero-Knowledge | 验证者除"陈述为真"外,学不到任何关于秘密的信息 |
交互式 vs 非交互式
最早的零知识协议需要多轮交互(挑战-响应)。Fiat-Shamir 变换通过哈希模拟挑战,把交互协议转化为单条消息——这正是"非交互式"(Non-Interactive)的来源,也是链上验证的前提:
交互式:
Prover ──承诺──► Verifier ──随机挑战──► Prover ──响应──► Verifier 验证
Fiat-Shamir 变换:
Prover 用 hash(承诺) 作为"挑战",自己完成对话,输出 (承诺, 响应)
Verifier 重算 hash(承诺) 并校验响应
一句话:零知识证明的本质是"用多项式承诺与算术电路,把’我知道秘密’这一事实压缩成可验证的数学命题"。
二、zk-SNARK:简洁非交互式知识论证
定位
zk-SNARK(Succinct Non-interactive Argument of Knowledge)是目前链上使用最广泛的证明系统。其核心优势是证明体积小、验证极快,代价是通常需要可信设置(Trusted Setup)。
Groth16:经典的配对友好证明系统
Groth16(2016 年论文)是当前最紧凑的 zk-SNARK:
- 证明大小:128 字节(2 个 G1 元素 + 1 个 G2 元素,BN254 曲线)
- 验证:单个双线性配对方程,约 1-3 ms
- 可信设置:每个电路都需要一次设置仪式(Generate/Prove/Verify 三个 key)
- 用于 Zcash、Tornado Cash、以及大量 zkRollup 内部
Groth16 证明 π = (π_A ∈ G1, π_B ∈ G2, π_C ∈ G1)
验证方程(简化):
e(π_A, π_B) = e(g, h)^α · e(π_C, h) · e(vk_x, h)^γ · ...
其中 e 为双线性配对,vk_x 由公开输入决定
可信设置仪式(Trusted Ceremony)
可信设置的核心风险是:若参与者销毁了随机数(toxic waste),系统就是安全的;否则可伪造证明。因此:
- Zcash 2016 年仪式:6 名参与者,其中一人可攻破(后被审计修正)
- 2019 年 Powers of Tau 仪式:Power of Tau 支持通用设置(一次仪式服务所有电路),201 名参与者
- PLONK 等系统利用"结构化参考字符串"(SRS),将可信设置从"每电路"降到"每曲线"
一句话:Groth16 用"每电路一次可信设置"换来了全场最小证明与最快验证,适合对安全仪式有信心的场景。
三、zk-STARK:可扩展透明论证
zk-STARK(Scalable Transparent Argument of Knowledge)在 2018 年由 Eli Ben-Sasson 等人提出,从根上消除可信设置:
| 特性 | 具体表现 |
|---|---|
| 透明(Transparent) | 只用公开随机数,无需可信设置 |
| 抗量子 | 基于哈希(Merkle 树 + Reed-Solomon 编码),非椭圆曲线 |
| 证明大小 | 数十到数百 KB(简单语句约 45 KB,复杂语句可达 1 MB+) |
| 验证复杂度 | O(log² n),多项式对数级别 |
| 证明者复杂度 | 准线性(Prover 较慢,内存开销大) |
底层技术:FRI 协议
STARK 的核心是 FRI(Fast Reed-Solomon Interactive Oracle Proof):
1. 把计算约束编码为多项式 P(x)
2. 在大量点上对 P 求值(域扩张,提高冗余度)
3. 进行多轮"折叠":P(x) = P_even(x²) + x·P_odd(x²)
4. 每轮用哈希承诺,验证者随机抽查少量点
5. 最终用 Merkle 根固定整个轨迹
StarkWare(Starknet、dYdX v3)与 RISC Zero、Succinct(SP1)是主要工程推动者。
代表性进展
- STIR(2024):把 FRI 的证明规模再降一个数量级
- 递归证明:把多个证明"折叠"成一个,实现无限规模验证(如 Plonky2/Plonky3)
- 预计算证明:验证成本可摊销到每笔交易低于 100k gas(如 Polygon Miden)
一句话:STARK 用更大的证明体积换来了"无需信任任何仪式"与"抗量子",是未来证明系统的主航道之一。
四、SNARK vs STARK 对比
| 维度 | zk-SNARK(Groth16) | zk-SNARK(PLONK) | zk-STARK |
|---|---|---|---|
| 可信设置 | 每电路 | 通用(一次/曲线) | 无(透明) |
| 证明大小 | ~128 B | ~数百 B | 数十~数百 KB |
| 验证时间 | ~ms(1 次配对) | ~ms(多次配对) | ~ms(哈希运算) |
| Prover 速度 | 快 | 中 | 慢(内存高) |
| 抗量子 | 否(椭圆曲线) | 否 | 是(哈希) |
| 递归支持 | 困难 | 良好 | 良好(Plonky2 等) |
| 代表 | Zcash、Tornado | Aztec、zkSync(部分) | Starknet、RISC Zero |
一句话:选 Groth16 看重最小体积,选 PLONK 看重通用设置与递归,选 STARK 看重透明与抗量子。
五、电路与证明系统:Circom / SnarkJS / PLONK
算术电路:把程序变成多项式约束
零知识电路用约束(constraints)描述计算。Circom 2 是最流行的电路语言:
// 电路:证明"我知道秘密 s 使得 out = s * s - 1,且 s != 0"
pragma circom 2.1.9;
template SquareLessOne() {
signal input s;
signal output out;
signal sq <== s * s; // <== 生成乘法约束
out <== sq - 1; // 线性约束
// 约束 s != 0:引入逆元 inv,使 s * inv == 1
signal inv;
inv <-- s != 0 ? 1 / s : 0; // <-- 仅做赋值,不做约束
s * inv === 1; // === 显式约束:只有 s != 0 才可能
}
component main = SquareLessOne();
用 SnarkJS 完成全流程
# 1. 编译电路为 R1CS + wasm
circom square.circom --r1cs --wasm --sym -o build
# 2. 初始化 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="Alice" -v
# 3. Phase 2:为电路生成 zkey(PLONK/Groth16)
snarkjs plonk setup build/square.r1cs build/pot12_0001.ptau build/square.zkey
snarkjs zkey export verificationkey build/square.zkey build/verification_key.json
# 4. 计算 witness
echo '{"s": 5}' | node -e "const w=require('fs').readFileSync(0); process.stdout.write(w)" \
&& snarkjs wtns calculate build/square_js/square.wasm input.json build/witness.wtns
# 5. 生成证明并验证
snarkjs plonk prove build/square.zkey build/witness.wtns build/proof.json build/public.json
snarkjs plonk verify build/verification_key.json build/public.json build/proof.json
链上验证:Solidity Verifier
// Groth16 链上验证器(由 snarkjs 生成,核心调用)
interface IVerifier {
function verifyProof(
uint256[2] calldata a, // π_A
uint256[2][2] calldata b, // π_B
uint256[2] calldata c, // π_C
uint256[4] calldata input // 公开输入(含电路公共输出)
) external view returns (bool);
}
contract MyZKApp {
IVerifier private immutable verifier =
IVerifier(0x...); // 部署后的 Verifier 合约地址
// 通过零知识证明验证"成年人身份":只公开年龄 ≥ 18 的事实
function verifyAdult(uint256[2] calldata a,
uint256[2][2] calldata b,
uint256[2] calldata c,
uint256[4] calldata input) external returns (bool) {
require(verifier.verifyProof(a, b, c, input), "invalid proof");
return true;
}
}
PLONK 的工程优势
PLONK(Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge)由 Aztec 团队 2019 年提出:
- 通用设置:一次 Powers of Tau 仪式可用于任意电路
- 自定义门:可定义高级门(如加法、乘、范围检查),减少约束数量
- 查表(Plookup):把 SHA-256、Keccak 等复杂操作"查表"化,大幅压缩电路
- 递归友好:证明可以验证另一条证明,是 zkVM 递归折叠的基础
一句话:PLONK 是"电路语言"与"证明系统"解耦的胜利,让开发者聚焦业务约束而非密码学细节。
六、隐私交易:Tornado Cash 的混币原理
流程
Tornado Cash 通过 Merkle 树 + 零知识证明实现"存款隐私":
存款流程(公开):
用户生成随机秘密 (nullifier, secret)
commitment = PedersenHash(nullifier || secret)
将 commitment 存入合约(同时存入等额 ETH)
→ 将 commitment 追加进 20 层 Merkle 树
提款流程(隐私):
用户构造 Merkle 证明 path(证明 commitment 在树中)
构造 Groth16 证明 π:
- 存在某叶子 L 等于我的 commitment(用 Merkle proof 验证)
- 揭示 nullifier(用于防双花)
- 我拥有秘密 secret(哈希关系)
合约验证 π 且 nullifier 未被使用后,向新地址转出 ETH
// 简化版 Tornado 提款核心逻辑
contract Tornado {
uint256 public constant LEVELS = 20;
mapping(uint256 => bool) public nullifierHashes; // 防双花
IHasher public immutable hasher;
function withdraw(
bytes calldata _proof,
bytes32 _root,
bytes32 _nullifierHash,
address payable _recipient
) external {
require(!nullifierHashes[_nullifierHash], "Already used");
require(verifyProof(_proof, [uint256(_root), uint256(_nullifierHash)]), "Invalid proof");
nullifierHashes[_nullifierHash] = true; // Effect:先标记防双花
_recipient.transfer(amount); // Interaction:后转账
}
}
为什么匿名集有效
- 每个面额(0.1 / 1 / 10 / 100 ETH)独立一棵树,匿名集 = 该树所有未取出的叶子
- 取款地址与存款地址无链上关联,除非链下信息泄露
- 0.1% 手续费 + 固定小费用于激励中继(Relayer,避免链上关联)
一句话:Tornado 的隐私来自"Merkle 包含证明"——只要证明在集合内,验证者无法定位是哪一片叶子。
七、zkRollup 与 zkEVM:扩容的另一半
原理
zkRollup 在 L2 执行交易,把整批交易的状态转换压缩成一条 SNARK/STARK 证明,提交到 L1:
L2 批量执行 1000 笔交易
│
▼
生成有效性证明 π(证明:旧状态 + 这批交易 = 新状态)
│
▼
L1 合约验证 π → 更新全局状态根(state root)
资产安全由密码学保证,无需乐观挑战期
主流 zkEVM 对比
| 项目 | 证明系统 | EVM 兼容度 | 特点 |
|---|---|---|---|
| zkSync Era | SNARK(编译级) | 高(LLVM 前端) | Solidity 支持好,账户抽象内置 |
| Polygon zkEVM | SNARK(字节码级) | 全 EVM | 逐字节码验证,兼容最彻底 |
| Scroll | SNARK(字节码级) | 全 EVM | 社区驱动,兼容优先 |
| Starknet | STARK | 非 EVM(Cairo) | 原生 Cairo 语言,性能强 |
| Linea | SNARK | 高 | ConsenSys 出品,工具链成熟 |
关键难点:哈希与证明成本
zkEVM 的核心工程挑战是把 EVM 的 Keccak-256 / MPT(Merkle Patricia Trie) 电路化——这两个操作在电路中极贵。解决思路:
- 使用二进制字段 + Plookup 查表优化 Keccak 电路
- 用递归证明把多个子证明折叠,控制 L1 验证 gas
- 部分方案改用 Poseidon(ZK 友好哈希)重构状态树(如 Miden、Aztec)
一句话:zkRollup 把"计算"搬下链,把"证明"搬上链——链上只做一次配对验证,gas 与吞吐量同时优化。
八、身份与可验证凭证(VC)
选择性披露
传统身份验证泄露全部信息(如身份证号)。零知识让凭证变为可选择性披露:
可验证凭证(VC)由签发方签名,包含若干属性:
{ name, age, country, email, memberSince, ... }
持证者在需要时只披露"age ≥ 18"或"country = JP",
而不暴露其他属性——用 ZK 证明"我是该凭证的合法持有者"即可
链上 DID 架构
┌────────────┐ 签发 ┌────────────┐
│ 签发方 │ ───────────► │ 持证者钱包 │
│ (Issuer) │ 签名 VC │ (Holder) │
└────────────┘ └─────┬──────┘
│ 出示"可验证出示"(VP)
│ 仅含 ZK 证明 + 必要属性
▼
┌────────────┐
│ 验证方 │
│ (Verifier) │ 链上校验签发方公钥 + ZK 证明
└────────────┘
- DID 方法:did:ethr、did:key、Polygon ID、Ceramic 等
- 代表性项目:Anon Aadhaar(印度 Aadhaar 匿名化)、Semaphore(匿名投票/群签名)、zkLogin(Sui)
应用场景
| 场景 | 传统方式 | ZK 方式 |
|---|---|---|
| KYC | 提交身份证照片 | 只证明"已通过 KYC" |
| 年龄/权限 | 出示证件原件 | 证明"≥18 岁" |
| 空投/会员 | 校验链上快照 | 证明"持有某凭证"且匿名 |
| 投票 | 实名+黑名单 | 匿名+防双投(Semaphore) |
一句话:可验证凭证把"身份"从中心化数据库迁移到用户自持,ZK 则是隐私合规的桥梁。
九、工具链与工程实践
开发栈速览
| 工具 | 用途 | 备注 |
|---|---|---|
| Circom 2 | 电路语言 | 约束即代码,生态最大 |
| SnarkJS | 证明生成/验证 | Groth16/PLONK,全流程 CLI |
| arkworks / bellman | Rust 证明库 | 工程化 ZK 基础设施 |
| halo2 | PLONK 系(Halo2) | zcash 后续、Aztec 使用 |
| RISC Zero / SP1 | zkVM | 用通用语言写"程序"而非电路 |
| Noir | 高级电路语言 | Aztec 出品,类 Rust 语法 |
工程实践要点
- 只在必要时用 ZK:简单校验用 Merkle 证明,避免过度工程
- 可信设置管理:使用通用设置(PLONK/halo2)减少仪式风险
- 证明上链成本:Groth16 约 250k-500k gas,STARK 验证依赖 EIP-4844(blob)分摊
- 隐私≠匿名:隐私保护交易内容,匿名保护身份,二者可组合
- 审计重点:电路约束完备性、输入范围检查、Merkle 路径正确性、nullifier 防双花
# 常用 ZK 工程命令
circom circuit.circom --r1cs --wasm --sym # 编译
snarkjs zkey export solidityverifier # 导出 Solidity 验证器
snarkjs info build/circuit.r1cs # 查看约束数量
npx snarkjs groth16 setup # Groth16 专用 setup
总结
| 维度 | 要点 |
|---|---|
| 核心思想 | 完备性 + 可靠性 + 零知识,Fiat-Shamir 变换实现非交互 |
| 证明系统 | Groth16(小体积)/ PLONK(通用设置)/ STARK(透明抗量子) |
| 电路工程 | Circom 编写约束,SnarkJS 生成证明,Solidity Verifier 上链 |
| 隐私应用 | Tornado 混币 = Merkle 包含 + nullifier 防双花 |
| 扩容应用 | zkRollup 把计算下放、证明上链,zkEVM 兼容 EVM |
| 身份应用 | 可验证凭证 + 选择性披露,KYC 匿名化 |
| 趋势 | 递归证明、zkVM、证明预计算让链上验证成本趋近零 |
零知识证明已从密码学论文走向生产环境:隐私币、zkRollup、隐私 DeFi、去中心化身份都在大规模使用。对开发者而言,掌握电路思维(约束即真相)、熟练 Circom/SnarkJS 工具链,并理解证明系统的信任假设,是进入 ZK 世界的三条必经之路。随着 Plonky3、STIR、zkVM 等新基建成熟,ZK 将像 Gas 一样成为公链的基础设施,而非锦上添花的特性。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。