二次算术程序(Quadratic Arithmetic Program)是一种算术电路,具体来说是将秩 1 约束系统(R1CS)表示为一组多项式。它是在秩 1 约束系统上使用拉格朗日插值推导出来的。与 R1CS 不同,二次算术程序(QAP)可以通过 Schwartz-Zippel 引理在 时间内进行相等性测试。
核心思想
在关于 Schwartz-Zippel 引理的章节中,我们看到可以通过将两个向量转换为多项式,然后对多项式运行 Schwartz-Zippel 引理测试,从而在 时间内测试它们是否相等。(需要澄清的是,测试本身是 时间的,而将向量转换为多项式会产生额外的开销)。
因为秩 1 约束系统完全由向量运算组成,我们的目标是测试
是否在 时间而不是 时间内成立(其中 是 、 和 中的行数)。
但在此之前,我们需要了解表示向量的多项式与向量之间关系的一些关键属性。
对于这里的所有数学运算,我们假设我们在有限域中进行,但为了简洁起见,我们省略了 符号。
向量加法和多项式加法之间的同态
向量加法同态于多项式加法
如果我们取两个向量,用多项式对它们进行插值,然后将这些多项式相加,我们得到的多项式,与我们将向量相加然后再对求和后的向量进行插值得到的多项式是相同的。
用更数学化的语言来说,令 为对向量 使用 作为 值进行拉格朗日插值所得到的多项式,其中 是 的长度。以下等式成立:
换句话说,对向量 和 进行插值后得到的多项式之和,与对向量 进行插值得到的多项式是相同的。
示例演示
令 且 , 插值 或向量 , 插值 。
向量之和为 ,很明显 对其进行了插值。令 。
用 Python 测试数学逻辑
对提出的数学恒等式进行单元测试并不能证明它的绝对真实性,但它确实说明了正在发生的事情。鼓励读者尝试几个不同的向量,以验证该恒等式是否成立。
import galois
import numpy as np
p = 17
GF = galois.GF(p)
xs = GF(np.array([1,2,3]))
# two arbitrary vectors
v1 = GF(np.array([4,8,2]))
v2 = GF(np.array([1,6,12]))
def L(v):
return galois.lagrange_poly(xs, v)
assert L(v1 + v2) == L(v1) + L(v2)
标量乘法
令 为一个标量(具体来说,是有限域中的一个域元素)。那么
示例演示
假设我们的 3 个点对应 。对其进行插值的多项式为 。如果我们将向量乘以 3,我们得到 。对其进行插值的多项式是
from scipy.interpolate import lagrange
x_values = [1, 2, 3]
y_values = [9, 18, 33]
print(lagrange(x_values, y_values))
# 2
# 3 x + 6
,它等于 。
代码示例演示
import galois
import numpy as np
p = 17
GF = galois.GF(p)
xs = GF(np.array([1,2,3]))
# arbitrary vector
v = GF(np.array([4,8,2]))
# arbitrary constant
lambda_ = GF(15)
def L(v):
return galois.lagrange_poly(xs, v)
assert L(lambda_ * v) == lambda_ * L(v)
标量乘法本质上是向量加法
当我们说“将向量乘以 3”时,我们实际上是在说“将向量自身相加三次”。因为我们只在有限域中进行运算,所以我们不需要关心像“0.5”这样标量的解释。
我们可以将逐元素加法下的向量(在有限域中)和加法下的多项式(也在有限域中)都视为群(groups)。
本章最重要的结论是
有限域中加法下的向量群同态于有限域中加法下的多项式群。
这很关键,因为向量相等性测试需要 时间,而多项式相等性测试只需 时间。
因此,尽管测试 R1CS 的相等性需要 时间,我们可以利用这种同态性在 时间内测试 R1CS 的相等性。
这就是二次算术程序(Quadratic Arithmetic Program)的本质。
多项式形式的秩 1 约束系统
考虑矩形矩阵和向量之间的矩阵乘法可以写成向量加法和标量乘法的形式。
例如,如果我们有一个 的矩阵 和一个 4 维向量 ,那么我们可以将矩阵乘法写为
我们通常将向量 想象为“翻转”并与每一行进行内积(广义点积),即
然而,我们也可以将矩阵 拆分为一系列向量,如下所示:
然后将每个向量乘以向量 中的一个标量:
我们已纯粹用向量加法和标量乘法表达了 和 之间的矩阵乘法。
因为我们之前已经确定,有限域中加法下的向量群同态于有限域中加法下的多项式群,所以我们可以用表示这些向量的多项式来表达上述计算。
简洁地测试
假设我们有矩阵 和 满足
以及向量 和
我们想要测试
是否成立。
显然,我们可以执行矩阵算术运算,但最终的检查将需要 次比较,其中 是 和 的行数。我们希望在 时间内完成。
首先,我们将矩阵乘法 和 转换为加法下的向量群:
我们现在希望在多项式群中找到
的同态等价物。
让我们在 值 的区间内将每个向量转换为多项式:
我们将调用一些 Python 代码来计算拉格朗日插值:
import galois
import numpy as np
p = 17
GF = galois.GF(p)
x_values = GF(np.array([1, 2]))
def L(v):
return galois.lagrange_poly(x_values, v)
p1 = L(GF(np.array([6, 4])))
p2 = L(GF(np.array([3, 7])))
q1 = L(GF(np.array([3, 12])))
q2 = L(GF(np.array([9, 6])))
print(p1)
# 15x + 8 (mod 17)
print(p2)
# 4x + 16 (mod 17)
print(q1)
# 9x + 11 (mod 17)
print(q2)
# 14x + 12 (mod 17)
最后,我们可以通过调用 Schwartz-Zippel 引理来检查
是否为真:
import random
u = random.randint(0, p)
tau = GF(u) # a random point
left_hand_side = p1(tau) * GF(2) + p2(tau) * GF(4)
right_hand_side = q1(tau) * GF(2) + q2(tau) * GF(2)
assert left_hand_side == right_hand_side
最后的 assert 语句能够通过进行单次比较(而不是 次比较)来测试 。
R1CS 到 QAP:简洁地测试
既然我们知道如何简洁地测试 ,我们是否也可以简洁地测试 呢?
这些矩阵有 列,因此让我们将每个矩阵分解为 个列向量,并在 上对它们进行插值,以各自生成 个多项式。
令 为对 的列向量进行插值的多项式。
令 为对 的列向量进行插值的多项式。
令 为对 的列向量进行插值的多项式。
不失一般性,假设我们有 4 列()和 3 行()。
直观上,这可以表示为
因为将列向量乘以标量同态于将多项式乘以标量,所以每个多项式都可以乘以见证(witness)中各自对应的元素。
例如,
变成了
观察最终结果是一个度数至多为 的单一多项式,因为 的度数至多为 。
这源于我们构造它们的方式: 的每一列都有 个项,并且通过拉格朗日插值对 个点进行插值会产生一个度数至多为 的多项式。
在一般情况下,将 个列分别转换为多项式之后, 可以写为
使用与上面相同的步骤,R1CS 中的每个矩阵-见证乘积可以转换为
由于每个求和项都产生一个单一多项式,我们可以将它们写为:
为什么要对所有的列进行插值?
由于同态性 和 ,如果我们将 计算为 ,得到的结果与对 的各列应用拉格朗日插值,然后将每个多项式乘以 中对应的元素并对结果求和得到的结果是相同的。
换句话说,
那么为什么不只计算一次拉格朗日插值而不是 次呢?
我们需要区分是谁在使用 QAP。验证者(以及我们稍后将介绍的可信设置)不知道见证 ,因此无法计算 。这是证明者可以做的一个优化,但零知识(ZK)协议中的其他参与方无法使用该优化。
所有相关方都需要在进行任何证明和验证之前,对 QAP——即矩阵的多项式插值——达成共识。
多项式度数不平衡
然而,我们不能简单地将最终结果表达为
因为多项式的度数(degree)不匹配。
将两个多项式相乘,其结果多项式的度数等于相乘的两个多项式度数之和。
因为 、 和 各自的度数都是 ,所以 通常的度数将是 ,而 的度数是 ,因此尽管它们相乘所对应的底层向量是相等的,但多项式本身并不相等。
这是因为我们之前建立的同态仅针对向量加法,而不是哈达玛乘积(Hadamard product)。
然而, 所插值的向量,即
与 所插值的向量是相同的,即
换句话说
尽管“底层”的向量相等,但对它们进行插值的多项式并不相等。
底层相等的例子
假设 是对以下点进行插值的多项式
而 是对以下点进行插值的多项式
如果我们视 插值了向量 而 插值了向量 ,那么我们可以看到它们的乘积多项式插值了这两个向量的哈达玛乘积。 和 的哈达玛乘积为 。
如果我们将 和 乘在一起,我们得到 。
我们可以从下面的图表中看到,乘积多项式插值了这两个向量的哈达玛乘积 。

那么如果它们在 上插值了相同的 值,我们怎么才能“使得” 等于 呢?
对 向量进行插值
如果 ,那么 。
我们不需要用拉格朗日插值对 进行插值并得到 (记住拉格朗日插值会找到最低度数的插值多项式),我们可以使用一个更高度数的多项式来平衡度数的不匹配。
例如,下图中黑色的多项式()插值了 :

现在,由于 是 的有效插值,我们可以将原始等式写为
等式就平衡了!
是简单地通过 计算得出的(蓝色多项式减去红色多项式)
然而,我们不能让证明者挑选任意的 ,否则他们即使在没有插值相同向量(在我们的例子中是 )的情况下,也可以挑选一个能平衡 和 的 。证明者在选择 时拥有太多的灵活性。具体来说,我们希望要求 在 处有根——也就是说,去插值 向量。这样一来, 的多项式转换依然会遵循底层的向量。
为了限制他们对 的选择,我们可以使用以下定理:
多项式乘积的根的并集
定理:如果 ,且 有根的集合 , 有根的集合 ,那么 拥有的根为 。
示例
令 且 。那么 的根为 。
我们可以利用上面的定理强制使得 的根在 处。
强制 为零向量
我们将 分解为 ,其中 是多项式
那么与 相乘的任何多项式对应的也将是零向量,因为它必然会在 处有根。
因此,我们将在我们的等式中把 替换为 。
从而,我们的等式将变为
我们利用基础代数就可以计算出 :
QAP 端到端
假设我们有一个具备矩阵 、 和 以及见证向量 的 R1CS。
这些矩阵有 列和 行,其中 且 。
即,、 和 如下所示:
且见证向量 是
我们将每个矩阵分解为 个列向量,并在 上对它们进行插值,每个矩阵生成 个多项式。
每一个矩阵-向量乘积 、 和 在同态上等价于以下多项式:
在我们的例子中, 将是
而 将是
原始 R1CS 的 QAP 表示的最终公式为
QAP 的最终公式
QAP 就是以下公式:
其中 、 和 分别是对 、 和 的列进行插值的多项式, 为 ,其中 是 、 和 的行数,而 是
使用二次算术程序的简洁零知识证明
假设我们有一种方法使得验证者可以向证明者发送一个随机值 ,并且证明者会以如下形式回应:
验证者可以检查 并接受证明者拥有一个有效的、同时满足 R1CS 和 QAP 的见证 。
然而,这将要求验证者信任证明者正在正确地求值这些多项式,而我们目前并没有一种机制来强制证明者这样做。
在下一章中,我们将基于本章的讨论展示 将 R1CS 转换为 QAP 的 Python 代码。
接下来我们将讨论可信设置(trusted setups),开始着手解决如何让证明者诚实地对多项式进行求值的问题。
原文首发于 2023 年 8 月 23 日