Circom 介绍
Circom
是基于Rust开发的编译器,主要编译circom
语言开发的电路,输出约束系统的表示,由snarkjs
生成证明。
Rank-1 constraint system
R1CS 约束系统具有如下形式:
证明者需要提供有效的witness
生成零知识证明。
示例
电路编译
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
-
--r1cs
: 生成multiplier2.r1cs
, 包含约束系统的描述; -
--wasm
: 生成Wasm
代码,用于生成witness
; -
--sym
: 生成·multiplier2.sys
符号文件用于调试; -
--c
: 生成C代码用于生成witness
.
生成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
算术电路主要在有限域上的元素, 通过标识符答命名,用关键字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算术化运算定义在域 上, 的值为:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617.
使用 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
通常需要考虑以下几种表达式:
-
Constant values
: 仅允许常量值; -
Linear expresion
: 例如2*x + 3*y + 2
; -
Quadratic expression
: 二次表达式为A*B-C
的形式,例如(2*x + 3*y + 2) * (x+y) + 6*x + y – 2
; -
Non quadratic expression
: 其它的算术化表达式。
约束通过运算符===
添加,创建简化的恒等约束:
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 基本的数据类型有:
- Field element values: 模 的域元素值
- Arrays: 类型相同的有限的元素数组。
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 的signals
和components
必须有全局的范围,需要在最顶层定义。
如下代码会报错:
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
具有两个编译阶段:
- 构建: 生成约束条件;
-
代码生成: 生成计算
witness
的代码。
参考
https://github.com/iden3/circom
https://github.com/iden3/snarkjs
https://github.com/iden3/circomlib
https://github.com/iden3/wasmsnark