Circom, if-statements के उपयोग को लेकर बहुत सख्त है। निम्नलिखित नियमों का पालन किया जाना चाहिए:
- Signals का उपयोग if-statement के व्यवहार (behavior) को बदलने के लिए नहीं किया जा सकता है।
- if-statement के अंदर किसी signal को वैल्यू (value) असाइन नहीं की जा सकती है।
नीचे दिया गया उदाहरण सर्किट इन दोनों उल्लंघनों को दर्शाता है:
template Foo() {
signal input in;
signal input cond;
signal output out;
// if-statements cannot depend on
// values not known at compile time
if (in == 3) {
// assigning a value inside an if-statement
// whose value is unknown at compile time
// is not allowed
out <== 4;
}
}
If-statements तभी स्वीकार्य हैं जब वे किसी भी signals से प्रभावित न हों, और किसी भी signals को प्रभावित न करें।
प्रभावी रूप से, वे अंतर्निहित Rank 1 Constraint system (R1CS) का हिस्सा नहीं हैं।
उदाहरण के लिए, यदि हम किसी सूची में अधिकतम मान (maximum value) की गणना करना चाहते हैं (बिना constraints जनरेट किए), तो हम निम्नलिखित सामान्य समाधान का उपयोग कर सकते हैं, जिसे Circom स्वीकार करता है क्योंकि इसमें कोई signals शामिल नहीं हैं:
var max;
for (var i = 0; i < n; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
यह गणना कोई constraints नहीं बनाती है, यह केवल सुविधा के लिए है।
Circom में ब्रांचिंग (Branching)
ऐसा लग सकता है कि Circom कंडीशनल ब्रांचिंग में सक्षम नहीं है, लेकिन ऐसा नहीं है। Circom में कंडीशनल ब्रांच बनाने के लिए, किसी स्टेटमेंट की सभी ब्रांचेज (branches) को एग्जीक्यूट किया जाना चाहिए, जिसमें ‘अवांछित’ (unwanted) ब्रांचेज को शून्य से गुणा किया जाता है और ‘सही’ (correct) ब्रांच को एक से गुणा किया जाता है।
ब्रांचेज के साथ गणना (computation) का उदाहरण
मान लीजिए कि हम निम्नलिखित गणना को मॉडल कर रहे हैं:
def foo(x):
if x == 5:
out = 14
elif x == 9:
out = 22
elif x == 10:
out = 23
else
out = 45
return out
x और out के बीच कोई स्पष्ट गणितीय संबंध न होने के कारण, इस कंडीशनल को यथासंभव प्रत्यक्ष रूप से मॉडल करना सबसे अच्छा है। यहाँ बताया गया है कि हम कंडीशनल स्टेटमेंट को गणितीय रूप से कैसे वर्णित करते हैं:
x_eq_51 के बराबर होता है यदिx5 के बराबर है, और अन्यथा शून्य होता है, जिसेIsEqual()([x, 5])के साथ पूरा किया जा सकता है।x_eq_91 के बराबर होता है जबx9 के बराबर होता है, अन्यथा शून्य।x_eq_101 के बराबर होता है जबx10 के बराबर होता है, अन्यथा शून्य।otherwise1 के बराबर होता है जब उपरोक्त सभी (x_eq_5,x_eq_9,x_eq_10) 0 होते हैं।
हम Circomlib के IsEqual() टेम्पलेट का उपयोग करके x_eq_5, x_eq_9, x_eq_10, और otherwise signals को वैल्यू असाइन कर सकते हैं — यह यह भी सुनिश्चित करेगा कि वे 0 या 1 हैं। यह सुनिश्चित करने के लिए कि ठीक एक signal 1 है और बाकी शून्य हैं, हम निम्नलिखित constraint का उपयोग करते हैं:
सामान्य तौर पर, हम “बाइनरी स्विच” (binary switches) बनाते हैं जो 1 होते हैं जब एक विशेष ब्रांच सक्रिय होती है और अन्यथा 0 होते हैं। फिर, हम सभी ब्रांचेज के इवैल्यूएशन (evaluation) को जोड़ते हैं, जिनमें से प्रत्येक को उनके स्विच से गुणा किया जाता है।
चूँकि की केवल एक ही ब्रांच सक्रिय होगी, बाकी इवैल्यूएशन 0 से गुणा हो जाते हैं और इसलिए कोई मायने नहीं रखते।
यहाँ पूरा सर्किट है:
include "./node_modules/circomlib/circuits/comparators.circom";
template MultiBranchConditional() {
signal input x;
signal output out;
signal x_eq_5;
signal x_eq_9;
signal x_eq_10;
signal otherwise;
x_eq_5 <== IsEqual()([x, 5]);
x_eq_9 <== IsEqual()([x, 9]);
x_eq_10 <== IsEqual()([x, 10]);
otherwise <== IsZero()(x_eq_5 + x_eq_9 + x_eq_10);
signal branches_5_9;
signal branches_10_otherwise;
branches_5_9 <== x_eq_5 * 14 + x_eq_9 * 22;
branches_10_otherwise <== x_eq_10 * 23 + otherwise * 45;
out <== branches_5_9 + branches_10_otherwise;
}
component main = MultiBranchConditional();
हमारे कोड को अधिक स्पष्ट (clean) बनाने के लिए, फोर-वे (four-way) ब्रांच को एक अलग कंपोनेंट के रूप में रखना बेहतर होगा — इस तरह, हम ब्रांचिंग टेम्पलेट का दोबारा उपयोग (re-use) कर सकते हैं।
include "./node_modules/circomlib/circuits/comparators.circom";
template Branch4(cond1, cond2, cond3, branch1, branch2, branch3, branch4) {
signal input x;
signal output out;
signal switch1;
signal switch2;
signal switch3;
signal otherwise;
switch1 <== IsEqual()([x, cond1]);
switch2 <== IsEqual()([x, cond2]);
switch3 <== IsEqual()([x, cond3]);
otherwise <== IsZero()(switch1 + switch2 + switch3);
signal branches_1_2 <== switch1 * branch1 + switch2 * branch2;
signal branches_3_4 <== switch3 * branch3 + otherwise * branch4;
out <== branches_1_2 + branches_3_4;
}
template MultiBranchConditional() {
signal input x;
signal output out;
component branch4 = Branch4(5,9,10,14,22,23,45);
branch4.x <== x;
branch4.out ==> out; // same as out <== branch4.out
}
component main = MultiBranchConditional();
जब कई ब्रांचेज शामिल हों तब कोड
ऊपर दिए गए कोड में, हमें स्पष्ट रूप से switch1, switch2,…, otherwise लिखना पड़ा, जो कि अगर कोड में बहुत सारी ब्रांचेज हों तो बहुत उबाऊ (tedious) हो सकता है।
इसके बजाय, हम अपनी गणना को स्विच और ब्रांचेज के इनर प्रोडक्ट (inner product - सामान्यीकृत डॉट प्रोडक्ट) के रूप में सोच सकते हैं:
यह उपरोक्त फॉर्मूलेशन सुनिश्चित करता है कि ठीक एक स्विच सक्रिय है (1 के बराबर), जबकि अन्य सभी 0 हैं, जिससे संबंधित ब्रांच आउटपुट बन जाती है।
Circom में इसे कुशलतापूर्वक लागू करने के लिए, हम multiplexer.circom से EscalarProduct टेम्पलेट का उपयोग करते हैं। यह टेम्पलेट लंबाई n के दो वेक्टर्स लेता है, उन्हें एलिमेंट के अनुसार गुणा करता है, और परिणाम को जोड़ता है। निम्नलिखित कोड ब्लॉक में, हम प्रत्येक स्विच को प्रत्येक ब्रांच से गुणा करने के लिए EscalarProduct का उपयोग करते हैं। ध्यान दें कि अंतिम स्विच और ब्रांच को थोड़ा अलग तरीके से हैंडल किया जाता है क्योंकि अंतिम शर्त (condition) एक “catch-all” else स्टेटमेंट है।
include "./node_modules/circomlib/circuits/comparators.circom";
include "./node_modules/circomlib/circuits/multiplexer.circom";
template BranchN(n) {
assert(n > 1); // too small
signal input x;
// conds n - 1 is otherwise
signal input conds[n - 1];
// branch n - 1 is the otherwise branch
signal input branches[n];
signal output out;
signal switches[n];
component EqualityChecks[n - 1];
// only compute IsEqual up to the second-to-last switch
for (var i = 0; i < n - 1; i++) {
EqualityChecks[i] = IsEqual();
EqualityChecks[i].in[0] <== x;
EqualityChecks[i].in[1] <== conds[i];
switches[i] <== EqualityChecks[i].out;
}
// check the last condition
var total = 0;
for (var i = 0; i < n - 1; i++) {
total += switches[i];
}
// if none of the first n - 1 switches
// are active, then `otherwise` must be 1
switches[n - 1] <== IsZero()(total);
component InnerProduct = EscalarProduct(n);
for (var i = 0; i < n; i++) {
InnerProduct.in1[i] <== switches[i];
InnerProduct.in2[i] <== branches[i];
}
out <== InnerProduct.out;
}
template MultiBranchConditional() {
signal input x;
signal output out;
component branchn = BranchN(4);
var conds[3] = [5, 9, 10];
var branches[4] = [14, 22, 23, 45];
for (var i = 0; i < 4; i++) {
if (i < 3) {
branchn.conds[i] <== conds[i];
}
branchn.branches[i] <== branches[i];
}
branchn.x <== x;
branchn.out ==> out; // same as out <== branch4.out
}
component main = MultiBranchConditional();
if-statements का उपयोग करना कब सही है?
मान लीजिए कि हम एक ऐसा टेम्पलेट बनाना चाहते हैं जो सर्किट पैरामीटर के आधार पर पूरी तरह से अलग सर्किट लौटाता है। उदाहरण के लिए, यदि हम एक Max कंपोनेंट बना रहे हैं जो एक एरे in[n] लेता है और अधिकतम मान (max) लौटाता है, तो यदि n 1 के बराबर है तो इंडेक्स में 0वीं (0th) आइटम को वापस करना अधिक कुशल होगा।
नीचे, हम constraints को परिभाषित करने के साथ उपयोग किए जाने पर if-statement के वैध उपयोग का एक उदाहरण दिखाते हैं। यहाँ, if-statement को कंपाइल टाइम पर एग्जीक्यूट किया जाता है, इसलिए टेम्पलेट एक सुपरिभाषित (well-defined) सर्किट तैयार करेगा:
include "./node_modules/circomlib/circuits/comparators.circom";
template Max(n) {
signal input in[n];
signal output out;
assert(n > 0);
if (n == 1) {
out <== in[0];
}
// it is okay to declare signals inside
// the if-statement because the evaluation
// of the if-statement is known at compile time
else if (n == 2) {
signal zeroGtOne;
signal branch0;
signal branch1;
zeroGtOne <== GreaterThan(252)([in[0], in[1]]);
branch0 <== zeroGtOne * in[0];
branch1 <== (1 - zeroGtOne) * in[1];
out <== branch0 + branch1;
}
else {
// case for n > 2
}
}
component main = Max(2);
कंडीशनल स्टेटमेंट्स zk-friendly नहीं होते हैं
एक प्रमुख डिज़ाइन निहितार्थ (implication) यह है कि Circom सर्किट में प्रत्येक शर्त इसके आकार को दोगुना कर देती है क्योंकि ब्रांचेज को “शॉर्ट-सर्किट” नहीं किया जा सकता है। पारंपरिक प्रोग्रामिंग के विपरीत, सभी ब्रांचेज की गणना की जाती है।
किसी गणना को साबित करने के लिए ZK का उपयोग करते समय, हम निम्नलिखित के लिए ऑप्टिमाइज़ करना चाहते हैं:
- कम से कम ब्रांचेज का होना, क्योंकि प्रत्येक ब्रांच प्रूवर (prover) का काम बढ़ा देती है।
- सभी ब्रांचेज में कुल कम्प्यूटेशनल लागत (computational cost) को कम से कम करना, न कि केवल किसी ब्रांच की प्रायिकता (probability) के आधार पर अपेक्षित गणना को।
- जहाँ संभव हो वहाँ कंडीशनल स्टेटमेंट्स से बचना।