# 深入简出zkSNARKs 原创作者:香橙@cyberorange。 ###### tags: `zkSNARKs`, `Groth16` ## 1. 零知识证明初探 零知识证明是近几年密码学技术中最引人注目的技术之一,在区块链领域更是高频词,但是由于其涉及较多的数学知识,对一般的开发人员来说理解难度较大,而且资料多为英文,对国内区块链技术爱好者等学习可能造成困难。 所以我就综合论文和其他博客进行一个总结,外加点自己的理解成文,但因为我本身并非密码学专业,描述如有不当,请指正。 本文偏数学推导,非科普也不讲应用,感兴趣的话,可准备纸笔一路推导下去。 本文撰写前参考了Zcash博客,安比实验室博客,Vitalik等等许多博客以及多篇相关论文,但本文并非学术论文,所以就不一一列出引用了。 首先说说零知识证明,其实这个概念的出现已经很久了,零知识证明协议有很多种,zkSNARKs是目前应用最广泛的一种(包括各种改进方案),其他还有zk-STARKs、bulletproofs等等。 其基本概念就是一个证明者Prover向验证者Verifier证明某个陈述(Statement)是真是假,但在证明过程中不揭露任何其他信息。比如身份证明,某一个组织让组织内成员提供身份证明,但不想泄露任何信息,就可以使用零知识证明来完成。但是注意,这里的说的证明并非数学意义上命题证明意义上的证明,而是Prover去让Verifier相信某个陈述为真,属于概率证明,和数学推导的证明不一样。 目前,zcash,ethereum,filecoin等很多项目都使用了零知识证明来完成某些任务,比如隐私交易,链下交易验证,存储证明等等。 证明的简单形式化定义如下:设S是一个集合,那么一个(P,V)的证明系统必须满足下面的条件: 1)完备性(Completeness) $Prob[(P,V)(x) = Accept | x \in S] \geq \epsilon$ 2. 正确性(Soundness) $ProbP,V = Accept | x \notin S] \leq \delta$ 其中 $\epsilon \in (\frac{1}{2}, 1]$,$\delta \in [0,\frac{1}{2})$,分别为完备性概率和正确性概率,我们肯定是希望$\epsilon$越大越好,$\delta$越小越好。 简单解释,就是我们希望Prover拥有关于Statement的证据(Witness)的时候,Prover可以让Verifier相信陈述为真(完备性),而一个恶意的Prover将无法欺骗Verifier,来通过验证(正确性)。 那么,除了完备性和正确性以外,如何理解一个证明协议是否是零知识呢? 上面也说了,零知识要求证明者提供的证明除了可以验证陈述为真外,不会泄露任何信息,即证明本身在其概率空间内是均匀分布的(从熵来理解,即不包含任何信息冗余)。那么如何去定量地分析一个协议是否是零知识的? 对于零知识证明来说,证明其为零知识就需要为证明本身构造一个模拟器(Simulator),意思就是如果verifier不需要prover参与,可以自己构造出一个不可区分的证明出来的话,那么就能证明该协议是零知识的。 听起来有点绕,但逻辑很简单,就是如果不需要prover参与就可以构造证明的话,那这个过程本身就没有任何知识的参与,自然也就是零知识的,如果通过Simulator构造出来的证明,在概率分布上无法和prover提交的证明区分开,那么就相当于prover提交的证明也是零知识的。 举个例子,假设有AB两个电脑,我向A电脑输入一个字符串,B电脑自己生成随机串,如果两个字符串在概率分布上相同,则证明其实我什么有效信息也没有输进去,所以是零知识的。 下面举一个具体的例子。 - Schnorr协议 假设prover拥有私钥sk, pk = sk*G, G为椭圆曲线群上的一个生成元。 verifier拥有pk。 现在prover想向verifier证明自己拥有私钥,步骤如下: 1. 首先prover选取一个随机数k,并计算$K = k\cdot G$ ,将$K$发送给verifier; 1. verifier选取一随机数c发送给prover; 1. prover计算$r = k + c\cdot sk$,将r发送给verifier; 1. verifier验证$rG \space ?= \space K + c \cdot pk$。 由于椭圆曲线离散对数难题,有pk = sk * G,已知pk和G是无法逆推得sk的。 上面验证的关键在于Prover无法事先知道c,根据椭圆曲线离散对数难题,prover无法找到一对sk和k,使得计算的$r = k + c\cdot sk$满足验证,所以协议是满足正确性的。 而如果Prover确实知道sk,那么验证肯定是可以通过的,所以协议是满足完备性的。 但是如何证明上面的验证是零知识的呢?我们现在假设没有Prover参与,让Verifier在自娱自乐,去模拟一个零知识证明交互过程。 构造simulator: Verifier选取随机数c以及r,计算$K = rG-c\cdot pk$。此时全流程就已经被模拟出来了。 Prover发送的K,以及Verifier选取的c,最后响应的r都有了,但是这个过程本身并没有Prover参与,构造出来的证明与Prover的证明在概率上是无法区分的,所以协议本身是零知识的,而这个模拟器是依靠在发送k之前就已经知道c来构造的。 从上面的分析也可以看出,Prover一定不能事先知道c,否则也是可以模拟证明的,所以说上述零知识证明的关键就在于这个随机数。 ## 2. NP与QAP 通俗来讲,P问题即为在多项式时间内可解的一类问题,而NP问题属于在多项式时间内不可解,但是给定一个解,可以在多项式时间内验证解是否正确的一类问题,此外还有NP完全问题,属于NP问题中最难的一种,任何NP问题都可以归约(Reduction)到一个NP完全问题,所以只要找到了一个NP完全问题的多项式时间解法,由于可以相互归约,相当于解决了所有的NP问题,这个问题我也不懂,就不多说了。 zkSNARKs是基于NP问题的,因为如果是一个P问题的话,那么直接求解就行了,证明就无从谈起,所以zkSNARKs的依托于某种NP问题,很难求解但是可以快速验证。 目前zkSNARKs使用最广泛的NP完全问题是QAP(Quadratic Arithmetic Program),我们可以很快的去验证解,而很难用有限资源去求解。 简单解释QAP即,给一系列的多项式以及一个目标多项式,是否能根据这一系列的多项式,求得它们的一个线性组合,刚好可以整除多项式。即如下描述: - 给定一系列多项式$\{l_i(x),r_i(x),o_i(x) \}_{i=0}^n$,目标t(x) - 求一个线性组合 $\{a_i\}_{i=0}^n$ ,使得 $\sum_{i=0}^n a_i\times l_i(x) \cdot \sum_{i=0}^n a_i\times r_i(x)- \sum_{i=0}^n a_i\times o_i(x))=t(x)h(x)$ ,可记为 $L(X)\cdot R(x) - O(x) = t(x)h(x)$ 。 这个问题如果给出了一个解$\{a_i\}_{i=0}^n$,那么验证很简单,直接除t(x)即可验证是否满足,但是想求解就很难。 zkSNARKs的思路就是,将原问题的解转化为一个QAP问题的解,然后公开QAP问题,这样的话拥有解的人用证明公开自己拥有一个解,其他人可以简单验证证明是否成立。 当然,上面的描述过于笼统,而且也没有说明zkSNARKs到底是什么东西,也没有说明如何将一个问题转化成QAP问题。 ## 3. zkSNARKs概述 首先简单说一说zkSNARKs,它是zero-knowledge succinct non-interactive arguments of knowledge,分开来将: - zero-knowledge:表示零知识,证明某个陈述为真但不揭露任何其他信息。 - succinct代表简洁:表示证明大小很短且易于验证 - no-interactive:非交互,上面说的schnorr协议需要verifier生成一个随机数,故而是交互证明,而非交互,则不需要这样的角色,prover可以自行生成证明,给所有人验证。 - arguments:注意这里没有用proofs这个词,proof(证明)和argument(论证)在零知识证明里,是有区别的,在proof system中,即便是计算能力无限的prover也没法欺骗verifier去相信一个错误的陈述为真(统计可靠性),而在argument system中,只有多项式时间计算能力的没法欺骗(计算可靠性),比如说假设能破解椭圆曲线离散对数问题,那么可能就不再安全。 - of Knowledge:如果prover没有相应的witness(证据),那么构建相应的proof(对于proof system)或argument(对于argument system)是不可能的。 简洁之类的后面再讲,但有一个问题先说下,上面举的例子schnorr是交互型协议,其基础来源于prover无法事先预知verifier给的随机数,但是如果不能交互的话,没有第三方提供随机数,怎么构造证明?怎么构造模拟器? 对于schnorr,我们其实可以在prover生成随机数r时,用哈希函数c = hash(pk,r),来生成这个c,然后后面的过程与原本的类似,就可以不用交互了,但是这个依赖于hash函数的安全性,我们需要将其认定为是一个随机预言机( 随机预言机定义:就是它对任何输入都回传一个真正均匀随机的输出,但对相同的输入每次都会回传一模一样的输出,相当于一个黑盒。事实上哈希函数不是随机预言机 ),可以看到,其实这样schnorr零知识证明就变成一个数字签名协议了,就是schnorr签名协议。 这个过程叫Fiat-Shamir Transform,即用随机预言机解决这个随机的问题,其他有一些零知识证明方案就采用了这个思路去掉了可信setup过程。 而对于zkSNARKs,则采取了可信Setup的方法,在初始的时候就把这个随机值先生成好了,再把这个值隐藏起来不让大家知道,生成人也要保证永不泄露,具体后面再讲。 ## 4. 问题转化 接下来我来先讲解如何将一个具体的问题转换为一个zkSNARKs能处理的问题(即QAP形式),然后再将如何在QAP上构建zkSNARKs。 比如说,我想要验证prover是否执行了下面的代码,但prover在证明过程中不想泄露自己的输入值,应该怎么做呢? ```go func calc(w bool, a, b int) int { if w { return 3*a*b } else { return 5*a+b } } ``` 一段代码逻辑,我们怎么把一段具体的代码逻辑转换为一个zkSNARKs可以处理的东西呢?上面说了zkSNARKS依赖某种NP问题,最常用的是QAP也就以QAP举例。讲怎么把代码逻辑转换成QAP。 第一步是将代码逻辑写成多项式,上述代码其实就是两个等式,其中v是返回值。 $w \cdot (3a\cdot b) + (1-w)\cdot(5a+b) = v,\space w \cdot (w-1)=0$ 我们需要将这两个多项式转换成一系列多项式$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,并且给出目标多项式t(x),最后代入解应该是应该是$L(X)\cdot R(x) - O(x) = t(x)h(x)$的形式。 我先用一种简单的方法计算一遍方便大家理解内在逻辑,再讲另一种标准做法。 简单做法: 先对上面多项式的形式进行一些改变,转换为近似A op B = C的形式。, $w \cdot (3a\cdot b) + (1-w)\cdot(5a+b) = v$ -> $w \cdot (3a\cdot b) +5a+b -w(5a+b)=v$ ->$w \cdot (3a\cdot b - 5a-b) +5a+b=v$ ->$1:\space w \cdot (3a\cdot b - 5a - b ) = v - 5a -b \space$ $w\cdot (w-1)=0$ ->$2:\space w\cdot w = w \space$ 如上,将两个式子都转换成了$a\times b = c$的形式了,但式(1)在乘法内部也出现了乘法,我们可以再取一个中间值,得: $1:\space 1 \cdot a \times 1\cdot b = 1\cdot m\space$ $2:\space 1 \cdot w \times (3\cdot m + - 5\cdot a + - 1\cdot b ) = 1\cdot v + -5\cdot a + -1 \cdot b \space$ $3: \space 1 \cdot w \times 1 \cdot w = 1 \cdot w$ 现在我们拥有了三个多项式,由上我们可以看到,$\{l_i(x)\}_{i=0}^n$只与a,w有关,$\{r_i(x)\}_{i=0}^n$与m, a, b, w有关,$\{o_i(x)\}_{i=0}^n$与m, v, a, b, w有关 现在我们把多项式里变量的系数当成另一个多项式的函数值,则有三个多项式,为了方便,就假设这些系数是取点1, 2, 3时的函数值。 相当于: $1:\space l_a(1) \cdot a \times r_b(1)\cdot b =o_m(1) \cdot m\space$ $2:\space l_w(2) \cdot w \times (r_m(2)\cdot m +r_a(2)\cdot a + r_b(2)\cdot b ) = o_v(2)\cdot v + o_a(2)\cdot a + o_b(2) \cdot b \space$ $3: \space l_w(3) \cdot w \times r_w(3) \cdot w = o_w(3) \cdot w$ 可得: $\{l_i(x) \}_{i=0}^n$: $l_a(1)=1,l_a(2)=0,l_a(3)=0$(因为左操作数里,a只在第一个式子出现,下面与其类似) $l_w(1)=0,l_w(2)=1,l_w(3)=1$ $\{r_i(x) \}_{i=0}^n$: $r_m(1)=0,r_m(2)=3,r_m(3)=0$ $r_a(1)=0,r_a(2)=-5,r_a(3)=0$ $r_b(1)=1,r_m(2)=-1,r_b(3)=0$ $r_w(1)=0,r_w(2)=0,r_w(3)=1$ $\{o_i(x) \}_{i=0}^n$: $o_m(1)=1,o_m(2)=0,o_m(3)=0$ $o_a(1)=0,o_a(2)=-5,o_a(3)=0$ $o_b(1)=0,o_b(2)=-1,o_m(3)=0$ $o_w(1)=0,o_w(2)=0,o_m(3)=1$ $o_v(1)=0,o_v(2)=1,o_v(3)=0$ 通过上面的定点,我们就分别把三个式子中参数的系数,变成了函数值,然后我们可以用拉格朗日插值法(不懂的自行百度),将上面的描点拟合成曲线,最后可以计算得。 $\{l_i(x)\}_{i=0}^n$: $l_a(x)=\frac{1}{2}x^2-\frac{5}{2}x+3$ $l_w(x)=-\frac{1}{2}x^2+\frac{5}{2}x-2$ $\{r_i(x)\}_{i=0}^n$: $r_m(x)=-3x^2+12x-9$ $r_a(x)=5x^2-20x+15$ $r_b(x)=\frac{3}{2}x^2-\frac{13}{2}x+6$ $r_w(x)=\frac{1}{2}x^2-\frac{3}{2}x+1$ $\{o_i(x)\}_{i=0}^n$: $o_m(x)=\frac{1}{2}x^2-\frac{5}{2}x+3$ $o_a(x)=5x^2-20x+15$ $o_b(x)=x^2-4x+3$ $o_w(x)=\frac{1}{2}x^2-\frac{3}{2}x+1$ $o_v(x)=-x^2+4x-3$ 由此,我们就求得了一系列多项式$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,而t(x)=(x-1)(x-2)(x-3),这里的逻辑可能有点费解,要理清楚。 细讲一下,我们在上面用系数当成点1,2,3处的函数值得出了一系列多项式,现在我们把正确的m, v, a, b, w当做系数给这些多项式做排列组合后得到$P(x) = L(x)\cdot R(x) - O(x)$,代入 1 相当于就退化为$P(1) = 1 \cdot a \times 1\cdot b - 1\cdot m = 0$,代入2,3也类似,所以组合成的多项式P(x)一定有根1, 2, 3,自然有因式t(x)=(x-1)(x-2)(x-3)。 我们是从系数中得到多项式,而原来多项式中变量的值当做系数,知道一个正确的系数,就相当于执行了下面这个函数逻辑。 假设输入$w=1,a=2,b=3$,则可以求出$m=6, v=18$,用这个参数排列组合上述多项式一定满足条件。 ```go func calc(w bool, a, b int) int { if w { return 3*a*b } else { return 5*a+b } } ``` 不相信可以用上面的值算一下: $L(x)=2l_a(x)+l_w(x)=2\cdot(\frac{1}{2}x^2-\frac{5}{2}x+3)-\frac{1}{2}x^2+\frac{5}{2}x-2 = \frac{1}{2}x^2-\frac{5}{2}x+4$ $R(x)=6r_m(x)+2r_a(x)+3r_b(x)+r_w(x)=-3x^2+11x-5$ $O(x)=6o_m(x)+2o_a(x)+3o_b(x)+o_w(x)+18o_v(x)=-\frac{3}{2}x^2+\frac{7}{2}x+4$ 再计算$P(x)=L(x)R(x)-O(x)=-\frac{3}{2}x^4+13x^3-\frac{81}{2}x^2+53x-24$ 接下来验证P(x)能否整除t(x)=(x-1)(x-2)(x-3),$h(x)=\frac{P(x)}{(x-1)(x-2)(x-3)}=-\frac{3}{2}x+4$,可以看到,代入正确的witness确实是可以得到一个满足的多项式的。 此时我们完成了zk-SNARKs的第一步,即将验证代码的执行转成了验证多项式是否成立。 上面讲了一个简单版的怎么将问题转化为QAP,下面讲一个更通用的方法,但其实原理和上面的过程是一样的,所以下面的过程有什么地方理解不了,看看上面的简化版就行了,只不过下面拆解得更基础,电路更多了而已。 用constraint system来做,一般举例都用的R1CS(rank 1 constraint system),R1CS是由一系列三向量组(x,y,z)组成的序列,R1CS有一个解向量s,需要满足$s \cdot \mathbf{x}\times s\cdot \mathbf{y}-s \cdot \mathbf{z} = 0$。 所谓constraint system就是要限制,比如说w(w-1)=0,就限制了w只能为1或0,(w-2)*1=0,就限制了w只能为2。 我们首先需要将多项式转成算术电路,因为电路的可满足性是一个NP问题,我们可以等会再将这个NP问题规约成QAP问题,还是以上面的多项式为例。 - From 多项式 to R1CS - 算术电路拍平 首先将多项式的所有操作转化为 $x\space (op) \space y = z$ 的形式,op可以是+,-,*,/ 等。 $1:w \cdot (3a\cdot b - 5a-b) +5a+b=v$ $2:\space w\cdot w = w \space$ 那么转化为$x\space (op) \space y = z$ 以后,可以得到下面这个电路: ``` sym_1 = a * b sym_2 = 3 * sym_1 sym_3 = 5 * a sym_4 = sym_2 - sym_3 sym_5 = sym_4 - b sym_6 = w * sym_5 sym_7 = sym_3 + b v = sym_6 + sym_7 w = w * w ``` - 可以看出,为了将所有操作都转成 $x\space (op) \space y = z$ 的形式,加入了sym_1...sym_7等中间变量。 上面的形式即算数电路形式,向电路输入参数得到结果,即将代码逻辑拆成了电路的计算,计算完以后就会得到一组满足电路的值,至于为什么叫电路,我就不画图了,引一张图,这是$x^3+x+5=out$的算术电路形式,上面的电路化成图自己脑补。 - ![](C:\Users\xcshuan\OneDrive\业余社区\r1cs.png) - 电路转成R1CS 现在我们把所有变量包括中间变量组成一组向量,`s = [one,a,b,w,v,sym_1,sym_2,sym_3,sym_4,sym_5,sym_6,sym_7]^T`,s即为R1CS中的解向量,其中one表示数字1。 现在我们对上面的式子进行转换,这部分应该很好理解,下面向量里的值作用相当于简单版里的「系数」,原理和简单版差不多,除了式子更多些而已。 a * b = sym_1 ``` x = [0,1,0,0,0,0,0,0,0,0,0,0] y = [0,0,1,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,0,1,0,0,0,0,0,0] ``` - 可知对于上面的x,y,x和s,向量式$s \cdot x\times s\cdot y-s \cdot z = 0$是满足的。 3 * sym_1 = sym_2 ``` x = [3,0,0,0,0,0,0,0,0,0,0,0] y = [0,0,0,0,0,1,0,0,0,0,0,0] z = [0,0,0,0,0,0,1,0,0,0,0,0] ``` - 5 * a = sym_3 ``` x = [5,0,0,0,0,0,0,0,0,0,0,0] y = [0,1,0,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,0,0,0,1,0,0,0,0] ``` - (sym_2 - sym_3) * 1 = sym_4 ``` x = [0,0,0,0,0,0,0,1,-1,0,0,0] y = [1,0,0,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,0,0,0,0,1,0,0,0] ``` - (sym_4 - b) * 1 = sym_5 ``` x = [0,0,-1,0,0,0,0,0,1,0,0,0] y = [1,0,0,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,0,0,0,0,0,1,0,0] ``` - w * sym_5 = sym_6 ``` x = [0,0,0,1,0,0,0,0,0,0,0,0] y = [0,0,0,0,0,0,0,0,0,1,0,0] z = [0,0,0,0,0,0,0,0,0,0,1,0] ``` - (sym_3 + b) * 1 = sym_7 ``` x = [0,0,1,0,0,0,0,1,0,0,0,0] y = [1,0,0,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,0,0,0,0,0,0,0,1] ``` - (sym_6 + sym_7) * 1 = v ``` x = [0,0,0,0,0,0,0,0,0,0,1,1] y = [1,0,0,0,0,0,0,0,0,0,0,0] z = [0,0,0,0,1,0,1,0,0,0,0,0] ``` - w * w = w ``` x = [0,0,0,1,0,0,0,0,0,0,0,0] y = [0,0,0,1,0,0,0,0,0,0,0,0] z = [0,0,0,1,0,0,0,0,0,0,0,0] ``` - From R1CS to QAP 我们就得到了一组$\mathbf{x,y,z}$向量,$\mathbf{x,y,z}$各自组合可以得到一个矩阵,每一行均满足$s \cdot \mathbf{x}\times s\cdot \mathbf{y}-s \cdot \mathbf{z} = 0$。 我们可以对每一列进行根简单版本一样的处理,即使用拉格朗日插值法算出多项式。 比如可以算出代入x=1,2,3,4,5,6,7,8,9,用x,y,z的每一列的值当做函数值,然后用拉格朗日插值法,即可得多项式系列$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,基本思路和简单方案一致,只不过这个方案中多项式的阶数更高, t(x) = (x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8)(x-9)。 举个例子,可以计算$l_1(x)$如下: $l_1(x)=\frac{29}{10080}x^8-\frac{101}{840}x^7+\frac{127}{60}x^6-\frac{4889}{240}x^5+\frac{18597}{160}x^4-\frac{95489}{240}x^3+\frac{494329}{630}x^2-{83647}{105}x+312$ 其他的一样求出,后续思路与简单版一样,代入正确的s可以得到一个能整除t(x)的P(x)。 当一个prover知道一组正确的解向量s时,即可通过验证,跟简单形式的理解是一样的。 ## 5. 细节详解 上面花了很大篇幅讲述如何将一个代码逻辑问题转换为一个zkSNARKs可以处理的QAP表达,现在来讲讲,针对一个QAP问题,怎么做zkSNARKs的证明过程呢? 让我们慢慢去思考这个问题,首先,我们有一系列的多项式$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,以及一个t(x),在非零知识的过程中我们只需要让prover传回一个解即可,但如果要做成零知识呢?我们一步一步引入密码工具完成zk-SNARKs。 我们首先明确我们的目的,对于一个QAP问题,witness就是$\{a_i\}_{i=0}^n$,我们想达到的是,prover知道这么一组满足给定QAP问题的系数witness,是prover能通过验证的充要条件,但是witness的信息不会泄露。 我们先思考需要交互的情况: 1)此时verifier随机选取一个数s,发送给prover。 2)prover根据$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,代入自己的系数计算出L(x),R(x),O(x),并令P(x) = L(x)R(x)-O(x),求得P(x)/t(x) = h(x),发送L(s),R(s),O(s)与h(s)给verifier。 3. verifier计算$L(s)R(s)=h(s)\cdot t(s)+O(s)$是否成立。 但是很明显,上述协议根本不是一个证明协议,证明本身是可以任意伪造的,因为我们只通过一次检验,没法保证prover如实计算了P(x)以及h(x),这个不能用来证明的协议放在这里这里仅作一个引子。 另外,最开始我们不考虑通用化的问题,所以要证明的statement就体现在构造出来的QAP问题中,后面会讲通用化。 接下来要引入密码学工具一步一步限制prover达到目的,**准备纸笔开始推公式吧!** **圈一个重点,免得大家迷失方向,下面做的所有工作都是为了这三件事**: 1. 验证证明是用给定CRS公共参考串里面的值线性组合而来。 1. 验证证明中L R O三者的系数是一致的。 1. 验证给出的证明是满足QAP的解。 ### 5.1 同态隐藏 首先我们要用上同态隐藏,假设有一个映射E(),映射前的某些操作可以在映射后体现,就说明映射具有同态性。 定义: (1)对于一个给定的x,知道E(x),很难逆推得到x。 (2)对于不同的x和y,$E(x) \neq E(y)$。 (3)对于某些操作,映射后依然存在,比如E(x+y)可以从E(x)和E(y)中计算出来。 一个简单的例子是椭圆曲线域上的幂运算,$E(x) = g^x$,g为生成元,根据椭圆曲线离散对数问题,知道E(x)和g并不能反推回x。 $E(x+y) = g^{(x+y)} = g^x\space \times g^y\space =E(x)E(y)$ 在未引入双线性映射的时候,图方便用E(s)来写,引入后都用幂形式$g^s$写,两者在本文意义相同。 举个例子,verifier可以选择一个随机数s,然后发送$\{g^{\{s^i\}}\}_{i=0}^n = g^s, g^{s^1}, g^{s^2}...g^{s^n}$给prover,verifier知道一个多项式$f(x) = 3x^2+2$,想验证prover知不知道此多项式,可将$\{g^{\{s^i\}}\}_{i=0}^n$发送给prover,prover计算$g^{f(s)} =g^{3s^2+2}= {(g^{s^2})}^3\cdot (g)^2$,将$g^{f(s)}$发回,verifier既可验证,计算过程prover无法知道具体的s值为多少。 概念上科普下同态隐藏——上面的计算好像是对一堆黑盒子做操作,虽然不知道黑盒子里面是什么(隐藏性质),但操作完又能得到想要的值(同态性质)。在zkSNARKs里我们不需要全同态操作,全同态操作目前尚不实用。 根据同态隐藏的性质,我们修改下上面的基础协议看看能不能行: 1)此时verifier随机选取一个数s,计算$\{ E(s^i) \}_{i=0}^d$(d为h的阶,可以算出来)以及$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$,发送给prover。 2)prover根据$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,代入自己的系数计算出L(x),R(x),O(x),并令P(x) = L(x)R(x)-O(x),求得P(x)/t(x) = h(x)。用$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$计算出E(P(x)),用$\{ E(s^i) \}_{i=0}^d$计算出E(h(s)),并将计算结果发送给verifier。 3)verifier计算$E(P(s))=E(h(s)\cdot t(s))$是否成立。 虽然prover现在可以用参数计算结果了,但仔细揣摩会发现这个协议依然是无效的,因为没法保证prover一定是用$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$的排列组合计算出来的值,比如Verifier发送$E(l_i(5))$,prover也可以用随便选个E(3)进行计算。verifier无法识别这种情况,看来我们需要引出另一个工具来解决这个问题了。 ### 5.2 KCA(Knowledge of Coefficient Test and Assumption) 假如Verifier对$\{E(s^i)\}_{i=0}^n$,每一个值再计算相应的$\{E(\alpha\cdot s^i)\}_{i=0}^n$,而$\alpha$对于prover是未知的,因为离散对数难题,prover也无法从中提取出$\alpha$,那么我们就能限制prover只能用我发送的值进行计算了。 比如Verifier发送$E(5), E(5^1), E(5^2)...E(5^n),E(\alpha5), E(\alpha5^1), E(\alpha5^2)...E(\alpha5^n)$,那么prover就只能被迫用5这个值进行计算$E(f(5))$,以及$E(\alpha f(5))$,最终验证prover返回的两个值,$E(f(5))^\alpha ?= E(\alpha f(5))$即可保证prover一定是诚实的按照$E(5^i)$的排列组合计算出来的,如果prover想用$E(4)$进行计算,可是他提供不了$E(\alpha 4)$就通过不了这一步验证了。 #### 5.2.1 串用问题 但是上面的解决方案可能导致串用问题,因为所有的$\alpha$都一样,prover可能用$l_i(x)$串在R(x)的计算中,那么我们给$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$三个不同的 $\{ \alpha_l,\alpha_r,\alpha_o \}$,所以verifier需要事先计算$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$以及对应的$\{E(\alpha_l\cdot l_i(x)),E(\alpha_r\cdot r_i(x)),E(\alpha_o\cdot o_i(x))\}_{i=0}^n$,即可保证都是根据各自的隐藏值算的。 这样的话,prover就可以直接通过$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$计算$E(L(s)),E(R(s)),E(O(s))$,通过$\{E(s^i)\}_{i=0}^d$计算$E(h(s))$,并用$\{E(\alpha_l\cdot l_i(x)),E(\alpha_r\cdot r_i(x)),E(\alpha_o\cdot o_i(x))\}_{i=0}^n$计算得$L_{\alpha_l} =E(\alpha_l\cdot L(x)),R_{\alpha_r} =E(\alpha_r\cdot R(x)),O_{\alpha_o} =E(\alpha_o\cdot O(x))$,并将结果全部发送给verifier。 verifier在收到以后,验证$E(L(s))^{\alpha_l}=E(L_{\alpha_l})$,$E(R(s))^{\alpha_r}=E(R_{\alpha_r})$,$E(O(s))^{\alpha_o}=E(O_{\alpha_o})$几个等式是否成立,再验证$E(L(s)\cdot R(s))=E(h(s)\cdot t(s)+O(s))$是否成立。(**注意,此时方案已经不成立,因为在验证中出现了乘法,但是verifier无法得到隐藏值的乘法值,这个问题在下面的双线性映射部分会得到解决,因为双线性映射具备乘法同态,这一步放这里起一个递进的作用**) 等于多加了一步验证解决了**"证明是用给定参考串里面的值线性组合计算而来。"** #### 5.2.2 系数一致性问题 但是又有新的问题,我们知道QAP问题中,$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,三个多项式分别排列组合后得到L(x), R(x),O(x),他们的系数序列$\{a_i\}_{i=0}^n$应该是相同的。 但仅仅通过不同的$\alpha$并不能保证prover一定会用相同的系数序列计算,我们可以用不同的几组系数a1, a2, a3计算,也可能能通过验证。我们又需要新的思路,经过上面一步的经验你们应该已经清楚了,没错,我们再加一步验证。 此时我们搬出检验和思路,可以通过检验和来解决这个问题,verifier除了需要事先计算$\{E(s^i)\}_{i=0}^d$,$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$,$\{E(\alpha_l\cdot l_i(x)),E(\alpha_r\cdot r_i(x)),E(\alpha_o\cdot o_i(x))\}_{i=0}^n$之外,还需要计算一个$\{E(\beta(l_i(s)),E(\beta(r_i(s)),E(\beta(o_i(s)))\}_{i=0}^n$,相当于对每一个$\{l_i(s),r_i(s),o_i(s)\}$三元组,都计算一个检验和,最后加起来,需要系数序列相等才可能满足$\beta$的系数约束。 基于这个新的序列,prover在生成证明的时候还需要多计算一个结果,$E(z_i(s))=E(\beta\cdot (l_i(s)+r_i(s)+o_i(s)))^{a_i}$,这个$a_i$就是针对每一组$\{l_i(x),r_i(x),o_i(x)\}$解中相同的系数,得$Z = E(Z(s))=\prod_{i=0}^n E(z_i(s))$发送给verifier。 verifier再多一步验证$E(Z(s)) = E(L(s)+R(s)+O(s))^\beta$,即可验证线性组合是否相同。 但是如果只用一个$\beta$,其实prover还是有操作空间的,比如当l(s)=r(s)的时候,有检验和$\beta(v_L\cdot l(s)+v_R\cdot r(s)+v_O\cdot o(s))$,按理说需要$v_L=v_R=v_O$,才能提取出一个公因子$v\cdot \beta$,变成统一的$v\cdot \beta(l(s)+r(s)+o(s))$。 但由于l(s)=r(s),则可以进行如下操作: $\beta((2v_O-v_R)\cdot l(s)+v_R\cdot r(s)+v_O\cdot o(s))$ $=\beta(2v_O\cdot l(s)-v_R\cdot l(s)+v_R\cdot r(s)+v_O\cdot o(s))$ $=\beta(2v_O\cdot l(s)+v_O\cdot o(s))=v_O\beta(2l(s)+o(s))$. 也得到了满足提取公因子的形式,可以通过验证,但此时系数并不一样。为了解决这个问题,我们可以对不同的$\{l_i(x),r_i(x),o_i(x)\}{i=0}^n$选择不同的$\beta$,即分别生成${E(\beta_l\cdot l_i(s)),E(\beta_r\cdot r_i(s)),E(\beta_o\cdot o_i(s)}{i=0}^n$,可避免这种情况,此时prover计算$E(z_i(s))=E(\beta_l\cdot l_i(s)+\beta_r\cdot r_i(s)+\beta_o\cdot o_i(s))^{a_i}$,$Z = E(Z(s))=\prod_{i=0}^n E(z_i(s))$替代上面得操作,verifier验证$E(Z(s)) = E(\beta_lL(s)+\beta_rR(s)+\beta_oO(s))$是否成立。 总之这一步我们的目的就是确保prover计算的时候,三个系数序列一定要是相等的。 ### 5.3. 变为非交互 上面说了一堆,其实还是针对交互型协议的,每一次verifier每次都需要计算特别多的数字序列,如$\{E(s^i)\}_{i=0}^d$,$\{E(\alpha\cdot s^i)\}_{i=0}^n$,$\{E(l_i(s)),E(r_i(s)),E(o_i(s))\}_{i=0}^n$,$\{E(\alpha_l\cdot l_i(x)),E(\alpha_r\cdot r_i(x)),E(\alpha_o\cdot o_i(x))\}_{i=0}^n$,$\{E(\beta_l\cdot l_i(s)+\beta_r\cdot r_i(s)+\beta_o\cdot o_i(s)\}_{i=0}^n$。 现实情况中,多项式的阶数都非常高(即算数电路特别多),一般都几百万甚至上亿,每一次参数生成需要几小时乃至几天,对于verifier太耗时了,但如果$s,\alpha,\beta,\gamma$等值没有泄露的话,其实所有的prover可以用相同的参数生成证明,这个就叫公共参考串(CRS)。 但如果用公共参考串这里就引出两个问题: - 如何保证生成公共参考串的人不泄露这几个隐私参数? - 在verifier验证的时候,很多地方都直接用上了$s,\alpha,\beta,\gamma$这几个参数才可以验证,但是这几个参数在非交互情况下又必须隐藏,怎么保证还可以验证呢? #### 5.3.1 安全多方计算 关于上面两个问题,第一个问题是通过安全多方计算解决的,就是会有很多人参与生成公共参考串,每个人贡献出自己的随机数,最后通过某些手段拼合,由于没人知道所有的随机值,那么只要还剩一个人没有作恶,协议就是安全的。 举个例子,计算$\{E(s^i)\}_{i=0}^n$,$\{E(\alpha\cdot s^i)\}_{i=0}^n$; 让a先计算$\{E(s_a^i)\}_{i=0}^n$,$\{E(\alpha\cdot_a s_a^i)\}_{i=0}^n$,交给b; b在此基础上计算,$\{E(s_a^i)^{s_b^i}\}_{i=0}^n$,$\{E(\alpha_a\cdot s_a^i) ^ {\alpha_b\cdot s_b^i}\}_{i=0}^n$,得到$\{E(s_as_b)^i\}_{i=0}^n$,$\{E(\alpha_a\alpha_b \cdot (s_as_b)^i )\}_{i=0}^n$; 以此类推,可以扩展到计算其他参数以及更多人参与,细节就不赘述了,这里能理解即可。 #### 5.3.2 双线性映射 第二个问题是通过双线性映射(椭圆曲线配对,Pairing)解决的,双线性映射是具有如下性质的映射函数: 双线性映射可以用五元组$(p,G_1,G_2,G_T,e,g_1,g_2)$来描述,G1,G2,GT是三个素数阶乘法循环群,阶数皆为p,定义在这三个群上的一个映射关系e:$G1\times G2 —>GT$,$g_1$是$G_1$的生成元,$g_2$是$G_2$的生成元,$e(g_1,g_2)$是$G_T$的生成元$g_t$,若$G_1=G_2$则称为对称双线性映射,否则为非对称双线性映射。下面举例子为了方便,未特定指明都用对称双线性映射计算,因为非对称还需注明哪些元素属于G1,哪些属于G2,不过要清楚,两种双线性映射都可以用于zk-SNARKs,且由于非对称效率更高,实际中一般用的都是非对称,比如基于BLS12-381曲线构造的Pairing。 并满足以下性质: - 双线性性:对于任意$a,b, c\in Zp$,有$e(g_1^a, g_2^b) = e(g_1, g_2)^{ab}$,$e(g_1^a\cdot g_1^b,g_2^c)=e(g_1^a,g_2^c)e(g_1^b,g_2^c)$; - 非退化性:存在$a\in G_1,b\in G_2$,使得$e(a,b)\neq(1)G_t$( $(1)G_t$ 代表GT群的单位元); - 可计算性:对任意的$R\in G1,S\in G2$,存在有效的算法计算e(R, S)的值。 具体关于这样的双线性配对如何构造就超出本文谈论范围了。 那么如何利用双线性配对在不知道$s,\alpha,\beta,\gamma$这几个参数的时候,进行验证呢? 举个例子,在上面验证$\alpha$对的验证中,本来需要verifier知道$\alpha$的值,验证$E(L(s))^{\alpha_l}=E(L_{\alpha_l})$,现在可以改为验证$e(g^{L(s)},g^{\alpha_l})=e(g^{L_{\alpha_l}},g)$是否成立即可,这样的话,就可以只公开$g^{\alpha_l}$而不是用私密参数$\alpha_l$本身做验证了。 #### 5.3.3 malleable问题 但这样的话又会导致新的问题,即证明得可操作问题-malleable问题,因为prover此时可以获取$g^{\alpha_l},g^{\alpha_r},g^{\alpha_o},g^{\beta_l},g^{\beta_r},g^{\beta_o}$。这样的话,prover甚至非prover都可以对证明做一些小操作了。 比如对于证明中的$g^{L(x)}$,我们给它加一个数5,$g^{L(s)+5}$,其他的不做任何改动,由于我们知道$g^{\alpha_l},g^{\beta_l}$,则可以算出$g^{\alpha_lL(s)}*({g^{\alpha_l}})^5=g^{\alpha_l(L(s)+5)}$,$g^{\beta_lL(s)}*({g^{\beta_l}})^5=g^{\beta_l(L(s)+5)}$。 这样的话,也可以通过验证$e(g^{L(s)+5},g^{\alpha_l})=e(g^{\alpha_l(L(s)+5)},g)$,以及$e(g^{L(s)+5},g^{\beta_l})\cdot e(g^{R(x)},g^{\beta_r})\cdot e(g^{O(x)},g^{\beta_o})=e(g^{Z(s)}\cdot ({g^{\beta_l}})^5,g)$,其中$g^z=g^{\beta L(s)}\cdot g^{\beta R(s)}\cdot g^{\beta O(s)}$,这个计算上面已经讲过了,也就是说,我可能很容易用一个证明伪造出另一个可以通过的证明。 那么为了避免这种可操作性,需要新加一层限制,这个改进可以有多种思路。 我们首先可以把$\beta$验证变得更严格,即把在生成证明的proving key中将$\{g^{(\beta_l\cdot l_i(s)+\beta_r\cdot r_i(s)+\beta_o\cdot o_i(s)}\}_{i=0}^n$改为$\{g^{\gamma(\beta_l\cdot l_i(s)+\beta_r\cdot r_i(s)+\beta_o\cdot o_i(s)}\}_{i=0}^n$,这样的话,在trust Setup时就需要多一个$\gamma$,与之相应的,用于验证的verification key中就需要加入$\{g^{\beta_l\gamma},g^{\beta_r\gamma},g^{\beta_o\gamma},g^{\gamma}\}$。 验证这一步时变为,$e(g^{L(s)},g^{\beta_l\gamma})\cdot e(g^{R(x)},g^{\beta_r\gamma})\cdot e(g^{O(x)},g^{\beta_o\gamma})=e(g^Z,g^\gamma)$,由于无法求出$\gamma$,也就无法进行操作了。 不过上面的解决方案有一个问题就是,这一步验证需要四次Pairing双线性计算,而双线性计算是很耗时的,那我们能不能从$\alpha$验证入手来解决这个问题呢?也是可以的,针对$L,R,O$选取随机$\rho_l,\rho_r$,并令$\rho_o=\rho_l\cdot\rho_r$,此时给他们分配不同的生成元$g_l=g^{\rho_l},g_r=g^{\rho_r},g_r=g^{\rho_o}$。 这样的话,proving key变为$(\{g^{s^i}\}_{i=0}^d,\{g_l^{l_i(s)},g_r^{r_i(s)},g_o^{o_i(s)},g_l^{\alpha_ll_i(s)},g_r^{\alpha_rr_i(s)},g_o^{\alpha_oo_i(s)},g_l^{\beta l_i(s)},g_r^{\beta r_i(s)},g_o^{\beta o_i(s)}\}_{i=0}^n )$,然后verification key变为$\{g^{\alpha_l},g^{\alpha_r},g^{\alpha_o},g^{\beta\gamma},g^{\gamma}\}$。 则在做$\alpha$对验证时,变为$e(g_l^{l_{\alpha_l}},g)=e(g_l^{l},g^{\alpha_l})$,其他类似,由于不知道$\rho_l,\rho_r$,那就无法进行操作了,同时$\beta$共用,则验证变为,$e(g_l^{L(s)}\cdot g_r^{R(s)}\cdot g_o^{O(s)},g^{\beta\gamma})=e(g^{Z(s)},g^{\gamma})$,只用两次配对计算。 这样就在非交互的前提下解决了这个问题。 ### 5.4 σ偏移 此外,prover为了保证零知识,可能连$g_l^{L(s)},g_r^{R(s)},g_o^{(O(s))}$这些值都不想透露,因为在解比较少的时候,可以通过暴力法匹配计算出L(s)的值,所以我们可以对每个值都加一点只有prover自己知道的偏移,但同时保证验证可以通过,就能避免被暴力破解。 由于等式最终要满足$L(s)\cdot R(s) - O(s) = t(s)\cdot h(s)$成立,而t(s)又是所有人都知道的,所以我们只能改动$L(s),R(s),O(s)$,观察这个式子可知,我们只能$L(s),R(s),O(s)$上偏移t(s)的倍数,否则没法保证还能整除。 我们可以计算一下偏移的具体数值: $(L(s)+\sigma_lt(s))\cdot (R(s)+\sigma_rt(s))-(O(s)+\sigma_ot(s))=t(s)(\Delta+h(s))$ 现在我们可以将$\Delta$求出来。 $L(s)R(s)-O(s)+t(s)(\delta_rL(s)+\delta_lR(s)+\delta_l\delta_rt(s)-\delta_o)=t(s)\Delta+t(s)h(s)$ 由于$L(s)R(s)-O(s)=t(s)h(s)$ 所以约掉以后得: $\Delta=\delta_rL(s)+\delta_lR(s)+\delta_l\delta_rt(s)-\delta_o$ 这个式子放到证明过程中即为, $g_l^{L_p(s)}=(g_l^{t(s)})^{\sigma_l} \cdot g_l^{L(s)}$,$g_l^{L_{\alpha p}(s)}=(g_l^{\alpha_l t(s)})^{\sigma_l} \cdot g_l^{\alpha_l L(s)}$(L(s)的计算和上面差不多,然后$g_r^{R_p(s)},g_o^{O_p(s)}$用同样的方法求得。 同时计算$h_{\sigma}(x)=\frac{L(x)R(x)-O(x)}{t(x)}+\delta_rL(x)+\delta_lR(x)+\delta_l\delta_rt(x)-\delta_o$,并计算出$g^{h(s)}$。 $g^{z_p(s)}=(g_l^{\beta t(s)})^{\sigma_l}(g_r^{\beta t(s)})^{\sigma_r}(g_o^{\beta t(s)})^{\sigma_o}g_l^{\beta L(s)}\cdot g_r^{\beta R(s)}\cdot g_o^{\beta O(s)}$ 然后也是可以通过验证的,不过通过上面我们也可以发现要在proving key中加入$\{g_l^{t(x)},g_r^{t(x)},g_o^{t(x)},g_l^{\alpha_lt(x)},g_r^{\alpha_rt(x)},g_o^{\alpha_ot(x)},g_l^{\beta t(x)},g_r^{\beta t(x)},g_o^{\beta t(x)}\}$供prover所用,才能在偏移后生成证明。 verifier进行下面的验证: $e(g_l{L_p(s)},g{\alpha_l})=e(g_l^{L_{\alpha p}(s)},g)$,对R和O做相同验证。 $e(g_l^{L_p(s)}g_r^{R_p(s)}g_o^{O_p(s)},g^{\beta\gamma})=e(g^Z_p(s),g^\gamma)$ $e(g_l^{L_p(s)},g_r^{R_p(s)})=e(g_o^{t(s)},g^{h(s)})\cdot(g_o^{O_p(s)},g)$ 这几个式子的成立,是可以从前面推出来的,不相信的同学依旧可以动手推导一番,用一点加减乘除以及双线性计算的拆分组合即可。 由于这个偏移的存在,即便想暴力匹配也是不可能的了。 ### 5.5 通用化 上面写了这么多内容,但是却有一个问题,那就是,针对每一个问题,都需要一次参数生成。 proving key:$(\{g^{s^i}\}_{i=0}^d,\{g_l^{l_i(s)},g_r^{r_i(s)},g_o^{o_i(s)},g_l^{\alpha_ll_i(s)},g_r^{\alpha_rr_i(s)},g_o^{\alpha_oo_i(s)},g_l^{\beta l_i(s)},g_r^{\beta r_i(s)},g_o^{\beta o_i(s)}\}_{i=0}^n)$。 verification key:$\{g^{\alpha_l},g^{\alpha_r},g^{\alpha_o},g^{\beta\gamma},g^{\gamma}\}$。 虽然针对这个具体的问题,参数可以一直复用,但换一个问题,似乎又得做一遍重复操作了。 此时,我们希望我们设计的协议是通用的,即我们可以往协议中加入一个公共输入,针对不同的公共输入,就对应不同的问题。 这个该怎么做呢?首先回想一下,最开始我们想证明什么?是给定一系列多项式$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,目标t(x),然后想求一个线性组合$\{a_i\}_{i=0}^n$,使得 $\sum_{i=0}^n a_il_i(x) \cdot \sum_{i=0}^n a_ir_i(x)-\sum_{i=0}^n a_io_i(x)=t(x)h(x)$ 。 那么我们可不可以针多项式,从里面摘出一部分系数做验证,prover再生成证明呢?或者针对同一个多项式,我加入一些限制条件呢?比如说,按上面那个例子,我可以要求给出的证明必须满足,输出值为183,在此前提下给出证明。 即prover生成的证明必须是要和公共输出对应,针对的是同一个问题,这样的话,我每次选择不同多项式的局部,prover都得生成针对性的证明才行,就可以对不同的问题复用参数生成。 现在我有一个特定的$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,对应一个特定的问题,我可以取出前l个用于验证。 即将上面的多项式分为$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^l$和$\{l_i(x),r_i(x),o_i(x)\}_{i=l+1}^n$,只对后面这部分进行KCA处理,而前面一部分$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^l$放入verification key,对应的$\{a_i\}_{i=0}^l$直接当成公共输入(可认为是prover要证明的论述statement)用来限制prover的证明,根据不同的问题,这个公共输入也不一样。 这样prover生成证明时,verifier验证证明时也将公共输入输进去,如果依然成立就能保证prover是对一个特定问题生成证明,相当于把问题本身的一部分当成了公共输入。 下面以数学形式进行推导理解: 结合上面所有的改进,我们对协议进行一个总结: 整个协议过程变为: - Setup: - 选择两个椭圆曲线群$G_1,G_2$以及一个双线性映射$e()$; - 将一个具体的问题转化为QAP问题,即得到一系列多项式$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^n$,目标t(x); - 通过安全多方计算生成参数,注意有$\rho_o=\rho_l\cdot\rho_r,g_l=g^{p_l},g_r=g^{p_r},g_o=g^{p_o}$:proving Key:$(\{g^{s^i}\}_{i=0}^d,\{g_l^{l_i(s)},g_r^{r_i(s)},g_o^{o_i(s)}\}_{i=0}^n,\{g_l^{\alpha_ll_i(s)},g_r^{\alpha_rr_i(s)},g_o^{\alpha_oo_i(s)},g_l^{\beta l_i(s)},g_r^{\beta r_i(s)},g_o^{\beta o_i(s)}\}_{i=l+1}^n,g_l^{t(x)},g_r^{t(x)},g_o^{t(x)},g_l^{\alpha_lt(x)},g_r^{\alpha_rt(x)},g_o^{\alpha_ot(x)},g_l^{\beta t(x)},g_r^{\beta t(x)},g_o^{\beta t(x)})$, 可以看到proving key中只对l+1...m范围内的多项式组进行了KCA。 verification key: $(g,\{g_o^{t(x)},{g_l^{l_i(s)},g_r^{r_i(s)},g_o^{o_i(s)}}\}_{i=0}^l,g^{\alpha_l},g^{\alpha_r},g^{\alpha_o},g^{\gamma},g^{\beta\gamma})$  在verification key中也加入了部分多项式组。 - Proving - 随机选取$\delta_l,\delta_r,\delta_o$,计算$L(x) = \sum_{i=0}^n a_il_i(x)$_,_$g_l^{L_p(s)}=(g_l^{t(s)})^{\sigma_l} \cdot \prod_{i=l+1}^n (g_l^{l_i(s)})^{a_i}$_,_$g_l^{L_{\alpha p}(s)}=(g_l^{\alpha_l t(s)})^{\sigma_l} \cdot \prod_{i=l+1}^n (g_l^{\alpha_ll_i(s)})^{a_i}$,这里只有部分加入了$\alpha$对的计算,并用同样的方法求出$g_r^{R_p(s)},g_r^{R_{\alpha p}(s)},g_o^{O_p(s)},g_o^{O_{\alpha p}(s)}$。 - 计算$h_\delta(x)=\frac{L(x)R(x)-O(x)}{t(x)}+\delta_rL(x)+\delta_lR(x)+\delta_l\delta_rt(x)-\delta_o$_,_并计算$g^{h_\delta(s)}$。 - 计算$g^{z_p(s)}=(g_l^{\beta t(s)})^{\sigma_l}(g_r^{\beta t(s)})^{\sigma_r}(g_o^{\beta t(s)})^{\sigma_o}\prod_{i=l+1}^n (g_l^{\beta l_i(s)}g_r^{\beta r_i(s)}g_o^{\beta o_i(s)})^{a_i}$,这里也只有部分参与KCA计算,最后生成完整证明$\{g_l^{L_p(s)},g_r^{R_p(s)},g_o^{O_p(s)},g_l^{L_{\alpha p}(s)},g_r^{R_{\alpha p}(s)},g_o^{O_{\alpha p}(s)},g^{h_\delta(s)},g^{z_p(s)}\}$。 - 验证 - 加入对公共输入加入特定系数:$g_l^{L_v(s)}=\prod_{i=0}^m(g_l^{l_i(s)})^{a_i}$,用同样的方法生成$g_r^{R_v(s)},g_o^{O_v(s)}$. - 验证$\alpha$对,即验证$e(g_l^{L_p(s)},g^{\alpha_l})=e(g_l^{L_{\alpha p}(s)},g)$,对R和O做相同验证。 - 验证系数一致性,即验证$e(g_l^{L_p(s)}g_r^{R_p(s)}g_o^{O_p(s)},g^{\beta\gamma})=e(g^Z_p(s),g^\gamma)$ - 验证答案是否正确:$e(g_l^{L_p(s)}g_l^{L_v(s)},g_r^{R_p(s)}g_r^{R_v(s)})=e(g_o^{t(s)},g^{h(s)})\cdot(g_o^{O_p(s)}g_o^{O_v(s)},g)$ 在完整的写完协议后,我们可以分析下怎么做到的,首先由于同态性,上面的式子肯定是成立的,并且由于verifier在验证时,已经输入了一部分系数,所以说prover给出的证明必须要在verifier给出系数的前提下才可以通过。 讲个具体实例可能比较好理解,比如说你想设计一个电路,用于验证prover持不持有跟某个比特币地址对应的私钥,如果没有公共输入,你要么给每个地址都生成一次参数,要么就只能证明prover有没有某个私钥对应任意一个比特币地址(这个没有任何意义)。 但是如果加入公共输入,那就可以通用化,我们可以把地址的限制当成公共输入,那么prover的证明就必须得有与公共地址对应的私钥才行。 比如将比特币创世地址`1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa`转化为公共输出的限制,如果有人能证明自己拥有对应的私钥,那就有大概率可以证明自己是中本聪了。 ### 5.6 模拟器 对了,前面说到,如果零知识证明要是零知识的,协议要可以构造模拟器出来,且模拟器生成的证明在概率分布上与prover生成的证明无法区分。 那么对于zk-SNARKs,如何构造模拟器呢? 其实很简单,只要我们知道$\alpha_l,\alpha_r,\alpha_o,\beta,\gamma,s$这些被隐藏的值具体是什么,很容易就能伪造出一个证明,且不需要任何知识,所以这些参数又被称为陷门(trapdoor)或者有毒废料(toxic waste),是不能公开的,否则证明就无意义了。 具体推导就不推了,这个很简单,就跟最前面用来做引子的例子里,没有任何安全防护的状态是一样的。 ### 5.7 改进-Groth16 上面的分析应该已经足够去理解zk-SNARKs了,不过最后给出的方案其实还有改进空间,或者说还可以权衡。上面方案prover的证明一共包括$\{g_l^{L_p(s)},g_r^{R_p(s)},g_o^{O_p(s)},g_l^{L_{\alpha p}(s)},g_r^{R_{\alpha p}(s)},g_o^{O_{\alpha p}(s)},g^{h_\delta(s)},g^{z_p(s)}\}$。 即证明中包括八个椭圆曲线群元素,但我们知道,如果零知识证明要上链的话,我们肯定希望证明的size越小越好。 Jens Groth在16年发布了《On the Size of Pairing-based Non-interactive Arguments》里面提出了一种新的zk-SNARKs改进-被称为Groth16,证明只需要三个椭圆曲线群元素,大大减少了proof Size,按证明上链来说,等于一个区块可以塞下更多证明了,而且通信消耗也降低了。(当然,改进一直在继续),那么Groth16是怎么做的呢? 为了方便,我这就按最简单的写一些推导,示例是用非对称双线性映射,所以区分$g_1,g_2且g_t=e(g_1,g_2)$: proving key:$(g_1^{\alpha},g_1^{\beta},g_1^{\delta},g_2^{\beta},g_2^{\gamma},g_2^{\delta},\{g_1^{s^i}\}_{i=0}^{n-1},\{g_2^{s^i}\}_{i=0}^{n-1},\{g_1^{\frac{\beta l_i(s)+\alpha r_i(s)+o_i(s)}{\gamma}}\}_{i=0}^{l},\{g_1^{\frac{\beta l_i(s)+\alpha r_i(s)+o_i(s)}{\delta}}\}_{i=l+1}^{m},\{g_1^{\frac{s^it(s)}{\delta}}\}_{i=0}^{n-2})$ 以及$\{l_i(x),r_i(x),o_i(x)\}_{i=0}^m$ verification key: $(g_1,g_2,g_2^{\gamma},g_2^{\delta},g_t^{\alpha\beta},\{g_1^{\frac{\beta l_i(s)+\alpha r_i(s)+o_i(s)}{\gamma}}\}_{i=0}^{l}\})$ 生成证明:选取两个随机数$r_l,r_r$用于偏移,计算$h(x)=\frac{\sum_{i=0}^n (a_il_i(x) \cdot a_ir_i(x)-a_io_i(x))}{t(x)}$,通过$\{g_1^{\frac{s^it(x)}{\delta}}\}_{i=0}^{n-2}$线性组合算出$\{g_1^{\frac{h(s)t(s)}{\delta}}\}_{i=l+1}^{m}$ $g_1^{A(s)}=g_1^{\alpha}\cdot \prod_{i=0}^m g_1^{a_il_i(s)}\cdot g_1^{r_l\delta}$ $g_2^{B(s)}=g_2^{\beta}\cdot \prod_{i=0}^m g_2^{a_ir_i(s)}\cdot g_2^{r_r\delta}$ $g_1^{B(s)}=g_1^{\beta}\cdot \prod_{i=0}^m g_1^{a_ir_i(s)}\cdot g_1^{r_r\delta}$ $g_1^{C(s)}=\prod_{i=l+1}^m g_1^{\frac{a_i(\beta l_i(s)+\alpha r_i(s)+o_i(s))}{\delta}} \cdot {g_1^{\frac{h(s)t(s)}{\delta}}}\cdot g_1^{r_rA(s)}\cdot g_1^{r_lB(s)} \cdot g_1^{-r_lr_r\delta}$ 将$(g_1^{A(s)},g_2^{B(s)},g_1^{C(s)})$当成证明。 verifier计算:$g_1^{C_v(s)}=\prod_{i=0}^l g_1^{a_i}\cdot \{g_1^{\frac{\beta l_i(s)+\alpha r_i(s)+o_i(s)}{\gamma}}\}_{i=0}^{l}$(这里的$a_i$即statement,prover要求解的问题) 最后验证$e(g_1^{A(s)},g_2^{B(s)})=e(g_1^{\alpha},g_2^{\beta})\cdot e(g_1^{C_v(s)},g_2^\gamma)\cdot e(g_1^{C(s)},g_2^{\delta})=g_t^{\alpha\beta}\cdot e(g_1^{C_v(s)},g_2^\gamma)\cdot e(g_1^{C(s)},g_2^{\delta})$ 等式是否成立。 详细分析见星想法的文章:[零知识证明 - Groth16算法介绍](https://mp.weixin.qq.com/s/SguBb5vyAm2Vzht7WKgzug)以及Groth16原始论文,且除了Groth16,zk-SNARKs还有很多其他改进方案。 这样的话,证明就只有三个椭圆曲线群元素了,大大减少了证明大小,但是groth16是有取舍的。 groth16用$\alpha$ 和$\beta$保证系数一致性,并且用$\alpha \cdot \beta$限制证明中必包含这两个,然后用$\gamma$和$\delta$限制l r o之间的串用问题。但是groth16无法保证no-malleable,也就是说,你可以很简单的用已有的groth16证明生成一个新的有效证明。 比如有证明$(g_1^{A(s)},g_2^{B(s)},g_1^{C(s)})$,我们可以简单得到$(g_1^{A(s)},g_2^{B(s)}\cdot g_2^{x\delta},g_1^{C(s)}\cdot g_1^{x}\cdot g_1^{A(s)})$,亦可通过验证。 PPIO的朋友针对groth16的可操作性问题写了一篇详细分析:[如何利用 Groth16 的漏洞伪造证明](https://mp.weixin.qq.com/s/PAulyP65AfL2M14zJU606w) ### 5.8 进一步了解 可以进一步阅读零知识证明相关论文,或者直接了解一些零知识证明开源库,思考下和自己熟悉的领域有什么结合点。 安比试验室收集了一堆零知识证明的资料:[零知识证明学习资源汇总](https://mp.weixin.qq.com/s/-LU4FsFKt7ebX4sbKXNylA) 以及github仓库[awesome-zero-knowledge-proofs](https://github.com/matter-labs/awesome-zero-knowledge-proofs) 下面是一些开源库,可以实操一下零知识证明。 - [libsnark (C++)](https://github.com/scipr-lab/libsnark) - [great tutorial](https://github.com/christianlundkvist/libsnark-tutorial) - [bellmnan (rust)](https://github.com/zkcrypto/bellman/) - [demo circuit](https://github.com/ebfull/bellman-demo) - [jsnark (Java, bindings to libsnark)](https://github.com/akosba/jsnark) - [snarky (Ocaml, from authors of Coda)](https://github.com/o1-labs/snarky) - [zokrates (toolbox for zkSNARKs on Ethereum)](https://github.com/Zokrates/ZoKrates) 谢谢大家的阅读,如发现文章有错漏请指正,可联系微信@xcshuan0608,备注:区块链技术交流+组织+姓名。