如果我们有一个包含对向量 的承诺的 Pedersen vector commitment (即 ),我们可以通过将 发送给验证者来证明我们知道该承诺的打开(opening),验证者将检查 。这需要向验证者发送 个元素(假设 的长度为 )。
在上一章中,我们展示了如何在零知识的情况下做到这一点。在本章中,我们将展示如何通过发送少于 个元素来证明对承诺打开的知识,但不具备零知识属性。
动机
我们在此开发的技术将成为一个重要的基础模块,用于证明内积的有效计算,其证明大小为 ,其中 是向量的长度。
在上一章中,我们展示了如何证明我们正确执行了内积计算,而无需泄露向量或结果。然而,由于证明者发送 和 的步骤,该证明的大小为 。
本文中的子程序对于减小证明的大小将非常重要。本文不涉及零知识,因为之前讨论的算法本身就具备零知识属性。也就是说, 和 一开始并不是秘密,因此没有必要对它们进行混淆。
问题陈述
给定一个商定的基向量 ,证明者向验证者提供一个 Pedersen 向量承诺 ,其中 是对 的非盲化(non-blinding)承诺,即 ,并希望在发送少于 个项(即不发送整个向量 )的情况下,证明他们知道该承诺的打开。
小于 的证明
缩小证明大小依赖于三个洞见:
洞见 1:内积 是外积的对角线
我们要利用的第一个洞见是,内积是外积的对角线。换句话说,从某种意义上说,外积“包含”了内积。在一维向量的语境中,外积是一个二维矩阵,由第一个一维向量中的每个元素与第二个向量中的每个元素相乘形成。例如:
这似乎是朝着错误方向迈出的一步,因为外积需要 个步骤来计算。然而,接下来的洞见表明,可以在 的时间里间接地计算外积。
洞见 2:外积的和等于原向量和的乘积
第二个观察结果是,外积各项的和等于各个向量和的乘积。也就是说,
对于我们向量 和 的例子,这等同于:
在图形上,这可以直观地理解为一个尺寸为 的矩形的面积,与 的“面积”相同:
在我们的例子中, 向量实际上是椭圆曲线点的基向量,所以我们的意思是:
请注意,我们最初的 Pedersen 承诺
被嵌入到了外积中带框的项里:
因此,通过将向量各项的和相乘,我们也计算了外积的和。
既然内积是外积的对角线,我们就通过将向量各项的和相乘,间接地计算了内积。为了证明我们知道内积,我们需要证明我们同样知道外积中不属于内积的那些项。
对于长度为 的向量,我们将外积中不属于内积的部分称为非对角乘积(off-diagonal product)。
在下方,我们用 标记构成非对角乘积的项,用 标记构成内积的项:
我们现在可以正式声明我们在后续将要依赖的恒等式。如果 ,那么:
如果其中一个向量是椭圆曲线点的向量(即使它们的离散对数是未知的),这个恒等式同样成立。
对于 的情况,证明内积的知识意味着证明者需要使验证者相信他们知道下方紫色阴影区域的“面积”。

在 时简洁地传达这一信息比较棘手,因此我们稍后会再来讨论。
在 的情况下,该面积仅仅就是那些非对角元素。
洞见 3:如果 ,那么内积等于外积
一个重要的边缘情况是当我们有一个长度为 的向量时。在这种情况下,证明者只需将 (其长度为 )发送给验证者,验证者只需将 的唯一元素与 的唯一元素相乘即可。
算法草图
现在,我们可以为 的情况制定算法的初稿,该算法能够证明我们已经计算了 和 的内积,这等同于展示我们知道承诺 。
证明者与验证者之间的交互如下:
- 证明者将其承诺 发送给验证者。
- 证明者将 中的所有项相加,并作为 发送给验证者(请注意,向量各分量的和是一个标量,因此对 的元素求和会得到标量 )。此外,证明者计算 的非对角项(即 ,),并将 和 发送给验证者。
在图形上, 和 可以表示如下:
- 验证者通过计算 (其中 )间接计算出 ,并检查:
展开形式下,上述方程为:
请注意,上述检查等价于早前给出的恒等式:
安全漏洞:多重打开(multiple openings)
然而,存在一个安全问题——证明者可以为同一个承诺找到多个证明。例如,证明者可以随机选择 ,然后计算
为了防止这种情况,我们重新借用在讨论零知识乘法时一个类似的想法——证明者必须在其计算中包含验证者提供的随机数 。他们还必须在获得 之前 发送 和 ,这样就无法“投机地”选择 和 。
证明者之所以必须单独发送 和 ,而不是发送总和 ,是因为证明者能够通过在 和 之间无限制地转移数值来破坏协议。也就是说,由于
证明者可以选择某个椭圆曲线点 ,并由此计算出伪造的 和 :
我们需要强制证明者保持 和 分离。
这是修正此漏洞后的更新算法:
-
证明者和验证者商定一个基向量 ,其中的点是随机选择的,并且它们的离散对数是未知的。
-
证明者计算并向验证者发送 :
-
验证者回复一个随机标量 。
-
证明者计算并发送 :
- 验证者现在拥有了 ,接着检查:
其背后的底层展开是:
如果证明者正确计算了 、 和 ,这个等式是恒等成立的。
请注意,验证者将 作用于 ,而证明者将 作用于 。这导致原始内积的各项成为结果多项式的一次(线性)系数。
由于 和 之间被由验证者控制的 分隔开,这防止了恶意证明者执行前面描述的攻击。也就是说,证明者不能将数值从 转移到 ,因为转移的数值必然会被 缩放,而证明者又必须在收到 之前就发送 和 。
算法的另一种解释:将 的维度减半
验证者只进行了一次乘法,即 乘以 。即使我们从长度为 的向量开始,验证者也只需执行 次点乘。
的操作将长度为 的向量 变成了长度为 的向量。因此,在给定证明者的向量和验证者的随机数 的情况下,证明者和验证者双方正在共同构造一个新的长度为 的向量。
因为他们都已将原始向量压缩为一个长度为 的向量,所以验证者可以在 的情况下使用恒等式 。在此, 且 。
算法的安全性
算法总结
作为对该算法的快速总结,
- 证明者向验证者发送 。
- 验证者回复 。
- 证明者计算并发送 。
- 验证者检查:
现在让我们看看为什么证明者无法作弊。
在步骤 3 中,证明者拥有的唯一“自由度”是 。
要想给出一个满足
的 ,证明者需要知道 和 的离散对数。具体而言,他们必须解出
其中
- 和 分别是 和 的离散对数
- 和 分别是 和 的离散对数, 是 的离散对数。
- 和 是证明者已知的,因为证明者在步骤 1 中生成了 和 。
然而,证明者并不知道离散对数 和 ,因此他们无法计算出 。
变量 只有两个有效解
对于满足 的 ,只有两个有效值。请注意,方程左侧的 构成了一个关于变量 的二次多项式,而右侧的 构成了一个一次多项式。根据 Schwartz-Zippel Lemma(Schwartz-Zippel 引理),该方程最多有两个解。只要域的阶(order) ,那么证明者恰好找到能够使 相交于一点的 的概率就可以忽略不计。
Bulletproofs 论文中注入随机数的方法
在 Bulletproofs 论文中,证明者并不是将 和 组合为 ,而是将它们组合成 ,验证者则计算 。请注意,这两个向量的幂次是按相反顺序应用的。当我们计算外积时,内积项中的 将相互抵消:
可以说,这种方法“更干净”,所以我们将在后续采用这种方法。
引入
这种计算在 Bulletproofs 中非常频繁,因此给它起个名字会很方便,我们称之为 。第一个参数 是我们正在折叠的向量(其长度必须为偶数,如果不是,我们用 进行填充)。Fold 操作将长度为 的向量 分割为 对,并返回如下长度为 的向量:
如果我们执行 ,我们的意思是:
当 时, 仅仅是 ,而 。
带有 的算法描述
我们现在使用 Bulletproofs 论文中处理随机数的方法重新陈述该算法:
- 证明者向验证者发送他们对 的承诺 ,以及计算得到的 和 :
- 验证者回复一个随机标量 。
- 证明者计算并发送 :
- 验证者计算:
假设证明者是诚实的,最后的检查底层可展开为:
如何处理 的情况
假设数组 具有偶数长度(如果不是,我们可以添加一个零元素使其长度变为偶数),我们可以对该数组进行成对划分(pairwise-partition)。下面是一个成对划分的示例:
类似地,我们也可以对 进行成对划分。
然后,每个子对都可以视为计算内积的一个实例,直接使用前面讲过的 情况:

接着,我们可以证明我们知道这四个 承诺 、、 和 ,这就等价于证明我们知道原承诺的打开。
然而,这会为我们所证明的每个对额外产生四组 项——也就是说,在证明者传输的数据大小方面没有任何效率提升。
一种朴素的解决方案是证明者承诺并发送:
在图形上,这可以表示如下:
作为一个(关键的!)优化,我们将每一对中的所有 、 和 项相加,变成单独的点 、 和 。换言之,证明者只需发送:
上述操作如下方动画所示:
将所有承诺和非对角项加在一起的安全性
对于这种优化,最初的担忧是:由于证明者将更多的项加在了一起,就有了更多的机会来隐藏不诚实的计算。
现在我们来证明:一旦证明者发送了 (以及 和 ),他们就只能创建出唯一的证明来表明自己知道 的打开。
观察到 的计算方式为 , 的计算方式为 。它们不包含任何共同的椭圆曲线点。因此,证明者无法将数值从 “转移” 到 ,因为他们不知道任何这些点的离散对数。实际上, 是向基向量 作出的关于 的 Pedersen 向量承诺。Pedersen 向量承诺的安全假设是证明者只能生成一种可能的向量打开。在发送承诺后还要“转移数值”,将意味着证明者能够计算出与 不同的另一个向量,且产生相同的承诺。但这违背了我们的假设,即对于一个 Pedersen 承诺,证明者只能产生唯一一个有效的向量。对于 也可以进行类似的论证。
是四个 Pedersen 承诺(分别对向量 、、、 的承诺)的和。然而,从安全性的角度来看,多个 Pedersen 承诺相加这一事实是无关紧要的。无论这些承诺是单独计算再相加,还是直接将 作为 的向量进行计算,并没有任何区别。请考虑:
例如,证明者可能会将数值从 “转移”到 。
剩下的唯一担忧是,证明者可能会将数值从 中的 转移到 中的 ,因为它们共享同一个椭圆曲线点。然而,如前所述,这会被来自验证者的随机数 所阻止。
因此,一旦证明者发送了以本节所描述方式计算的 ,他们就只能创建一种可能的打开,从而也就只能创建出一种可能的证明。
证明我们知道 的打开,同时只发送 的数据
- 证明者向验证者发送 。证明者还发送 和 。
- 验证者发送一个随机数 。
- 证明者计算 并将 发送给验证者。
- 验证者检查 。
我们将其作为一个练习留给读者:推导一个例子来检验在证明者诚实的情况下,最终的验证检查在代数上是否等价。我们建议使用一个小的例子,例如 。
对 的另一种解释
是原始向量 关于基向量 的承诺。 是成对外积中由左侧非对角线分量组成的向量承诺, 是成对外积中由右侧非对角线分量组成的向量承诺。
总和 本身就是向量 相对基底 的向量承诺,其大小为 。
我们在下面以图形方式展示这一关系:

要证明我们知道一个大小为 的承诺的打开,我们只需发送该大小为 的向量即可,在本例中为 。
使用这种解释,该算法正在执行以下操作:
- 证明者发送 、 和 。
- 验证者发送 。
- 现在验证者拥有了一个相对基向量 的承诺 。
- 证明者通过发送 来证明他们知道 的打开。
验证速度的限制
因为验证者需要计算 ,这将要求遍历整个 向量,这将花费 的时间。尽管证明大小可以小于原始向量,但验证该证明仍将花费线性时间。
总结
我们展示了证明者如何能够在仅发送 个元素(折叠后的 )的情况下,展示他们知道 Pedersen 向量承诺 的打开。
在下一章中,我们将展示如何递归地应用这个算法,从而使证明者仅需发送 个元素。
练习: 实现本章描述的算法。使用以下代码作为起点:
from py_ecc.bn128 import G1, multiply, add, FQ, eq, Z1
from py_ecc.bn128 import curve_order as p
import numpy as np
from functools import reduce
import random
def random_element():
return random.randint(0, p)
def add_points(*points):
return reduce(add, points, Z1)
# if points = G1, G2, G3, G4 and scalars = a,b,c,d vector_commit returns
# aG1 + bG2 + cG3 + dG4
def vector_commit(points, scalars):
return reduce(add, [multiply(P, i) for P, i in zip(points, scalars)], Z1)
# these EC points have unknown discrete logs:
G_vec = [(FQ(6286155310766333871795042970372566906087502116590250812133967451320632869759), FQ(2167390362195738854837661032213065766665495464946848931705307210578191331138)),
(FQ(6981010364086016896956769942642952706715308592529989685498391604818592148727), FQ(8391728260743032188974275148610213338920590040698592463908691408719331517047)),
(FQ(15884001095869889564203381122824453959747209506336645297496580404216889561240), FQ(14397810633193722880623034635043699457129665948506123809325193598213289127838)),
(FQ(6756792584920245352684519836070422133746350830019496743562729072905353421352), FQ(3439606165356845334365677247963536173939840949797525638557303009070611741415))]
# return a folded vector of length n/2 for scalars
def fold(scalar_vec, u):
pass
# your code here
# return a folded vector of length n/2 for points
def fold_points(point_vec, u):
pass
# your code here
# return L, R as a tuple
def compute_secondary_diagonal(G_vec, a):
pass
# your code here
a = [9,45,23,42]
# prover commits
A = vector_commit(G_vec, a)
L, R = compute_secondary_diagonal(G_vec, a)
# verifier computes randomness
u = random_element()
# prover computes fold(a)
aprime = fold(a, u)
# verifier computes fold(G)
Gprime = fold_points(G_vec, pow(u, -1, p))
# verification check
assert eq(vector_commit(Gprime, aprime), add_points(multiply(L, pow(u, 2, p)), A, multiply(R, pow(u, -2, p)))), "invalid proof"
assert len(Gprime) == len(a) // 2 and len(aprime) == len(a) // 2, "proof must be size n/2"