第五十二章 零知识证明
证明我知道秘密,但不透露秘密本身
零知识证明(Zero-Knowledge Proof, ZKP)是密码学中最迷人的概念之一。它允许证明者向验证者证明某个陈述为真,而无需透露任何超出陈述真实性之外的信息。本章将介绍零知识证明的基本原理、主要类型及其在 Rust 中的实现。
52.1 什么是零知识证明
直观理解
想象一个场景:Alice 声称她知道一个迷宫的秘密出口,但她不想透露出口的位置。她可以这样做:
- Alice 进入迷宫
- Bob 在入口等待,随机喊“从 A 口出“或“从 B 口出“
- Alice 从指定的出口出现
- 重复多次
如果 Alice 真的知道秘密路径,她每次都能从指定出口出现;如果她只是运气好,连续多次猜对的概率会指数级下降。关键是,Bob 从未看到 Alice 在迷宫中的路径——他只知道 Alice“似乎“知道出口。
零知识证明的三要素
一个完整的零知识证明系统必须满足三个性质:
- 完备性(Completeness):如果陈述为真,诚实的证明者可以说服诚实的验证者
- 可靠性(Soundness):如果陈述为假,任何欺骗性的证明者都无法以不可忽略的概率说服验证者
- 零知识性(Zero-Knowledge):验证者除了“陈述为真“之外,无法获得任何额外信息
形式化定义
公共输入 x
证明者私有输入 w(见证/witness)
关系 R(x, w) = true 表示"w 是 x 的有效见证"
证明者知道 w 使得 R(x, w) = true
↓
零知识证明协议
↓
验证者确信"存在 w 使得 R(x, w) = true"
但不知道 w 的任何信息
52.2 交互式 vs 非交互式
交互式零知识证明
经典的零知识证明是交互式的:证明者和验证者需要进行多轮通信。
示例:离散对数的零知识证明
公共参数:循环群 G,生成元 g,素数阶 p
公共输入:y = g^x mod p(Alice 声称知道 x)
1. Alice 选择随机数 r,计算 a = g^r mod p,发送 a 给 Bob
2. Bob 选择随机挑战 c,发送给 Alice
3. Alice 计算 s = r + c * x mod (p-1),发送 s 给 Bob
4. Bob 验证:g^s ≡ a * y^c (mod p)
验证等式成立的原因:
g^s = g^(r + c*x) = g^r * g^(c*x) = g^r * (g^x)^c = a * y^c
这个协议是零知识的,因为 Bob 可以从自己生成的随机数构造出与真实协议不可区分的“模拟“ transcript。
非交互式零知识证明(NIZK)
交互式证明在实际应用中很不方便。Fiat-Shamir 启发式方法可以将交互式协议转换为非交互式:
将验证者的随机挑战替换为哈希函数的输出:
c = Hash(g, y, a)
这样证明者可以一次性生成完整证明,验证者独立验证。
非交互式零知识证明的优势:
- 证明可以公开广播,任何人都可以验证
- 适合区块链等无需许可的环境
- 证明可以被重复使用
交互式 vs 非交互式对比
| 特性 | 交互式 ZKP | 非交互式 ZKP |
|---|---|---|
| 通信轮数 | 多轮 | 一轮(证明者 → 验证者) |
| 可公开验证 | 否 | 是 |
| 应用场景 | 私有协议 | 区块链、公开审计 |
| 转换方法 | — | Fiat-Shamir 启发式 |
52.3 zk-SNARKs 简介
什么是 zk-SNARK
zk-SNARK(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)是目前最实用的零知识证明系统之一,具备以下特性:
- 零知识(Zero-Knowledge):不泄露见证信息
- 简洁(Succinct):证明大小恒定(几百字节),验证时间极短(毫秒级)
- 非交互(Non-Interactive):单条消息完成证明
- 知识论证(Argument of Knowledge):证明者确实“知道“见证,而不仅仅是陈述为真
zk-SNARK 的工作原理(概述)
zk-SNARK 的核心思想是将计算转换为算术电路,然后将电路转换为多项式约束:
程序/计算
↓
算术电路(加法门、乘法门)
↓
R1CS(Rank-1 Constraint System,一阶约束系统)
↓
QAP(Quadratic Arithmetic Program,二次算术程序)
↓
多项式承诺(如 KZG、FRI)
↓
简洁证明
关键概念:
- 可信设置(Trusted Setup):某些 zk-SNARK 变体需要生成公共参考字符串(CRS),这个过程必须安全执行
- 通用可信设置:如 Groth16 需要为每个电路单独设置
- 透明设置:如 STARKs、Bulletproofs 不需要可信设置
主流 zk-SNARK 方案
| 方案 | 证明大小 | 验证时间 | 可信设置 | 后量子安全 |
|---|---|---|---|---|
| Groth16 | 192 字节 | 1.5ms | 每个电路一次 | 否 |
| PLONK | ~400 字节 | ~3ms | 通用(一次) | 否 |
| STARKs | ~50KB | ~10ms | 无需 | 是 |
| Bulletproofs | ~1KB | ~线性 | 无需 | 否 |
零知识证明的应用场景
1. 隐私交易
区块链上的交易通常是透明的。ZKP 可以隐藏交易金额和参与方,同时保证交易有效性:
传统交易:
Alice --10 BTC--> Bob(全网可见)
Zcash 隐私交易:
??? --??--> ???(金额和地址隐藏)
但 ZKP 证明:发送方有足够余额,交易有效
2. 身份验证
证明你年满 18 岁,而不透露出生日期;证明你的信用评分高于阈值,而不透露具体分数。
3. 计算外包验证
将复杂计算外包给第三方,用 ZKP 验证计算结果的正确性,而无需重新执行计算。
4. 区块链扩容(Rollups)
将大量交易在链下执行,只向主链提交一个简洁的 ZKP 证明,证明所有链下交易都有效。
52.4 Rust 实现:零知识证明示例
使用 bellman 库实现简单电路
bellman 是 Zcash 团队开发的 Rust zk-SNARK 库,基于 Groth16 证明系统。
# Cargo.toml
[dependencies]
bellman = "0.14"
ff = "0.13"
bls12_381 = "0.8"
rand = "0.8"
#![allow(unused)]
fn main() {
use bellman::{Circuit, ConstraintSystem, SynthesisError};
use bls12_381::Scalar;
use ff::PrimeField;
// 证明:我知道 x 和 y,使得 x * y = public_output
struct MultiplicationCircuit {
x: Option<Scalar>,
y: Option<Scalar>,
}
impl Circuit<Scalar> for MultiplicationCircuit {
fn synthesize<CS: ConstraintSystem<Scalar>>(
self,
cs: &mut CS,
) -> Result<(), SynthesisError> {
// 分配私有输入 x
let x = cs.alloc(|| "x", || {
self.x.ok_or(SynthesisError::AssignmentMissing)
})?;
// 分配私有输入 y
let y = cs.alloc(|| "y", || {
self.y.ok_or(SynthesisError::AssignmentMissing)
})?;
// 分配公共输出
let public_output = self.x.and_then(|x| {
self.y.map(|y| x * y)
});
let output = cs.alloc_input(|| "output", || {
public_output.ok_or(SynthesisError::AssignmentMissing)
})?;
// 约束:x * y = output
cs.enforce(
|| "multiplication constraint",
|lc| lc + x,
|lc| lc + y,
|lc| lc + output,
);
Ok(())
}
}
}
生成和验证证明
use bellman::groth16::{generate_random_parameters, create_random_proof, verify_proof, PreparedVerifyingKey};
use bls12_381::{Bls12, Scalar};
use rand::rngs::OsRng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rng = &mut OsRng;
// 1. 可信设置:为电路生成参数
let params = {
let c = MultiplicationCircuit { x: None, y: None };
generate_random_parameters::<Bls12, _, _>(c, rng)?
};
// 准备验证密钥(加速验证)
let pvk = PreparedVerifyingKey::from(params.vk.clone());
// 2. 创建证明:我知道 3 和 5,它们的乘积是 15
let public_input = Scalar::from(15);
let proof = {
let c = MultiplicationCircuit {
x: Some(Scalar::from(3)),
y: Some(Scalar::from(5)),
};
create_random_proof(c, ¶ms, rng)?
};
// 3. 验证证明
let is_valid = verify_proof(&pvk, &proof, &[public_input])?;
println!("证明有效: {}", is_valid);
// 尝试验证错误的输入
let wrong_input = Scalar::from(100);
let is_invalid = verify_proof(&pvk, &proof, &[wrong_input]).is_ok();
println!("错误输入验证通过: {}", is_invalid);
Ok(())
}
简化的零知识证明演示(不依赖外部库)
为了理解零知识证明的核心原理,我们实现一个极简的“颜色证明“示例——基于图同态的零知识证明概念:
use sha2::{Sha256, Digest};
use rand::Rng;
// 简化的承诺方案
fn commit(secret: &str, nonce: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
hasher.update(nonce);
format!("{:x}", hasher.finalize())
}
// 证明者声称"我知道一个满足某种性质的值"
// 这里简化为:我知道一个值,其哈希的前 4 位是 0
struct Prover {
secret: String,
}
impl Prover {
fn new(secret: String) -> Self {
Prover { secret }
}
// 生成承诺
fn commit_secret(&self) -> (String, Vec<u8>) {
let nonce: Vec<u8> = (0..16).map(|_| rand::thread_rng().gen::<u8>()).collect();
let commitment = commit(&self.secret, &nonce);
(commitment, nonce)
}
// 响应挑战
fn respond(&self, challenge: u8) -> String {
if challenge == 0 {
// 揭示秘密
self.secret.clone()
} else {
// 揭示其他信息(简化示例)
format!("response_{}", challenge)
}
}
}
struct Verifier {
commitment: String,
}
impl Verifier {
fn new(commitment: String) -> Self {
Verifier { commitment }
}
// 生成随机挑战
fn challenge(&self) -> u8 {
rand::thread_rng().gen_range(0..2)
}
// 验证响应
fn verify(&self, response: &str, nonce: &[u8], challenge: u8) -> bool {
if challenge == 0 {
// 验证承诺是否对应揭示的秘密
let recomputed = commit(response, nonce);
recomputed == self.commitment
} else {
// 其他验证逻辑
response.starts_with("response_")
}
}
}
fn main() {
println!("=== 简化的零知识证明演示 ===\n");
// 场景:证明者知道一个"特殊"字符串(比如 SHA-256 前导 0)
let secret = "my_secret_value_42".to_string();
let prover = Prover::new(secret);
// 证明者生成承诺
let (commitment, nonce) = prover.commit_secret();
println!("承诺: {}", &commitment[..16]);
// 验证者存储承诺并发起挑战
let verifier = Verifier::new(commitment);
// 多轮交互
let mut success_count = 0;
let rounds = 10;
for round in 1..=rounds {
let challenge = verifier.challenge();
let response = prover.respond(challenge);
let valid = verifier.verify(&response, &nonce, challenge);
println!(
"轮次 {}: 挑战={}, 验证={}",
round, challenge, valid
);
if valid {
success_count += 1;
}
}
println!("\n成功率: {}/{} = {:.0}%", success_count, rounds,
(success_count as f64 / rounds as f64) * 100.0);
if success_count == rounds {
println!("验证者确信:证明者知道秘密!");
}
}
范围证明概念(Bulletproofs 风格)
范围证明是 ZKP 的重要应用:证明一个值在某个范围内,而不透露具体值。
use sha2::{Sha256, Digest};
// 概念演示:证明 value 在 [0, 2^n) 范围内
// 实际实现需要使用 Pedersen 承诺和内部乘积论证
struct RangeProofConcept {
n: usize, // 位数
}
impl RangeProofConcept {
// 将值分解为二进制位
fn decompose(value: u64, n: usize) -> Vec<bool> {
let mut bits = Vec::with_capacity(n);
for i in 0..n {
bits.push(((value >> i) & 1) == 1);
}
bits
}
// 验证二进制分解的正确性
fn verify_decomposition(value: u64, bits: &[bool]) -> bool {
let reconstructed: u64 = bits.iter()
.enumerate()
.map(|(i, &b)| if b { 1u64 << i } else { 0 })
.sum();
reconstructed == value
}
// 概念:证明每个位是 0 或 1
fn prove_bit_is_binary(bit: bool) -> &'static str {
// 在实际 ZKP 中,这转化为约束:b * (1 - b) = 0
// 即 b 只能是 0 或 1
if bit {
"bit = 1: 1 * (1 - 1) = 0 ✓"
} else {
"bit = 0: 0 * (1 - 0) = 0 ✓"
}
}
}
fn main() {
let proof = RangeProofConcept { n: 32 };
let value = 12345u64;
let bits = RangeProofConcept::decompose(value, 32);
println!("值: {}", value);
println!("二进制分解: {:?}", bits);
println!("分解验证: {}", RangeProofConcept::verify_decomposition(value, &bits));
println!("\n每位验证:");
for (i, &bit) in bits.iter().take(8).enumerate() {
println!(" 位 {}: {}", i, RangeProofConcept::prove_bit_is_binary(bit));
}
// 范围验证
let max_value = (1u64 << 32) - 1;
println!("\n范围验证: 0 <= {} <= {} → {}", value, max_value,
value <= max_value);
}
使用 arkworks 进行现代 ZKP 开发
arkworks 是 Rust 生态中更现代的零知识证明框架,提供了模块化的代数组件。
#![allow(unused)]
fn main() {
// arkworks 概念示例
use ark_ff::Field;
use ark_bls12_381::Fr;
// 在有限域上运算
fn field_operations() {
let a = Fr::from(3u64);
let b = Fr::from(5u64);
let sum = a + b;
let product = a * b;
let inverse = a.inverse().expect("可逆");
println!("3 + 5 = {:?}", sum);
println!("3 * 5 = {:?}", product);
println!("3^-1 = {:?}", inverse);
println!("3 * 3^-1 = {:?}", a * inverse); // 应等于 1
}
}
52.5 零知识证明的挑战与前沿
当前挑战
- 可信设置的安全性:Groth16 等方案需要可信设置,如果设置过程被泄露,整个系统的安全性将崩溃
- 计算开销:生成证明的计算成本仍然很高
- 电路复杂性:将程序转换为算术电路需要专业知识
- 量子计算威胁:基于椭圆曲线的 ZKP 面临量子计算的潜在威胁(STARKs 是后量子安全的替代方案)
前沿发展方向
- 硬件加速:GPU、FPGA、ASIC 用于加速证明生成
- 递归证明:在一个证明中验证另一个证明,实现无限压缩
- zk-EVM:在零知识证明中执行以太坊虚拟机指令
- 身份与合规:KYC/AML 与隐私保护的结合
52.6 本章总结
| 概念 | 说明 | Rust 生态 |
|---|---|---|
| 交互式 ZKP | 多轮通信完成证明 | 理论演示 |
| 非交互式 ZKP | 单条消息,可公开验证 | Fiat-Shamir 转换 |
| zk-SNARK | 简洁非交互零知识证明 | bellman, arkworks |
| zk-STARK | 透明设置、后量子安全 | winterfell |
| 范围证明 | 证明值在范围内 | bulletproofs |
| 电路编译 | 程序 → 算术电路 | circom-compat |
练习建议
-
基础练习:运行上述
bellman乘法电路示例,尝试修改电路证明其他关系(如 x + y = z)。 -
中级练习:实现一个简化的离散对数零知识证明(Sigma 协议),使用 Fiat-Shamir 转换为非交互式。
-
高级练习:使用
arkworks构建一个更复杂的电路,如证明你知道一个 Merkle 树中的叶子节点( membership proof)。 -
实践项目:研究 Zcash 或 Filecoin 中零知识证明的应用,尝试运行它们的 Rust 节点或客户端。
密码学箴言:零知识证明是密码学的“魔术“——它让你确信对方知道秘密,同时确信自己什么都没学到。在隐私与验证之间,ZKP 找到了完美的平衡。