零知识证明:支付场景中的 Rust 实现
零知识证明 (Zero-Knowledge Proof, ZKP) 是一种密码学技术,用于在不泄露秘密信息的前提下,证明某个断言的真实性。本文不仅通过两个代码示例介绍如何在支付场景中利用零知识证明技术保护隐私并验证支付操作的正确性,还将介绍零知识证明的应用领域以及 Rust 在该领域的地位。
什么是零知识证明?
零知识证明的核心思想是:证明者(Prover)能够让验证者(Verifier)相信一个陈述是真实的,而无需透露任何额外信息。换句话说,验证者只能知道“某件事是真的”,但无法得知为什么是真的。
经典案例:密码门问题
我们用一个直观的例子来说明零知识证明原理:
一条走道中间有一道密码门,如果从左边进入走到,要打开门才能从右边出来,同样,右进左出也需要打开密码门。你知道密码能够开门,你不能把密码告诉验证者的情况下向他证明你知道密码。
验证者在走道外看到走道两侧进出口的地方观察,你进入走道,从进入的另一侧出来; 你按照验证者的要求进行左进右出或者左出右进。
经过多次随机测试,如果你每次都能正确响应验证者的要求,那么验证者将相信你知道密码。
数学性质
在数学上,零知识证明需要满足以下三个性质:
完备性(Completeness):如果陈述是真的,诚实的验证者总能接受证明者的证明。 可靠性(Soundness):如果陈述是假的,欺骗性的证明者无法说服诚实的验证者。 零知识性(Zero-Knowledge):如果陈述是真的,验证者在验证过程中不会获得任何额外的信息。
这些性质构成了零知识证明的理论基础。
背景场景
假设我们有一个支付系统,支付方需要向收款方证明自己有足够的余额来完成支付,同时不能泄露初始余额的具体数值。为此,我们将通过两个实现案例,分别展示如何使用零知识证明技术来验证支付。
实现 1:基于 Pedersen 承诺
Pedersen 承诺是一种基于同态加密的加密方案,能够同时实现隐私保护和验证。
数学原理
Pedersen 承诺的公式为:
其中:
C:承诺值,公开共享。 v:被承诺的值,例如余额或支付金额。 r:盲化因子,用于保护隐私。 G,H:生成元,公开已知。
其关键特性是:
绑定性:一旦生成承诺值 C,无法找到其他 v 和 r 满足相同的承诺。 隐私性:从 C 无法推导出 v 和 r。
利用承诺的同态性,还可以验证余额支付是否正确:
若满足该关系,则证明支付操作合法。其中是承诺值的差如余额与新余额的差,等式右边 就是实际值的差,支付方(证明者)只需要把 的值告诉收款方(验证方)不用把真实的余额和新余额告诉收款方,收款方通过支付金额和 就可以算出支付金额的Pedersen 承诺值 判断 是否和 相等,从而验证余额是否够支付。
Rust 代码实现
1.定义生成元 G 和 H
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
use curve25519_dalek::scalar::Scalar;
use rand::rngs::OsRng;
struct PedersenGens {
g: RistrettoPoint,
h: RistrettoPoint,
}
impl PedersenGens {
fn new() -> Self {
let mut rng = OsRng;
let h = RistrettoPoint::random(&mut rng);
PedersenGens {
g: RISTRETTO_BASEPOINT_POINT,
h,
}
}
fn commit(&self, value: Scalar, blinding: Scalar) -> RistrettoPoint {
self.g * value + self.h * blinding
}
fn verify(&self, commitment: RistrettoPoint, value: Scalar, blinding: Scalar) -> bool {
let recomputed_commitment = self.commit(value, blinding);
commitment == recomputed_commitment
}
}
G 对应代码中的 RISTRETTO_BASEPOINT_POINT
,它是固定基点。H 是通过 RistrettoPoint::random
生成的随机点。
2. 执行承诺计算
fn commit(&self, value: Scalar, blinding: Scalar) -> RistrettoPoint {
self.g * value + self.h * blinding
}
3. 执行验证
fn verify(&self, commitment: RistrettoPoint, value: Scalar, blinding: Scalar) -> bool {
let recomputed_commitment = self.commit(value, blinding);
commitment == recomputed_commitment
}
这里就是支付后新余额的值,注意这里盲化因子也是相减的关系。
4. 支付方代码(证明方)
fn prover()->(PedersenGens,RistrettoPoint,RistrettoPoint,Scalar) {
// 初始化随机数生成器
let mut rng = OsRng;
// 定义 Pedersen 承诺的生成元
let pedersen_gens = PedersenGens::new();
// 余额
let balance: u64 = 100;
let balance_scalar = Scalar::from(balance);
// 生成余额盲化因子
let blinding_balance: Scalar = Scalar::random(&mut rng);
// 计算余额承诺
let balance_ct: RistrettoPoint = pedersen_gens.commit(balance_scalar, blinding_balance);
// 支付后余额:
let new_balance:u64 = 40;
let new_balance_scalar = Scalar::from(new_balance);
// 生成支付后余额盲化因子
let blinding_new_balance: Scalar = Scalar::random(&mut rng);
// 计算支付后余额承诺
let new_balance_ct: RistrettoPoint = pedersen_gens.commit(new_balance_scalar, blinding_new_balance);
let blinding_payment = blinding_balance - blinding_new_balance;
(pedersen_gens,balance_ct,new_balance_ct,blinding_payment)
}
支付方生成余额和支付后的新余额的 Pedersen 承诺,以及用于验证支付金额的盲化因子。
5. 收款方代码(验证方)
fn verifier(
pedersen_gens: PedersenGens,
balance_ct: RistrettoPoint,
new_balance_ct: RistrettoPoint,
blinding_payment: Scalar,
) {
// 根据承诺的同态特性计算支付承诺
let payment_ct = balance_ct - new_balance_ct;
// 收款方已知支付金额
let payment: u64 = 60;
let payment_scalar = Scalar::from(payment);
// 不需要知道余额的情况下验证支付金额是否正确
if pedersen_gens.verify(payment_ct, payment_scalar, blinding_payment) {
println!("Commitment verified successfully!");
} else {
println!("Failed to verify the commitment.");
}
}
6. 调用示例
let (pedersen_gens, balance_ct, new_balance_ct, blinding_payment) = prover();
verifier(pedersen_gens, balance_ct, new_balance_ct, blinding_payment);
基于 Pedersen 承诺的实现完成,可以看出这种实现非常简洁,需要的计算量也不大。接下来介绍更复杂的 SNARK 的实现方式。
实现 2:基于 SNARK 的支付验证
SNARK(Succinct Non-Interactive Argument of Knowledge)是一种支持复杂逻辑的零知识证明方案,特别适用于多约束验证。
数学原理
SNARK 的核心是 R1CS (Rank-1 Constraint System),它通过线性约束定义关系。例如,在支付场景中:
B为余额,P为支付后的新余额,B′为余额,这些约束被编码为稀疏矩阵 A,B,C 然后用于生成 SNARK 证明。
SNARK 的高效性体现在:
证明长度短(通常为常数大小)。 验证复杂度低(与约束规模无关)。
支付方通过R1CS生成证明,收款方不需要知道余额和新余额,只需要输入支付金额就可以验证证明,从而确认支付余额足够支付。
1.R1CS 生成代码
fn produce_balance_r1cs(
balance_initial: u64,
balance_new: u64,
payment_amount: u64,
) -> (
usize,
usize,
usize,
usize,
Instance,
VarsAssignment,
InputsAssignment,
) {
// 我们将创建一个约束系统来验证:
// B - P = B'
// parameters of the R1CS instance rounded to the nearest power of two
let num_cons = 2;
let num_vars = 3; // B, P, temp
let num_inputs = 1; // B'
let num_non_zero_entries = 4;
// 创建稀疏矩阵 A, B, C
let mut A: Vec<(usize, usize, [u8; 32])> = Vec::new();
let mut B: Vec<(usize, usize, [u8; 32])> = Vec::new();
let mut C: Vec<(usize, usize, [u8; 32])> = Vec::new();
// 使用 curve25519-dalek 的标量字段
let one = Scalar::ONE.to_bytes();
let minus_one = -Scalar::ONE;
// constraint 0 entries in (A,B,C)
// B - P = temp
A.push((0, 0, one)); // B
A.push((0, 1, minus_one.to_bytes())); // -P
B.push((0, num_vars, one));
C.push((0, 2, one)); // temp
// constraint 1 entries in (A,B,C)
// temp - B' = 0
A.push((1, 2, one)); // temp
B.push((1, num_vars, one));
C.push((1, num_inputs + num_vars, one)); // B'
let inst = Instance::new(num_cons, num_vars, num_inputs, &A, &B, &C).unwrap();
// 计算满足条件的赋值
let B = Scalar::from(balance_initial);
let P = Scalar::from(balance_new);
let temp = B - P; // temp = B - P
let B_prime = Scalar::from(payment_amount);
// 创建变量赋值
let mut vars = vec![Scalar::ZERO.to_bytes(); num_vars];
vars[0] = B.to_bytes();
vars[1] = P.to_bytes();
vars[2] = temp.to_bytes();
let assignment_vars = VarsAssignment::new(&vars).unwrap();
// 创建用于生成证明的输入赋值
let mut witness_inputs = vec![Scalar::ZERO.to_bytes(); num_inputs];
witness_inputs[0] = B_prime.to_bytes();
let assignment_witness_inputs = InputsAssignment::new(&witness_inputs).unwrap();
// 检查我们创建的实例是否可满足
let res = inst.is_sat(&assignment_vars, &assignment_witness_inputs);
assert_eq!(res.unwrap(), true);
(
num_cons,
num_vars,
num_inputs,
num_non_zero_entries,
inst,
assignment_vars,
assignment_witness_inputs,
)
}
这段代码的主要目的是用 R1CS(Rank-1 Constraint System) 表示一个简单的代数约束系统,并验证以下公式是否成立:
其中:
B:初始余额 balance_initial
。P:新的余额 payment_amount
。B′:支付金额 balance_new
。
R1CS 是一种表示约束系统的方法,广泛用于零知识证明中,例如 zk-SNARK。代码实现了以下逻辑:
定义一个约束系统。 为变量赋值。
代码详解
函数定义
fn produce_balance_r1cs(
balance_initial: u64,
balance_new: u64,
payment_amount: u64,
) -> (
usize,
usize,
usize,
usize,
Instance,
VarsAssignment,
InputsAssignment,
)
输入: balance_initial
:初始余额。balance_new
:新余额。payment_amount
:支付金额。返回: 约束、变量、输入数量等元信息。 Instance
:R1CS 实例,表示约束系统。VarsAssignment
和InputsAssignment
:分别是变量和输入的具体赋值。
R1CS 参数设置
let num_cons = 2; // 约束的数量
let num_vars = 3; // 变量的数量:B, P, temp
let num_inputs = 1; // 输入的数量:B'
let num_non_zero_entries = 4; // 稀疏矩阵中非零条目总数
约束数量:公式拆解为两条独立约束:
变量数量:
B:初始余额。 P:支付后金额。 :中间变量,存储 B−P。
输入数量: B′:支付金额。
稀疏矩阵 A, B, C 的定义
R1CS 的约束系统定义为:
其中:
A,B,C:稀疏矩阵。 X:变量向量。
矩阵 A,B,C 的条目定义如下:
第一条约束:B−P=temp
A.push((0, 0, one)); // A[0][0] = 1,表示 B 的系数为 1
A.push((0, 1, minus_one.to_bytes())); // A[0][1] = -1,表示 -P 的系数为 -1
B.push((0, num_vars, one)); // B[0][3] = 1,对应 temp
C.push((0, 2, one)); // C[0][2] = 1,目标是 temp
第二条约束:temp−B′=0
A.push((1, 2, one)); // A[1][2] = 1,表示 temp 的系数为 1
B.push((1, num_vars, one)); // B[1][3] = 1
C.push((1, num_inputs + num_vars, one)); // C[1][4] = 1,表示目标 B'
创建 R1CS 实例
let inst = Instance::new(num_cons, num_vars, num_inputs, &A, &B, &C).unwrap();
使用 Instance::new
方法创建 R1CS 系统。参数包括约束数量、变量数量、输入数量和稀疏矩阵 A,B,C。
变量赋值
let B = Scalar::from(balance_initial);
let P = Scalar::from(new_balance);
let temp = B - P; // temp = B - P
let B_prime = Scalar::from(payment_amount);
B,P 是输入的初始余额和支付后余额。 temp 是中间计算结果。 B′ 是支付金额
将变量转换为标量字段值:
let mut vars = vec![Scalar::ZERO.to_bytes(); num_vars];
vars[0] = B.to_bytes();
vars[1] = P.to_bytes();
vars[2] = temp.to_bytes();
let assignment_vars = VarsAssignment::new(&vars).unwrap();
公共输入赋值
let mut witness_inputs = vec![Scalar::ZERO.to_bytes(); num_inputs];
witness_inputs[0] = B_prime.to_bytes();
let assignment_witness_inputs = InputsAssignment::new(&witness_inputs).unwrap();
输入双方都知道的支付金额 B′ 。
这段代码的核心思想是利用 R1CS 表达简单代数关系,为零知识证明的构造提供基础支持。
支付方(验证方代码)
use libspartan::{InputsAssignment, Instance, SNARKGens, VarsAssignment, SNARK,ComputationCommitment};
use merlin::Transcript;
fn prover_snark() -> (SNARK,SNARKGens,ComputationCommitment){
// 初始余额 B
let balance_initial: u64 = 100;
// 支付金额 P
let payment_amount: u64 = 40;
// 支付后的新余额 B'
let balance_new: u64 = balance_initial - payment_amount;
// 生成 R1CS 实例及其约束
let (
num_cons,
num_vars,
num_inputs,
num_non_zero_entries,
inst,
assignment_vars,
assignment_witness_inputs,
) = produce_balance_r1cs(balance_initial, balance_new, payment_amount);
// 生成公共参数
let gens = SNARKGens::new(num_cons, num_vars, num_inputs, num_non_zero_entries);
// 创建 R1CS 实例的承诺
let (comm, decomm) = SNARK::encode(&inst, &gens);
// 生成证明
let mut prover_transcript = Transcript::new(b"balance_verification");
(SNARK::prove(
&inst,
&comm,
&decomm,
assignment_vars,
&assignment_witness_inputs, // 使用见证输入来生成证明
&gens,
&mut prover_transcript,
),gens,comm)
}
支付方通过R1CS约束,输入余额,新余额生成SNARK证明。
收款方(验证方代码)
fn verifier_snark(proof:SNARK,gens:SNARKGens,comm:ComputationCommitment) {
// 支付金额 P
let payment_amount: u64 = 40;
let num_inputs = 1; // B'
let mut public_input = vec![Scalar::ZERO.to_bytes(); num_inputs];
// 验证时的公共输入
public_input[0] = Scalar::from(payment_amount).to_bytes();
let assignment_public_inputs = InputsAssignment::new(&public_input).unwrap();
// 验证证明
let mut verifier_transcript = Transcript::new(b"balance_verification");
assert!(proof
.verify(
&comm,
&assignment_public_inputs,
&mut verifier_transcript,
&gens
)
.is_ok());
println!("proof verification successful!");
}
收款方把支付金额作为公共输入对支付方的证明进行验证。
调用示例
let (proof,gens,comm) = prover_snark();
verifier_snark(proof, gens, comm);
先调用prover_snark获取证明,然交给收款方验证。
总之,零知识证明为隐私保护和验证提供了强大的工具:
Pedersen 承诺 适合简单数值验证,依赖同态性实现高效计算。 SNARK 适合复杂逻辑验证,能在支付、身份认证等领域广泛应用。
未来,零知识证明有望在隐私保护、区块链和支付系统中发挥更重要的作用。
两种实现的对比分析
特性 | Pedersen 承诺 | SNARK |
---|---|---|
复杂度 | 简单 | 复杂 |
应用场景 | 数值简单的验证 | 复杂逻辑或多约束场景 |
隐私保护 | 中等 | 高 |
计算和验证成本 | 较低 | 较高 |
最后介绍以下零知识证明的应用场景。
零知识证明的实际应用场景
1. 数字货币
在数字货币领域,零知识证明常用于保护交易隐私。例如:
Zcash 使用 zk-SNARK 技术确保交易金额和参与者的隐私。 Monero 利用环签名和零知识证明隐藏交易的发送方、接收方和金额。 **去中心化交易所 (DEX)**:零知识证明帮助验证交易是否有效,同时隐藏订单细节,如价格和数量。
2. 智能合约
智能合约在区块链上执行透明的逻辑,但也因此面临隐私问题。零知识证明可以实现:
隐私保护合约:例如,在保密投票场景中,投票结果在公链上验证,但个体投票内容保密。 条件验证:例如证明用户满足某些条件(如身份验证或资产证明)但不披露具体细节。
示例:用户可以使用 zk-SNARK 证明其年龄大于 18 岁,而无需透露确切年龄。
3. 隐私计算
零知识证明在隐私计算中用于多方安全计算(MPC)和可信计算环境(TEE):
数据联合分析:多方合作分析联合数据集,通过零知识证明确保参与方数据隐私。 数据验证:用户可以向第三方证明计算结果的正确性,而无需公开输入。
例如,在医疗数据共享场景中,医院可以证明其模型基于真实的患者数据训练,而不暴露患者数据本身。
Rust 在零知识证明领域的重要性
Rust 作为一种系统编程语言,在零知识证明领域具有显著的优势:
安全性:Rust 的所有权模型和内存安全特性有助于防止常见的编程错误,如空指针引用和缓冲区溢出,这对于处理复杂的密码学算法至关重要。 性能:Rust 提供了接近 C/C++ 的性能,这对需要大量计算的零知识证明协议尤为重要。 活跃社区和库支持:Rust 拥有活跃的开发社区,提供了丰富的密码学库和工具,例如 curve25519-dalek
、bulletproofs
和arkworks
,简化了开发者的工作。跨平台兼容性:Rust 编译器可以生成针对多种目标平台的代码,这使得应用程序能够在不同环境中运行。