यह ट्यूटोरियल Circom लैंग्वेज और इसके उपयोग के तरीके के साथ-साथ सामान्य गलतियों (pitfalls) का परिचय देता है। हम सामान्य डिज़ाइन पैटर्न से परिचित कराने के लिए circomlib लाइब्रेरी के एक बड़े हिस्से की भी व्याख्या करेंगे।
प्रोडक्शन उपयोग के बारे में एक नोट
Circom ZK-SNARKS सीखने के लिए एक शानदार टूल है। हालाँकि, क्योंकि यह काफी low-level है, इसलिए अनजाने में सूक्ष्म बग्स (subtle bugs) जुड़ने की संभावना अधिक होती है। वास्तविक एप्लिकेशन में, प्रोग्रामर्स को उच्च level zero knowledge programming languages के उपयोग पर विचार करना चाहिए। यूज़र फंड रखने वाले स्मार्ट कॉन्ट्रैक्ट को डिप्लॉय करने से पहले आपको हमेशा ऑडिट करवाना चाहिए, लेकिन यह ZK circuits के लिए विशेष रूप से सच है, क्योंकि इनके अटैक वेक्टर्स कम ज्ञात हैं।
पूर्वापेक्षाएँ (Prerequisites)
हालाँकि यह समझे बिना भी Circom में प्रोग्रामिंग करना संभव है कि Rank 1 Constraint System क्या है, लेकिन अगर आप इसे समझते हैं तो आपको इस लैंग्वेज का मेंटल मॉडल विकसित करने में बहुत आसानी होगी। Circom अनिवार्य रूप से Rank 1 Constraint Systems बनाने के लिए एक एर्गोनोमिक रैपर है।
इंस्टॉलेशन (Installation)
install circom के लिए यहाँ दिए गए स्टेप्स को फॉलो करें।
आपको snarkjs इंस्टॉल करने की भी आवश्यकता होगी:
npm install -g snarkjs@latest
Hello World
ZK-circuits का hello world एक गुणा (multiplication) करना है, तो आइए उसी से शुरू करते हैं।
pragma circom 2.1.6;
template Multiply() {
signal input a;
signal input b;
signal output out;
out <== a * b;
}
component main = Multiply();
उपरोक्त कोड को multiply.circom के रूप में सेव करें और टर्मिनल में यह चलाएँ:
circom multiply.circom
template instances: 1
Everything went okay, circom safe
यह सुनिश्चित करने के लिए कि यह कंपाइल होता है।
R1CS फ़ाइल जनरेट करना
सर्किट को R1CS में कन्वर्ट करने के लिए, निम्नलिखित टर्मिनल कमांड चलाएँ:
circom multiply.circom --r1cs --sym
--r1cs फ्लैग circom को एक R1CS फ़ाइल जनरेट करने के लिए कहता है और --sym फ्लैग का अर्थ है “वेरिएबल नामों को सेव करें।” यह थोड़ी देर में स्पष्ट हो जाएगा।
दो नई फ़ाइलें बनाई जाती हैं:
multiply.r1cs
multiply.sym
यदि आप multiply.r1cs खोलते हैं, तो आपको बहुत सारा बाइनरी डेटा दिखाई देगा, लेकिन .sym फ़ाइल के अंदर, आपको वेरिएबल्स के नाम दिखाई देंगे।
R1CS फ़ाइल को पढ़ने के लिए, हम snarkjs का इस प्रकार उपयोग करते हैं:
snarkjs r1cs print multiply.r1cs
और हमें निम्नलिखित आउटपुट मिलेगा:
[INFO] snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0
याद रखें, जब अरिथमेटिक सर्किट (arithmetic circuits) से निपटा जाता है तो सब कुछ एक finite field में किया जाता है, इसलिए आप जो विशाल संख्या देखते हैं वह अनिवार्य रूप से -1 है। R1CS समीकरण इसके समतुल्य है:
-1 * a * b - (-1*out) = 0;
थोड़े से बीजगणित (algebra) के साथ, हम देख सकते हैं कि यह a * b = out के समतुल्य है, जो हमारा मूल सर्किट है।
बीजगणित के स्टेप्स इस प्रकार हैं:
-1 * a * b - (-1*out) = 0;
-1 * a * b = -1*out;
a * b = out;
नॉन-क्वाड्रेटिक कंस्ट्रेंट्स की अनुमति नहीं है!
एक वैध R1CS में प्रति कंस्ट्रेंट (constraint) ठीक एक गुणा होना चाहिए (एक कंस्ट्रेंट R1CS में एक पंक्ति है, और Circom में <== या === है)। यदि हम दो (या अधिक) गुणा करने का प्रयास करते हैं, तो यह विफल हो जाएगा। एक से अधिक गुणा वाले सभी कंस्ट्रेंट्स को दो कंस्ट्रेंट्स में विभाजित करने की आवश्यकता होती है। निम्नलिखित (कम्पाइल न होने वाले) उदाहरण पर विचार करें:
pragma circom 2.1.6;
template Multiply() {
signal input a;
signal input b;
signal input c;
signal output out;
out <== a * b * c;
}
component main = Multiply();
जब हम circom multiply.circom चलाते हैं तो हमें निम्नलिखित एरर मिलेगा:
error[T3001]: Non quadratic constraints are not allowed!
┌─ "multiply.circom":9:3
│
9 │ out <== a * b * c;
│ ^^^^^^^^^^^^^^^^^ found here
│
= call trace:
->Multiply
previous errors were found
Circom में एक कंस्ट्रेंट को <== ऑपरेटर द्वारा दर्शाया जाता है। इस विशेष कंस्ट्रेंट के लिए, हमारे पास एक कंस्ट्रेंट के लिए दो गुणा हैं। इसे हल करने के लिए, हमें एक अलग कंस्ट्रेंट बनाने की आवश्यकता है ताकि प्रत्येक कंस्ट्रेंट में केवल एक गुणा हो।
नॉन-क्वाड्रेटिक कंस्ट्रेंट्स को तोड़ना
उपरोक्त समस्या को ठीक करना सीधा है। हम एक मध्यवर्ती सिग्नल s1 पेश करते हैं और इसके द्वारा पहले गुणा को कंस्ट्रेंट करते हैं, फिर s1 के आउटपुट को तीसरे इनपुट के साथ मिलाते हैं। अब हमारे पास दो कंस्ट्रेंट्स और दो गुणा हैं, जैसा कि Circom को इसकी अपेक्षा है।
pragma circom 2.1.6;
template Multiply() {
signal input a;
signal input b;
signal input c;
signal s1;
signal output out;
s1 <== a * b;
out <== s1 * c;
}
component main = Multiply();
जब हम R1CS को रीजेनरेट और प्रिंट करते हैं, तो हमें निम्नलिखित मिलता है:
[INFO] snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.s1 ] * [ main.c ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0
थोड़े से बीजगणित के साथ, इसका अनुवाद यह होता है:
a * b = s1
s1 * c = out
और हम देख सकते हैं कि यह हमारे सर्किट से उसी गणना (computation) को एनकोड कर रहा है।
विटनेस (witness) की गणना करना
विटनेस वेक्टर जनरेट करने के लिए कोड बनाने हेतु निम्नलिखित कमांड चलाएँ:
circom multiply.circom --r1cs --sym --wasm
यह R1CS और सिंबल फ़ाइल को फिर से जनरेट करेगा, लेकिन multiply_js/ नामक एक फोल्डर भी बनाएगा। उस फोल्डर में cd करें।
आगे, हमें उस फोल्डर में एक input.json फ़ाइल बनाने की आवश्यकता है। यह इनपुट नामित सिग्नल्स के नाम से उस वैल्यू तक का एक मैप है जो प्रूवर (prover) उन्हें प्रदान करेगा। आइए अपनी input.json को निम्नलिखित वैल्यूज़ के साथ सेट करें:
{"a": "2","b": "3","c": "5"}
यदि हमने पहले वाली .sym फ़ाइल नहीं बनाई होती, तो Circom के लिए यह जानना संभव नहीं होता कि हम किन इनपुट सिग्नल्स को मैप करने का प्रयास कर रहे थे। चूँकि s1 और out इनपुट सिग्नल नहीं हैं, इसलिए हम उनके लिए की-वैल्यू पेयर नहीं बनाते हैं। यहाँ a, b, और c इनपुट सिग्नल्स के नामों से मेल खाते हैं।
signal input a;
signal input b;
signal input c;
अब हम निम्नलिखित कमांड के साथ विटनेस की गणना और निर्यात (export) करते हैं:
node generate_witness.js multiply.wasm input.json witness.wtns
snarkjs wtns export json witness.wtns
cat witness.json
[
"1",
"30",
"2",
"3",
"5",
"6"
]
कैलकुलेट किए गए विटनेस में [1, 30, 2, 3, 5, 6] वैल्यूज़ हैं। 2, 3, और 5 इनपुट हैं, और 6 मध्यवर्ती सिग्नल s1 है जो 2 और 3 का गुणनफल (product) है।
यह [1, out, a, b, c, s1] के रूप में R1CS वेरिएबल्स के अपेक्षित लेआउट का पालन करता है।
पब्लिक इनपुट्स
क्या होगा यदि हम कुछ इनपुट्स को पब्लिक बनाना चाहते हैं? उदाहरण के लिए नलिफायर स्कीम्स (nullifier schemes) में, हम दो संख्याओं के कॉनकैटेनेशन (concatenation) को हैश करते हैं और बाद में उनमें से एक को प्रकट करते हैं।
मोटिवेशन: नलिफायर स्कीम्स
एक त्वरित जानकारी के रूप में, नलिफायर स्कीम दो संख्याओं को जोड़ने (concatenating) और फिर उन्हें हैश करने का काम करती है। उनका उपयोग उस संदर्भ में किया जाता है जहाँ हमारे पास हैश का एक सेट होता है, और हम यह साबित करना चाहते हैं कि हम यह बताए बिना कि वह कौन सा है, उनमें से किसी एक के प्रीइमेज (preimage) को जानते हैं।
यदि हैश केवल एक संख्या के हैश होते, तो उस संख्या को प्रकट करने से यह पता चल जाता कि हम किस हैश के प्रीइमेज को जानते हैं। हालाँकि, यदि हम कोई जानकारी प्रकट नहीं करते हैं, तो हम हैश में से किसी एक के प्रीइमेज को जानने के दावे को कई बार दोहरा सकते हैं। यदि इस क्रिया में किसी स्मार्ट कॉन्ट्रैक्ट से पैसे निकालना शामिल हो तो यह बहुत समस्याग्रस्त होगा!
दो हैश की गई संख्याओं में से एक को प्रकट करके, हम प्रीइमेज का फिर से उपयोग नहीं कर सकते, लेकिन हम यह भी प्रकट नहीं करते कि हम किस हैश के प्रीइमेज को जानते हैं क्योंकि दो संख्याओं में से केवल एक को प्रकट करना पर्याप्त नहीं है।
यहाँ बताया गया है कि Circom पब्लिक इनपुट्स को कैसे पूरा करता है:
template SomePublic() {
signal input a;
signal input b;
signal input c;
signal v;
signal output out;
v <== a * b;
out <== c * v;
}
component main {public [a, c]} = SomePublic();
उपरोक्त उदाहरण में, a और c पब्लिक इनपुट्स हैं, लेकिन b छिपा रहता है। जब main कंपोनेंट को इंस्टेंशिएट किया जाता है, तो public कीवर्ड पर ध्यान दें।
Circom में एरेज़ (Arrays)
आइए एक ऐसा कंपोनेंट बनाएँ जो इनपुट की n शक्तियों (powers) की गणना करेगा।
मैन्युअल रूप से n बार signal input लिखना काफी कष्टप्रद होगा, इसलिए Circom ऐसा करने के लिए सिग्नल्स का एक एरे टाइप (array type) प्रदान करता है। यह रहा कोड:
pragma circom 2.1.6;
template Powers(n) {
signal input a;
signal output powers[n];
powers[0] <== a;
for (var i = 1; i < n; i++) {
powers[i] <== powers[i - 1] * a;
}
}
component main = Powers(6);
इस उदाहरण के साथ कुछ नए सिंटैक्टिक फीचर्स पेश किए गए हैं: टेम्पलेट आर्ग्युमेंट्स और वेरिएबल्स।
टेम्पलेट आर्ग्युमेंट्स
उपरोक्त उदाहरण में, हम देखते हैं कि टेम्पलेट को n के साथ पैरामीटराइज़ (parameterized) किया गया है, अर्थात Powers(n)।
एक Rank 1 Constraint System फिक्स्ड और इम्यूटेबल (immutable) होना चाहिए, इसका मतलब है कि एक बार परिभाषित होने के बाद हम पंक्तियों या कॉलमों की संख्या नहीं बदल सकते हैं, और हम मैट्रिसेस या विटनेस की वैल्यूज़ नहीं बदल सकते हैं। यही कारण है कि अंतिम पंक्ति में हार्ड-कोडेड आर्ग्युमेंट Powers(6) है, यह आकार फिक्स होना चाहिए।
हालाँकि, यदि हम बाद में एक अलग आकार के सर्किट को सपोर्ट करने के लिए इस कोड का फिर से उपयोग करना चाहते हैं, तो टेम्पलेट को अपने आकार को ऑन-द-फ्लाई (on the fly) बदलने में सक्षम होना अधिक एर्गोनोमिक है। इसलिए, कंपोनेंट्स कंट्रोल फ्लो और डेटा स्ट्रक्चर्स को पैरामीटराइज़ करने के लिए आर्ग्युमेंट्स ले सकते हैं, लेकिन यह प्रति सर्किट फिक्स होना चाहिए।
Circom वेरिएबल्स
उपरोक्त उदाहरण निम्नलिखित के समतुल्य है:
pragma circom 2.1.6;
template Powers() {
signal input a;
signal output powers[6];
powers[0] <== a;
powers[1] <== powers[0] * a;
powers[2] <== powers[1] * a;
powers[3] <== powers[2] * a;
powers[4] <== powers[3] * a;
powers[5] <== powers[4] * a;
}
component main = Powers();
हालाँकि हमें एक समान R1CS मिलता है, लेकिन वह कोड भद्दा (ugly) है। हालाँकि, यह उदाहरण उपयोगी रूप से स्पष्ट करता है कि कोई भी सर्किट जो अपनी गणना में वेरिएबल्स का उपयोग करता है, उसे वेरिएबल्स को शामिल न करने के लिए फिर से लिखा जा सकता है।
वेरिएबल्स हेल्पर कोड बनाते हैं जो R1CS के बाहर मौजूद होता है। वे सर्किट को परिभाषित करने में मदद करते हैं, लेकिन वे सर्किट का हिस्सा नहीं होते हैं।
वेरिएबल var i सर्किट के निर्माण के दौरान लूप इटेरेशन को ट्रैक करने के लिए केवल बहीखाता (bookkeeping) था, यह कंस्ट्रेंट्स का हिस्सा नहीं है।
सिग्नल बनाम वेरिएबल
एक सिग्नल इम्यूटेबल है और इसका उद्देश्य R1CS के कॉलमों में से एक होना है। एक वेरिएबल R1CS का हिस्सा नहीं है। इसका उद्देश्य R1CS को परिभाषित करने में मदद के लिए R1CS के बाहर वैल्यूज़ की गणना करना है।
सिग्नल्स को “इम्यूटेबल वेरिएबल्स” और वेरिएबल्स को “म्यूटेबल वेरिएबल्स” के रूप में सोचना सटीक नहीं होगा, जिस तरह से कुछ लैंग्वेजेस उन्हें असाइन करती हैं। सिग्नल्स के इम्यूटेबल होने का कारण यह है कि R1CS में विटनेस एंट्रीज़ की एक फिक्स्ड वैल्यू होती है। R1CS में एक समाधान वेक्टर (solution vector) जो अपनी वैल्यू बदलता है, उसका कोई मतलब नहीं है, क्योंकि आप इसके लिए एक प्रूफ (proof) नहीं बना सकते हैं।
<--, <==, और === ऑपरेटर्स का उपयोग सिग्नल्स के साथ किया जाता है, वेरिएबल्स के साथ नहीं। हम जल्द ही अपरिचित ऑपरेटर्स की व्याख्या करेंगे।
वेरिएबल्स के साथ काम करते समय, Circom एक सामान्य C-जैसी लैंग्वेज की तरह व्यवहार करता है। ऑपरेटर =, ==, >=, <=, और !=, ++, और -- उसी तरह व्यवहार करते हैं जैसा आप उनसे उम्मीद करते हैं। यही कारण है कि लूप का उदाहरण परिचित लगता है।
निम्नलिखित उदाहरणों की अनुमति नहीं है:
signal a;
a = 2; // using a variable assignment for a signal
var v;
v <-- a + b; // using a signal assignment for a variable is not allowed
=== बनाम <==
निम्नलिखित सर्किट समतुल्य हैं:
pragma circom 2.1.6;
template Multiply() {
signal input a;
signal input b;
signal output c;
c <-- a * b;
c === a * b;
}
template MultiplySame() {
signal input a;
signal input b;
signal output c;
c <== a * b;
}
<== ऑपरेटर गणना करता है, फिर असाइन करता है, फिर एक कंस्ट्रेंट जोड़ता है। यदि आप केवल कंस्ट्रेंट करना चाहते हैं, तो === का उपयोग करें।
आमतौर पर, आप === ऑपरेटर का उपयोग तब करेंगे जब आप यह लागू (enforce) करने का प्रयास कर रहे हों कि <-- ने सही वैल्यू असाइन की है। जब हम IsZero टेम्पलेट को देखेंगे तो आप इसे क्रियाशील रूप में देखेंगे।
लेकिन उससे पहले, आइए एक वास्तविक उदाहरण देखें। मान लीजिए कि हम चाहते हैं कि प्रूवर इनपुट्स और आउटपुट दोनों की आपूर्ति करे। === ऑपरेटर का उपयोग करके हम इसे इस तरह करेंगे।
pragma circom 2.1.6;
template Multiply() {
signal input a;
signal input b;
signal input c;
c === a * b;
}
component main {public [c]} = Multiply();
Circom को आउटपुट सिग्नल के मौजूद होने की आवश्यकता नहीं है, क्योंकि वह केवल एक पब्लिक इनपुट के लिए सिंटैक्टिक शुगर (syntactic sugar) है। याद रखें, “इनपुट” केवल विटनेस वेक्टर की एक प्रविष्टि (entry) है, इसलिए जीरो नॉलेज प्रूफ के नजरिए से सब कुछ एक इनपुट है। उपरोक्त उदाहरण में, कोई आउटपुट सिग्नल नहीं है, लेकिन यह उचित कंस्ट्रेंट्स के साथ एक पूरी तरह से वैध सर्किट है।
टेम्पलेट्स को एक साथ वायर करना (Wiring templates together)
Circom टेम्पलेट्स दोबारा उपयोग किए जाने योग्य (reusable) और संयोज्य (composable) हैं जैसा कि निम्नलिखित उदाहरण स्पष्ट करता है। यहाँ, Square एक टेम्पलेट है जिसका उपयोग SumOfSquares द्वारा किया जाता है। ध्यान दें कि इनपुट्स a और b कंपोनेंट Square() से कैसे “वायर्ड” हैं।
pragma circom 2.1.6;
template Square() {
signal input in;
signal output out;
out <== in * in;
}
template SumOfSquares() {
signal input a;
signal input b;
signal output out;
component sq1 = Square();
component sq2 = Square();
// wiring the components together
sq1.in <== a;
sq2.in <== b;
out <== sq1.out + sq2.out;
}
component main = SumOfSquares();
आप <== ऑपरेटर को उनके इनपुट्स या आउटपुट्स को रेफरेंस करके कंपोनेंट्स को एक साथ “वायरिंग” करने के रूप में सोच सकते हैं।
एक कंपोनेंट में मल्टीपल इनपुट्स
उपरोक्त उदाहरण Square इनपुट के रूप में एक सिंगल सिग्नल लेता है, लेकिन यदि कोई कंपोनेंट मल्टीपल इनपुट्स लेता है, तो इसे array in[n] के रूप में निर्दिष्ट करना पारंपरिक है। निम्नलिखित कंपोनेंट दो संख्याएँ लेता है और उनका गुणनफल (product) लौटाता है:
template Mul {
signal input in[2]; // takes two inputs
signal output out; // single output
out <== in[0] * in[1];
}
यह जानना कि टेम्पलेट्स को एक साथ कैसे वायर किया जाए, तब के लिए एक पूर्वापेक्षा (prerequisite) है जब हम सर्किट्स की एक लाइब्रेरी लाते हैं, जिसे हम आने वाले सेक्शन में दिखाएंगे।
सिग्नल नेमिंग कन्वेंशंस
इनपुट सिग्नल्स का नाम in या array in[] के रूप में रखना और आउटपुट सिग्नल(स) को out या out[] के रूप में रखना पारंपरिक है।
Unsafe Powers, <-- से सावधान रहें
जब क्वाड्रेटिक कंस्ट्रेंट्स की समस्या सामने आती है, तो कंपाइलर को चुप कराने के लिए <-- ऑपरेटर का उपयोग करना लुभावना हो सकता है। निम्नलिखित कोड कंपाइल हो जाता है, और हमारे पिछले powers उदाहरण के समान काम पूरा करता हुआ प्रतीत होता है।
pragma circom 2.1.6;
template Powers {
signal input a;
signal output powers[6];
powers[0] <== a;
powers[1] <== a * a;
powers[2] <-- a ** 3;
powers[3] <-- a ** 4;
powers[4] <-- a ** 5;
powers[5] <-- a ** 6;
}
component main = Powers();
हालाँकि, जब हम R1CS बनाते हैं, तो हमारे पास केवल एक ही कंस्ट्रेंट होता है!
(base) ➜ hello-circom circom bad-powers.circom --r1cs
template instances: 1
non-linear constraints: 1 ### only one constraint ###
linear constraints: 0
public inputs: 0
public outputs: 6
private inputs: 1
private outputs: 0
wires: 7
labels: 8
Written successfully: ./powers.r1cs
Everything went okay, circom safe
केवल एक कंस्ट्रेंट के साथ, प्रूवर को केवल एरे में पहले एलीमेंट को सही ढंग से सेट करना होता है, लेकिन वह अन्य 5 के लिए जो चाहे वह वैल्यू रख सकता है! आप ऐसे सर्किट से निकलने वाले प्रूफ्स पर भरोसा नहीं कर सकते!
जीरो नॉलेज एप्लिकेशंस में अंडरकंस्ट्रेंट्स (Underconstraints) सुरक्षा बग्स का एक प्रमुख स्रोत हैं, इसलिए तीन बार चेक करें कि R1CS में कंस्ट्रेंट्स वास्तव में उसी तरह जनरेट हुए हैं जैसी आप उनसे अपेक्षा करते हैं!
यही कारण है कि हमने Circom लैंग्वेज सीखने से पहले Rank 1 Constraint Systems को समझने पर जोर दिया, अन्यथा बग्स की एक पूरी श्रेणी है जिसका पता लगाना आपके लिए मुश्किल होगा!
आप हमारे अन्य ट्यूटोरियल में सीख सकते हैं कि exploit underconstrained ZK circuits कैसे करें।
<-- का उपयोग कब करें
यदि <-- अंडरकंस्ट्रेंट्स की ओर ले जाता है, तो लैंग्वेज में ऐसा फूट गन (foot gun) क्यों शामिल होगा?
नेचुरल कोड के बजाय सर्किट का उपयोग करके एल्गोरिदम का वर्णन करते समय, “गणना करें, फिर कंस्ट्रेंट करें” (compute, then constrain) की मानसिकता अपनानी चाहिए। कुछ ऑपरेशंस को अकेले शुद्ध कंस्ट्रेंट्स के साथ मॉडल करना बेहद मुश्किल होता है। इसका एक उदाहरण आने वाले सेक्शन में दिखाया गया है।
Circomlib
Iden3 उपयोगी Circom टेम्पलेट्स की एक रिपॉजिटरी मेंटेन करता है जिसे circomlib कहा जाता है। इस बिंदु पर, हमारे पास इन टेम्पलेट्स का अध्ययन शुरू करने के लिए पर्याप्त आवश्यक Circom ज्ञान है, और अब एक सरल लेकिन उपयोगी टेम्पलेट पेश करने का एक अच्छा समय है जो <-- के उपयोग को प्रदर्शित करता है।
IsZero
यदि इनपुट सिग्नल शून्य है तो IsZero टेम्पलेट 1 लौटाता है, और यदि इनपुट सिग्नल शून्य नहीं (non-zero) है तो शून्य लौटाता है।
यदि आप कुछ समय यह सोचने में बिताते हैं कि केवल गुणा का उपयोग करके किसी संख्या के शून्य होने का परीक्षण कैसे किया जाए, तो आप खुद को फँसा हुआ पाएंगे; यह एक बेहद मुश्किल समस्या साबित होगी।
आइए देखें कि circomlib component IsZero इसे कैसे पूरा करता है।
template IsZero() {
signal input in;
signal output out;
signal inv;
inv <-- in!=0 ? 1/in : 0;
out <== -in*inv +1;
in*out === 0;
}
उपरोक्त उदाहरण में, inv एक वैध सर्किट बनाने को आसान बनाने के लिए एक सहायक (auxiliary) इनपुट सिग्नल है। हम R1CS के बाहर inv को या तो शून्य या in के व्युत्क्रम (inverse) के रूप में कंप्यूट करते हैं, फिर कंस्ट्रेंट्स के हिस्से के रूप में inv को सही होने के लिए बाध्य करते हैं। यह “गणना करें, फिर कंस्ट्रेंट करें” के पैटर्न का अनुसरण कर रहा है।
सिग्नल inv अभी भी शून्य या in के मॉड्यूलर इनवर्स होने के लिए कंस्ट्रेंट है।
पाठक के लिए एक अभ्यास के रूप में, निम्नलिखित संभावित संयोजनों को दिखाने वाली एक ट्रुथ टेबल (truth table) बनाएँ:
out:
inv:
in:
आप देखेंगे कि कंस्ट्रेंट्स को संतुष्ट करना केवल तभी संभव है जब in के 0 होने पर out को 1 पर सेट किया जाए, और in के नॉन-जीरो होने पर out को 0 पर सेट किया जाए। आप inv के साथ छेड़छाड़ नहीं कर सकते, इसे नियमों का पालन करना ही होगा। यह “गणना करें, फिर कंस्ट्रेंट करें” के पैटर्न को स्पष्ट करता है।
तीर एक कंस्ट्रेंट बनाता है और एक वैल्यू भी असाइन करता है। उपरोक्त कोड में, out को -in*inv + 1 के बराबर होने के लिए कंस्ट्रेंट किया गया है। हालाँकि, अंतिम पंक्ति in*out को शून्य असाइन नहीं कर रही है। बल्कि, यह लागू कर रही है कि in*out वास्तव में शून्य के बराबर है।
हालाँकि आप आमतौर पर वेरिएबल्स के साथ टर्नरी ऑपरेटर (ternary operator) (या सामान्य रूप से C-जैसी चीजों) का उपयोग करेंगे, लेकिन यदि आप <-- ऑपरेटर का उपयोग करते हैं तो आप सिग्नल्स के साथ पारंपरिक प्रोग्रामिंग सिंटैक्स का उपयोग कर सकते हैं।
पाठक के लिए अभ्यास: क्या आप एक ऐसा टेम्पलेट बना सकते हैं जो IsZero टेम्पलेट का उपयोग किए बिना, IsZero के विपरीत (opposite) काम करता हो?
यदि ऑपरेशन क्वाड्रेटिक नहीं है और ऑप्टिमाइज़र चालू है तो === और <== कोई कंस्ट्रेंट नहीं बनाएंगे
यहाँ कंपाइलर की एक विचित्रता (quirk) है जिस पर ध्यान देना चाहिए:
template FactorOfFiveFootgun() {
signal input in;
signal output out;
out <== in * 5;
}
component main = FactorOfFiveFootgun();
यहाँ हम कह रहे हैं कि हम x को जानते हैं, जैसे कि 5x = out, जहाँ out पब्लिक है। तो यदि out 100 है, तो हम उम्मीद करते हैं कि x 20 होगा, सही है ना? यदि हम R1CS को देखें, तो हम देखते हैं कि यह वास्तव में खाली है, कोई कंस्ट्रेंट्स नहीं बनाए गए हैं!
ऐसा इसलिए है क्योंकि यद्यपि <== एक कंस्ट्रेंट है, लेकिन कंपाइलर ऑप्टिमाइज़र उन कंस्ट्रेंट्स को हटा देता है जिनमें गुणा शामिल नहीं होता है।
जब हम सर्किट को R1CS में कंपाइल करते हैं, तो हम देखते हैं कि यह खाली है।
(base) ➜ hello-circom circom footgun.circom --r1cs
template instances: 1
non-linear constraints: 0 ### no constraints ###
linear constraints: 0
public inputs: 0
public outputs: 1
private inputs: 1
private outputs: 0
wires: 2
labels: 3
Written successfully: ./footgun.r1cs
Everything went okay, circom safe
हालाँकि, यदि हम ऑप्टिमाइज़र को इसके माध्यम से बंद करके सर्किट को कंपाइल करते हैं:
circom footgun.circom --r1cs --O0
तो हम देखते हैं कि एक कंस्ट्रेंट बनाया गया है:
template instances: 1
non-linear constraints: 0
linear constraints: 1 ### Constraint created here
public inputs: 0
private inputs: 1
public outputs: 1
wires: 3
labels: 3
Written successfully: ./footgun.r1cs
Everything went okay
कंपाइलर द्वारा बनाए जाने वाले कंस्ट्रेंट्स की संख्या की हमेशा पुष्टि (sanity check) करें।
Circom में फंक्शंस (Functions)
एक चीज़ जो Circom को थोड़ा भ्रमित करने वाली बनाती है, वह यह है कि यह अरिथमेटिक कंस्ट्रेंट्स बनाने के लिए एक लैंग्वेज है, और साथ ही प्रोग्रामेटिक रूप से कंस्ट्रेंट्स बनाने के लिए एक लैंग्वेज है और साथ ही नियमित गणना करने के लिए एक लैंग्वेज भी है।
यहाँ गणना को एक फंक्शन में अलग करने का एक उदाहरण दिया गया है। ध्यान दें कि यहाँ “average” की गणना मॉड्यूलर अरिथमेटिक (modular arithmetic) का उपयोग करके की जाती है, इसलिए यह पारंपरिक अंकगणितीय माध्य (arithmetic mean) के अनुरूप नहीं होगा।
pragma circom 2.1.6;
include "node_modules/circomlib/circuits/comparators.circom";
function invert(x) {
return 1 / x;
}
template Average(n) {
signal input in[n];
signal denominator;
signal output out;
var sum;
for (var i = 0; i < n; i++) {
sum += in[i];
}
denominator <-- invert(n);
component eq = IsEqual();
eq.in[0] <== denominator;
eq.in[1] <== n;
0 === eq.out;
out <== sum * denominator;
}
component main = Average(5);
शर्त (condition) के मूल्य (value) के आधार पर कंस्ट्रेंट्स होते हैं और यह कंस्ट्रेंट जनरेशन चरण के दौरान अज्ञात हो सकता है
Circom कोड लिखते समय होने वाली एक और आसान गलती यह है: सिग्नल किसी if स्टेटमेंट या लूप का इनपुट नहीं हो सकता है।
template IsOver21() {
signal input age;
signal output oldEnough;
if (age >= 21) {
oldEnough <== 1;
} else {
oldEnough <== 0;
}
}
यदि आप उपरोक्त कोड को कंपाइल करने का प्रयास करते हैं, तो आपको इस सेक्शन के हेडर वाला एरर मिलेगा। यदि आप लूप को निष्पादित करने की संख्या निर्धारित करने के लिए सिग्नल का उपयोग करते हैं तो आपको समान एरर मिलेगा (इसे आज़माएं!)। इसकी अनुमति नहीं होने का कारण यह है कि हम R1CS में कंस्ट्रेंट्स की संख्या को R1CS इनपुट्स में से किसी एक का फंक्शन बना रहे हैं, लेकिन इसका कोई मतलब नहीं है।
तो हम कैसे चेक करें कि कोई 21 साल से बड़ा है?
यह वास्तव में जितना लगता है उससे कहीं अधिक कठिन है, क्योंकि एक फाइनाइट फील्ड (finite field) में संख्याओं की तुलना करना काफी पेचीदा है।
Circom में संख्याओं की तुलना करने की कमियाँ (Pitfalls)
यदि है, तो क्या वह विशाल संख्या वास्तव में एक से बड़ी है या 1 से कम है?
आप एक फाइनाइट फील्ड में संख्याओं की तुलना नहीं कर सकते, क्योंकि यह कहने का कोई मतलब नहीं है कि एक संख्या दूसरी संख्या से बड़ी है।
हालाँकि, हम अभी भी संख्याओं की तुलना करने में सक्षम होना चाहेंगे!
इससे पहले कि हम कोई तुलना करें, पहली आवश्यकता यह है कि उन्हें फील्ड आकार से छोटा होना चाहिए, और हम फील्ड में किसी भी संख्या को पॉजिटिव इंटिजर्स (positive integers) के रूप में मानेंगे, और अंडरफ्लो तथा ओवरफ्लो से सावधानीपूर्वक बचाव करेंगे।
यदि आप एक क्षण के लिए सोचते हैं कि विशुद्ध रूप से R1CS प्रतिनिधित्व का उपयोग करके निम्नलिखित कथन का प्रतिनिधित्व कैसे किया जाए:
तो आप फँस जाएंगे। ऐसा करना असंभव है।
जब तक हम संख्याओं को फील्ड ऑर्डर के भीतर रहने के लिए बाध्य करते हैं, तब तक हम उनकी सार्थक तुलना कर सकते हैं। > ऑपरेटर को क्वाड्रेटिक कंस्ट्रेंट्स के एक सेट में ट्रांसलेट करना संभव नहीं है।
हालाँकि, यदि हम किसी फील्ड एलिमेंट को संख्या के बाइनरी प्रतिनिधित्व में परिवर्तित करते हैं, तो तुलना करना संभव है।
अब हम इंटिजर्स की तुलना करने के उद्देश्य से दो और circomlib टेम्पलेट्स पेश कर सकते हैं: Num2Bits और LessThan।
Num2Bits
निम्नलिखित Circomlib Num2Bits टेम्पलेट दिखाता है कि कैसे circom एक सिग्नल को सिग्नल्स के एक एरे में बदलता है जो बाइनरी प्रतिनिधित्व को धारण करते हैं।
template Num2Bits(n) {
signal input in;
signal output out[n];
var lc1=0;
// this serves as an accumulator to "recompute" in bit-by-bit
var e2=1;
for (var i = 0; i<n; i++) {
out[i] <-- (in >> i) & 1;
out[i] * (out[i] -1 ) === 0; // force out[i] to be 1 or 0
lc1 += out[i] * e2; //add to the accumulator if the bit is 1
e2 = e2+e2; // takes on values 1,2,4,8,...
}
lc1 === in;
}
किसी संख्या को बिट्स के साथ प्रस्तुत करने योग्य होने के लिए कंस्ट्रेंट करना यह कहने के समतुल्य है कि यह से कम है।
LessThan
हम सामान्य < और > ऑपरेटर्स का उपयोग किए बिना और केवल एक बाइनरी रूपांतरण (binary conversion) का उपयोग करके 9,999 या उससे कम की दो संख्याओं की तुलना कैसे कर सकते हैं? यदि हमें दो बाइनरी रूपांतरणों की अनुमति दी जाती है, तो यह आसान है, लेकिन यह एक बड़ा सर्किट बनाता है।
यह एक पेचीदा समस्या है, लेकिन यहाँ बताया गया है कि circomlib इसे कैसे करता है।
मान लें कि हम और की तुलना कर रहे हैं।
चूँकि 9,999 वह सबसे बड़ी वैल्यू है जो हमारे इनपुट्स ले सकते हैं, हम में 10,000 जोड़ते हैं और तथा के योग से घटाते हैं।
चाहे कुछ भी हो, पॉजिटिव होगा, भले ही 9,999 हो और 0 हो। चूँकि हम यहाँ फील्ड एलिमेंट्स के साथ काम कर रहे हैं, इसलिए हम नहीं चाहते कि कोई अंडरफ्लो हो, और इस मामले में, एक अंडरफ्लो नहीं हो सकता है।
यहाँ मुख्य भाग है। यदि है, तो 10,000 और 19,999 के बीच होगा और यदि है, तो की सीमा (range) में होगा।
यह जानने के लिए कि कोई संख्या की सीमा में है या नहीं, हमें केवल 10,000वें स्थान के अंक को देखना होगा, अर्थात 1x,xxx। यदि हमारे पास वहाँ है, तो हम जानते हैं कि की सीमा में है और यदि हमारे पास 0x,xxx के रूप में कुछ है, तो मूल रूप से से बड़ा रहा होगा।
Circom बस डेसिमल (decimal) प्रतिनिधित्व के बजाय बाइनरी में यही काम करता है।
डेसिमल सादृश्यता (decimal analogy) में, हमने एक ऐसी संख्या में 10,000 जोड़ा जिसके 9,999 से बड़ा न होने की गारंटी है। बाइनरी मामले में, हम बाइनरी नंबर
को उस संख्या में जोड़ते हैं जिसके अधिकतम बिट्स बड़े होने की गारंटी है। ध्यान दें बिट्स द्वारा 1 लेफ्ट-शिफ्ट किया गया (left-shifted), अर्थात 1 << n है:
यह देखने के लिए कि ऐसा क्यों है, विचार करें कि जब हम 1 << 0 और 1 << 1 की गणना करते हैं तो क्या बन जाता है।
यहाँ Circom में LessThan टेम्पलेट है:
template LessThan(n) {
assert(n <= 252);
signal input in[2];
signal output out;
component n2b = Num2Bits(n+1);
// add 1 << n then subtract the number we are comparing
n2b.in <== in[0] + (1<<n) - in[1];
// check if the n-th bit is 1 or 0
out <== 1-n2b.out[n];
}
कोड को द्वारा पैरामीटराइज़ किया गया है जो बिट्स में संख्याओं का अधिकतम आकार है, हालाँकि alias bug से बचने के लिए फील्ड आकार की सीमा से नीचे रहने के लिए 252 बिट्स की एक लागू ऊपरी सीमा (enforced upper limit) है।
संख्याएँ से बड़ी नहीं हो सकती हैं, इसलिए 1<<n जोड़ने से यह सुनिश्चित होता है कि टर्म in[0] + (1<<n) हमेशा in[1] से बड़ा होगा। अंतर को बाइनरी में बदल दिया जाता है, और यदि उच्चतम बिट (highest bit) अभी भी 1 है, तो in[0] in[1] से बड़ा है। अंतिम टर्म 1 माइनस वह बिट है, इसलिए यह इनवर्ट (invert) कर देता है कि बिट मौजूद है या नहीं।
इस प्रकार, कंपोनेंट out को 1 होने के लिए कंस्ट्रेंट करता है यदि in[0] in[1] से कम है और अन्यथा 0।
Over21 फंक्शनल उदाहरण
अब जब हमारे पास आवश्यक टूलबॉक्स है, तो आइए एक एज चेकर (age checker) बनाएँ जो वास्तव में काम करता है।
Circomlib GreaterThan नामक एक तुलनित्र (comparator) प्रदान करता है जो एक साधारण ट्विस्ट के साथ LessThan ही है, इसलिए हम इसे यहाँ नहीं समझाएंगे। इच्छुक पाठक सीधे सोर्स कोड देख सकते हैं।
circomlib टेम्पलेट का उपयोग करने के लिए, एक खाली node_modules/ डायरेक्टरी बनाएँ और फिर चलाएँ:
npm install circomlib
फिर निम्नलिखित सर्किट बनाएँ:
pragma circom 2.1.6;
include "node_modules/circomlib/circuits/comparators.circom";
template Over21() {
signal input age;
signal input ageLimit;
signal output oldEnough;
// 8 bits is plenty to store age
component gt = GreaterThan(8);
gt.in[0] <== age;
gt.in[1] <== 21;
0 === gt.out;
oldEnough <== gt.out;
}
component main = Over21();
यह सत्यापित करने का सुरक्षित तरीका है कि उम्र वास्तव में 21 से अधिक है (हालांकि वास्तविक एप्लिकेशन में, आपको उम्र को प्रमाणित करने के लिए किसी अथॉरिटी की आवश्यकता होगी)।
Comparators.circom
इस फ़ाइल में प्रदान किए गए कंपैरेटर्स (comparators) हैं:
IsZeroIsEqual(दो इनपुट्स को घटाता है और उसे IsZero में पास करता है)LessThanLessEqThan(LessThan से व्युत्पन्न)GreaterThan(LessThan से व्युत्पन्न)GreaterEqThan(LessThan से व्युत्पन्न)ForceEqualIfEnabled
अंतिम वाला क्या करता है?
ForceEqualIfEnabled
टेम्पलेट ForceEqualIfEnabled का कोड नीचे दिखाया गया है:
template ForceEqualIfEnabled() {
signal input enabled;
signal input in[2];
component isz = IsZero();
in[1] - in[0] ==> isz.in;
(1 - isz.out)*enabled === 0;
}
ForceEqualIfEnabled हमें “कंस्ट्रेंट्स को चालू और बंद करने” की अनुमति देता है। यह एक कंडीशनल कंस्ट्रेंट, या एक if स्टेटमेंट की तरह व्यवहार करता है। यदि enabled 0 है, तो इससे कोई फर्क नहीं पड़ता कि in[0] in[1] के बराबर है या नहीं; कंस्ट्रेंट को प्रभावी रूप से अनदेखा कर दिया जाता है क्योंकि अंतिम कंस्ट्रेंट 0 === 0 होगा। दूसरी ओर, यदि enabled शून्य नहीं है, तो कंस्ट्रेंट पास होने के लिए 1 - isz.out शून्य होना चाहिए (अर्थात in[1], in[0] के बराबर है)।
Circom assert
भ्रमित करने वाली बात यह है कि Circom में एक assert स्टेटमेंट है जो बिल्कुल वैसा नहीं करता जैसा आप उम्मीद करते हैं।
assert statement कोई कंस्ट्रेंट्स नहीं जोड़ता है।
यह केवल एक सुरक्षा जाँच (safety check) है ताकि डेवलपर अवांछनीय गुणों (undesirable properties) वाले सर्किट न बनाए।
उदाहरण के लिए, हम इसका उपयोग यह सुनिश्चित करने के लिए कर सकते हैं कि यदि शून्य से भाग देने की संभावना बनती है तो टेम्पलेट्स को शून्य एरे लंबाई (length zero arrays) के साथ इनिशियलाइज़ न किया जाए।
आपको हमेशा यह मान लेना चाहिए कि एक दुर्भावनापूर्ण प्रूवर (malicious prover) केवल सर्किट कोड को कॉपी कर सकता है, assert स्टेटमेंट को हटा सकता है, और एक प्रूफ बना सकता है। वह प्रूफ सर्किट के अनुकूल (compatible) होगा, क्योंकि सर्किट assert स्टेटमेंट को शामिल नहीं करता है।
बुलियन ऑपरेटर्स (Boolean operators)
उम्मीद है कि अब तक आप जान गए होंगे कि आप सिग्नल्स पर वेरिएबल्स के लिए लक्षित (intended) ऑपरेटर्स का उपयोग नहीं कर सकते हैं। निम्नलिखित कोड कंपाइल नहीं होगा:
template And() {
signal input in[2];
signal output c;
c <== in[0] && in[1];
}
याद रखें, सिग्नल्स फील्ड एलिमेंट्स हैं, और && फील्ड एलिमेंट्स के लिए एक वैध ऑपरेटर नहीं है।
हालाँकि, यदि आप a और b को 0 और 1 होने के लिए कंस्ट्रेंट करते हैं, तो आप c, a, और b को एक AND गेट (AND gate) के व्यवहार का पालन करने के लिए कंस्ट्रेंट कर सकते हैं।
template And() {
signal input in[2];
signal output c;
// force inputs to be zero or one
in[0] === in[0] * in[0];
in[1] === in[1] * in[1];
// c will be 1 iff in[0] and in[1] are 1
c <== in[0] * in[1];
}
पाठक के लिए यह सोचना एक उपयोगी अभ्यास है कि अन्य बुलियन ऑपरेशंस, NOT, OR, NAND, NOR, XOR, और XNOR को कैसे पूरा किया जाए। यद्यपि प्रत्येक बुलियन गेट का निर्माण NAND गेट से किया जा सकता है, लेकिन यह सर्किट को आवश्यकता से अधिक बड़ा बना देगा, इसलिए गेट्स का बार-बार उपयोग करने से बचें।
समाधान circomlib’s gates.circom फ़ाइल में हैं। ध्यान दें कि वे इनपुट को शून्य या एक होने के लिए कंस्ट्रेंट नहीं करते हैं, ऐसा करना सर्किट डिज़ाइनर का काम है।
ZK-फ्रेंडली हैश फंक्शंस
आप कल्पना कर सकते हैं कि उत्पादित सर्किट्स की विधि का उपयोग करके एक हैश फंक्शन बनाना कितना बड़ा होगा। आइए देखें कि 256 बिट्स लेने वाला एक sha256 हैशर कितना बड़ा होगा:
pragma circom 2.0.0;
include "node_modules/circomlib/circuits/sha256/sha256.circom";
component main = Sha256(256);
यह हानिरहित सा दिखने वाला छोटा सर्किट लगभग 30,000 कंस्ट्रेंट्स पैदा करेगा:
(base) ➜ hello-circom circom Sha256-example.circom --r1cs
template instances: 99
non-linear constraints: 29380 ### WOW! ###
linear constraints: 0
public inputs: 0
public outputs: 256
private inputs: 256
private outputs: 0
wires: 29325
labels: 204521
Written successfully: ./Sha256-example.r1cs
Everything went okay, circom safe
पारंपरिक क्रिप्टोग्राफ़िक हैश फंक्शंस में इतने सारे कंस्ट्रेंट्स होने का कारण यह है कि वे 32-बिट नंबरों पर काम करते हैं, इसलिए फील्ड एलिमेंट्स को 32-बिट नंबरों का “अनुकरण” (simulate) करने के लिए कंस्ट्रेंट किया जाना चाहिए।
यह प्रूवर के लिए काफी काम पैदा करता है। प्रतिक्रिया के रूप में, शोध समुदाय (research community) ने सर्किट प्रतिनिधित्व के लिए अनुकूलित (optimized) हैश फंक्शंस का निर्माण किया है। sha256 के अतिरिक्त आप पाएंगे:
- Mimc
- Pedersen
- Poseidon
दूसरी ओर ZK-फ्रेंडली हैश फंक्शंस सीधे फील्ड एलिमेंट्स का उपयोग करते हैं और ऐसे ऑपरेशंस से बचते हैं जो बिट-शिफ्टिंग या XOR ऑपरेशंस जैसे बहुत सारे कंस्ट्रेंट्स जोड़ते हैं।
हैश फंक्शंस के आउटपुट R1CS के आकार की तुलना करना पाठक के लिए एक अभ्यास है। ध्यान रखें कि कुछ फंक्शंस “राउंड्स” (rounds) की संख्या को पैरामीटर के रूप में लेते हैं, जो स्वाभाविक रूप से सर्किट के आकार को बढ़ा देगा।
ZK फ्रेंडली हैश फंक्शंस एक बड़ा विषय है जो अपनी अलग चर्चा के लिए सबसे उपयुक्त है, इसलिए हम इसे भविष्य के लेख के लिए टालते हैं।
बाकियों के बारे में क्या?
शेष टेम्पलेट्स स्व-व्याख्यात्मक (self-explanatory) होने के लिए काफी छोटे हैं, या जीरो नॉलेज डिजिटल सिग्नेचर स्कीम्स और कंप्यूटिंग एलिप्टिक कर्व्स (elliptic curves) से संबंधित हैं।
ZK सर्किट के अंदर एलिप्टिक कर्व्स की गणना करने का मोटिवेशन यह है कि कुछ ZK-फ्रेंडली हैश फंक्शंस एक कोर प्रिमिटिव (core primitive) के रूप में एलिप्टिक कर्व्स पर भरोसा करते हैं।
यह एक और बड़ा विषय है जिसे हम दूसरे लेख के लिए टालते हैं।
वास्तविक दुनिया के उदाहरणों (Real World Examples) से Circom सीखना
अध्ययन करने के लिए दो क्लासिक लेकिन सुलभ सर्किट्स Tornado Cash और Semaphore हैं।
RareSkills के साथ और जानें
circom को सीखने का सबसे अच्छा तरीका इसका उपयोग करना है। हमारे Zero Knowledge Puzzles आपके लिए इस लैंग्वेज को सीखने के लिए बढ़ती जटिलता की छोटी-छोटी चुनौतियाँ प्रदान करते हैं। यह सूचित करने के लिए यूनिट टेस्ट के साथ एक डेवलपमेंट एनवायरमेंट प्रदान किया गया है कि आपने पज़ल को सफलतापूर्वक पूरा कर लिया है या नहीं। ध्यान रखें कि अंडरकंस्ट्रेंट (underconstrained) सर्किट्स के साथ टेस्ट पास करना संभव है, क्योंकि सामान्य रूप से अंडरकंस्ट्रेंट के लिए टेस्ट करना बेहद मुश्किल होता है। कम से कम, आपको जनरेट हुए कंस्ट्रेंट्स की संख्या की जाँच करनी चाहिए और यह सुनिश्चित करना चाहिए कि वह संख्या उचित (make sense) है।
यह सामग्री हमारे जीरो नॉलेज कोर्स का हिस्सा है। अधिक जानने के लिए कृपया प्रोग्राम देखें।
मूल रूप से 26 सितंबर, 2023 को प्रकाशित