gnark零知识证明实战指南

gnark 零知识证明实战指南

从零到一掌握 zkSNARK 开发:理论、实践与应用场景全解析

📚 目录

  1. 零知识证明基础
  2. gnark 框架介绍
  3. 核心概念详解
  4. 开发实战
  5. 应用场景
  6. 性能优化
  7. 最佳实践
  8. 常见问题

零知识证明基础

什么是零知识证明?

零知识证明(Zero-Knowledge Proof, ZKP)是一种密码学协议,允许一方(证明者 Prover)向另一方(验证者 Verifier)证明某个陈述是真实的,而无需透露任何额外信息

经典例子:阿里巴巴的洞穴

想象一个环形洞穴,中间有一扇需要密码才能打开的门:

1
2
3
4
5
6
   入口
|
/--+--\
A B
\ /
\门/
  • Alice 声称她知道密码
  • Bob 想验证但不想知道密码
  • Alice 进入洞穴,随机选择 A 或 B 路径
  • Bob 在入口随机喊 “从 A 出来” 或 “从 B 出来”
  • 如果 Alice 真的知道密码,她总能从指定出口出来
  • 重复多次后,Bob 确信 Alice 知道密码,但他自己并不知道密码是什么

三大核心属性

  1. 完备性(Completeness)

    • 如果陈述为真,诚实的证明者总能说服验证者
    • 正确的输入 → 证明一定通过
  2. 可靠性(Soundness)

    • 如果陈述为假,欺诈的证明者无法欺骗验证者
    • 错误的输入 → 证明一定失败
    • 即使攻击者有巨大算力也无法伪造
  3. 零知识性(Zero-Knowledge)

    • 验证者除了"陈述为真"之外,学不到任何其他信息
    • 私有数据保持完全隐私

zkSNARK vs zkSTARK

特性 zkSNARK zkSTARK
全称 Zero-Knowledge Succinct Non-Interactive ARgument of Knowledge Zero-Knowledge Scalable Transparent ARgument of Knowledge
证明大小 很小(~200 bytes) 较大(~100 KB)
验证速度 极快(~1 ms) 快(~10 ms)
生成速度 较慢
可信设置 需要 不需要
量子抗性 ❌ 无 ✅ 有
适用场景 区块链、隐私交易 大规模计算证明

本教程聚焦于 zkSNARK(Groth16),它是目前最成熟、应用最广的方案。


gnark 框架介绍

为什么选择 gnark?

gnark 是 ConsenSys 开发的高性能 Go 语言 zkSNARK 框架。

核心优势

高性能

  • 生产级性能优化
  • 支持并行计算
  • 编译速度快

易用性

  • 简洁的 API 设计
  • 强类型检查
  • 丰富的文档

多后端支持

  • Groth16(最流行)
  • PlonK
  • 即将支持 STARK

工业级应用

  • 被多个区块链项目采用
  • 经过大量审计
  • 活跃的社区支持

完整生态

  • gnark-crypto:底层密码学库
  • gnark:电路编译和证明生成
  • 丰富的标准库(哈希、签名、Merkle 树等)

框架对比

框架 语言 后端 成熟度 学习曲线
gnark Go Groth16, PlonK ⭐⭐⭐⭐⭐ 中等
circom JavaScript Groth16, PlonK ⭐⭐⭐⭐⭐ 较低
bellman Rust Groth16 ⭐⭐⭐⭐ 较高
libsnark C++ Groth16 ⭐⭐⭐⭐

核心概念详解

1. 电路(Circuit)

电路是零知识证明的核心,定义了要证明的计算逻辑。

电路结构

1
2
3
4
5
6
7
8
9
10
type MyCircuit struct {
// 私有输入(证明者知道,但不会泄露)
PrivateInput frontend.Variable `gnark:"private_input"`

// 公开输入(所有人都能看到)
PublicInput frontend.Variable `gnark:",public"`

// 公开输出(计算结果)
Output frontend.Variable `gnark:",public"`
}

约束定义

1
2
3
4
5
6
7
8
9
10
func (circuit *MyCircuit) Define(api frontend.API) error {
// 定义计算逻辑和约束条件
// 所有约束必须满足,证明才能生成

// 示例:output = private_input + public_input
sum := api.Add(circuit.PrivateInput, circuit.PublicInput)
api.AssertIsEqual(circuit.Output, sum)

return nil
}

2. 约束系统(Constraint System)

约束系统是电路的数学表示形式。

R1CS(Rank-1 Constraint System)

R1CS 是最常见的约束系统,每个约束的形式为:

1
(a₁·x₁ + a₂·x₂ + ... ) · (b₁·x₁ + b₂·x₂ + ... ) = (c₁·x₁ + c₂·x₂ + ... )

gnark 自动将高级电路代码编译为 R1CS。

约束类型

  • 等式约束api.AssertIsEqual(a, b)
  • 不等式约束api.AssertIsLessOrEqual(a, b)
  • 布尔约束api.AssertIsBoolean(a)
  • 算术约束Add, Sub, Mul, Div

3. 见证(Witness)

见证是电路中所有变量的具体赋值。

1
2
3
4
5
6
7
8
// 创建见证
assignment := MyCircuit{
PrivateInput: 42, // 私有值
PublicInput: 10, // 公开值
Output: 52, // 输出值
}

witness, _ := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())

4. 证明系统流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────┐
│ Setup 阶段(一次性) │
│ 1. 定义电路 │
│ 2. 编译为 R1CS │
│ 3. 生成证明密钥 (pk) 和验证密钥 (vk) │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ Prove 阶段(每次证明) │
│ 1. 准备见证(私有+公开输入) │
│ 2. 使用 pk 生成证明 │
│ 3. 输出:proof + 公开输入 │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ Verify 阶段(快速验证) │
│ 1. 接收:proof + 公开输入 │
│ 2. 使用 vk 验证 │
│ 3. 输出:✅ 通过 / ❌ 拒绝 │
└─────────────────────────────────────────────────────────┘

开发实战

案例 1:年龄验证(入门)

场景:证明你年满 18 岁,但不泄露具体年龄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
)

// AgeCircuit 证明年龄 >= 18
type AgeCircuit struct {
Age frontend.Variable `gnark:"age"` // 私有:实际年龄
AgeLimit frontend.Variable `gnark:",public"` // 公开:年龄限制(18)
IsAdult frontend.Variable `gnark:",public"` // 公开:是否成年
}

func (circuit *AgeCircuit) Define(api frontend.API) error {
// 约束1:年龄必须 >= 限制
api.AssertIsLessOrEqual(circuit.AgeLimit, circuit.Age)

// 约束2:isAdult 必须为 1(表示成年)
api.AssertIsEqual(circuit.IsAdult, 1)

return nil
}

func main() {
// Setup
var circuit AgeCircuit
ccs, _ := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)
pk, vk, _ := groth16.Setup(ccs)

// Prove:Alice 25 岁
assignment := AgeCircuit{
Age: 25, // 私有
AgeLimit: 18, // 公开
IsAdult: 1, // 公开
}
witness, _ := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
publicWitness, _ := witness.Public()
proof, _ := groth16.Prove(ccs, pk, witness)

// Verify
err := groth16.Verify(proof, vk, publicWitness)
if err == nil {
println("✅ 验证通过:用户年满 18 岁")
println(" 但我们不知道用户的具体年龄!")
}
}

关键点

  • 验证者只看到 AgeLimit=18IsAdult=1
  • 实际年龄 Age=25 完全保密
  • 无法伪造(如果年龄 < 18,证明生成会失败)

案例 2:数独解答验证(进阶)

场景:证明你知道数独的解,但不泄露答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type SudokuCircuit struct {
// 私有:完整的解答(81个数字)
Solution [81]frontend.Variable `gnark:"solution"`

// 公开:初始谜题(0 表示空格)
Puzzle [81]frontend.Variable `gnark:",public"`
}

func (circuit *SudokuCircuit) Define(api frontend.API) error {
// 约束1:谜题中的非零数字必须与解答一致
for i := 0; i < 81; i++ {
// 如果谜题位置有数字,解答必须相同
isGiven := api.IsZero(circuit.Puzzle[i])
match := api.IsZero(api.Sub(circuit.Puzzle[i], circuit.Solution[i]))
api.AssertIsEqual(api.Or(isGiven, match), 1)
}

// 约束2:每行包含 1-9(无重复)
for row := 0; row < 9; row++ {
checkUnique(api, circuit.Solution[row*9:(row+1)*9])
}

// 约束3:每列包含 1-9(无重复)
for col := 0; col < 9; col++ {
var column [9]frontend.Variable
for row := 0; row < 9; row++ {
column[row] = circuit.Solution[row*9+col]
}
checkUnique(api, column[:])
}

// 约束4:每个 3x3 宫格包含 1-9(无重复)
// ... 类似实现

return nil
}

应用价值

  • 在线游戏:验证玩家解答正确性
  • 竞赛系统:防止作弊但保护答案
  • 教育平台:自动批改但不泄露标准答案

案例 3:密码登录(实用)

场景:证明知道密码,但不在网络上传输密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type PasswordCircuit struct {
// 私有:用户的密码
Password frontend.Variable `gnark:"password"`

// 公开:密码的哈希(服务器存储)
PasswordHash frontend.Variable `gnark:",public"`
}

func (circuit *PasswordCircuit) Define(api frontend.API) error {
// 计算密码的哈希
mimc, _ := mimc.NewMiMC(api)
mimc.Write(circuit.Password)
computedHash := mimc.Sum()

// 验证哈希匹配
api.AssertIsEqual(circuit.PasswordHash, computedHash)

return nil
}

优势

  • 密码永不传输(即使被监听也安全)
  • 服务器只存储哈希
  • 抗量子攻击(取决于哈希函数选择)

案例 4:Merkle 树成员证明(高级)

场景:证明某个数据在 Merkle 树中,但不泄露其他数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import "github.com/consensys/gnark/std/accumulator/merkle"

type MerkleCircuit struct {
// 私有:叶子节点数据
Leaf frontend.Variable `gnark:"leaf"`

// 私有:Merkle 路径
Path []frontend.Variable `gnark:"path"`

// 公开:Merkle 根
Root frontend.Variable `gnark:",public"`
}

func (circuit *MerkleCircuit) Define(api frontend.API) error {
// 从叶子节点向上计算到根
currentHash := circuit.Leaf

for i := 0; i < len(circuit.Path); i++ {
mimc, _ := mimc.NewMiMC(api)
mimc.Write(currentHash)
mimc.Write(circuit.Path[i])
currentHash = mimc.Sum()
}

// 验证计算出的根与公开的根一致
api.AssertIsEqual(currentHash, circuit.Root)

return nil
}

应用场景

  • 区块链:轻客户端验证交易
  • 隐私投票:证明投票权但不泄露身份
  • 空投资格:证明在白名单中但不公开列表

应用场景

1. 区块链与加密货币

隐私交易(如 Zcash)

1
2
3
4
5
6
7
传统交易:
Alice → Bob: 10 ETH
↑ 所有人都能看到金额和地址

ZK 隐私交易:
Alice → Bob: ??? ETH
↑ 只能看到交易有效,金额和地址保密

Layer 2 扩容(如 zkSync, StarkNet)

1
2
3
4
5
链上:只存储证明(小)
链下:执行大量交易

验证成本:O(1)
交易吞吐量:↑↑↑

2. 身份认证

年龄验证

  • 进入成人网站
  • 购买酒精/烟草
  • 不泄露出生日期

信用评分

1
2
3
4
5
type CreditScoreCircuit struct {
Score frontend.Variable `gnark:"score"` // 私有
Threshold frontend.Variable `gnark:",public"` // 公开:如 700
IsQualified frontend.Variable `gnark:",public"` // 公开
}
  • 贷款审批
  • 信用卡申请
  • 不泄露具体分数

3. 数据隐私

医疗数据

  • 证明已接种疫苗,不泄露其他健康信息
  • 证明符合临床试验条件,不泄露病史

财务审计

  • 证明收入在某个范围,不泄露具体金额
  • 证明纳税合规,不泄露交易细节

4. 游戏与娱乐

公平游戏

  • 扑克:证明出牌合法但不泄露手牌
  • 彩票:可验证的随机性

成就系统

  • 证明游戏通关但不泄露策略
  • 证明技能达标但不泄露练习数据

5. 供应链与物流

产品溯源

  • 证明产品来自认证供应商
  • 不泄露供应链细节(商业机密)

合规证明

  • 证明符合环保标准
  • 不泄露生产工艺

6. 机器学习

模型推理

1
2
3
4
5
6
7
用户:我的数据 + 模型
服务商:推理结果

ZK 保护:
- 用户数据不泄露
- 模型参数不泄露
- 结果可验证

联邦学习

  • 证明模型训练正确
  • 不泄露本地数据

性能优化

1. 减少约束数量

约束数量直接影响性能:

1
2
3
4
5
6
7
// ❌ 低效:多次哈希
hash1 := hashOnce(data)
hash2 := hashOnce(hash1)
hash3 := hashOnce(hash2)

// ✅ 高效:批量哈希
hashBatch(data, iterations)

2. 选择 ZK 友好的操作

操作 约束成本 建议
加法/减法 ✅ 优先使用
乘法 ✅ 适量使用
除法 ⚠️ 尽量避免
SHA256 极高(~25K) ❌ 避免
MiMC 低(~200) ✅ 推荐
Poseidon 低(~150) ✅ 最优

3. 使用查找表

对于固定映射关系,使用查找表比计算更高效:

1
2
3
4
5
// 查找表:输入 → 输出
table := []int{0, 1, 4, 9, 16, 25} // x²

// 电路中使用
api.Lookup(input, table)

4. 并行化

1
2
3
4
5
6
// Setup 可以离线并行化
// Prove 可以多核加速
// Verify 本身就很快(1-2ms)

// 批量验证多个证明
groth16.BatchVerify(proofs, vks, publicWitnesses)

5. 选择合适的椭圆曲线

1
2
3
4
5
6
7
8
// BN254:最流行,以太坊兼容
ecc.BN254

// BLS12-381:更安全,Filecoin/Chia 使用
ecc.BLS12_381

// BLS12-377:适合递归证明
ecc.BLS12_377

最佳实践

1. 电路设计原则

明确隐私边界

1
2
3
4
5
type WellDesignedCircuit struct {
// ✅ 明确标记私有/公开
Secret frontend.Variable `gnark:"secret"`
Public frontend.Variable `gnark:",public"`
}

最小化公开信息

1
2
3
4
5
6
7
8
9
10
11
// ❌ 过度公开
type BadCircuit struct {
Age frontend.Variable `gnark:",public"` // 不需要公开具体年龄
IsAdult frontend.Variable `gnark:",public"`
}

// ✅ 最小公开
type GoodCircuit struct {
Age frontend.Variable `gnark:"age"` // 私有
IsAdult frontend.Variable `gnark:",public"` // 只公开结果
}

2. 安全考虑

可信设置

Groth16 需要可信设置,必须安全进行:

1
2
3
方案1:使用已有的可信设置(如 Powers of Tau)
方案2:举办多方计算(MPC)仪式
方案3:使用不需要可信设置的后端(PlonK, STARK)

防止侧信道攻击

1
2
3
4
5
6
7
// ❌ 危险:条件分支可能泄露信息
if secret > 100 {
// ... 不同的计算路径
}

// ✅ 安全:恒定时间操作
result := api.Select(api.Cmp(secret, 100), branch1, branch2)

3. 测试策略

完整的测试覆盖

1
2
3
4
5
6
7
8
9
10
func TestCircuit(t *testing.T) {
// 测试1:正常情况
testValid(t)

// 测试2:边界情况
testBoundary(t)

// 测试3:异常情况(应该失败)
testInvalid(t)
}

模糊测试

1
2
3
4
5
// 使用随机输入测试电路鲁棒性
for i := 0; i < 1000; i++ {
randomInput := generateRandom()
testCircuit(randomInput)
}

4. 生产部署

密钥管理

1
2
3
4
5
Setup 阶段:
1. 在安全环境中生成 pk, vk
2. 销毁 Setup 的随机性(toxic waste)
3. pk 加密存储(证明生成方)
4. vk 可以公开(验证方)

版本控制

1
2
3
4
5
6
type Circuit struct {
Version frontend.Variable `gnark:",public"`
// ... 其他字段
}

// 确保证明和电路版本匹配

监控与日志

1
2
3
4
// 记录关键指标
log.Printf("Prove time: %v", proveTime)
log.Printf("Constraints: %d", ccs.GetNbConstraints())
log.Printf("Proof size: %d bytes", len(proof))

常见问题

Q1: 如何选择合适的哈希函数?

A: 根据场景选择:

1
2
3
4
通用场景:MiMC 或 Poseidon
需要与外部系统兼容:SHA256(但性能差)
递归证明:Poseidon(专门优化)
最佳性能:Poseidon

Q2: 约束数量如何影响性能?

A:

  • Prove 时间:线性相关(约束越多越慢)
  • Verify 时间:几乎恒定(~1-2ms,与约束数量无关!)
  • Proof 大小:Groth16 固定(~200 bytes)

Q3: 如何调试电路?

A:

1
2
3
4
5
6
7
8
9
// 方法1:打印中间值
fmt.Printf("Debug value: %v\n", value)

// 方法2:使用 gnark 的调试模式
frontend.Compile(..., frontend.WithDebug())

// 方法3:单元测试每个约束
testConstraint(t, constraint1)
testConstraint(t, constraint2)

Q4: 能否在电路中使用浮点数?

A: 不能直接使用。ZK 电路工作在有限域上(整数运算)。

解决方案:

1
2
3
4
5
// 定点数:放大 10^6 倍
price := 123.45
priceInCircuit := 123450000 // 乘以 10^6

// 电路内计算后再还原

Q5: 如何处理大数组?

A:

1
2
3
4
5
6
7
8
9
10
// ❌ 不推荐:数组太大
type Circuit struct {
Data [10000]frontend.Variable
}

// ✅ 推荐:使用 Merkle 树
type Circuit struct {
MerkleRoot frontend.Variable
MerklePath []frontend.Variable
}

Q6: Groth16 vs PlonK 如何选择?

特性 Groth16 PlonK
可信设置 每个电路需要 通用设置(一次性)
证明大小 更小(~200B) 较大(~400B)
验证速度 最快
灵活性 高(支持自定义门)
成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

建议

  • 生产环境、性能关键 → Groth16
  • 快速迭代、频繁修改电路 → PlonK

Q7: 如何实现递归证明?

递归证明:在电路内验证另一个证明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type RecursiveCircuit struct {
// 内部证明
InnerProof frontend.Variable

// 验证密钥
InnerVK frontend.Variable
}

func (c *RecursiveCircuit) Define(api frontend.API) error {
// 在电路内验证 InnerProof
verifier := groth16.NewVerifier(api)
verifier.Verify(c.InnerProof, c.InnerVK)
return nil
}

应用:区块链压缩(证明链压缩为单个证明)


学习资源

官方文档

推荐阅读

理论基础

实战教程


项目实战:构建完整系统

端到端示例:隐私投票系统

完整代码见本仓库的 lesson*.go 文件。

系统架构

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────┐
│ 客户端 │ → 生成投票证明
└──────┬──────┘
│ proof + 公开信息

┌─────────────┐
│ 服务器 │ → 验证证明
└──────┬──────┘
│ 记录投票

┌─────────────┐
│ 区块链/DB │ → 存储结果
└─────────────┘

核心特性

✅ 投票隐私(不知道谁投给谁)
✅ 一人一票(防止重复投票)
✅ 可验证性(任何人都能验证结果)
✅ 抗强制(无法证明你的投票)