Circomの基本
https://scrapbox.io/files/621a3d40925e89001e07b06b.png
circomはzk-SNARKsのライブラリで、検証用のスマコンを自動生成してくれたり、snarkjsというフロントエンドのライブラリを用意してくれてたりとzkpアプリ作るには色々便利
非対話性をもったzkpにするため検証用のスマコンは誰でも検証可能
本格的に書いていきたいけど、独特な文法なのでコチラをもとにまとめる コンパイルの流れ
circomは2つのコンパイルフェーズを持つ。
1. constructionフェーズ:constraints(制約)が生成される
2. code generation フェーズ:witnessを計算するためのコードを生成する
Templates
汎用的な回路をcircomではTemplatesと呼ぶ
このインスタンスは新しいcircuitオブジェクトとして生成される。
他の回路のコンポーネントとして使い、より大きな(複雑な)回路を書くことも可能
この辺はオブジェクト指向の考え方と同じ
Templatesにローカル関数やTemplatesの定義を含めることはできない
下のようにsignal input(入力信号)が定義されているTemplatesの中でsignal inputに値を代入すると、エラーになる
code:ダメな代入
pragma circom 2.0.0;
template wrong (N) {
signal input a;
signal output b;
a <== N;
}
component main = wrong(1);
インスタンス化
「ダメな代入」の最終行に示すように、Templatesのインスタンス化では、キーワードcomponentと必要なパラメータを指定する
code:Templatesのインスタンス化
component c = tempid(v1,...,vn);
Templatesのインスタンスなのにcomponentと呼ぶのは違和感あるけど、単体の回路の利用を想定していないとか?
また、パラメータの値は、コンパイル時に既知の定数(constants)じゃなきゃだめなので、下のコードはエラーになる
code:パラメータが不正
pragma circom 2.0.0;
template Internal() {
signal output out;
}
template Main() {
signal output out;
component c = Internal ();
c.out ==> out; // c.in1 is not assigned yet c.in1 <== in1; // this line should be placed before calling c.out }
component main = Main();
outはreturn的な感じか(全ての回路は一つの出力をもつ)
回路==template/=制約(constraint)=式
signalがどのconstraintにも使われていない場合
警告メッセージが表示され、もしそれがsignal input xであれば、コンパイラはx * 0 === 0という形のconstraintを追加することを提案する
以下のコードでは「in」が使用されていないので、コンパイラからin*0===0を提案される
code:inが使われていない
pragma circom 2.0.0
template A(N){
signal input in;
signal intermediate;
signal output out;
intermediate <== 1;
out <== intermediate;
}
componet main {public in} = A(1); Components
Componentsは演算回路を定義するもので、N個の入力信号(input signals)を受け、M個の出力信号(output signals)とK個の中間信号(intermediate signals)を生成する
さらに、制約(constraints)のセットを生成することができる。
先述のようにComponentsのインスタンス化は、その入力信号がすべて具体的な値に代入されるまで起動されない。
したがって、Components作成命令は、Componentsオブジェクトの実行を意味するのではなく、すべての入力が設定されたときに完了するインスタンス化プロセスの作成を意味する。
Componentsの出力信号は、すべての入力が設定されたときにのみ使用することができ、そうでない場合はコンパイラエラーが生成される
Componentsはimmutable(不変)
Componentsは最初に宣言し、後で初期化することができる
複数の初期化命令がある場合、それらはすべて同じtemplatesのインスタンス化である必要がある
例えば以下のコードのa = A(0);をa = C(0)に置き換えると、コンパイルに失敗エラーメッセージが表示される
code:a
template A(N) {
signal input in;
signal output out;
out <== in;
}
template C(N) {
signal output out;
out <== N;
}
template B(M){
signal output out;
component a; //宣言
if(N > 0){ //場合わけして初期化する
a = A(N);
}
else{
a=A(0);
}
}
component main = B(1);
componentsの配列は、先に述べたサイズに関する制限にしたがって定義することができる。
componentsの配列の定義では、初期化は許されず、配列の位置にアクセスして、componentsごとにインスタンス化する
次の例でわかるように,配列内のすべてのcomponentsは,同じtemplatesのインスタンスでならない
code:a
template MultiAND(n){
signal output out;
component and;
var i;
if (n==1){
}else if (n==2){
and = AND();
out <== and.out;
else{
and = AND();
var n1= n\2;
var n2 = n-n\2;
for (i=0; i<n1; i++)ands0.ini <== ini; for (i=0; i<n2; i++)ands1.ini <== inn1+i; out <== and.out;
}
}
componentsが独立している(入力が互いの出力に依存しない)場合,次の行に示すように,parallelタグを使ってこれらの部分の計算を並列に行うことができる.
template parallel NameTemplate(...){...}.
このタグを使用すると,生成されるC++ファイルには,witnessを計算するための並列化されたコードが含まれる
並列化は大きな回路を扱うときに特に重要になる
main Components
実行を開始するためには、初期Components「main」を与えなければならない。
したがって、main Componentsは、何らかのTemplatesでインスタンス化する必要がある
これにより回路のグローバルな入力と出力信号を定義できる
他のComponentsと比較して、パブリック入力信号のリストという特別な属性を持っている
main Componentsの作成のシンタックスは以下の通り
code:main_componet
pragma circom 2.0.0;
template A(){
signal input in1;
signal input in2;
signal output out;
out <== in1 * in2;
}
component main {public in1}= A(); Function
circomではjsライクな関数を書くことができ、componentsで使う計算処理を記述したりできる。
こういう処理を回路に組み込む場合、制約が増えてしまうので実際にEthereumでzkpするとめちゃんこgasがかかる
なので、componentsで書かないで処理はfnctionを使いましょうってことかな
例えばこんな感じの関数が書ける
code:function_example
/*
This function calculates the number of extra bits
in the output to do the full sum.
*/
function nbits(a) {
var n = 1;
var r = 0;
while (n-1<a) {
r++;
n *= 2;
}
return r;
}
関数はsignalの宣言や制約の生成はできない
その場合はtemplateを使う
Signals
そもそもzk-SNARKsでは設計した回路を以下のように算術演算に置き換えている
下図の回路の入力信号が(signal)
https://scrapbox.io/files/621b029bee930d001dfb7ee0.png
Circomを用いて構築された演算回路は、Z/pZのフィールド要素を含むシグナルを演算対象とする。
signalは識別子で名前を付けるか、配列に格納しキーワードsignalで宣言する
signalはinput(入力信号)またはoutput(出力信号)として定義することができ、それ以外はIntermediate(中間信号)とみなされる
signalは常にプライベート。プログラマは、main Componentを定義するときに、パブリックな入力信号のリストを提供することによってのみ、パブリックとプライベートな信号を区別することができる
code:a
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(); この例では、main Componentの入力信号 in1 と in2 をパブリック信号として宣言している。
main Componentの出力信号=パブリック
main Componentの入力信号=プライベート
上記のようにキーワードpublicを使用するとパブリックになる
残りの信号はすべてプライベートで、パブリックにすることはできない
したがって、プログラマーから見ると、回路の外からはパブリックな入出力信号しか見えないので、Intermediateにはアクセスできない。
以下のコードでは、outAが出力信号として宣言されていないため、アクセスできず、コンパイルエラーになる
code:a
pragma circom 2.0.0
template A(){
signal input in;
signal outA;
outA <== in;
}
template B(){
signal output out;
component comp =A();
out <== comp.outA;
}
component main = B();
シグナルもcomponent同様にimmutableであり、一度値が割り当てられると、この値はそれ以上変更することができない。
したがって、シグナルが2回割り当てられると、コンパイルエラーが発生する
次の例では、シグナルoutが2回代入され、コンパイルエラーになる
code:signal_immutable
pragma circom 2.0.0
template A(){
signal input in;
signal output outA;
outA <== in;
}
template B({
signal outpuy out;
out <== 0;
component comp =A();
comp.in <== 0;
out <== comp.outA;
}
componet main = B();
コンパイル時、信号の内容はたとえ定数(constant)がすでに割り当てられていたとしても、常に未知(unknowns)とみなされる その理由は、シグナルが常に定数値を持つかどうかを検出するコンパイラの能力に 依存することなく、どの構文が許され、どれが許されないかを正確に定義するため
下の例では、signal outAの値がsignal inの値に依存しているため、コンパイルエラーが発生する
code:dependes
pragma circom 2.0.0
template A(){
signal input in;
signal output outA;
var i = 0;var out;
while (i<in){
out++;i++;
}
outA <== out;
}
template B(){
componet a = A();
a.in <== 3;
}
componet main = B();
signalは、左側にsignalがある場合は <-- または <== 、右側にシグナルがある場合は --> または ==> という演算を使ってのみ代入することが可能
安全な選択肢は<==と===>で、これらは値を代入すると同時に制約も生成する
<--と-->の使用は、一般的に危険であり、次の例のように、割り当てられた式が制約に含まれない場合にのみ使用されるべき
Unknowns
コンパイラの動作を理解するために、コンパイル時に何がUnknownsとみなされるかを知るのは大事
すでに述べたように、signalの内容は常にUnknownsとみなされ、constraint(定数)やTemplateのパラメータだけが既知とみなされる。
値が未知数に依存するvarはUnknowns
以下の例では当たり前だが、n1,n2,in2はunknownsになる
code:unknown
template A(n1, n2){
signal input in;
singal input in2;
var x;
while(n1>0){
x += in;
}
}
従って、while(n1>0)より、var xの値もUnknownsなinの値に依存するため、Unknownsとみなされる。
同様に、Unknownsに依存する式もUnknownsとみなされる。
さらに、Unknownsの位置にUnknownsの式で配列を変更した場合、配列のすべての位置がUnknownsとなる
例えば以下のような形
code:unknown_array
pragma circom 2.0.0
template A(n1,n2){
signal input in;
signal output out;
}
componet main = A(1,2);
Unknownsのパラメータを持つ関数呼び出しの結果もUnknownsになる
code:unknown_params
pragma circom 2.0.0;
function F(n){
return n*n;
}
template A(n1, n2){
signal input in;
signal output out;
var end = F(in);
var j = 0;
for(var i = 0; i < end; i++){
j += 2;
}
out <== j;
}
component main = A(1,2);
このコードでVar endは未知のパラメータinで呼び出された関数Fの結果であるため,Unknownsとみなされる.