Circom का प्राथमिक उद्देश्य Rank 1 Constraint System (R1CS) में कंपाइल करना है, लेकिन इसका द्वितीयक उद्देश्य witness को पॉप्युलेट करना है।
अधिकांश सर्किट्स के लिए, कुछ signals की वैल्यू यह निर्धारित करती है कि बाकी signals क्या होंगे।
उदाहरण के लिए, निम्नलिखित template में c को इनपुट के रूप में देना थोड़ा अनावश्यक लग सकता है क्योंकि इसकी वैल्यू पूरी तरह से a और b पर निर्भर है:
template Mul() {
signal input a;
signal input b;
signal input c;
c === a * b;
}
एक और अधिक प्रेरित करने वाला उदाहरण नीचे दिया गया है।
एक non-quadratic constraint को तोड़ना
मान लीजिए कि हम a * b * c === d के लिए एक R1CS बनाना चाहते हैं। चूंकि R1CS प्रति constraint केवल एक multiplication की अनुमति देता है, इसलिए हमें multiplication को तोड़ने के लिए एक और signal s और एक अतिरिक्त constraint बनाना होगा:
template Mul3() {
signal input a;
signal input b;
signal input c;
signal input d;
signal input s;
s === a * b;
d === s * c;
}
हर बार जब हम एक से अधिक multiplication करते हैं, तो एक और इनपुट देना बेहद थकाऊ होगा, विशेष रूप से कई multiplications वाले बड़े सर्किट्स में। इसके अलावा, ऊपर दिए गए उदाहरण में s की वैल्यू निश्चित रूप से a और b पर निर्भर है।
Intermediate signals और assignment
s प्रदान करने की परेशानी से बचने के लिए, Circom ==> और <== ऑपरेटर्स प्रदान करता है जो s की वैल्यू को Circom द्वारा कैलकुलेट किए जाने के लिए assign करते हैं (याद रखें कि Circom की कार्यक्षमता का एक हिस्सा witness जनरेट करना है)। इस प्रकार, s की वैल्यू को इनपुट के रूप में देने की आवश्यकता नहीं होगी। ==> और <== ऑपरेटर्स का (सटीक रूप से) अर्थ “assign और constrain” करना है:
template Mul3() {
signal input a;
signal input b;
signal input c;
signal input d;
// no longer an input
signal s;
a * b ==> s;
s * c === d;
}
Circom तीर की दिशा को लेकर लचीला है, a * b ==> s का अर्थ वही है जो s <== a * b का है।
ऊपर दिए गए कोड में, s को एक intermediate signal कहा जाता है। एक intermediate signal वह signal होता है जिसे input कीवर्ड के बिना केवल signal कीवर्ड के रूप में परिभाषित किया जाता है। इसलिए, signal s एक intermediate signal है, लेकिन signal input a नहीं है।
ऊपर दिए गए दोनों templates के बीच अंतर्निहित R1CS बिल्कुल समान है। ==> बस हमें इनपुट के हिस्से के रूप में s के लिए वैल्यू प्रदान करने की परेशानी से बचाता है।
यह मानते हुए कि witness vector को [1, a, b, c, d, s] के रूप में रिप्रजेंट किया गया है, अंतर्निहित R1CS इस प्रकार होगा:
इसे इस तरह समझा जा सकता है कि हम Circom को witness [1, a, b, c, d, _] पास कर रहे हैं और Circom इनपुट के आधार पर पूरा witness [1, a, b, c, d, s] कंप्यूट कर रहा है।
s का assignment R1CS के बाहर होता है। R1CS केवल यह चेक करता है कि एक मैट्रिक्स समीकरण witness vector द्वारा संतुष्ट है या नहीं। R1CS उम्मीद करता है कि witness प्रदान किया जाएगा और यह इसकी किसी भी वैल्यू को कंप्यूट नहीं करता है। यह दृष्टिकोण R1CS संरचना को अपरिवर्तित रखते हुए सर्किट डिज़ाइन को सरल बनाता है और मैन्युअल प्रयास को कम करता है।
Signal Values को <== के साथ Re-Assign नहीं किया जा सकता
एक signal witness vector में एक ठोस एंट्री को रिप्रजेंट करता है। इसलिए, एक बार सेट होने के बाद यह वैल्यू बदल नहीं सकता। इस प्रकार, निम्नलिखित कोड कंपाइल नहीं होगा:
template CannotReassign() {
signal input a;
signal input b;
signal c;
c <== a * b;
// not allowed
// c already set
c <== a * a;
}
वास्तविक उदाहरण: Array के Product को चेक करना
हमारे सर्किट में जितने अधिक multiplications होंगे, ==> ऑपरेटर उतना ही उपयोगी हो जाता है क्योंकि यह अतिरिक्त इनपुट signals प्रदान करने से बचाता है।
मान लीजिए कि हम यह लागू करना चाहते हैं कि इनपुट signal k, array in[n] के सभी signals के गुणनफल का परिणाम है। दूसरे शब्दों में, हम यह चेक कर रहे हैं:
यह बड़ी मात्रा में intermediate signals पेश करेगा। कोड को क्लीन रखने के लिए, हम सभी intermediate signals को एक अलग array में assign कर सकते हैं जैसे कि:
template KProd(n) {
signal input in[n];
signal input k;
// intermediate signal array
signal s[n];
s[0] <== in[0];
for (var i = 1; i < n; i++) {
s[i] <== s[i - 1] * in[i];
}
k === s[n - 1];
}
ऊपर दिए गए कोड के आधार पर, s[n - 1] यह वैल्यू होल्ड करता है:
जिसे फिर हम k के बराबर होने के लिए constrain कर सकते हैं।
Circom को Templates में तोड़ना
अब जब हम <== ऑपरेटर को समझ गए हैं, तो हम समझ सकते हैं कि कैसे Circom कोड को अधिक मॉड्यूलर बनाने के लिए templates का उपयोग करता है।
हमारे Mul3 उदाहरण के समान, मान लीजिए कि हमारे पास एक सर्किट है जो 3 इनपुट्स लेता है और यह लागू करता है कि उनका गुणनफल 4था है (यहाँ कोड फिर से दिया गया है):
template Mul3() {
signal input a;
signal input b;
signal input c;
signal input d; // d === a * b * c
// no longer an input
signal s;
a * b ==> s;
s * c === d;
}
लेकिन मान लीजिए कि हमें आठ इनपुट्स के साथ ऐसा दो बार करना पड़ा। इस मामले में, (a,b,c,d) और (x,y,z,u) इनपुट्स के लिए कोड को दो बार कॉपी और पेस्ट करना आकर्षक लग सकता है, लेकिन यह भद्दा होगा।
template Mul3x2() {
signal input a;
signal input b;
signal input c;
signal input d; // d === a * b * c
signal input x;
signal input y;
signal input z;
signal input u; // u === x * y * z
// ugly code here
}
इसके बजाय, हम Mul3 को एक अलग template के रूप में इस प्रकार रख सकते हैं:
// separate template
template Mul3() {
signal input a;
signal input b;
signal input c;
signal input d; // d === a * b * c
// no longer an input
signal s;
a * b ==> s;
s * c === d;
}
// main component
template Mul3x2() {
signal input a;
signal input b;
signal input c;
signal input d; // d === a * b * c
signal input x;
signal input y;
signal input z;
signal input u; // u === x * y * z
component m3_1 = Mul3();
m3_1.a <== a;
m3_1.b <== b;
m3_1.c <== c;
m3_1.d <== d;
component m3_2 = Mul3();
m3_2.a <== x;
m3_2.b <== y;
m3_2.c <== z;
m3_2.d <== u;
}
ध्यान दें:
- हम
component m3_1 = Mul3();सिंटैक्स के साथ components को डिक्लेयर करते हैं। यह वही सिंटैक्स है जिसका उपयोग हम main component डिक्लेयर करने के लिए करते हैं। - हम
<==ऑपरेटर का उपयोग करके signals को “कनेक्ट” करते हैं। - उपरोक्त कोड पूरी तरह से
Mul3के कोर लॉजिक को दो बार कॉपी और पेस्ट करने के समतुल्य है।
Templates से Results को वापस भेजना
कुछ स्थितियों में यह उपयोगी होगा यदि कोई sub-component उस component को “results वापस भेज सके” जिसने इसे बनाया है।
उदाहरण के लिए, निम्नलिखित main component एक sub-component Square का उपयोग करता है ताकि out को in का वर्ग assign और constrain किया जा सके।
template Square() {
signal input in;
signal output out;
out <== in * in;
}
template Main() {
signal input a;
signal input b;
signal input sumOfSquares;
component a2 = Square();
component b2 = Square();
a2.in <== a;
b2.in <== b;
// assert that a^2 + b^2 === sum of Squares
a2.out + b2.out === sumOfSquares;
}
component main = Main();
Sub-components के संदर्भ में, एक output signal वह signal होता है जिसे <== ऑपरेटर के माध्यम से एक वैल्यू assign किए जाने की अपेक्षा होती है और इसका उपयोग उसे बनाने वाले component को वैल्यूज़ वापस भेजने के लिए किया जा सकता है।
main component के संदर्भ में — output signal का अर्थ कुछ बिल्कुल अलग होता है — हम इसे बाद के अध्याय में समझाएंगे।
उदाहरण: Binary से Number
circomlib library विभिन्न सामान्य ऑपरेशन्स के लिए Circom templates की एक लाइब्रेरी है। ऐसा ही एक ऑपरेशन बाइनरी array को signal में कनवर्ट करना है। हमने पहले देखा है कि इसे के साथ पूरा किया जा सकता है। यहाँ बताया गया है कि हम इसे एक अलग component में कैसे कर सकते हैं। निम्नलिखित template Circom लाइब्रेरी की bitify.circom फाइल में पाया जा सकता है:
template Bits2Num(n) {
signal input in[n];
signal output out;
// lc is short for "linear combination"
// it serves as an accumulator variable
var lc1=0;
var e2 = 1;
for (var i = 0; i<n; i++) {
lc1 += in[i] * e2;
e2 = e2 + e2; // could also be e2 *= 2;
}
lc1 ==> out;
}
हमें लाइब्रेरी से कोड कॉपी और पेस्ट करने की आवश्यकता नहीं है — इसे उसी तरह “include” किया जा सकता है जैसे अन्य भाषाएँ अन्य फाइलों को आयात करती हैं:
include "circomlib/bitify.circom";
template Main(n) {
signal input in[n];
signal input v;
// instantiate the Bits2Num component
component b2n = Bits2Num(n);
// loop over each binary value
// and assign and constrain it to the
// b2n input array
for (var i = 0; i < n; i++) {
b2n.in[i] <== in[i];
}
b2n.out === v;
}
component main = Main(4);
/* INPUT = {"in": [1, 0, 0, 1], "v": 9} */
उपरोक्त component का परीक्षण zkrepl में किया जा सकता है, लेकिन यदि इसे लोकली चलाया जा रहा है, तो डायरेक्टरी कैसे कॉन्फ़िगर की गई है, उसके अनुसार इम्पोर्ट पाथ सेट करने की आवश्यकता होती है। आमतौर पर, Circomlib को yarn या npm के साथ इंस्टॉल किया जाता है।
One line component उदाहरण
इनपुट signals को component में अलग-अलग assign करने के बजाय, उन्हें एक आर्गुमेंट के रूप में प्रदान करना संभव है। इसे “anonymous component” कहा जाता है। निम्नलिखित उदाहरण पर विचार करें:
template Mul() {
signal input in[2];
signal output out;
out <== in[0] * in[1];
}
template Example() {
signal input a;
signal input b;
signal output out;
// one line instantiation
out <== Mul()([a, b]);
}
component main = Example();
Output signals को अनदेखा नहीं किया जाना चाहिए
एक output signal को उस component में constraints का हिस्सा होना चाहिए जिसने इसे इंस्टेंशिएट किया है। यदि एक output signal को “फ्लोटिंग” छोड़ दिया जाता है, तो कुछ परिस्थितियों में, एक द्वेषपूर्ण प्रूवर इसे कोई भी वैल्यू assign कर सकता है। इस बारे में अधिक जानकारी hacking underconstrained circuits में दी जाएगी।
सारांश
<==और==>हमें input.json में स्पष्ट रूप से signal की वैल्यू प्रदान करने की परेशानी से बचाते हैं।- हम
<==या==>का उपयोग तब कर सकते हैं जब एक signal की वैल्यू सीधे दूसरे की वैल्यू द्वारा निर्धारित होती है। <==,==>के समतुल्य है। आर्गुमेंट्स बस उलट दिए जाते हैं, लेकिन प्रभाव समान होता है।- Components अन्य sub-components को इंस्टेंशिएट कर सकते हैं और
<==या==>का उपयोग करके उनके इनपुट signals पर वैल्यूज़ भेज सकते हैं। - एक sub-component के
outputsignals को उस component में अन्य signals के बराबर constrain किया जाना चाहिए जिसने इसे इंस्टेंशिएट किया है।