翻译 & 注解:even@安比实验室(even@secbit.io)前面我们已经理顺了一个zk-SNARK 简单方案的思路,它包含了 zk-SNARK 大部分内在机制,现在我们继续完善方案,使它能执行零知识程序。Algorithm 1: Operation depends on an input
————————————————————————————————————————————————————————————————————
function calc(w, a, b)
if w then
return a × b
else
return a + b
end if
end function
从高处看,这段程序和我们协议里面的多项式并没有什么关系。我们现在就需要找到一种方式来将程序转换成多项式。第一步是将程序转换成数学语言,这个相对简单一些,声明可以表达成下面的形式(假定 w 是 0 或 1):f(w,a,b) = w(a · b)+(1-w)(a+b)执行calc(1, 4, 2)和对 f (1, 4, 2) 求值都可以得到相同的结果:8。如果执行 calc(0, 4, 2) 和 f(0,4,2) 也都能够得到 6。其实我们可以用这种方法表达任何形式的有限程序。接下来(上面的例子)需要我们证明的是,对于表达式 f(w,a,b) ,输入为 (1, 4, 2) 时输出为 8,换句话说,我们要校验多项式:even@安比实验室: 猜想一下,是否只要是能够用多项式表示的程序都可以做证明?尽管已经将程序转换成了由数学语言表达的一般计算形式,我们还是需要再把它翻译成多项式。我们先来仔细研究一下计算的本质。任何计算的内核都是由以下形式的基本运算组成的:两个操作数(即值)与一个运算符(即+,–,×,÷)放在一起执行运算。例如操作数是 2 和 3,运算符为乘,那么运算出来就是 2 × 3 = 6。由于任何复杂的计算(或者程序)都是由一系列这样的基本运算组成的,所以首先我们需要知道在多项式中单个运算是怎么表示的。我们先来看一下如何将多项式和算术运算关联起来。例如有两个多项式 f(x) 和 g(x),尝试将他们相乘使得 h(x) = f(x) × g(x),在任意一个 x= r 处 h(x) 的计算结果都是 f(r) 和 g(r) 的乘积。再看一下如下两个多项式 f(x) = 2x2 – 9x+ 10 和 g(x) = – 4x2 + 15x – 9。以图形的形式展示:
当 x = 1 时 f(1) = 2 – 9 + 10 = 3, g(1) = – 4 + 15 – 9 = 2。把两个多项式相乘:h(x) = f(x) × g(x) = – 8x4 + 66x3 – 193x2 + 231x– 90。从图中可以看出相乘的结果:x = 1 时,计算 f(x) × g(x) 结果为:h(1) = – 8 + 66 – 193 + 231 – 90 = 6,也就是说当 x = 1时,h(x) 就是 f(x) 和 g(x) 相乘的结果 ,在 x 取其它值的时候也一样。同样的如果我们将 f(x) 和 g(x) 相加,在 x=1 处的计算结果就是 5。注意:在其它的 xs 取值处,多项式相加的计算结果也是将两者多项式的值加在一起的结果,例如你可以验证一下 x=2, x=3 处的结果。如果我们可以将操作数的值表示为多项式(我们也确实可以这么做),那么利用算术属性,我们就能够得到操作数的计算结果了。even@安比实验室: 回忆一下,在本系列的第一篇——多项式的性质与证明中,我们曾经说过“任何多项式在任意点的计算结果都可以看做是其唯一身份的表示。”反过来当我们知道某个多项式的时候,是不是也就意味着我们知道多项式上某个点的取值。这就是借助多项式来完成证明的依据。如果一个 prover 声称某两个数字的乘积,verifier 要怎样去验证呢?为了证明单个计算的正确性,我们就必须首先确保所提供的操作数的输出(结果)的正确性。我们再来看一下运算的形式:
l(x) — 表示(运算结果为)左操作数
r(x) — 表示(运算结果为)右操作数
o(x) — 表示运算结果(输出)
因而在计算过程中如果操作数和结果都可以用多项式的形式正确地表示出来,那么 l(a) operator r(a) = o(a) 就能够成立。也就是说假如输出多项式 o(x) 所代表的值是由运算符 在操作数多项式 l(x) 和 r(x) 上进行乘法运算得出的正确结果,那么我们把输出多项式 o(x) 放到等式的左边就能够得到:l(a) operator r(a) – o(a) = 0,这也就表明了当取值为 a 时多项式 l(x) operator r(x) – o(x) 计算结果为 0 。那么只要多项式是有效的,运算多项式 就一定有一个根 a。因此,根据前面的基础这个多项式里面一定包含因式(x-a)(可以看因式分解一节),这就是我们要证明的目标多项式,即 t(x) = x – a。可以用一个简单的多项式表示它:l(x) = 3x, r(x) = 2x, o(x) = 6x,取 a=1 进行计算,即 l(1) = 3;r(1) = 2;o(1) = 6。这个运算多项式就变成了:
l(x) × r(x) = o(x)
3x × 2x =6x
6x² -6x =0
在图上表示为:
注意,这个多项式有一个因子 (x = 1) :
6x² -6x = 6x(x-1)
因而如果 prover 用 l(x), r(x), o(x) 这些多项式来代替 p(x) ,因为它们依然可以被 t(x) 整除,所以 verifier 就可以认为它们是有效的。相反,如果 prover 尝试用 4 来代替输出值去欺骗 verifier ,即 o(x) = 4x,那么运算多项式就变成了 6x2 – 4x= 0:
图中这个多项式并没有 x=1 的解,因而 l(x) × r(x) – o(x) 就不能被 t(x) 整除:因而 verifier 不能接受这个不一致的计算结果(就像因式分解这一章描述的那样)even@安比实验室: 在前面的协议中,我们要证明的多项式是 p(x) = t(x)h(x),这里我们修改 p(x),使得 p(x) = l(x)r(x)-o(x)。这里t(x)目标多项式的根就是对应能够计算出数学表达式的值的 x。上面例子里面取 x=1 这个特殊值作为运算编码的位置。当然这里的 1 可以换成任何别的值,比如说换成 x=2,3,或101 等等。在[GGPR]与[PHGR]论文中,这个取值是一个随机值,被称为 “root”。
现在我们来修改一下最新协议使它支持单个乘法计算的证明。回到前面多项式的 SNARK一章,我们已经能够证明多项式 p(x) 的知识了,只不过现在要计算的是三个多项式 l(x), r(x), o(x)的知识。我们可以定义 p(x) = l(x) × r(x) – o(x),但这里存在两个争议点。首先,在我们的协议中证明阶段是不能做加密值乘法计算的(即, l(s) × r(s)),因为配对只能用一次,这个要用在校验多项式的约束上。第二,这里给证明者留下了一个可以修改多项式结构但依然保留有效因式 t(x) 的机会,例如可以令 p(x) = l(x) 或者 p(x) = l(x) – r(x) 甚至是 p(x) = l(x) × r(x) + o(x),只要这里的 p(x) 还保留着根 a 就可以了。这个修改本质上是让证明的内容变成了别的声明,所以这样是不行的。
所以 prover 必须要分别提供多项式 l(s), r(s), o(s) 值的证明,即协议必须修改要证明的多项式的知识。verifier 在加密空间中要验证的是 l(s) × r(s) – o(s) = t(s)h(s)。verifier 可以使用密码学配对来执行乘法,但还有一个小问题就是做减法(– o(x))是非常昂贵的计算(这需要去找到一个 gᵒ⁽ˢ⁾ 的逆向转换),这里我们可以把 o(x) 移到等式右边来:l(x)r(x) = t(x)h(x) + o(x)。在加密空间中,verifier 的验证就可以转换成:
e(gl(s), gr(s)) = e(gt(s),gh(s)) · e(go(s),g)
e(g,g)l(s)r(s) =e(g,g)t(s)h(s) ·e(g,g)o(s)
e(g,g)l(s)r(s) =e(g,g)t(s)h(s)+o(s)
注意:回忆一下加密配对的结果是支持通过乘法实现加密值的相加的,参见 加密值相乘 这一章节
保持 setup 阶段不变,协议更新为:这个协议就能够证明两个值相乘的计算结果是正确的了。你可能注意到了在这个新的协议中我们放弃了零知识 部分。这么做是为了简化协议的变换。后面的章节我们会再变回零知识。even@安比实验室: 上面例子里面取 x=1 这个特殊值作为运算编码的位置。当然这里的 1 可以换成任何别的值,比如说换成 x=2,3,或101 等等。在[GGPR]与[PHGR]论文中,这个取值是一个随机值,被称为 “root”。
现在我们已经能够证明单个算术运算了,但是要怎么扩展到多个运算上呢(这是我们的最终目标)?我们来尝试一下增加一个基本运算。想一想计算 a × b × c 乘积的需求,在元素操作模型中,这代表着两个操作:
a × b = r1
r1 × c = r2
前面的讨论中我们通过对运算符多项式在任意取值 x 处 ,例如1,计算一个对应值,来表示一个操作数或者结果。有了这个性质的多项式并不会约束我们在不同 x 取值处用多项式来表示其他值。如示例中的 2,即:
这种独立性允许我们一次同时执行 两个运算并且又不会把这两者搞乱,即相互之间不会妨碍。这个多项式算术的结果就变成了:可以看出这个多项式有两个根 x=1 和 x=2。因而也就是两次计算都被正确执行了。even@安比实验室:这里的示例为了理解方便,选择了 x=1, x=2 两个位置。记住在完整的 zkSNARK 方案中,这些 “root” 值必须为有限域内的随机数,否则就会引入安全漏洞。我们再来看一个有三个乘法运算的例子 2 × 1 × 3 × 2,它按照下面的步骤执行:
我们要把它们表示为操作数多项式,对于由 x ∈ {1, 2, 3} 所表示的计算, l(x) 相应的要等于 2,2 和 6。即通过点 (1, 2), (2, 2), (3, 6),同样的 r(x) ∋ (1, 1),(2, 3),(3,2) ,o(x) ∋ (1, 2), (2, 6), (3, 12)。
但是,我们要怎么找到经过这些点的多项式呢?对于任何包含超过一个点的例子,就必须要使用特定的数学方法了。为了构造操作数 和 输出多项式,我们需要一种方法来用给定的一组点去构造一个能经过所有这些点的弯曲 多项式,这叫插值。有几种不同的方法可以算出这个多项式:
一组未知方程
牛顿多项式
内维尔算法
拉格朗日多项式
快速傅里叶变换
我们以第一个方法为例。这个方法的思路是存在一个系数未知 ,阶数至多为 n 的特定的多项式 p(x) 能够经过给定的 n+1 个点,对于每个点 {(xᵢ, yᵢ)}, i ∈ [n+1]来说,多项式在 xᵢ 处的计算结果都等于 yᵢ,即对于所有的 i,p(xᵢ) = yᵢ 。在我们的例子中,三个点就可以变成一个用以下形式所表示的阶数为 2 的多项式:ax2 + bx +c =y。我们令左运算多项式(绿色标出的)在每个点处与多项式结果值相等,然后对每个系数用其它术语表示出来再计算等式:现在我们有了代表三个运算的操作数多项式,再进一步看一下怎么去校验每个计算的正确性。回到 verifier 寻找等式 l(x) · r(x) – o(x) = t(x)h(x) 的过程。在这个例子中,因为计算是在点 x ∈ {1, 2, 3} 处被表示出来的,所以目标多项式在这些 xs 点处的计算结果必须为 0,换句话说, t(x)的根必须是 1,2 和 3,它的基本形式就是:第二步:从 l(x) × r(x) 的结果中将 o(x) 减去:
这里就已经可以看出每一个操作数相乘都对应了正确的结果。最后一步 prover 要算出一个有效因式:
通过长除法可以算出:
代入h(x) = – 3x+ 4a,verifer 可以计算 t(x)h(x):现在显然 l(x) × r(x) – o(x) = t(x)h(x) ,这就是我们要证明的内容。even@安比实验室:这里只需要一组多项式 l(x),r(x), o(x) 就可以将所有计算的约束关系表示出来了,有几个计算也就对应着目标多项式 t(x) 有几个根。当前的协议似乎存在一些缺陷,多项式只能证明 prover 拥有一组多项式 l(x),r(x), o(x) ,在 t(x) 的几个根的取值处 l(x)·r(x)= o(x),无法证明这组多项式符合我们要证明的数学表达式:1)多个计算关系之间也是分开表示的,这些算式之间的关系也同样无法进行约束2)由于 prover 生成的证明中只有计算结果,左操作数,右操作数,输出在计算中混用也不会被发现3)由于左操作数,右操作数,输出是分开表示的,互相之间的关系无法进行约束。
有了前文介绍的方法来解决证明多个计算多项式的问题,我们就可以一次证明多个运算(如上百万个甚至更多)了,但是这个方案还有一个关键的缺点。
如果证明中执行的“程序”在不同运算中使用了相同的变量作为操作数或输出,例如:
a × b = r1
a × c = r2
这里 a 代表两个运算中的左操作符多项式,如:
然而,因为我们的协议中是允许 prover 为多项式设置任何系数的,所以他可以不受限制得为不同计算(即,用一些 x 表示的多项式)中的 a 设置不同的值,即:
这个自由打破了一致性,有空间允许 prover 能去证明了一些并非 verifier 感兴趣的其它无关的程序执行。因而我们必须要确保每一个变量在所有运算中出现的地方都只有一个取值。
注意:文中的变量与常规的计算机科学中变量的定义不同,这里的变量是不可改变的而且每次执行都只赋值一次。even@安比实验室:请务必注意“变量”的定义,它是程序中的变量,但是却又不能改,这不是很矛盾吗?其实,这里变量是指本文开头的示例伪代码中的那些不会被修改的变量。在 zkSNARK 论文中,这个「变量」其实有一个对应的名词叫做 assignment,是算术电路的「赋值」。而所有的assignments 是一个算术电路可满足性问题的解,包含了算术电路的输入值以及电路运算过程中输出的中间结果值。
我们来看一个简单的例子(就是当前的例子),在左操作符多项式 l(x) 表示的所有左操作数中只包含一个变量(即 a)。我们要找出是否有可以确保这个多项式在每一个运算中 a 都相同的方法。prover 可以设置不同值是因为他可以任意控制 x 的每个次幂的系数。因而如果这些系数是固定的,就可以解决问题了。我们来仔细看看包含相等值的多项式。例如检查一下下面两个多项式,他们分别都表示了有两个相等值对应的运算(即在 x=1 和 x=2 处),这里第一个多项式的值为 1,第二个多项式的值为 2: