区块链应用开发指南:业务场景剖析与实战
上QQ阅读APP看书,第一时间看更新

3.4 libsnark开源实践简介

libsnark是目前实现zk-SNARKs电路最重要的框架,在众多私密交易或隐私计算相关项目间广泛应用,其中最著名当然要数Zcash。Zcash在Sapling版本升级前一直使用libsnark来实现电路(之后才替换为bellman)。毫不夸张地说,libsnark支撑并促进了zk-SNARKs技术的首次大规模应用,填补了零知识证明技术从最新理论到工程实现间的空缺。

libsnark是用于开发zk-SNARKs应用的C++代码库,由SCIPR Lab开发并维护。libsnark工程实现背后的理论基础是近年来(尤其是2013年以来)零知识证明特别是zk-SNARKs方向的一系列重要论文。

从Github上可以看到这个项目的主要开发者,如:

  • 马达斯·维嘉(Madars Virza)
  • 霍华德·吴(Howard Wu)
  • 伊兰·特鲁莫(Eran Tromer)

他们大多都是这个领域内顶尖的学者或研究牛人。扎实的理论基础和工程能力,让libsnark的作者们能够化繁为简,将论文中的高深理论和复杂公式逐一实现,高度工程化地抽象出简洁的接口供广大开发者方便地调用。

libsnark的模块总览如图3-11所示,摘自libsnark代码贡献量第一作者马达斯·维嘉在MIT的博士论文。

图3-11 libsnark的模块总览图

libsnark框架提供了多个通用证明系统的实现,其中使用较多的是BCTV14a和Groth16。

查看libsnark/libsnark/zk_proof_systems路径,就能发现libsnark对各种证明系统的具体实现,并且均按不同类别进行了分类,还附上了实现依照的具体论文。

其中,zk_proof_systems/ppzksnark/r1cs_ppzksnark对应的是BCTV14a,zk_proof_systems/ppzksnark/r1cs_gg_ppzksnark对应的是Groth16。

ppzksnark是指preprocessing zkSNARK。这里的pp/preprocessing其实就是指我们常说的trusted setup,即在证明生成和验证之前,需要通过一个生成算法来创建相关的公共参数(proving key和verification key)。我们也把这个提前生成的参数称为“公共参考串“(Common Reference String),或简称为CRS。

基本原理与步骤

使用libsnark库进行开发zk-SNARKs应用,从原理上可简要概括为主要4个步骤:

(1)将待证明的命题表达为R1CS(Rank One Constraint System);

(2)使用生成算法(G)为该命题生成公共参数;

(3)使用证明算法(P)生成R1CS可满足性的证明;

(4)使用验证算法(V)来验证证明。

R1CS是一种表示计算的方法,使其能够满足零知识证明。基本上任何计算都可以简化(或平铺)为一个R1CS。例如向量w上的一个秩为1的约束被定义为

第一步:我们需要将函数C(x, out)在libsnark中进行表达。此处先省略,后面介绍详细过程。

第二步:对应下面的Generator函数G,lambda为随机产生,也就是常说的trusted setup过程中产生的“toxic waste”。人们喜欢称它为“有毒废物”,是因为它必须被妥善处理(如必须销毁,不能让任何人知道),否则会影响证明协议安全。

     lambda <- random()
(pk, vk) = G(C, lambda)

最终生成proving key(pk)和verification key(vk)。

第三步:对应使用Prove函数(P)生成证明。这里想证明的是prover知道一个秘密值x和计算结果out 可使等式满足。因此将xout还有pk 作为输入一起传给P,最终生成证明proof。

     proof = P(pk, out, x)

第四步:对应使用Verify函数(V)验证证明,将proofout还有vk 传给G,即可在不暴露秘密的情况下证明存在一个秘密值可使等式满足。

     V(vk, out, proof) ?= true

开发者主要工作量就集中在第一步,需要按照libsnark的接口规则手写C++电路代码来描述命题,由代码构造R1CS约束。整个过程也就对应图3-12的计算(Computation)→算法电路(Arithmetic Circuit)→R1CS。

图3-12 电路代码过程图

具体的例子,可参见如下两个项目:

  • https://github.com/howardwu/libsnark-tutorial
  • https://github.com/christianlundkvist/libsnark-tutorial

根据霍华德·吴(libsnark作者之一)的libsnark_tutorial,run_r1cs_gg_ppzksnark()是主要部分。很容易发现,真正起作用的实质代码只有下面5行。

我们从“超长”的函数名上能直观地看出每一步是在做什么,但是却看不到如何构造电路的细节。实际上这里仅仅是调用了自带的r1cs_example,隐去了实现细节。

下面通过一个更直观的例子来学习电路细节。研究src/test.cpp,这个例子改编自克里斯汀·伦德伟(Christian Lundkvist)的libsnark-tutorial。

代码开头仅引用了三个头文件:

前面提到r1cs_gg_ppzksnark对应的是Groth16方案。这里加了gg是为了区别r1cs_ppzksnark(也就是BCTV14a方案),表示Generic Group Model(通用群模型)。Groth16安全性证明依赖Generic Group Model,以更强的安全假设换得了更好的性能和更短的证明。

第一个头文件是为了引入default_r1cs_gg_ppzksnark_pp类型,第二个则为了引入证明相关的各个接口。pb_variable 则是用来定义电路相关的变量。

下面需要进行一些初始化工作,定义使用的有限域,并初始化曲线参数。这相当于准备工作。

     typedef libff::Fr<default_r1cs_gg_ppzksnark_pp> FieldT;
default_r1cs_gg_ppzksnark_pp::init_public_params();

接下来就需要明确“待证命题“是什么。这里不妨沿用之前的例子,证明秘密x满足等式x3 + x + 5 == out。这实际也是维塔利(Vitalik)博客文章 Quadratic Arithmetic Programs: from Zero to Hero中用的例子。如果对下面的变化陌生,可尝试阅读该文章。

通过引入中间变量sym_1、y、sym_2将x3 + x + 5 = out扁平化为若干个二次方程式,几个只涉及简单乘法或加法的式子,对应到算术电路中就是乘法门和加法门。你可以很容易地在纸上画出对应的电路。

     x * x = sym_1
sym_1 * x = y
y + x = sym_2
sym_2 + 5 = out

通常文章到这里便会顺着介绍如何按照R1CS的形式编排上面几个等式,并一步步推导出具体对应的向量。这对理解如何把Gate转换为R1CS有帮助,然而却不是本书的核心目的。所以此处省略这些内容。

下面定义与命题相关的变量。首先创建的protoboard是libsnark中的一个重要概念,顾名思义就是“原型板”或者“面包板”,用来快速搭建电路,在zk-SNARKs电路中则是用来关联所有变量、组件和约束。接下来的代码定义了所有需要外部输入的变量以及中间变量。

下面将各个变量与protoboard连接,相当于把各个元器件插到“面包板”上。allocate()函数的第二个string类型变量仅是用来方便DEBUG时的注释,方便DEBUG时查看日志。

     out.allocate(pb, "out");
x.allocate(pb, "x");
sym_1.allocate(pb, "sym_1");
y.allocate(pb, "y");
sym_2.allocate(pb, "sym_2");
pb.set_input_sizes(1);

注意,此处第一个与pb连接的是out变量。我们知道zk-SNARKs中有public input和private witness的概念,分别对应libsnark中的primary和auxiliary变量。那么如何在代码中进行区分呢?我们需要借助set_input_sizes(n)来声明与protoboard连接的public/primary变量的个数n。在这里n = 1,表明与pb连接的前n = 1个变量属性是public,其余变量的属性都是private。

至此,所有变量都已经顺利与protoboard相连,下面需要确定的是这些变量间的约束关系。这个也很好理解,类似元器件插至面包板后,需要根据电路需求确定它们之间的关系再连线焊接。如下调用protoboard的add_r1cs_constraint()函数,为pb添加形如a * b = c的r1cs_constraint,即r1cs_constraint(a, b, c)中参数应该满足a * b = c。根据注释,不难理解每个等式和约束之间的关系。

至此,变量间的约束添加完成,针对命题的电路构建完毕。下面进入前文提到的“4个步骤”中的第二步:使用生成算法(G)为该命题生成公共参数(pk和vk),即trusted setup。生成出来的proving key和verification key分别可以通过keypair.pk和keypair.vk获得。

进入第三步,生成证明。先为public input以及witness提供具体数值。不难发现,x = 3, out = 35是原始方程的一个解,则依次为x、out以及各个中间变量赋值。

再把public input以及witness的数值传给prover函数进行证明,可分别通过pb.primary_input()和pb.auxiliary_input()访问。生成的证明用proof变量保存。

    const r1cs_gg_ppzksnark_proof<default_r1cs_gg_ppzksnark_pp> proof
= r1cs_gg_ppzksnark_prover<default_r1cs_gg_ppzksnark_pp>(keypair.pk,
pb.primary_input(), pb.auxiliary_input());

最后我们使用verifier函数校验证明。如果verified = true则说明证明验证成功。

    bool verified = r1cs_gg_ppzksnark_verifier_strong_IC<default_r1cs_gg_
ppzksnark_pp>(keypair.vk, pb.primary_input(), proof);

从日志输出中可以看出验证结果为true,R1CS约束数量为4,public input和private input数量分别为1和4。日志输出符合预期。

实际应用中,trusted setup、prove、verify会由不同角色分别开展,最终实现的效果就是prover给verifier一段简短的proof和public input,verifier可以自行校验某命题是否成立。对于前面的例子,就是能在不知道方程的解x具体是多少的情况下,验证prover知道一个秘密的x可以使得x3 + x + 5 = out成立。

通过上面短短的几十行代码,就已经使用了zk-SNARKs。

使用它进行实战,我们可以参见安比实验室的开源代码示例:https://github.com/secbit/libsnark_abc。