Circom 介绍

2022-11-23  本文已影响0人  雪落无留痕

Circom 是基于Rust开发的编译器,主要编译circom 语言开发的电路,输出约束系统的表示,由snarkjs 生成证明。

Rank-1 constraint system

R1CS 约束系统具有如下形式:
(a_1*s_1+\cdots + a_n*s_n) * (b_1*s_1+\cdots + b_n*s_n) + (c_1*s_1+\cdots + c_n*s_n) = 0
证明者需要提供有效的witness (s_1, \cdots, s_n) 生成零知识证明。

示例

电路编译

pragma circom 2.0.0;

template Multiplier2() {
    signal input a;
    signal input b;
    signal output c;
    c <== a*b;
 }

 component main = Multiplier2();

然后对电路编译:

circom multiplier2.circom --r1cs --wasm --sym --c

生成witness

构建输入input.json 为:

{"a": 3, "b": 11}

然后调用Wasm 生成witness 为:

node generate_witness.js multiplier2.wasm input.json witness.wtns

生成证明

开启Powers of tau

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v

参与Powers of tau

snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v

开始阶段2

阶段2与电路相关,通过以下命令开始启动阶段2:

snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v

然后生成.zkey 文件包含证明和验证密钥,执行如下命令:

snarkjs groth16 setup multiplier2.r1cs pot12_final.ptau multiplier2_0000.zkey

参与阶段2:

snarkjs zkey contribute multiplier2_0000.zkey multiplier2_0001.zkey --name="1st Contributor Name" -v

导出验证密钥:

snarkjs zkey export verificationkey multiplier2_0001.zkey verification_key.json

生成的验证密钥verification_key.json为:

{
 "pi_a": [
  "12760910679062051146028245764180144304135176860171829439423433793987947801318",
  "15627351643566791700410887806223057411723190268680338266476116334981291160699",
  "1"
 ],
 "pi_b": [
  [
   "1765658782548091050330101314873972200746475466904382545933668714427728146407",
   "8831055067363820406189809670502192437243044552509000960737961795818836572464"
  ],
  [
   "17406360157052120611680078243109373801402264858772382543913010037295998951887",
   "13607675671111808291939915735439749555363789088081636489573695410874630259559"
  ],
  [
   "1",
   "0"
  ]
 ],
 "pi_c": [
  "2792640172422648679222543320314624195856069203876481047009502575189585240826",
  "5913917177125152097186179448098822785808387757499429789332191237284364705015",
  "1"
 ],
 "protocol": "groth16",
 "curve": "bn128"
}

生成证明

snarkjs groth16 prove multiplier2_0001.zkey witness.wtns proof.json public.json

其中public.json 为:

[
 "33"
]

proof.json 为:

{
 "pi_a": [
  "12760910679062051146028245764180144304135176860171829439423433793987947801318",
  "15627351643566791700410887806223057411723190268680338266476116334981291160699",
  "1"
 ],
 "pi_b": [
  [
   "1765658782548091050330101314873972200746475466904382545933668714427728146407",
   "8831055067363820406189809670502192437243044552509000960737961795818836572464"
  ],
  [
   "17406360157052120611680078243109373801402264858772382543913010037295998951887",
   "13607675671111808291939915735439749555363789088081636489573695410874630259559"
  ],
  [
   "1",
   "0"
  ]
 ],
 "pi_c": [
  "2792640172422648679222543320314624195856069203876481047009502575189585240826",
  "5913917177125152097186179448098822785808387757499429789332191237284364705015",
  "1"
 ],
 "protocol": "groth16",
 "curve": "bn128"
}

生成验证合约

可以生成Solidity 验证合约,如下所示:

snarkjs zkey export solidityverifier multiplier2_0001.zkey verifier.sol

生成合约调用参数:

snarkjs generatecall

Circom 语法

Signals

算术电路主要在有限域上Z/pZ的元素, 通过标识符答命名,用关键字signal 声明,如下所示, signal 可分为: 输入,输出,和中间信号。

signal input in;
signal output out[N];
signal inter;

signals 总是私有的,除非直接声明,如下所示:

pragma circom 2.0.0;

template Multiplier2(){
   //Declaration of signals
   signal input in1;
   signal input in2;
   signal output out;
   out <== in1 * in2;
}

component main {public [in1,in2]} = Multiplier2();

在 Circom2.0.4之后,对于中间和输出信号,可以在声明之后直接初始化,如下:

pragma circom 2.0.0;

template Multiplier2(){
   //Declaration of signals
   signal input in1;
   signal input in2;
   signal output out <== in1 * in2;
}

component main {public [in1,in2]} = Multiplier2();

所有的输出信号都是公开的,输入信号是私有的,除非直接用public 声明;其它的信号都是私有的。

从开发者的观点,只有公开输入和输出信号是可见的,中间的信号是无法访问,如下会报错:

pragma circom 2.0.0;

template A(){
   signal input in;
   signal outA; //We do not declare it as output.
   outA <== in;
}

template B(){
   //Declaration of signals
   signal output out;
   component comp = A();
   out <== comp.outA;
}

component main = B();

信号是不可变的,即无法多次赋值,如下 out 被赋值2次, 会产生编译错误:

pragma circom 2.0.0;

template A(){
   signal input in;
   signal output outA; 
   outA <== in;
}

template B(){
   //Declaration of signals
   signal output out;
   out <== 0;
   component comp = A();
   comp.in <== 0;
   out <== comp.outA;
}

component main = B();

变量

var x;
x = 234556;
var y = 0;
var z[3] = [1,2,3]

模块

模板

Circom 使用模板(templates)来生成通用电路。模板可以使用参数进行实例化,使用模块可以组合较大规模的电路,模板具有输入和输出信号:

template tempid ( param_1, ... , param_n ) {
 signal input a;
 signal output b;

 .....

}

不能对输入信号进行赋值,如下会报错:

pragma circom 2.0.0;

template wrong (N) {
 signal input a;
 signal output b;
 a <== N;
}

component main = wrong(1);

通过提供必要的参数,对模板实例化,如下所示:

pragma circom 2.0.0;

template wrong (N) {
 signal input a;
 signal output b;
 a <== N;
}

component main = wrong(1);

模板的参数必须在编译时赋予常量值,否则报错:

pragma circom 2.0.0;

template A(N1,N2){
   signal input in;
   signal output out; 
   out <== N1 * in * N2;
}


template wrong (N) {
 signal input a;
 signal output b;
 component c = A(a,N); 
}

component main {public [a]} = wrong(1);

若某个信号没有在任何约束中使用,会生成警告信息。

若模板中没有输出信号,也会产生警告信息。

组件

组件定义一个算术化电路,它接收 N 个输入信号,产生M 个输出信号,和 K 个中间信号,另外,它也会生成一些约束。

组件的输入和输出通过 . 进行访问,如下所示:

c.a <== y*z-1;
var x;
x = c.b;

组件实例化需要所有输入都赋值后,才会触发,因此必须所有的输入完成后,才能重新使用输出信号。如下会报错:

pragma circom 2.0.0;

template Internal() {
   signal input in[2];
   signal output out;
   out <== in[0]*in[1];
}

template Main() {
   signal input in[2];
   signal output out;
   component c = Internal ();
   c.in[0] <== in[0];
   c.out ==> out;  // c.in[1] is not assigned yet
   c.in[1] <== in[1];  // this line should be placed before calling c.out
}

component main = Main();

组件也是不可变的,并且对于不同的初始化路径,需要是同种类型。当组件相互独立,可以使用关键字parallet 进行并行计算,如下所示:

template parallel NameTemplate(...){...}

然后生成的 C++ 文件包含并行处理的代码,计算witness

component comp = parallel NameTemplate(...){...}

实例代码如下所示:

component rollupTx[nTx];
for (i = 0; i < nTx; i++) {
        rollupTx[i] = parallel RollupTx(nLevels, maxFeeTx);
}

2.0.6 版本后,引入定制模板,使用关键字custom , 如下所示:

pragma circom 2.0.6; // note that custom templates are only allowed since version 2.0.6
pragma custom_templates;

template custom Example() {
   // custom template's code
}

template UsingExample() {
   component example = Example(); // instantiation of the custom template
}

和标准模板的差别在于,定制模板不会生成r1cs 约束,主要用于PLONK 方案中。

Pragma

使用pragma 指令指明编译器版本,如下:

pragma circom xx.yy.zz;

对于定制模板,使用如下指令:

pragma custom_templates;
Functions

circom 中有函数定义,如下所示:

function funid ( param1, ... , paramn ) {

 .....

 return x;
}

函数可以递归定义,但是函数无法声明信号或生成约束。

Include

可以使用include 包含其它 circom 文件作为库,如下所示:

include "montgomery.circom";
include "mux3.circom";
include "babyjub.circom";
Main Component

为了执行circom, 需要定义一个 main 组件,需要定义全部的输入和输出信号:

component main {public [signal_list]} = tempid(v1,...,vn);

其中{public [signal_list]} 是可选的,模板中没有包含进列表的输入信号都是私有的,如有所示:

pragma circom 2.0.0;

template A(){
    signal input in1;
    signal input in2;
    signal output out;
    out <== in1 * in2;
}

component main {public [in1]}= A();

上例中有2个输入信号,in1 是公开的,in2 是私有信号,输出信号部是公开的。

有且仅有一个main component , 否则会报错。

语法

注释

可以写以下形式的注释:

//Using this, we can comment a line.

template example(){
    signal input in;   //This is an input signal.
    signal output out; //This is an output signal.
}

/*
All these lines will be 
ignored by the compiler.
*/
标识符
signal input _in; 
var o_u_t;
var o$o;
保留关键字

Circom 具有以下保留关键字:

signal input output public template component var function return if else for while do log assert include 
pragma circom
pragam custom_templates

基本运算符

Circom算术化运算定义在域 Z/pZ上, p 的值为:

p = 21888242871839275222246405745257275088548364400416034343698204186575808495617.

p 使用 GLOBAL_FIELD_P 定义。

条件表达式

条件表达式定义为:

var z = x>y? x : y;

布尔运算符

且: &&, 或: ||, 非: !.

关系运算符

关系运算有:<, > , <=, >=, ==, != 依赖于数学函数 val(x), 定义如下:

       val(z) = z-p  if p/2 +1 <= z < p

       val(z) = z,    otherwise.

算术化运算符

算术化运算符有:+, -, *, **, /(乘逆), \(商), %

算术化运算符可以和赋值结合: +=, -=, *=, **=, /=, \=, %=, ++, --.

按位运算符

位操作的运算符有:&, |, ~, ^, >>, <<

位操作运算符和赋值结合:&=, |=, ~=, ^=, >>=, <<=

约束生成

Circom 通常需要考虑以下几种表达式:

约束通过运算符=== 添加,创建简化的恒等约束:

a*(a-1) === 0;

<== 运算符能同时赋值和生成约束,例如:

out <== 1 - a*b;

等价于:

out === 1 – a*b;
out <-- 1 - a*b;

注: <-- 仅赋值,不添加约束。

只有二次表达式允许包含进约束中,例如下面的代码会报错;

template multi3() {
     signal input in;
     signal input in2;
     signal input in3;
     signal output out;
     out <== in*in2*in3;  // Not quadratic
}

控制流

条件表达式

if ( boolean_condition ) block_of_code else block_of_code

var x = 0;
var y = 1;
if (x >= 0) {
   x = y + 1;
   y += 1;
} else {
   y = x;
}
For 循环表达式

for ( initialization_code ; boolean_condition ; step_code ) block_of_code

var y = 0;
for(var i = 0; i < 100; i++){
    y++;
}
Loop 循环表达式

while ( boolean_condition ) block_of_code

var y = 0;
var i = 0;
while(i < 100){
    i++;
    y += y;
}

数据类型

circom 基本的数据类型有:

var x[3] = [2,8,4];
var z[n+1];  // where n is a parameter of a template
var dbl[16][2] = base;
var y[5] = someFunction(n);

Scoping

Circom 的signalscomponents 必须有全局的范围,需要在最顶层定义。

如下代码会报错:

pragma circom 2.0.0;

template Multiplier2 (N) {
   //Declaration of signals.
   signal input in;
   signal output out;

   //Statements.
   out <== in;
   signal input x;
   if(N > 0){
    signal output out2;
    out2 <== x;
   }
}

component main = Multiplier2(5);

关于可见性,可以访问组件的的信号,但是无法嵌套访问,即子组件的信号。

Code Quality

assert(bool_expression);

assert 检查是在执行的时候,若条件失败,则witness 生成会中断。

template Translate(n) {
assert(n<=254);
…..
}

当约束通过 === 引入时,会自动添加asset。

为了便于调试,引入 log, 具有非条件的表达式,如下所示:

log(135);
log(c.b);
log(x==y);

2.0.6 之后,允许输入表达式列表:

log("The expected result is ",135," but the value of a is",a);

Circom Insight

circom 具有两个编译阶段:

参考

https://docs.circom.io/

https://github.com/iden3/circom

https://github.com/iden3/snarkjs

https://github.com/iden3/circomlib

https://github.com/iden3/wasmsnark

https://github.com/iden3/rapidsnark.git

https://github.com/iden3/ffjavascript.git

上一篇下一篇

猜你喜欢

热点阅读