Circom 不允许在循环中直接实例化组件。例如,编译以下代码会导致下方的错误。
include "./node_modules/circomlib/circuits/comparators.circom";
template IsSorted(n) {
signal input in[n];
for (var i = 0; i < n; i++) {
component lt = LessEqThan(252); // error here
lt.in[0] <== in[0];
lt.in[1] <== in[1];
lt.out === 1;
}
}
component main = IsSorted(8);
Signal or component declaration inside While scope. Signal and component can only be defined in the initial scope or in If scopes with known condition
解决办法是声明一个组件数组,但不指定组件类型:
pragma circom 2.1.8;
include "./node_modules/circomlib/circuits/comparators.circom";
template IsSorted(n) {
signal input in[n];
// declare array of components
// but do not specify the component type
component lessThan[n];
for (var i = 0; i < n - 1; i++) {
lessThan[i] = LessEqThan(252); // specify type in the loop
lessThan[i].in[0] <== in[i];
lessThan[i].in[1] <== in[i+1];
lessThan[i].out === 1;
}
}
component main = IsSorted(8);
当以这种方式声明组件时,就不可能像下面展示的那样对信号进行“单行赋值”(one-line assignment):
pragma circom 2.1.8;
include "./node_modules/circomlib/circuits/comparators.circom";
template IsSorted() {
signal input in[4];
signal leq1;
signal leq2;
signal leq3;
// one line assignment to the signal
leq1 <== LessEqThan(252)([in[0], in[1]]);
leq2 <== LessEqThan(252)([in[1], in[2]]);
leq3 <== LessEqThan(252)([in[2], in[3]]);
leq1 === 1;
leq2 === 1;
leq3 === 1;
}
component main = IsSorted();
在循环外部,可以在单行上设置信号。然而在循环内部,我们必须分多个步骤写出赋值操作,就像我们在 lessThan[i] = LessEqThan(252); // specify type in the loop 中所做的那样。
示例 1:数组的最大值
为了说明在循环中声明组件的实用示例,我们将展示如何证明 k 是数组的最大值。为此,我们需要约束 k 大于或等于其他每个元素,并且它至少等于其中一个元素。要想理解为什么相等性检查是必要的,可以考虑 18 大于或等于 [7, 8, 15] 中的所有元素,但它并不是该数组的最大值。
下面的 Circom 代码在不生成约束的情况下计算数组的最大值。然后,它运行 n 个 GreaterEqualThan 组件来约束所提出的 max 值确实是最大值,并且还使用一个 IsEqual 组件数组来检查至少有一个元素等于 k。
include "./node_modules/circomlib/circuits/comparators.circom";
template Max(n) {
signal input in[n];
signal output out;
// no constraints here, just a computation
// to find the max
var max = 0;
for (var i = 0; i < n; i++) {
max = in[i] > max ? in[i] : max;
}
out <-- max;
// for each element in the array, assert that
// max ≥ that element
component GTE[n];
component EQ[n];
var acc;
for (var i = 0; i < n; i++) {
GTE[i] = GreaterEqThan(252);
GTE[i].in[0] <== out;
GTE[i].in[1] <== in[i];
GTE[i].out === 1;
// this is used in the
// next code block to ensure
// that out equals at
// least one of the inputs
EQ[i] = IsEqual();
EQ[i].in[0] <== out;
EQ[i].in[1] <== in[i];
// acc is greater than zero
// (acc != 0) if EQ[i].out
// equals 1 at least one time
acc += EQ[i].out;
}
// assert that out is
// equal to at least one of the
// inputs. if acc = 0 then
// none of the inputs equals
// out
signal allZero;
allZero <== IsEqual()([0, acc]);
allZero === 0;
}
component main = Max(8);
练习: 创建一个执行与上述相同操作的电路,但用于求最小值(min)。
示例 2:数组已排序
我们可以通过检查每个元素是否小于或等于后续元素来断言数组已排序。与前一个需要 n 个组件的示例不同,我们需要 n - 1 个组件,因为我们是相互比较相邻的值。由于我们有 n 个元素,所以将进行 n - 1 次比较。
这里有一个约束输入数组 in[n] 为已排序状态的模板(template)。请注意,如果一个数组只有一个元素,那么根据定义它就是已排序的,而下面的电路也与该场景兼容:
pragma circom 2.1.6;
include "circomlib/comparators.circom";
template IsSorted(n) {
signal input in[n];
component lt[n - 1];
// loop goes up to n - 1, not n
for (var i = 0; i < n - 1; i++) {
lt[i] = LessThan(252);
lt[i].in[0] <== in[i];
lt[i].in[1] <== in[i+1];
lt[i].out === 1;
}
}
component main = IsSorted(3);
示例 3:所有元素都是唯一的
要检查列表中的所有元素是否都是唯一的,最直接的方法是使用哈希映射(hashmap)——但在算术电路中无法使用哈希映射。第二种最高效的方法是对列表进行排序,但是在电路内进行排序非常棘手,因此我们目前先避免这样做。这就给我们留下了将每个元素与其他每个元素进行比较的暴力解决方案。这需要一个嵌套的 for 循环。
我们正在进行的计算可以说明如下:
通常,将有
次不等式检查,因此我们将需要这么多的组件。
我们在下面展示如何实现这一点:
pragma circom 2.1.8;
include "./node_modules/circomlib/comparators.circom";
template ForceNotEqual() {
signal input in[2];
component iseq = IsEqual();
iseq.in[0] <== in[0];
iseq.in[1] <== in[1];
iseq.out === 0;
}
template AllUnique (n) {
signal input in[n];
// the nested loop below will run
// n * (n - 1) / 2 times
component Fneq[n * (n-1)/2];
// loop from 0 to n - 1
var index = 0;
for (var i = 0; i < n - 1; i++) {
// loop from i + 1 to n
for (var j = i + 1; j < n; j++) {
Fneq[index] = ForceNotEqual();
Fneq[index].in[0] <== in[i];
Fneq[index].in[1] <== in[j];
index++;
}
}
}
component main = AllUnique(5);
总结
若要在循环内使用 Circom 组件,我们需要在循环外部声明一个组件数组,但不指定其类型。
然后在循环内部,我们再声明这些组件,并对组件的输入和输出施加约束。