Circom language tutorial with circomlib walkthrough 学习

本文主要学习文章:Circom language tutorial with circomlib walkthrough。基本把大致的内容都学习了一遍并记录在本文。

上述文章基本地介绍了Circom语言和如何使用它,以及介绍常见的陷阱。还解释了circomlib库中的Comparators.circom的部分template。

安装circom和snarkjs

安装install circom: 官网 https://docs.circom.io/getting-started/installation/

安装snarkjs : npm install -g snarkjs@latest

我使用的是circom 2.1.6版本以及snarkjs@0.7.1。

简单的乘法电路a*b

构建一个简单的乘法电路

使用命令 circom multiply.circom --r1cs --sym 会打印如下信息

template instances: 1  //模版实例化个数
non-linear constraints: 1  //非线性约束
linear constraints: 0  //线性约束,目前所编译到的约束都是非线性约束,类似A*B=C
public inputs: 0       //公开输入个数
public outputs: 1   //公开输出个数
private inputs: 2  //私有输入个数
private outputs: 0 //私有输出个数
wires: 4               //导线
labels: 4             //标签 ,

// 导线和标签数量不知道跟什么有关,猜测是跟后面的witness.json中的元素个数是相等。

编译后会生成multiply.r1cs multiply.sym文件。但是multiply.r1cs文件打不开,需要使用命令snarkjs r1cs print multiply.r1cs 来打印r1cs文件,最后的输出结果是

上述一长串的数字是相当于“-1”。因为在circom中的所有运算都在Zp中,其中p=21888242871839275222246405745257275088548364400416034343698204186575808495617

因此上述输出结果等价于 main.a * main.b = main.out

multiply.sym文件保留上述的变量名。multiply.sym文件内容为

1,1,0,main.out
2,2,0,main.a
3,3,0,main.b


非二次约束错误

有效的R1CS中的每个约束有且只有一个乘法。一个约束指的是R1CS中的每一行的等式。 如果一个约束有两个乘法就会报错。例如下图,试图使用两个乘法来构成一个约束就会报错。

如何实现上述两个乘法?加入一个中间信号,将两个乘法变成两个约束,而不是一个约束。我们重新生成对应circom文件的 r1cs文件并打印r1cs就会有如下输出:

等价于

a * b  = s1
s1 * c = out

计算 witness

文件存放位置的关系

  1. 根据上面的输入,我们首先要制作一个input.json文件,文件内容: {"a": "2","b": "3","c": "5"}。在 input.json文件中的name : value中的name,对应电路中的 signal input name。
  2.  我们还需要generate_witness.jsmultiply.wasm文件(由命令circom multiply.circom --wasm 生成一个multiply_js的目录,目录中有multiply.wasm, generate_witness.jswitness_calculator.js 共三个文件。注意生成的js文件是CommonJS文件)。
  3. 之后, 在multiply_js目录下执行命令: node generate_witness.js multiply.wasm input.json witness.wtns,会在multiply_js目录下生成witness.wtns文件。
  4. 我们可以使用命令 snarkjs wtns export json witness.wtns 来导出 witness 形成 witness.json文件。我们查看witness.json文件。里面的内容是["1","30","2","3","5","6"]。这就计算出来的witness,其中1是固定的,30是输出,2,3,5是输入,6是中间信号。因此wintness.json生成的形式如[1,out,a,b,c,s1]。

公开输入

如果我们想要让某些input是公开的。使用关键字public来让输入a,c是公开的。

数组

创建一个component用来计算输入的n次方。代码如下。注意下面例子中,template参数化为n. 但是r1cs系统必须是固定且不可变的。所以在最后一行中输入一个整数。

pragma circom 2.1.6;
template Powers(n) {
    signal input a;
    signal output powers[n];
    powers[0] <== a;
    for (var i = 1; i < n; i++) {
        powers[i] <==  powers[i - 1] * a;
    }
}
component main = Powers(6);

Signal vs variable 

信号是immutable,使用<--,<==,===操作符应用在Signal。使用=, ==, >=, <= !=, ++, -- 操作符应用在variable上面。

如果操作相反,则会出现右边错误。

=== vs <==

上述两个电路是相等的。 第一电路中先计算a*b然后赋值给c, 之后要求a*b是等于c(相等于添加了一个约束)。第二个电路实际上就是做先赋值后添加约束这种事情。

接下来,来看一下 === 在circuit中的用法。比如我们希望prover 提供部分的输入(i.e. 公共输入)。这时候就可以使用===符号。 Circom中可以不要求一个输出信号,即使全部都是私有输入信号也可以。但是这样的template无法向别人证明你知道某个东西。虽然语法满足,但是并没有任何实际意义。因此在写template时,还是要求至少有一个公共输入或者输出。

我们打印对应的r1cs文件,得到的结果是

[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.c ] = 0 等价于 a *b =c。

template调用和组合

template是可重用和可组合的,如下例所示. 这里,Square 是 SumOfSquares使用的template。 请注意输入 a 和 b 如何“连接”到组件 Square()。

pragma circom 2.1.6;
template Square() {
    signal input in;
    signal output out;
    out <== in * in;
}

template SumOfSquares() {
    signal input a;
    signal input b;
    signal output out;
    component sq1 = Square();
    component sq2 = Square();
    // wiring the components together
    sq1.in <== a;  //注意Square()中的输入名字是什么,这里对应输入名称也是什么。
    sq2.in <== b;  //例如Square()中的输入名为a,则这里改为sq1.a,sq2.a 。
    out <== sq1.out + sq2.out;
}

component main = SumOfSquares();

您可以将 <== 运算符视为通过引用组件的输入或输出将组件“连接”在一起。

谨慎使用 <--

当遇到二次约束问题时,很容易使用 <-- 运算符来使编译器保持silence。 下面的代码可以编译,并且似乎完成了与我们之前的 power 示例相同的任务。

之前, 现在,。但是,当我们编译后面一个电路时,结果是只有一个约束,打印r1cs结果是[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.powers[0] ] * [ main.powers[0] ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.powers[1] ] = 0。

You cannot trust proofs that come out of a circuit like this!

约束不足是零知识应用程序中安全错误的主要来源,因此请仔细检查约束是否确实按照您期望的方式在 R1CS 中生成! 这就是为什么我们强调在学习 Circcom 语言之前,需要事先了解 Rank 1 约束系统,否则有一整类的 bug 你将很难发现!


Circomlib库中的comparators.circom文件中的template

当使用电路而不是自然代码描述算法时,要采用的思维方式是“先计算,然后约束”。 有些操作仅用纯粹的约束来建模是极其困难的。

Iden3 维护着一个名为 circomlib 的有用 circom 模板存储库。 我们已经有了足够的必要 Circcom 知识来开始研究这些模板,现在是介绍一个简单但有用的模板来演示 <-- 用法的好时机。

IsZero

如果输入信号为零,则 IsZero template 返回 1;如果输入信号非零,则返回 0。

如果您花一些时间思考如何仅使用乘法来测试数字是否为零,您会发现自己陷入困境; 这将是一个极其困难的问题。

template IsZero() {
  signal input in;
  signal output out;
  signal inv;
  inv <-- in!=0 ? 1/in : 0;
  out <== -in*inv +1;
  in*out === 0; //为什么最后一行要强制相等?这个约束确保了当in=0时,out=1;当in!=0时,out=0的情况
}

在上述例子中,inv是一个辅助输入信号。我们计算inv 要么是0,要么是in的逆元。最后将计算结果赋值给inv。然后作为约束的一部分强制 inv 正确。但是为什么要让 inv 成为信号而不是变量呢? 如果我们将 inv 设置为 var 并将 <-- 替换为 =,我们将得到非二次约束错误,因为我们将信号 in 与变量相乘,该变量是从一些非平凡的计算中得出的 - 这肯定涉及一次以上的乘法。

in: {0, non-zero}

inv: {0, in^{-1}}

out: {1,0}

只有当 in 为 0 时将 out 设置为 1,而当 in 非零时将 out 设置为 0,才能满足约束。 这说明了“先计算,后约束”的模式。

在上面的代码中,out 被限制为等于 -in*inv + 1。但是,最后一行并未将零赋值给 in*out。 而是强制 in*out 等于 0。


使用 IsEqual template 而不使用 ===

当比较相等的组件时,您可以使用 IsEqual 电路模版,该模版将输入in[1],in[0]相减 之后将计算结果输入到 IsZero component 中。 如果输入相等,则IsZero输出结果为0,如果输入不等,则IsZero输出结果输出为1。

template IsEqual() {
    signal input in[2];
    signal output out;
    component isz = IsZero();
    in[1] - in[0] ==> isz.in;
    isz.out ==> out;
}

### no constraints ###

template FactorOfFiveFootgun() {
    signal input in;
    signal output out;
    out <== in * 5;
}
component main = FactorOfFiveFootgun();

上述template功能主要实现是Prover知道 x,使得 5*x == out,其中 out 是public。 但是如果我们编译template,我们会发现实际上没有创建任何约束!### no constraints ### 。 因为只有形如A*B+C=0 的等式才能产生约束,其中A,B,C是任意信号的线性组合。具体可以查看官方给的约束产生的规则:Constraint Generation - Circom 2 Documentation

解决方案是连接 IsEqual 模板以强制相等。

计算一组输入信号的的平均值

假设我们想要计算一组信号的平均值。 在一般的编程语言中执行此项操作是容易,但是仅使用二次约束来执行是较难的。

解决方案是使用常规编程计算平均值,然后限制输出正确。n 个信号的平均值是它们的总和除以信号数量。 有限域中的除法与乘以倒数相同,因此我们需要将信号相加,然后乘以数组长度的倒数.

以下看起来正确,然而却没有实现预期的效果。

pragma circom 2.1.6;
include "node_modules/circomlib/circuits/comparators.circom";
template AverageWrong(n) {
    signal input in[n];
    signal denominator_inv;
    signal output out;
    // n个输入和
    var sum;
    for (var i = 0; i < n; i++) {
        sum += in[i];
    }
    denominator_inv <-- 1 / n;     // 求n的倒数
    1 === denominator_inv *n;
    out <== sum * denominator_inv;
}

component main  = AverageWrong(5);

上述编译后的结果是。也就是并没有产生任何约束,这是因为这是因为n,sum,denominator_inv都不是signal。

pragma circom 2.1.6;
include "node_modules/circomlib/circuits/comparators.circom";
template Average(n) {
    signal input in[n];
    signal denominator_inv;
    signal output out;
    var sum;
    for (var i = 0; i < n; i++) {
        sum += in[i];
    }
    denominator_inv <-- 1 / n;
    component eq = IsEqual();
    eq.in[0] <== 1;
    eq.in[1] <== denominator_inv * n;
    out <== sum * denominator_inv;
}
component main  = Average(5);

上述方法会正确的生成约束。


查询是一个人年龄否大于21岁的template

template IsOver21() {
    signal input age;
    signal output oldEnough;
    if (age >= 21) {
        oldEnough <== 1;
    } else {
        oldEnough <== 0;
    }
}
如果编译上述代码,则会发生错误。 这是编写 circom 代码时容易犯的另一个错误:信号不能作为 if 语句的输入或for循环的输入。

那么我们如何查询一个人是否已经超过21岁呢?

这实际上比看起来更难,因为比较有限域中的数字相当棘手。

在 Circcom 中比较数字的陷阱 如果 1 - 2 = 21888242871839275222246405745257275088548364400416034343698204186575808495616,那么这个巨大的数字实际上是大于 1 还是小于 1? 您无法比较有限域中的数字,因为说一个数字大于另一个数字是没有意义的。 然而,我们仍然希望能够比较数字!

在进行任何比较之前,第一个要求是它们必须小于字段大小(例如,8位数的大小),并且我们会将字段中的任何数字视为正整数,并小心防止下溢和溢出。 只要当我们强制数字保持在字段顺序内,那么我们对它们才能进行有意义的比较。 不可能将 > 运算符转换为一组二次约束。 但是,如果我们将字段元素转换为数字的二进制表示形式,则可以进行比较。 现在我们可以再引入两个 circomlib 库中的comparators.circom文件中的模版:Num2BitsLessThan,其动机是比较整数。

Num2Bits

我们如何在不使用普通 < 和 > 运算符且仅使用一种二进制转换的情况下比较两个 9,999 或更小的数字? 如果我们允许进行两次二进制转换,这很容易,但它会导致更大的电路。

这是一个棘手的问题,但 circomlib 是如何做到这一点的。 假设我们正在比较 x 和 y。 由于我们的输入可以采用 9,999 个最大值,因此我们将 10,000 加到 x 上,并从 x 和 y 的总和中减去 y.

z = (10,000 + x) - y

无论如何,即使 y 是 9,999 并且 x 是 0,z 也将是正数。由于我们在这里处理字段元素,因此我们不希望发生任何下溢,并且在这种情况下,不能发生下溢。这是关键部分。 如果 x ≥ y,则 z 将在 10,000 到 19,999 之间,如果 x < y,则 z 将在 [0-9,999] 范围内。要知道一个数字是否在[10,000-19,999]范围内,我们只需要查看第10,000位的数字,即1x,xxx。 如果第1000位的数字是1,那么我们知道 z 在 [10,000-19,999] 范围内;否则第1000位的数字是0,则z在[0,9999]之间。 

Circcom 只是以二进制而不是十进制表示形式做同样的事情。 以下 Circcomlib Num2Bits 模板显示了 circom 如何将信号转换为保存二进制表示形式的信号数组。

template Num2Bits(n) {
    //将in进行位比特分解
    signal input in;
    signal output out[n];
    var lc1=0; 
    // this serves as an accumulator to "recompute" in bit-by-bit
    var e2=1;
    for (var i = 0; i<n; i++) {
        out[i] <-- (in >> i) & 1;  
        //in>>i 右移符号,将in的二进制位右移i位。之后再跟1进行 AND运算。
        out[i] * (out[i] -1 ) === 0; // 强制out[i]等于1或者等于0
        lc1 += out[i] * e2; //累加如果out[i]等于1,再累加的结果赋值给lc1。
        e2 = e2+e2; // e2= 1,2,4,8,16,32,64,128,256,512,1024,2028.....
    }

    lc1 === in;
}

LessThan

现在我们已经掌握了 Num2Bits,理解 LessThan 模板应该很简单.

template LessThan(n) {
    assert(n <= 252);
    signal input in[2];
    signal output out;
    component n2b = Num2Bits(n+1);
    n2b.in <== in[0] + (1<<n) - in[1];
    out <== 1-n2b.out[n];
}
输出1 表示 in[0] < in[1]
输出0 表示 in[0] > in[1]

考虑n=8, in[0]=21,in[1]=32
1<<8 的结果是256
n2b会将输入值看做9个比特的数,然后分解输出其二进制的位数。
21+256-32= 256-11 = 245,最高位是0。因此最后的out是1,表示in[1]>in[0]

如果 in[0]=32,in[1]=21
则 32+256-21= 256+11 = 267,最高位是1。因此最后的out是0,表示in[1]<in[0]

最后查询年龄是否大于21岁的 template

pragma circom 2.1.6;
include "node_modules/circomlib/circuits/comparators.circom";
template Over21() {
    signal input age;
    signal input ageLimit;
    signal output oldEnough;
    // 8 bits is plenty to store age
    component gt = GreaterThan(8);
    gt.in[0] <== age;
    gt.in[1] <== 21;
    oldEnough <== gt.out;
}

component main = Over21();

ForceEqualIfEnabled template

template ForceEqualIfEnabled() {
    signal input enabled;
    signal input in[2];
    component isz = IsZero();
    in[1] - in[0] ==> isz.in;
    (1 - isz.out)*enabled === 0;
}

ForceEqualIfEnabled 模板的行为类似于断言语句,它包含一个约束,即如果启用标志非零,则输入必须相等。 与其根据相等性返回 true 或 false(此处没有输出信号返回 0 或 1),否则如果输入不相等,则会导致电路无法满足。

Boolean operators

如果将 a 和 b 约束为 0 和 1,则可以使用乘法实现相同的效果

template And() {
    signal input in[2];
    signal output c;
    
    // force inputs to be zero or one
    in[0] === in[0] * in[0];
    in[1] === in[1] * in[1];
    
    // c will be 1 iff in[0] and in[1] are 1
    c <== in[0] * in[1];
}

对于读者来说,思考如何完成其​​他布尔运算(not、or、nand、nor、xor 和 xnor)是一个有用的练习。 尽管每个布尔门都可以由与非门构建,但这将使电路变得比所需的更大,因此不要过度重复使用门。 解决方案位于 circomlib 的 gates.circom 文件中。 请注意,它们并不将输入限制为 0 或 1,这取决于电路设计人员的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值