小白也能看懂的零知识证明与zk-SNARKs

作为刚刚踏入密码学的领域的一只小白,我最近在学习ZCash (ZeroCash) 的原理,也就是zk-SNARKs 。将一点点感悟和理解写下来,以抛砖引玉。(不得不说看得真让人头大啊!)

zk-SNARKs

zk-SNARKs 是 zero knowledge Succinct Non-interactive Argument of Knowledge 的缩写。其中的词语分别解释如下:

  • zero knowledge: 零知识证明。即验证者(Verifier)不需知道“内情”即可相信证明者(Prover)。在zk-SNARKs中具体指:证明者(Prover)声称自己手中有一个炒鸡复杂多项式方程的解s,验证者不需要知道s具体是什么就可以相信证明者(Prover)。是不是听起来很不可思议?将会在下面会具体说明。
  • Succinct: 简洁。主要指在验证过程中传输的数据量不那么大且验证方法简单。
  • Non-interactive:无交互。证明者只需要提供一些信息,公开后任何人都可以直接进行验证而不需要跟证明者进行交互。这对区块链来说极为重要,因为其意味着可以放在链上给矿工(Miners)验证。关于交互的含义接下来会有所补充。

零知识证明

接下来我们通过几个例子了解什么是零知识证明。

例子1:

阿里巴巴现在要向强盗证明他知道打开岛上宝库石门的口令,从而保住自己的小命,但是他又不能直接告诉强盗这个口令(要是强盗灭口怎么办?),他怎么向强盗证明自己确实知道这个口令呢?

有一个方法就是:阿里巴巴让强盗站在能看到他、能抓到他但又听不到他说话的地方,这时阿里巴巴说出口令打开石门,这样强盗看到了石门打开了却没有听到口令,阿里巴巴也就在不泄露口令的条件下向强盗证明了自己确实能打开石门。这就是零知识证明!

我们再举一个升级版的开门口令的例子来理解之前提到的交互性和简洁。

例子2:

这个例子引用于读懂区块链之零知识证明(zk-SNARK)。

图中所示是一个山洞,入口处有两条路A和B,而这两条路在山洞深处被一道门给隔开了,只有说出开门魔咒门才能打开。这里涉及两个角色P(Proofer)和V(Verifier),P试图向V证明,他知道开门魔咒;如果属实,V就饶ta不死。P自然不能直接将魔咒告诉V,因为万一ta知道后把自己干掉怎么办;V则一定不会轻易相信P。他们可以这么做:

  1. P从A、B两条路中随机选择一条走进去,如果P真有开门魔咒的话,他需要现在将门打开;这时,V在洞外等着,对P选择了哪条路一无所知。
  2. 等待足够长时间后,V进入山洞,然后也从A、B中随机选择一个并且大声喊出来,譬如,“B!”。
  3. P听到V的声音后便从对应的那条路走出来。如果P确实知道开门魔咒,那么无论自己和V分别选择的是A还是B,P都能正确地从V报出的路走出来。相反,如果P不知道魔咒,那么他只有1/2的概率会做到。而从V的角度来说,如果他看到P从正确的路出来了,他便有50%的把握肯定P确实知道魔咒;
  4. 将第1-3步重复N次,如果P每次都能做对,那么V便有1-(0.5)^N的把握相信P。例如,N=5,可靠性就是96.9%,已经足够好了。更重要的是,V对于魔咒仍然一无所知。这便是零知识证明。

小白也能看懂的零知识证明与zk-SNARKs_第1张图片
图1:我知道开门咒语

在这里有这么几个要点:

  1. V必须在山洞门口并且呼喊出自己选择的A、B中的一个,P必须听到V的呼喊才能做出有效的行动。换句话说,在验证过程中V和P必须保持互动和联系,这也就是交互性。
  2. 这个过程只做一次是不够的,必须做多次。换句话说,虽然看起来这种验证挺简单的,但它不算是简洁的。

QAP问题——计算式转化为多项式

zk-SNARKs只适合解决QAP问题(Quadratic Arithmetic Programs),通俗地讲,就是问题中得含有多项式,为什么得含有多项式我们后面再说。现在的主要矛盾是,如何将一个普通的问题转化为QAP问题。

举个例子,现在有一个方程:

x ^ 3 + x ^ 2 + x = 14

我们很容易就知道它的解是 x=2。下面我们将这个问题转化为QAP问题。

1.首先我们引入一些变量,将上面的方程转化为若干基本简单算式。这些简单算式要么是 x = y 要么是 x = y (op) z 的形式,其中op代表加减乘除(+,-,*,/)四种运算符。这些运算是可以利用数字电路完成的。
假如我们引入的变量设为 sym_1,sym_2,sym_3和~out,那么这些基本简单算式如下:

sym_1 = x * x
sym_2 = sym_1 * x
sym_3 = sym_2 + sym_1
~out = sym_3 + x 

2.接下来我们用向量內积的思想表达上面的基本简单算式。为了表达加法,还需要引入一个虚拟变量~one,看了下面就会懂的。

设解向量为s,具体表达式如下。然后将这些简单算式表示为 s.a = s.b * s.c 的形式。其中, . 表示內积,这样 s.b 是一个1*4的列向量。a,b,c是系数矩阵。

s = [~one, x, ~out, sym_1, sym_2, sym_3 ]

那么,对于第一个简单算式 sym_1 = x * x 而言,就可以表示为:

s . [0,0,0,1,0,0]  = (s . [0,1,0,0,0,0]) * (s . [0,1,0,0,0,0])

对于含有加法的简单算式,比如第三个 sym_3 = sym_2 + sym_1,就可以表示为:

s . [0,0,0,0,0,1] =  s . [0,0,0,1,1,0]  * s . [1,0,0,0,0,0]

有四个等式,将这四个式子按照原来的顺序排起来,a,b,c 就会组成三个矩阵 A,B,C。与上面的例子原理一样,A,B,C可分别经过计算得出,结果为:

A = [0,0,0,1,0,0         B = [0,1,0,0,0,0         C = [0,1,0,0,0,0
     0,0,0,0,1,0              0,0,0,1,0,0              0,1,0,0,0,0
     0,0,0,0,0,1              0,0,0,1,1,0              1,0,0,0,0,0
     0,0,1,0,0,0]             0,1,0,0,0,1]             1,0,0,0,0,0]

3.接下来的工作就是将A,B,C三个矩阵表示为多项式形式。例如 A → A(n)=[A1(n), A2(n), A3(n), A4(n), A5(n), A6(n)],每一个代表一列。方法是对矩阵A的每一列使用拉格朗日插值法。例如阵A的第三列为[0,0,0,1]T,也就是寻找一个多项式 A3(n),使得当 n = 1,2,3,4 时,A3(n) 的值分别为0,0,0,1。

按照拉格朗日插值法,A3(n) 可以视为四个式子之和:

  • A3_1(n) = k*(n-2)(n-3)(n-4)
  • A3_2(n) = l*(n-1)(n-3)(n-4)
  • A3_3(n) = m*(n-1)(n-2)(n-4)
  • A3_4(n) = t*(n-1)(n-2)(n-3)

容易得到,A3(n) = A3_1(n) + A3_2(n) + Ac(n) + A3_4(n).
且有,当 n=i 时,A3(i) = A3_i(n).
故,n = 1,2,3 时,A3_1(n), A3_2(n), A3_3(n) = 0, 故 k,l,m = 0.
n = 4时,A3_4(4) = t * 3 * 2 * 1 = 1,故 t = 1/6。

根据上面的原理,可以求出Ai(n),进而求出A(n)。类似的,还可以求出A(n),B(n),C(n)。

这样问题便转化为计算问题便转换为求取解向量s,使得等式s . C(n) - s . A(n) * s . B(n) = 0 在n=1,2,3,4时成立,等价于:

存在一个多项式H(n),使得s . C(n) - s . A(n) * s . B(n) = H(n) * Z(n),其中, Z(n) = (n-1)(n-2)(n-3)(n-4)。各位请注意: 方程式中如愿以偿地出现了多项式!

零知识雏形

那么,为什么要费尽心思搞一个多项式呢?就是为了搞出零知识来。试想一下,P要向V证明自己知道方程: x ^ 3 + x ^ 2 + x = 14 的解,似乎除了告诉他解 x = 2 似乎没有别的方法,但如果采用多项式表达的话,另有手段。

给出方程之后,A(n),B(n),C(n)就确定了,同时也是公开的。如果令多项式 P(n) = s . C(n) - s . A(n) * s . B(n) , 事实上,由于s是一个向量,內积过后仍为多项式,我们这里就用C(n)替代 s . C(n),下文皆是如此。也就是,P(n) = C(n) - A(n) * B(n)。那么 Prover 只需要利用自己知道的s计算出P(n),然后计算 H(n) = P(n) / Z(n),并将P(n)和H(n)发给Verifier,V通过验证 P(n) ?= H(n) * Z(n) 即可相信P拥有方程的解s。(?= 的含义是是否等于)

这虽然已经有了零知识证明的雏形,但却有很多很多的问题。我们将在下文具体解决这些问题。

抽样以满足简洁性

在ZCash的验证过程中,这些多项式是非常非常大的,有的多项式的最高次数会达到上百万,如果传输这样的多项式,会极大地降低传输效率。解决方案就是取一个抽样点n = t,这样P(t),H(t)实际上就是两个数了,这样就很“简洁”。

具体步骤如下:

  • Verifier随机选一个抽样点t,发给Prover。
  • Prover计算P(t),H(t)。注意,在这里P(t),H(t)不再是多项式,而是两个数了。
  • Prover将P(t),H(t)发给Verifier。
  • Verifier验证P(t) ?= H(t) * Z(t)

如果等式成立,基本可以确定Prover知道解向量s。这里我们可以计算一下Prover的把握有多大。

出现纰漏的情况是这样的,有另外一个多项式P’(n),其在一些点u1,u2······ui处有P’(ui) = P(ui),且取的抽样点t恰好是ui中的一个。那么这种概率多大呢?首先假如Prover随机选了个多项式P’(n),那么P’(n) = P(n)最多有2d个ui点。这里d是A(n),B(n),C(n)三个多项式的最高次数,由于P(n)中含有乘法,故P(n)的最高次数为2d。所以P’(n) = P(n)可以视为一元2d次方程,假如在实数域抽样,那么方程P’(n) = P(n)至多有2d个实根,也就是纰漏概率为2d/R,R表示实数域,由于2d终究是有限的,所以这个概率很小,因此我们基本可以确定Prover知道解向量s。

以上就解决了目前的传输过于复杂的问题。

抽样带来的抽样点泄露问题

新问题

上面的解决方案,很不开心的,又带来了新的问题。事实上,我们不希望Prover知道抽样点t,因为如果这样,Prover可以不像上面所说的随机选取P’(n),而是刻意构造出一个P’’(n)和H’’(n),使其满足P’’(t) = H’’(t) * Z’’(t)。

当然,这里还存在一个问题,就是Prover虽然不知道 s . C(n) - s . A(n) * s . B(n) = H(n) * Z(n)的解s,但却知道 s’’’ . C’’’(n) - s’’’ . A’’’(n) * s’’’ . B’’’(n) = H’’’(n) * Z(n)的解s’’’,也就是Prover可以用P’’’(n)和H’’’(n)来伪造为P(n)和H(n),也就是无法确保Prover确实用的是A(n),B(n)和C(n)构建的P(n)。但是这个问题我们先放着,以后再说。

为了解决提到的第一个问题,采取的策略是同态隐藏。

同态隐藏

满足下面三个条件的映射E(x),我们称之为加法同态。

  • 如果已知E(x)=X,很难根据X和E推出x的值。这也是哈希函数的一个重要性质之一。
  • 若x1!= x2,那么E(x1) 概率很小时才可能等于E(x2)。
  • 由 E(x1) 和 E(x2) 可以推出 E(x1+x2),这也就是加法同态。为了表达方便,这篇文章姑且认为 E(x1+x2) = E(x1) + E(x2)。 自然地,由上面容易推出E(x1 + x2 + ···· xi) = E(x1) + E(x2) + ······ E(xi)。也可以推出E(a* x1 + b* x2) = a* E(x1) + b* E(x2)。

利用同态隐藏进行改进

这样,我们可以利用加法同态对上面的零知识证明过程进行改进。过程具体如下:

  • Verifier不再发送给Prover抽样点t,而是发给Prover一系列指数1,t1,t2,······· t2d的映射值,E(1),E(t1),E(t2)······E(t2d)。
  • Prover不知道t,无法计算P(t)和H(t),而是根据同态隐藏的性质计算出 E[P(t)], E[H(t)]。(这是由于P(t)是tx的多项式组合(x<=2d),把tx替换成E(tx)后,就会出现······+a* E(tx) + b* E(tx+1) +····这种形式,由加法同态隐藏,其等于E(······+a * tx + b * tx+1 + ·······),而E括号内的内容恰好就是P(t)。)然后将E[P(t)], E[H(t)]发给Verifier。
  • Verifier通过检查 E[P(t)] ?= E[H(t) * Z(t)]。Prover不知道t,就无法可以构造出P’’(n)和H’’(n)了。

细心的读者可能会发现,这个计算过程中仍存在一个问题。那就是在计算P(t)的过程中有多项式乘法,即计算A(n) *B(n)时,会出现 E[A(t)] * E[B(t)] 的形式,(这里用的之前提到过的A(n)来替代s . A(n)),然而如果映射E只具有加法同态的性质,这个乘法是没法算的,所以我们又引入了乘法同态。

乘法同态

首先介绍一下双线性映射,这里引用了[逐舞传歌的文章]。

双线性映射指的是分别来自两个域的两个元素映射到第三个域中的一个元素:e(X, Y) → Z,同时在两个输入上都具备线性:

  • e(P+R, Q) = e(P, Q) + e(R, Q)
  • e(P, Q+S) = e(P, Q) + e(P, S)

假设对于x的任意两种因数分解(a, b)和(c, d)(即x=ab=cd),存在两个加法同态映射E1和E2,以及一个双线性映射e,使得以下等式总是成立:

e(E1(a), E2(b)) = e(E1(c ), E2(d)) = X
那么,x->X的映射也是加法同态映射,记作E。E的线性属性证明如下:
E(ax1+bx2)
= e(E1(ax1+bx2), E2(1))
= e(aE1(x1) + bE1(x2), E2(1))
= ae(E1(x1), E2(1)) + be(E1(x2), E2(1))
= aE(x1) + bE(x2)

如果上面这一块儿没怎么看懂,我大可告诉你:“没关系!”我们只需要知道,如果我们找到这种映射E,就有:E(xy) = e(E1(x), E2(y))。

现在的整个流程如下:

  1. Verifier向Prover发送一系列指数1,t1,t2,······· t2d的映射值,但这里发送两种E1和E2,也就是:E1(1),E1(t1),E1(t2)······E1(t2d);E2(1),E2(t1),E2(t2)······E2(t2d)。

  2. Prover计算 一些数值并发给Verifier。
    这里我们详细阐述一下。根据A(t),C(t)和H(t),可以求出E1[A(t)],E1[C(t)],E1[H(t)]。根据B(t),Z(t),可以计算出E2[B(t)],E2[Z(t)]。

  3. Verifier验证。
    根据上面的乘法同态介绍,有:

    E{ e[E1(C(t)),E2(1)] } = E { C(t) };
    E{ e[E1(A(t)),E2(B(t))] } = E { A(t) * B(t) };
    E{ e[E1(H(t)),E2(Z(t))] } = E { H(t) * Z(t) };

    这样,就能够计算乘法了。Verfier就能够验证

    E{P(t)}
    = E {C(t) - A(t) * B(t)}
    = E { C(t) } - E { A(t) * B(t) }
    = E{ e[E1(C(t)),E2(1)] } - E{ e[E1(A(t)),E2(B(t))] }
    ?= E{ e[E1(H(t)),E2(Z(t))] }
    = E { H(t) * Z(t) }

OK! 回顾上文,我们从抽样实现简洁性开始,不断解决问题,抽样点的隐藏,加法同态和乘法同态等问题。接下来我们将解决之前在新问题这一章节里面提到的第二个问题,即无法确保Prover确实用的是A(n),B(n)和C(n)构建的P(n)这一问题。解决的方法是KCA。

KCA——解决“答非所问”问题

假设有a、b,满足b=α * a的约束(α为整数,“α * a”相当于α个a相加),那么(a, b)称为一个“α对”。若a,b已知,α未知,需提供另一个α对。一个简单的想法就是先通过“b/a”求出α,然后再任意挑一个a’,并算出对应的b’就好。如果a和b是普通数字、加法是普通加法,“b除以a”是存在的,的确如此。可如果“b除以a”的运算做不了(这是可能的,后文解释),就只能分别将a和b乘以一个整数γ,则(a’, b’) = (γa, γb)也是一个α对,即b’ = γ * b = α * γ * a = α * a’。

接下来,将此问题扩展一下:如果预先提供的不是一个而是N个α对(a1, b1),(a2, b2),…,(aN, bN),仍需提供一个新的α对。方法是类似的,那就是返回一个由a系列和b系列值的相同线性组合组成的值对,即(c1a1 + c2a2+…+cNaN, c1b1 + c2b2+…+cNbN),其中cn是任意整数。反过来,从出题者的角度而言,Verifier通过检查Prover提供的(a’, b’)是否一个α对,便可基本确信:Prover的两个值a’和b’是Verifier所提供的a系列和b系列值的相同线性组合(为啥说“基本”呢?因为还无法从数学上证明,只能算“good enough”)。

有了KCA这个工具,便可以解决之前提出的“答非所问”问题。具体流程如下:

  1. Verifier提供给Prover的不是基本粒子E(tn),而是三组、每组M个α对,(α是Carl产生的随机值):(之前提到过,A(n),B(n),C(n)是公开的)

  2. 第一组数据
    E1(A1(t)), E1AA1(t)) (注:根据同态映射的性质,E1AA1(t)) = αAE1(A1(t)),下同。)
    E1(A2(t)), E1AA2(t))

    E1(Am(t)), E1AAm(t))

    第二组数据
    E2(B1(t)), E2BB1(t))
    E2(B2(t)), E2BB2(t))

    E2(BM(t)), E2BBM(t))

    第三组数据
    E1(C1(t)), E1CC1(t))
    E1(C2(t)), E1CC2(t))

    E1(C1(t)), E1CCM(t))

    除此之外,Verifier还会给Prover发送一些值,后面会提到。

    同时,Verifier要求Prover在响应中返回三个α对1(A(t)), E1AA(t))>、BB(t))>和1(C(t)), E1CC(t))>。Prover不知道这几个α的值,因此根据KCA推断:为了生成第一个α对,他只能以Verifier提供的α对(A1(t),···Am(t))的某种线性组合来合成E1(A(t))和E1(αA(t)),这就限定了Prover只能用A(t)而不是一个另外的A’’’(t):

    E1(A(t)) = E1(a1* A1(t) + a2*A2(t) + … + aM*AM(t)) = a1*E1(A1(t)) + a2*E1(A2(t)) + … + aM*E1(AM(t))
    E1AA(t)) = E1(a1AA1(t) + a2AA2(t) + … + aMAAM(t)) = a1*E1AA1(t)) + a2*E1AA2(t)) + … + aM*E1AAM(t))

    同理,Prover可以构建:

    E2(B(t)) = b1*E2(B1(t)) + b2*E2(B2(t)) + … + bM*E2(BM(t))
    E2BB(t)) = b1*E2BB1(t)) + b2*E2BB2(t)) + … + bM*E2BBM(t))
    E1(C(t)) = c1*E1(C1(t)) + c2*E1(C2(t)) + … + cM*E1(CM(t))
    E1CC(t)) = c1*E1CC1(t)) + c2*E1CC2(t)) + … + cM*E1CCM(t))

  3. Verifier接受到这三组数据后,可以验证Prover使用的多项式是否真的是A(n),B(n)和C(n)。

    方法就是验证:
    e {E1[A(t)] , E2A]} ?= e {E1AA(t)] , E2[1]}
    e {E1[B(t)] , E2B]} ?= e {E1BB(t)] , E2[1]}
    e {E1[C(t)] , E2C]} ?= e {E1CC(t)] , E2[1]}

    (这里E2A],E2B], E2C],E2[1],E2[1],E2[1]也是Verifier发给Prover的)

    从而确定Prover使用的多项式是否真的是A(n),B(n)和C(n)。

  4. List item

    下面的验证方法与上一节是一致的,即通过验证

    E{ e[E1(C(t)),E2(1)] } - E{ e[E1(A(t)),E2(B(t))] }?= E{ e[E1(H(t)),E2(Z(t))] }

    确认P(t) ?= H(t) * Z(t)

CRS:减少交互

其实,到上面为止,整个zk-SNARKs的原理已经是可实现得了。但是有的牛逼的读者可能会问:“这个过程不仅不是非交互的,还是‘及其交互的‘有木有啊!”而且其非常不简洁,因为要传输好多好多数据。

我们采用了一种称为CRS(COMMON REFERENCE STRING)的方式。原理很简单,实际上就是把随机数α和t内置于“系统”中。并把这些参数放在链上,任何人都可以参与验证。

所以终极版的zkSNARK过程就是:

  1. 系统产生内置的随机数α和t,然后产生上面各种之前Verifier发给Prover的参数值,这些参数值公示在链上。
  2. Prover返回若干参数值。
  3. Verifier验证。

结语

完结撒花!以上的各种数学工具,比如加法乘法同态隐藏什么的,来自于椭圆曲线,但是由于时(cai)间(shu)有(xue)限(qian),本人还没搞明白。所以就写到这里!

你可能感兴趣的:(小白也能看懂的零知识证明与zk-SNARKs)