Groth16 算法使得证明者能够基于在可信设置中派生出的椭圆曲线点来计算二次算术程序,并由验证者快速进行检查。它利用可信设置中的辅助椭圆曲线点来防止伪造证明。
先决条件
本文是 RareSkills Book of Zero Knowledge Proofs 中的一章。假定您已熟悉前面的章节。
符号说明
我们将属于 椭圆曲线群的椭圆曲线点记为 ,将属于 椭圆曲线群的椭圆曲线点记为 。 和 之间的配对 (pairing) 记作 ,其结果会生成 中的一个元素。加粗的变量(如 )表示向量,大写加粗字母(如 )表示矩阵,而域元素(有时非正式地称为“标量”)用小写字母表示(如 )。所有算术运算都在一个有限域中进行,该域的特征等于椭圆曲线群的阶。
给定一个算术电路(ZK电路),我们将其转换为秩1约束系统 (R1CS) ,其中矩阵的维度为 行 列,并具有一个见证向量 。然后,我们可以通过在 值 上将矩阵的列作为 值进行插值,将 R1CS 转换为二次算术程序 (QAP)。由于 、 和 都有 列,我们将得到三组每组 个的多项式:
由此,我们可以构造一个二次算术程序 (QAP):
其中
且
如果第三方通过 powers of tau 仪式创建了一个结构化参考字符串 (srs),那么证明者就可以在一个隐藏点 处计算 QAP 中的求和项(即 项)。假设结构化参考字符串如下计算:
我们将 称为通过内积在结构化参考字符串 上求值的多项式:
或者对于 srs 来说:
是上述表达式的简写,并产生一个椭圆曲线点。这并不意味着证明者知道 。
证明者可以通过计算以下内容在可信设置上对他们的 QAP 进行求值:
此计算的详细信息在我们的教程 Quadratic Arithmetic Programs over Elliptic Curves 中有讨论。
如果该 QAP 是平衡的,则以下等式成立:
动机
仅仅提供 并不能令人信服地证明证明者知道使得 QAP 平衡的 。
证明者可以简单地编造出满足 的值 、、,然后计算
并将这些椭圆曲线点 、、 呈现给验证者。
因此,验证者根本无法确定 究竟是不是通过满足的 QAP 计算得出的。
我们需要在不引入过多计算开销的情况下迫使证明者诚实行事。实现这一目标的第一个算法是“Pinocchio: Nearly Practical Verifiable Computation”。它的可用性足以让 ZCash 将其第一版区块链建立在它之上。
然而,Groth16 能够用更少的步骤完成同样的事情,而且该算法至今仍被广泛使用,因为在此之后还没有任何算法能在验证步骤上达到如此高的效率(尽管其他算法已经消除了可信设置或显著减少了证明者的工作量)。
2024 年更新: 密码学界发表了一篇标题相当具有胜利意味的论文“Polymath: Groth16 is not the limit”,展示了一种所需验证者步骤比 Groth16 更少的算法。不过,在撰写本文时,该算法尚无已知的实现。
防止伪造 第 1 部分:引入 和
一个“无法求解”的验证公式
假设我们将验证公式更新如下:
请注意,为了方便起见,我们对 群使用加法表示法。
这里, 是来自 的一个元素,具有未知的离散对数。
我们现在将展示,如果没有 的离散对数,验证者就不可能提供该方程的解 。
攻击 1:伪造 A 和 B 并推导 C
假设证明者随机选择 和 来生成 和 ,并试图推导出一个与验证者公式兼容的值 。
知道 和 的离散对数后,恶意的证明者试图通过以下计算求解
最后一行要求证明者求解 的离散对数,因此他们无法为 计算出一个有效的离散对数。
攻击 2:伪造 C 并推导 A 和 B
这里证明者选择一个随机点 并计算 。由于他们知道 ,他们可以尝试找到兼容的 和 组合,使得
这要求证明者在给定 的情况下,想出一对 和 ,它们配对后能产生 。
与离散对数问题类似,我们依赖于未经证实的密码学假设:这种计算(将 中的元素分解为一个 和一个 元素)是不可行的。在这种情况下,我们无法将 分解为 和 的假设被称为双线性 Diffie-Hellman 假设。感兴趣的读者可以参阅关于判定性 Diffie-Hellman 假设 (Decisional Diffie-Hellman Assumption) 的相关讨论。
(未经证实并不意味着不可靠。如果你能找到证明或反驳这个假设的方法,名誉和财富都在等着你!在实践中,没有已知的方法可以将 分解为 和 ,并且人们相信在计算上这是不可行的。)
和 的使用方式
在实践中,Groth16 并不使用 这一项。相反,可信设置会生成两个随机标量 和 ,并公布计算得到的椭圆曲线点 :
我们之前称为 的东西,实际上就是 。
重新推导证明和验证公式
为了使验证公式 变得“可解”,我们需要修改我们的 QAP 公式以融入 和 。
现在考虑如果我们在等式左侧引入项 和 会发生什么:
我们可以使用原始的 QAP 定义来替换最右侧的项:
现在我们可以引入一个具有以下定义的“扩展版” QAP:
为了让大家提前窥见我们接下来的目标,如果我们将 替换为 ,将 替换为 ,我们就能得到前面更新后的验证公式:
其中
证明者可以在不知道 、 或 的情况下计算 和 。给定结构化参考字符串( 的幂)以及椭圆曲线点 ,证明者按如下方式计算 和
这里, 并不意味着证明者知道 。证明者正在使用结构化参考字符串 为 计算 ,并使用 srs 计算 。
但是,目前在不知道 和 的情况下是不可能计算出 的。证明者无法将 与 配对,也无法将 与 配对,因为那将创建一个 点,而证明者为求 需要的是一个 点。
相反,可信设置需要为扩展 QAP 中存在问题的 项预计算出 个多项式。
通过一些代数变换,我们将所有的求和项合并为一个求和:
并将 提取出来:
可信设置可以利用上面方框中的项创建 个在 处求值的多项式,然后证明者可以利用它们来计算总和。确切的细节将在下一节展示。
迄今为止的算法总结
可信设置步骤
具体而言,可信设置计算以下内容:
可信设置公布
证明者步骤
证明者计算
请注意,我们将“有问题的”多项式
(包含 和 的那个)替换为了
验证者步骤
验证者计算:
支持公开输入
到目前为止的验证者公式尚不支持公开输入,即公开见证的一部分。
按照惯例,见证的公开部分是向量 的前 个元素。为了使这些元素公开,证明者只需揭示它们即可:
为了使验证者能够测试这些值是否确实被使用,验证者必须执行一些原本由证明者负责的计算。
具体来说,证明者现在这样计算:
请注意,只有 的计算发生了变化——证明者仅使用了从 到 的 和 项。
验证者计算求和的前 项:
而验证等式变为:
第 2 部分:通过 或 将公开输入和私有输入分开
通过滥用 的 伪造证明
上述等式中的假设是,证明者只使用 到 来计算 ,但没有任何机制能阻止不诚实的证明者使用 到 来计算 ,这会导致出现伪造的证明。
例如,这是我们当前的验证等式:
如果我们在底层展开 C 项,我们会得到以下形式:
假设(不失一般性) 且 。在这种情况下,见证的公开部分是 ,私有部分是 。
最终的等式将如下所示:
然而,没有任何机制能阻止证明者创建一个部分合法的公共见证,如 [1,2,0],并将被置为零的公开部分转移到计算的私有部分,如下所示:
上面的等式是有效的,但见证却不一定满足原始约束。
因此,我们需要防止证明者在计算 时使用 到 。
引入 和/或
为了避免上述问题,可信设置引入了一个新的标量: 或 ,以强行将 到 与 到 隔离开来。为此,可信设置会将私有项(构成 的部分)除以 (即乘以模逆),并且/或者将公开项(构成验证者计算的求和 的部分)除以 。
由于 项被嵌入在 中,这些项同样需要被除以 。如果 和 都具有未知的离散对数,那么前面描述的伪造以及可能的其他方法都可以被避免。这种方法被用于 Zcash 基于 Sapling 的可信设置中,其中 只是留在 中,而 在后来的可信设置阶段中仍会从 更新为一个随机值。
可信设置公布
证明者的步骤和以前一样:
而验证者的步骤现在包括通过与 和/或 配对来抵消分母:
第 3 部分:实现真正的零知识:r 和 s
我们的方案尚未实现真正的零知识。如果攻击者能够猜出我们的见证向量(如果有效输入的范围很小,这是可能的,例如来自特权地址的秘密投票),那么他们可以通过比较他们构造的证明和原始证明来验证他们的猜测是否正确。
举一个简单的例子,假设我们声称 和 的值要么是 ,要么是 。对应的算术电路将是
攻击者只需要猜测四种组合就能弄清楚见证是什么。也就是说,他们猜测一个见证,生成一个证明,然后看他们的答案是否与原证明匹配。
为了防止被猜测,证明者需要对其证明“加盐” (salt),同时需要修改验证公式以适应加盐后的证明。
证明者抽取两个随机域元素 和 ,将它们添加到 和 中,使得见证变得不可被猜测——攻击者现在必须同时猜中见证以及盐 和 :
为了推导最终的验证公式,让我们暂时忽略我们不知道希腊字母项的离散对数这一事实,直接计算验证等式的左侧 :
将各项展开后我们得到:
我们可以挑出原属于 的项
并将它们合并在左侧,将新引入的项留在右侧:
我们进一步重排带下划线的项,将其用 和 来表达。同时,我们将 拆分为 :
把与 和 相关的项组合在一起:
提出 和 :
代入 和 :
因此我们的最终等式为
我们现在将其分成公开和私有部分:
我们希望公开部分和私有部分分别由 和 隔离:
其中一部分项中的 可以被提取出来:
我们现在将此等式拆分为验证者和证明者两部分。带方框的项是验证者计算的部分,带下括号的项是证明者提供的部分:
Groth16 证明算法
我们现在准备端到端地展示 Groth16 算法。可信设置和验证步骤与我们之前结合了 和 的示例保持不变。只有证明者的计算为了包含 和 而发生了改变。
可信设置
可信设置公布
证明者步骤
证明者拥有一个见证 ,并生成随机标量 和 。
证明者公布 。
验证者步骤
验证者检查
在 Solidity 中验证 Groth16
到了这里,你已经具备了足够的知识去理解 Solidity 中的证明验证代码。这是 Tornado Cash 的证明验证代码。鼓励读者仔细阅读其源代码。如果读者熟悉 Solidity 汇编编程,那么理解这段源代码将不会很困难,因为变量名与本文中的变量名是一致的。
此外也有库支持 Solana 上的 Groth16。
需要注意的安全问题
Groth16 具有延展性 (Malleable)
Groth16 证明是具有延展性的。给定一个有效的证明
,攻击者可以计算 和 的点的逆元(取反点),并呈现一个新的证明 ,其中 且 。
要理解为什么 ,请考虑以下代码:
from py_ecc.bn128 import G1, G2, multiply, neg, eq, pairing
# chosen arbitrarily
x = 10
y = 100
A = multiply(G1, x)
B = multiply(G2, y)
A_p = neg(A)
B_p = neg(B)
assert eq(pairing(B, A), pairing(B_p, A_p))
直观地说,攻击者正在将 和 乘以 ,而 在配对操作中会自我抵消。
因此,如果验证公式接受
那么它同样也会接受
针对这种攻击的防御措施在下一节中描述。
你可以在这篇文章中看到这种攻击的概念验证。
证明者可以为同一个见证创建无限数量的证明
这本身并不算是一个“安全问题”——这是实现零知识所必需的。然而,应用程序需要一套机制来追踪哪些事实已经被证明过,并且不能依赖于证明的唯一性来实现这一点。
通过 RareSkills 了解更多
我们免费发布此类材料的能力依赖于我们学生的持续支持。请考虑注册我们的 Zero Knowledge Bootcamp、Web3 Bootcamps 或在 RareTalent 上寻找一份工作。
最初发布于 2023 年 8 月 31 日