文章详细介绍了如何将一组算术约束转换为Rank One Constraint System (R1CS),涵盖了转换中的优化和Circom库的实现方法。
这篇文章解释了如何将一组算术约束转化为 Rank One Constraint System (R1CS)。
本资源的重点是实现:我们讨论了这种转换的更多边界情况,讨论了优化,并解释了 Circom 库如何实现它。
Rank 1 Constraint System (R1CS) 是一个算术电路,其要求是每个等式约束有一个乘法(对加法的数量没有限制)。
这使得算术电路的表示与双线性配对的使用兼容。配对的输出 G1∙G2→GT 不能再次进行配对,因为在 GT 中的元素不能作为其他配对的输入。因此,我们每个约束只允许一个乘法。
在一个算术电路中,见证是一个对所有信号的赋值,使其满足方程的约束。
在 Rank 1 Constraint System 中,见证向量是一个 1×n 向量,包含所有输入变量、输出变量和中间值的值。它表明你已经从头到尾执行了电路,知道输入、输出以及所有中间值。
通过约定,第一个元素总是 1,以便使一些计算更简单,我们稍后将演示。
例如,如果我们有约束
z=x²y
我们宣称知道解,那么这必须意味着我们知道 x、y 和 z。因为 Rank One Constraint Systems 每个约束恰好要求一个乘法,上述多项式约束必须写作
₁₁v₁=xxz=v₁y
一个见证意味着我们不仅知道 x、y 和 z,我们必须知道这种展开形式中的每个中间变量。具体而言,我们的见证是一个向量:
₁[1,z,x,y,v₁]
其中每个项都有一个满足上述约束的值。
例如,
[1,18,3,2,9]
是一个有效的见证,因为当我们将值代入时,₁[constant=1,z=18,x=3,y=2,v₁=9] 它满足约束
v₁=x∗x→9=3⋅3
z=v₁∗y→18=9⋅2
额外的 1 项在这个例子中没有使用,它只是一个方便,我们会稍后解释。
在我们的例子中,我们将说我们在证明 41×103=4223。
因此,我们的见证向量是 [1,4223,41,103],作为对 [1,z,x,y] 的赋值。
在我们能够创建 R1CS 之前,我们的约束需要是以下形式
result = left_hand_side × right_hand_side
幸运的是,它已经是 z⏟result=x⏟左侧×y⏟右侧
这显然是一个简单的例子,但简单的例子通常是一个很好的起点。
要创建一个有效的 R1CS,你需要一个包含恰好一个乘法的公式列表。
稍后我们将讨论如何处理不只有一个乘法的情况,例如 ³z=x³ 或 ³z=x³+y。
我们的目标是创建一个形式为:
Oa=La∘Ra
的方程组,其中 O、L 和 R 是大小为 n x m 的矩阵(n 行和 m 列)。
矩阵 L 编码乘法左侧的变量,R 编码乘法右侧的变量。O 编码结果变量。向量 a 是见证向量。
具体而言,L、R 和 O 是列数与见证向量 a 相同的矩阵,每列表示索引使用的相同变量。
因此,对于我们的例子,见证有 4 个元素 (1,z,x,y),所以每个矩阵会有 4 列,所以 m=4。
行数将对应于电路中的约束数量。在我们的例子中,我们只有一个约束:z=x∗y,因此我们将只有一行,所以 n=1。
让我们直接跳入答案并解释我们是如何获得的。
Oa=La∘Ra [0100]⏟Oa=[0010]⏟La∘[0001]⏟Ra
[0100][1422341103]=[0010][1422341103]∘[0001][1422341103]
在这个例子中,矩阵中的每个项目都作为一个指示变量,用于表示与该列对应的变量是否存在。(从技术上讲,它是变量的系数,但我们稍后会讨论到)。
对于左侧条款,x 是出现在乘法左侧的唯一变量,因此如果这些列代表 [1,z,x,y],那么…
L 是 [0,0,1,0],因为 x 出现了,而其他变量没有。
R 是 [0,0,0,1] 因为乘法右侧的唯一变量是 y,并且
O 是 [0,1,0,0],因为我们在乘法的“输出”中只有 z 变量。
我们没有任何常数,因此 1 列在任何地方都为零(稍后我们将讨论它何时非零)。
此方程是正确的,我们可以在 Python 中验证这一点
import numpy as np
## 定义矩阵
O = np.matrix([[0,1,0,0]])
L = np.matrix([[0,0,1,0]])
R = np.matrix([[0,0,0,1]])
## 见证向量
a = np.array([1, 4223, 41, 103])
## 乘法 `*` 是逐元素的,而不是矩阵乘法。
## Result 包含一个布尔值,指示该元素的逐元素指示该等式对该元素为真。
result = np.matmul(O, a) == np.matmul(L, a) * np.matmul(R, a)
## 检查每个逐元素的等式是否为真
assert result.all(), "result contains an inequality"
你可能会想知道这有什么意义,我们是不是只是在用一种更不简洁的方式说 41×103=4223?
你是对的。
R1CS 可以相当冗长,但它们与 二次算术程序 (QAPs) 完美对应,可以简化。不过我们在这里不关心 QAP。
但这是 R1CS 的一个重要点。R1CS 传达的信息与原始算术约束完全相同,但每个等式约束仅包含一个乘法。在这个例子中,我们只有一个约束,但在下一个例子中我们将添加更多。
在这个稍微复杂一点的例子中,我们现在需要处理中间变量。我们每行的计算只能有一个乘法,因此我们必须按照以下方式分解方程:
v₁=xy v₂=zur r=v₁v₂
没有规则要求我们必须这样分解,下面的分解也是有效的:
v₁=xy v₂=v₁zr v₂u
我们将使用第一个变换作为这个例子的处理。
因为我们正在处理 7 个变量 (r,x,y,z,u,v₁,v₂),所以我们的见证向量将有八个元素(第一个是常数 1),我们的矩阵将有八列。
因为我们有三个约束,所以矩阵将有三行。
这个例子将强烈强调“左侧项”和“右侧项”的概念。具体而言,x、z 和 v₁ 是左侧项,而 y、u 和 v₂ 是右侧项。
v₁v₂r⏟ 输出项 === xzv₁⏟ 左侧项 ××× yuv₂⏟ 右侧项
让我们构造矩阵 A。我们知道它将有三行(因为有三个约束)和八列(因为有八个变量)。
[l₁,2 l₁,3 l₁,4 l₁,5 l₁,6 l₁,7 l₁,8 l₂,2 l₂,3 l₂,4 l₂,5 l₂,6 l₂,7 l₂,8 l₃,2 l₃,3 l₃,4 l₃,5 l₃,6 l₃,7 l₃,8]
我们的见证向量将与这个相乘,所以让我们定义我们的见证向量布局为:
[1 r x y u v₁ v₂]
这告诉我们 L 的列代表的是什么:
L=[l₁,1 l₁,r l₁,x l₁,y l₁,z l₁,u l₁,v₁ l₁,v₂ l₂,1 l₂,r l₂,x l₂,y l₂,z l₂,u l₂,v₁ l₂,v₂ l₃,1 l₃,r l₃,x l₃,y l₃,z l₃,u l₃,v₁ l₃,v₂]
在第一行,对于第一个左侧变量,我们有 ( v₁=xy ):
v₁ v₂ r === xz v₁ L ××× y u v₂
这意味着就左侧而言,变量 x 是存在的,其他变量则不存在。因此,我们将第一行转化如下:
L=[00100000 l₂,1 l₂,r l₂,x l₂,y l₂,z l₂,u l₂,v₁ l₂,v₂ l₃,1 l₃,r l₃,x l₃,y l₃,z l₃,u l₃,v₁ l₃,v₂]
请记住,L 的列标记如下:
[1 r x y u v₁ v₂]
我们看到 1 在 x 列中。
继续向下,我们看到只有 z 是出现在我们方程组的左侧。
v₁ v₂ r === xz v₁ L ××× y u v₂
因此,在该行中,我们将所有归零,除了代表 z 的列。
L=[0010000000001000 l₃,1 l₃,r l₃,x l₃,y l₃,z l₃,u l₃,v₁ l₃,v₂]
最后,我们在第三行中唯一出现在左侧运算符的变量是 ( v₁ )
v₁ v₂ r === xz v₁ L ××× y u v₂
这完成了矩阵 L。
L=[001000000000100000000010]
下图应使映射更清晰:
v₁ v₂ r === xz v₁ L ××× y u v₂ 1 r x y u v₁ v₂ [001000000000100000000010]
我们可以通过展开左侧值来完成相同的操作
v₁ v₂ r === xz v₁ ××× yu v₂
为
v₁=(0⋅1+0⋅r+1⋅x+0⋅y+0⋅z+0⋅u+0⋅v₁+0⋅v₂)× y v₂=(0⋅1+0⋅r+0⋅x+0⋅y+1⋅z+0⋅u+0⋅v₁+0⋅v₂)× u r=(0⋅1+0⋅r+0⋅x+0⋅y+0⋅z+0⋅u+1⋅v₁+0⋅v₂)× v₂
我们可以这样做是因为添加零项并不会更改值。我们只需小心展开零变量,使其具有与我们定义的见证向量相同的“列数”。
然后,如果我们将上面展开中的系数(以框表示)拿出来,
v₁=(0⋅1+0⋅r+1⋅x+0⋅y+0⋅z+0⋅u+0⋅v₁+0⋅v₂)× y v₂=(0⋅1+0⋅r+0⋅x+0⋅y+1⋅z+0⋅u+0⋅v₁+0⋅v₂)× u r=(0⋅1+0⋅r+0⋅x+0⋅y+0⋅z+0⋅u+1⋅v₁+0⋅v₂)× v₂
我们获得的 L 矩阵与刚才生成的相同。
L=[001000000000100000000010]
R=[r₁,1 r₁,r r₁,x r₁,y r₁,z r₁,u r₁,v₁ r₁,v₂ r₂,1 r₂,r r₂,x r₂,y r₂,z r₂,u r₂,v₁ r₂,v₂ r₃,1 r₃,r r₃,x r₃,y r₃,z r₃,u r₃,v₁ r₃,v₂]
矩阵 R 代表我们方程的右侧项:
v₁ v₂ r === xz v₁ ××× yu v₂ R
矩阵 R 必须有 1 代表 y、u 和 v₂。矩阵中的行对应于算术约束的行,即我们可以将约束(行)编号如下:
(1)(2)(3) v₁ v₂ r === xz v₁ ××× y u v₂ R
因此第一行在 y 列中有 1,第二行在 u 列中有 1,第三行在 v₂ 列中有 1。其他都是零。
这导致矩阵 R 得到如下结果:
1 r x y u v₁ v₂ [000100000000010000000001]
下图说明了变换。
v₁ v₂ r === xz v₁ ××× y u v₂ R 1 r x y u v₁ v₂ [000100000000010000000001]
是一项练习,读者确定矩阵 O 为
O=[000000100000000101000000]
使用与早先矩阵一致的列标签。
提醒一下,C 是通过乘法结果得出的
v₁ v₂ r C=== xz v₁ ××× y u v₂
列标签如下
1 r x y u v₁ v₂ [000000100000000101000000]
检测我们的反馈 r=x⋅y⋅z⋅u
import numpy as np
## 输入 A B 和 C
L = np.matrix([[0,0,1,0,0,0,0,0],
[0,0,0,0,1,0,0,0],
[0,0,0,0,0,0,1,0]])
R = np.matrix([[0,0,0,1,0,0,0,0],
[0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1]])
O = np.matrix([[0,0,0,0,0,0,1,0],
[0,0,0,0,0,0,0,1],
[0,1,0,0,0,0,0,0]])
## 为 x、y、z 和 u 选择随机值
import random
x = random.randint(1,1000)
y = random.randint(1,1000)
z = random.randint(1,1000)
u = random.randint(1,1000)
## 计算代数电路
r = x * y * z * u
v₁ = x * y
v₂ = z * u
## 创建见证向量
a = np.array([1, r, x, y, z, u, v₁, v₂])
## 逐元素乘法,而不是矩阵乘法
result = np.matmul(O, a) == np.multiply(np.matmul(L, a), np.matmul(R, a))
assert result.all(), "system contains an inequality"
如果我们想为以下构建一个 Rank One Constraint System 呢?
z=x∗y+2
这就是那一列 1 变得有用的地方。
你可能在 ZK-SNARKs 的上下文中听过“加法是免费的”这句话。这意味着当我们有一个加法操作时,不需要创建额外的约束。
我们可以把上面的公式写作
v₁=xy v₂=v₁+2
但这会使我们的 R1CS 比需要的要大。
相反,我们可以将其写为
−2+z=xy
然后变量 z 和常数 −2 在我们用见证向量乘以 O 时自动“结合”在一起。
我们的见证向量的形式是 [1, z, x, y]
,所以我们的矩阵 L、R 和 O 如下:
L=[0010]
R=[0001]
O=[−2100]
每当有加法常量时,我们简单地将其放置在 1 列中,按照约定这是第一列。
再次,我们来对我们的数学做一些单元测试:
import numpy as np
import random
## 定义矩阵
L = np.matrix([[0,0,1,0]])
R = np.matrix([[0,0,0,1]])
O = np.matrix([[-2,1,0,0]])
## 选择随机值来测试方程
x = random.randint(1,1000)
y = random.randint(1,1000)
z = x * y + 2 # 见证向量
a = np.array([1, z, x, y])
## 检查等式
result = O.dot(a) == np.multiply(np.matmul(L, a), R.dot(a))
assert result.all(), "result contains an inequality"
在上述所有示例中,我们从未使用常数乘以变量。这就是为什么 R1CS 中的条目始终是 1 的原因。正如你从上面的例子中可能猜到的,矩阵中的条目是变量乘以常数的相同值,以下示例将显示。
让我们解决以下的表达式
z=2x²+y
请注意,当我们说“每个约束一个乘法”时,我们指的是两个变量的乘法。与常数相乘不是“真实”的乘法,因为它实际上是对同一变量的重复加法。
以下解是有效的,但生成了不必要的行:
v₁=xx z=2v₁
更优化的解决方案如下:
−y+z=2xx
使用更优的解决方案,我们的见证向量将呈现为 [1, out, x, y]
。
矩阵将定义为:
L=[0020] R=[0010] O=[010−1]
用 [1, z, x, y]
在 r1cs 形式中进行符号乘法,给回我们的原始方程:
[010−1][1 zy x y] = [0020][1 zy x y]∘[0010][1 zy x y]
z–y=2xx z=2x²+y
所以我们知道我们正确设置了 L、R 和 O。
在这里我们有一行(约束)和一个“真实”的乘法。一般规则是:
Rank One Constraint system 中的约束数量应等于非恒定乘法的数量。
让我们做一些实施以下的内容,结合我们学到的所有知识。
假设我们有以下约束:
z=3x²y+5xy–x–2y+3
我们将按如下方式分解:
v₁=3xx v₂=v₁y–v₂+x+2y–3+z=5xy
请注意,所有加法项已移至左侧(这就是我们在加法示例中做的,但在这里更为明显)。
将右侧保留为 5xy 的第三行是任意的。我们可以将两边都除以 5,而最终约束为
−v₂+x5+2y5−35+z5=xy
然而这并不会改变见证,因此两者都是有效的。因为所有操作都是在有限域中进行的,这个操作是在左右两侧乘以 5 的乘法逆元。
我们的见证向量将是
[1,z,x,y,v₁,v₂]
我们的矩阵将有三行,因为我们有三个约束:
v₁=3xx v₂=v₁y−v₂+x+2y−3+z=5xy
我们将标记输出 O 为红色,左侧 L 为绿色,右侧 R 为紫色。这产生以下矩阵:
L=[003000000010005000]
R=[001000000100000100]
O=[000010000001−31120−1]
列标签为
[1 out x y v₁ v₂]
让我们照常检查我们的工作。
import numpy as np
import random
## 定义矩阵
L = np.array([[0,0,3,0,0,0],
[0,0,0,0,1,0],
[0,0,5,0,0,0]])
R = np.array([[0,0,1,0,0,0],
[0,0,0,1,0,0],
[0,0,0,1,0,0]])
O = np.array([[0,0,0,0,1,0],
[0,0,0,0,0,1],
[-3,1,1,2,0,-1]])
## 为 x 和 y 选择随机值
x = random.randint(1,1000)
y = random.randint(1,1000)
## 这是我们的原始公式
out = 3 * x * x * y + 5 * x * y - x - 2 * y + 3 # 包含中间变量的见证向量
v₁ = 3 * x * x
v₂ = v₁ * y
w = np.array([1, out, x, y, v₁, v₂])
result = O.dot(w) == np.multiply(L.dot(w),R.dot(w))
assert result.all(), "result contains an inequality"
为了简化问题,我们一直在使用如下形式的例子 z=xy+... 但大多数实际的算术约束将是一组算术约束,而不是单个约束。
例如,假设我们证明数组 ₁₂₃₄[x₁,x₂,x₃,x₄] 是二进制且 v 小于 16。约束集将是
₁²₁₂²₂₃²₃₄²₄₄₃₂₁x₁²=x₁x₂²=x₂x₃²=x₃x₄²=x₄v=8x₄+4x₃+2x₂+x₁
为了将这个转化为 Rank One Constraint System,我们注意到最后一行没有任何乘法,所以我们可以在第一个约束中替代 x₁:
₁²₄₃₂₂²₂₃²₃₄²₄x₁²=v–8x₄–4x₃–2x₂x₂²=x₂x₃²=x₃x₄²=x₄
假设我们的见证向量是 [1,v,x₁,x₂,x₃,x₄],我们可以创建 R1CS 如下:
L=[001000000100000010000001] R=[001000000100000010000001] O=[010−2−4−8000100000010000001]
进行替换并不是严格必要的,但它节省了 R1CS 中的一行。在后面的部分中,我们将展示一个有效的 R1CS,其中我们未进行替换。
在上述示例中,为了简化呈现,我们使用了传统算术,但现实世界的实现则使用模算术。
原因很简单:编码像 2/3 这样的数字会导致表现不佳的浮动数,这计算密集且容易出错。
如果我们在某个质数下进行所有数学计算,例如 23,那么编码 2/3 会变得简单。这和 2⋅3−1 是一样的,在模算术中乘以二和提升到负一的幂都是直观的。
在 Circom 中,一个构建 Rank 1 Constraint Systems 的语言,有限域 använder 的质数为 21888242871839275222246405745257275088548364400416034343698204186575808495617(这等于我们在 有限域上的椭圆曲线 中讨论的 BN128 曲线的阶)。
这意味着在该表示中的 −1 是
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
## 1 - 2 = -1
(1 - 2) % p
## 21888242871839275222246405745257275088548364400416034343698204186575808495616
如果我们在 Circom 中写 out = x * y
,它将看起来如下:
pragma circom 2.0.0;
template Multiply2() {
signal input x;
signal input y;
signal output out;
out <== x * y;
}
component main = Multiply2();
让我们将其转换为 R1CS 文件并打印出 R1CS 文件。
circom multiply2.circom --r1cs --sym
snarkjs r1cs print multiply2.r1cs
我们得到以下输出:
这看起来与我们的 R1CS 解决方案有所不同,但实际上它编码的是相同的信息。
Circom 实现中的差异如下:
那么 21888242871839275222246405745257275088548364400416034343698204186575808495616,实际上是 −1 呢?
Circom 的解决方案是
A=[00−10] B=[0001] C=[0−100]
尽管负一可能是意外的,但对于见证向量 [1 out x y]
,这实际上与 La∘Ra–Oa=0 的形式是一致的。(我们将看到 Circom 确实使用了此列分配)。
你可以为 x、y 和 out 插入值,看看 La∘Ra–Oa=0 这个等式是否成立。
让我们看看 Circom 的变量到列的分配。让我们使用 wasm 求解器重新编译我们的电路:
circom multiply2.circom --r1cs --wasm --sym
cd multiply2_js/
我们创建 input.json
文件
echo '{"x": "11", "y": "9"}' > input.json
并计算见证
node generate_witness.js multiply2.wasm input.json witness.wtns
snarkjs wtns export json witness.wtns witness.json
cat witness.json
我们得到以下结果:
显然 Circom 使用的是与我们使用的相同的列布局:[1, out, x, y],因为在我们的 input.json
中将 x 设置为 11,y 设置为 9。
如果我们使用 Circom 生成的见证(将巨大数字替换为 −1 以便易读),那么我们看到 Circom 的 R1CS 是正确的。
a=[199119] L=[00−10] → La=−11 R=[0001] → Ra=9 O=[0−100] → Oa=−99
Aw⋅Bw–Cw=0(−11)(9)–(−99)=0−99+99=0
L 在 x 中有一个 −1 的系数,R 在 y 中有一个 +1 的系数,而 O 中有 −1 的系数。在模形式中,这与终端输出的内容完全相同:
回顾一下,我们探讨的公式是
z=x∗y z=x∗y∗z∗u z=x∗y+2 z=3x²y+5xy–x–2y+3
我们刚刚在上面的部分讨论了第(1),在这一部分我们将说明非恒定乘法的数量是约束的数量。
约束的(2)的电路为:
pragma circom 2.0.8;
template Multiply4() {
signal input x;
signal input y;
signal input z;
signal input u;
signal v1;
signal v2;
signal out;
v1 <== x * y;
v2 <== z * u;
out <== v1 * v2;
}
template main = Multiply4();
考虑到我们迄今为止讨论的所有内容,Circom 输出及其注释应自解释。
考虑到这一点,其他公式的约束应如下:
z=x∗y 1 个约束z=x∗y∗z∗u 3 个约束z=x∗y+2 1 个约束z=3x²y+5xy–x–2y+3 3 个约束
这个是读者的练习,编写 Circom 电路并验证上述内容。
需要注意的是,在 Circom 代码中,我们在计算 R1CS 之前从未提供见证。我们之前提供见证只是为了使示例不那么抽象,并使其易于检查我们的工作,但这并不是必要的。这一点很重要,因为如果验证者需要见证来构建 R1CS,那么证明者将不得不放弃隐藏的解决方案!
当我们说“见证”时,我们指的是一个填充值的向量。验证者知道“见证”的“结构”,即变量到列的分配,但不知道值。
从多项式到 R1CS 的有效转换并不是唯一的。你可以使用更多的约束编码同一问题,而这效率较低。这里是一个例子。
在一些 R1CS 教程中,像
²z=x²+y
这样的公式的约束被转换为
₁₁v₁=xxz=v₁+y
正如我们所注意到的,这不是有效的。然而,你只需使用本文中的方法即可为此创建有效的 R1CS。我们只需添加一个虚拟乘法如下所示:
₁₁v₁=xxz=(v₁+y)∗1
我们的见证向量是形式为 [1,z,x,y,v₁],L、R 和 O 定义如下:
L=[0010000011]
R=[0010010000]
O=[0000101000]
L 的第二行实现了加法,而通过使用第二行的第一个元素来完成乘以一。
这是完全有效的,但解决方案比所需的多了一行多了一列。
如果我们想编码以下电路会如何呢?
z=x+y
这在实践中是非常无用的,但为了完整性,可以通过使用虚拟乘法将其解决。
out=(x+y)∗1
对于我们的典型见证向量布局 [1,z,x,y],我们有以下矩阵:
L=[0011] R=[1000] O=[0100]
原始关于 Groth16 的 论文 并没有提到 Rank One Constraint System 这个术语。 R1CS 从实现角度来看很方便,但从纯数学角度来看,它只是显式标记和分组不同变量的系数。因此,当你阅读关于该主题的学术论文时,通常会缺少因为它是更抽象概念的实现细节。
这篇博客来自我们 零知识课程 的学习资料。
- 原文链接: rareskills.io/post/rank-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!