本章涵盖了基本语法,你将在大多数 Circom 程序中看到它们。借助 Circom,我们能够使用代码来定义 Rank 1 Constraint System (R1CS),而无需显式地定义每一个约束。我们将在本章中探索这些工具。
模板参数
之前,我们查看过一个验证所提供的输入是否确实为二进制的电路(IsBinary)。该电路被硬编码为仅接受 2 个输入。
template IsBinary() {
signal input in[2];
in[0] * (in[0] - 1) === 0;
in[1] * (in[1] - 1) === 0;
}
component main = IsBinary();
虽然上述代码适用于两个输入,但如果要修改它以支持大量 n 个输入,则需要手动添加约束,这会导致糟糕的开发者体验。
因此,Circom 允许我们使用以下模式来约束任意数量的信号,从而自动生成约束:
template IsBinary(n) {
// array of n inputs
signal input in[n];
// n loops: n constraints
for (var i = 0; i < n; i++) {
in[i] * (in[i] - 1) === 0;
}
}
// instantiated w/ 4 inputs & 4 constraints
component main = IsBinary(4);
请注意,模板声明已经更改,在括号中包含了 n。
- 这里的
n被称为模板参数 n在电路内部用于指定数组in的大小- 在实例化模板时,我们必须指定
n的值
Circom 中的电路和约束必须具有固定且已知的结构
虽然可以通过编程方式生成约束,但约束的存在与配置不能有条件地依赖于信号。
尽管模板可以使用参数,但电路必须是静态且明确定义的。不支持“动态长度”的电路或约束——从一开始,一切都必须是固定且定义良好的。
想象一下,如果一个 R1CS 约束系统的结构可以根据输入信号的值发生改变。由于约束的数量没有固定下来,证明者和验证者都将无法运行。
n 的值必须在编译时设置。
For 循环和变量:for,var
我们现在解释一下上面介绍的 for 循环。
template IsBinary(n) {
// array of n inputs
signal input in[n];
// n loops: n constraints
for (var i = 0; i < n; i++) {
in[i] * (in[i] - 1) === 0;
}
}
// instantiated with 4 inputs & 4 constraints
component main = IsBinary(4);
- 输入和循环迭代次数均由
n定义 - 为每个输入定义了一个约束,目的是验证该输入是
0还是1
我们在电路中引入了两个新的关键字:for 和 var
for的用法与你所习惯的一样。var关键字声明了一个变量;在这个例子中,如循环定义所示,变量是i。- 等号
=将右侧的值赋给左侧的变量。
在这里,变量 i 用于在为输入数组中的不同信号创建约束时,以编程方式引用它们。能够以编程方式生成约束非常有用,因为当涉及数百或数千个约束时,手动执行此操作将极易出错。
变量
变量保存非信号数据且是可变的。以下是在循环外部声明变量的示例:
template VariableExample(n) {
var acc = 2;
signal s;
}
- 默认情况下,变量不属于 R1CS 约束系统的一部分。
- 我们很快就会看到,变量可以在 R1CS 中用作加法或乘法常数。
- 变量用于在 R1CS 之外计算数值,以帮助定义 R1CS。
- 在使用变量时,Circom 的行为就像一种普通的编程语言。
- 数学运算是在模
p的意义下进行的。完整的运算符列表可以在 Circom 官方文档中找到。如果习惯了类 C 语言(例如++、**、<=等),你会对这些运算符感到熟悉。不过请记住,/表示乘以乘法逆元,而\表示整数除法。 - 但是,对于信号唯一有效的运算符是
+、*、===、<--和<==。我们将在后续文章中讨论<--和<==。
If 语句
Circom 允许我们使用 if 语句有条件地创建约束——但这些条件必须是确定性的,并且在编译时是已知的。下面展示了一个示例:
示例:强制偶数索引处相等
假设我们有两个数组。我们可以使用以下模板生成约束,以强制偶数索引处的项相等(无需检查奇数索引处的项)
template EqualOnEven(n) {
signal input in1[n];
signal input in2[n];
for (var i = 0; i < n; i++) {
if (i % 2 == 0) {
in1[i] === in2[i];
}
// otherwise no constraint is generated
}
}
请注意,变量 i 决定了会生成哪些约束。
信号不能用作 if 语句或 for 循环中的分支条件
以下代码是不允许的,因为信号 a 被用作了 if 语句的条件:
template IfStatementViolation() {
signal input a;
signal input b;
if (a == 2) {
b === 3;
}
else {
b === 4;
}
}
在 Rank 1 Constraint System 中,信号之间只能有加法和乘法。Circom 仅仅是在 Rank 1 Constraint System 之上的一个浅层包装。因此,它无法将 if 语句“转换”为加法和乘法。
在 Circom 中,基于信号执行条件操作(if 语句)仍然是可能的——这是后续章节的主题。但就目前而言,请认为没有从 if 语句到单一乘法的“直接”转换。
将变量作为约束的一部分
变量可以用作约束的一部分。在下面的示例中,我们强制输入数组 in[n] 必须是一个斐波那契(Fibonacci)数列。请注意,变量数组的语法是 var varName[size]:
template IsFib(n) {
assert(n > 1);
signal input in[n];
// generate the Fibonacci sequence
var correctFibo[n];
correctFibo[0] = 0;
correctFibo[1] = 1;
for (var i = 2; i < n; i++) {
correctFibo[i] = correctFibo[i - 1] + correctFibo[i - 2];
}
// assert that the input is a Fibonacci sequence
for (var i = 0; i < n; i++) {
in[i] === correctFibo[i];
}
}
需要注意的是:
assert(n > 1)不会生成任何约束。如果模板参数的条件未得到满足,它可以防止该模板被实例化。- 我们能够通过
signal === var来强制要求信号具有某个特定值。这与signal === 5或其他常量的作用是相同的。
Circom 没有 Constant 关键字
作为替代,我们可以使用变量为一个魔术数字分配名称,以提高可读性。例如:
template Equality() {
signal input in[2];
var left = 0;
var right = 1;
// require the inputs
// to be equal
in[left] === in[right];
}
变量可以与其他信号相加或相乘
在 Circom 中,变量可以与信号相加或相乘,就像常量一样。在下面的示例中,我们要求 in2[] 是 in1[] 乘以其对应的索引。
例如,如果 in1[] = [3,5,6],那么必然有 in2[] = [0,5,12],因为 [3,5,6] 会与 [0,1,2] 进行逐元素相乘。
template IsIndexMultiplied(n) {
signal input in1[n];
signal input in2[n];
for (var i = 0; i < n; i++) {
in1[i] * i === in2[i];
}
}
component main = IsIndexMultiplied(3);
/* INPUT = {"in1": [0,1,2], "in2": [0,1,4]} */
// accept
// in1[] = [0,1,2]
// in2[] = [0,1,4]
// reject
// in1[] = [0,1,2]
// in2[] = [0,0,2]
你可以在这里测试代码。
核心要点
- 在底层,如果变量与信号相加或相乘,该变量在 R1CS 中会被编译为一个常量。
- 对于信号,不允许执行加法、减法或乘法以外的操作,因为 R1CS 只能包含加法或与常数的乘法。在底层的减法,其实只是与加法逆元相加。
- 如果一个信号除以一个常数(或保存常数的变量),它会将该信号乘以该常数的乘法逆元,除非常数为 0(在这种情况下代码将无法编译)。
练习题
尝试完成以下来自 ZK Puzzles 的练习题。运行测试以检查你的答案。