bellman是Zcash团队用Rust语言开发的一个zk-SNARK软件库,实现了Groth16算法。项目地址:
https://github.com/zcash/librustzcash/tree/master/bellman
Setup阶段最主要的工作是生成CRS数据,相关公式:
注1:公式中的x对应代码中的变量tau,相应的对应于powers_of_tau。powers_of_tau.z(&tau)
就是计算该值。2.1 参数数据结构
pub struct VerifyingKey<E: Engine> {
pub alpha_g1: E::G1Affine,
pub beta_g1: E::G1Affine,
pub beta_g2: E::G2Affine,
pub gamma_g2: E::G2Affine,
pub delta_g1: E::G1Affine,
pub delta_g2: E::G2Affine,
pub ic: Vec<E::G1Affine>
}
其中
pub struct Parameters<E: Engine> {
pub vk: VerifyingKey<E>,
pub h: Arc<Vec<E::G1Affine>>,
pub l: Arc<Vec<E::G1Affine>>,
pub a: Arc<Vec<E::G1Affine>>,
pub b_g1: Arc<Vec<E::G1Affine>>,
pub b_g2: Arc<Vec<E::G2Affine>>
}
其中,
最后3个参数a, b_g1, b_g2似乎公式中没有出现,实际上它们是根据算出来的QAP多项式的值,也就是,后面在计算proof的时候会用到。以a为例,假设其中一个多项式的系数等于,则,最后再映射到椭圆曲线上。
2.2 变量类型
Variable类型代表输入数据中的每一个值,分为公开的statement数据和私有的witness数据:
Input类型:即statement数据
Aux类型:即witness数据
pub enum Index {
Input(usize),
Aux(usize)
}
pub struct Variable(Index);
ConstraintSystem是一个接口,定义了下面几个函数用于产生不同类型的变量:
one(): 产生Input类型的变量,索引为0
alloc(): 产生Aux类型的变量,索引递增
alloc_input(): 产生Input类型的变量,索引递增
示例:
let a = cs.alloc(...)
let b = cs.alloc(...)
let c = cs.alloc_input(...)
cs.enforce(
|| "a*b=c",
|lc| lc + a,
|lc| lc + b,
|lc| lc + c
);
在上面这个例子里,c是statement,a和b是witness,需要验证a * b = c这个Circuit。
如果想验证a + b = c,需要写成下面这样:
cs.enforce(
|| "a*b=c",
|lc| lc + a + b,
|lc| lc + CS::one(),
|lc| lc + c
);
Circuit的synthesize()会调用ConstraintSystem的enforce()构建R1CS。
KeypairAssembly是ConstraintSystem的一个实现,R1CS的参数会保存在其成员变量中:
struct KeypairAssembly<E: Engine> {
num_inputs: usize,
num_aux: usize,
num_constraints: usize,
at_inputs: Vec<Vec<(E::Fr, usize)>>,
bt_inputs: Vec<Vec<(E::Fr, usize)>>,
ct_inputs: Vec<Vec<(E::Fr, usize)>>,
at_aux: Vec<Vec<(E::Fr, usize)>>,
bt_aux: Vec<Vec<(E::Fr, usize)>>,
ct_aux: Vec<Vec<(E::Fr, usize)>>
}
以上面的a * b = c为例:
num_inputs = 1
num_aux = 2
num_constraints = 1
后面6个字段就对应R1CS中的A、B、C矩阵:
接下来就是要完成R1CS到QAP的转换。其中有一步会利用逆离散快速傅立叶变换实现拉格朗日插值:
powers_of_tau.ifft(&worker);
let powers_of_tau = powers_of_tau.into_coeffs();
这里有一个单位根(root of unity)的概念:
如果,则称ω单位根。以复平面为例,实际上把单位圆等分成了n份:
最后,由于验证proof的时候需要用到下面公式里带中括号的那些参数:
pub fn prepare_verifying_key<E: Engine>(
vk: &VerifyingKey<E>
) -> PreparedVerifyingKey<E>
{
let mut gamma = vk.gamma_g2;
gamma.negate();
let mut delta = vk.delta_g2;
delta.negate();
PreparedVerifyingKey {
alpha_g1_beta_g2: E::pairing(vk.alpha_g1, vk.beta_g2),
neg_gamma_g2: gamma.prepare(),
neg_delta_g2: delta.prepare(),
ic: vk.ic.clone()
}
}
一个小细节:在有限域中如何计算除法?
显然,,那么乘性逆元素(multiplicative inverse)如何计算呢?
根据费马小定理:假如p是质数,且a和p互质,那么。
因此,在有限域中,。
对应代码:
fn inverse(&self) -> Option<Self> {
if <Fr as Field>::is_zero(self) {
None
} else {
Some(self.pow(&[(MODULUS_R.0 as u64) - 2]))
}
}
Groth16算法生成Proof的公式如下:
pub struct Proof<E: Engine> {
pub a: E::G1Affine,
pub b: E::G2Affine,
pub c: E::G1Affine
}
公式中其他的参数都是已知的,最主要的难点是计算h(x),下面会进行详细介绍。
回顾一下QAP,我们需要通过拉格朗日插值,把R1CS转化为一系列多项式:
然后我们需要验证:
因此我们需要计算。
一种方法是直接进行多项式运算,还有一种方法是算出它们在处的值,然后通过iFFT(逆快速傅立叶变换)获得多项式系数。
值得注意的是,当x分别取时,的值其实就是R1CS的值。因此,其实就是所有门电路的左输入,就是所有门电路的右输入,而就是所有门电路的输出。我们可以复用Circuit的synthesize()函数来进行计算。
生成proof的时候使用的是ConstraintSystem的另外一个实现ProvingAssignment来调用Circuit的synthesize()函数。
struct ProvingAssignment<E: Engine> {
...
a: Vec<Scalar<E>>,
b: Vec<Scalar<E>>,
c: Vec<Scalar<E>>,
input_assignment: Vec<E::Fr>,
aux_assignment: Vec<E::Fr>
}
和Setup阶段基本类似:
alloc():把witness数据送入aux_assignment中
alloc_input():把statement数据送入input_assignment中
enforce()/eval():计算出,分别放入a、b、c这3个成员变量中
然后就可以通过iFFT获得对应的多项式系数了。
有了,就可以计算h(x)了:
但是这里有个问题,,而x的取值是,所以分母为零!
显然我们是不能除以零的,因此需要对x做一定的“变换”或者“偏移”。用bellman里的术语,叫做从“Evaluation Domain”转换到“Coset”上。
1.定义,则(费马小定理)
2.先通过3次iFFT,计算出a,b,c的多项式系数
3.计算多项式在这个“偏移集合”上的值
4.再通过3次FFT,获得偏移后的点的集合a',b',c'
5.由于,计算h(x)在偏移集合上的点
6.做一次iFFT,计算出偏移后的多项式系数
7.根据尺度变换定理:,把上一步得到的结果除以σ,就获得了h(x)的多项式系数
根据以上分析,一共需要执行3次FFT,4次iFFT。
bellman基本上就是按照上面的算法来计算的,我们来详细分析一下。
首先,获取上一节的计算结果:
let mut a = EvaluationDomain::from_coeffs(prover.a)?;
let mut b = EvaluationDomain::from_coeffs(prover.b)?;
let mut c = EvaluationDomain::from_coeffs(prover.c)?;
然后,通过3次iFFT获取多项式系数,计算在coset上的值,再通过3次FFT获取偏移后的点:
a.ifft(&worker);
a.coset_fft(&worker);
b.ifft(&worker);
b.coset_fft(&worker);
c.ifft(&worker);
c.coset_fft(&worker);
接下来,计算:
a.mul_assign(&worker, &b);
drop(b);
a.sub_assign(&worker, &c);
drop(c);
a.divide_by_z_on_coset(&worker);
最后,通过iFFT获取多项式系数,然后除以σ:
a.icoset_fft(&worker);
另外,后面还有一步是计算,作为的一部分:
multiexp(&worker, params.get_h(a.len())?, FullDensity, a)
Groth16算法的proof验证公式如下:
let mut acc = pvk.ic[0].into_projective();
for (i, b) in public_inputs.iter().zip(pvk.ic.iter().skip(1)) {
acc.add_assign(&b.mul(i.into_repr()));
}
然后把公式略做变形,将问题转化为验证以下等式:
对应代码如下:
Ok(E::final_exponentiation(
&E::miller_loop([
(&proof.a.prepare(), &proof.b.prepare()),
(&acc.into_affine().prepare(), &pvk.neg_gamma_g2),
(&proof.c.prepare(), &pvk.neg_delta_g2)
].into_iter())
).unwrap() == pvk.alpha_g1_beta_g2)
至此,bellman源码的基本流程就分析完毕了,欢迎一起交流讨论~
星想法
技术改变世界
长按二维码关注我