Circom एक प्रोग्रामिंग भाषा है जिसका उपयोग Rank 1 Constraint Systems (R1CS) बनाने और R1CS के witness वेक्टर को पॉप्युलेट करने के लिए किया जाता है।
R1CS फॉर्मेट इसलिए महत्वपूर्ण है क्योंकि यह SNARKs, विशेष रूप से Groth16 के निर्माण के लिए उपयोगी है। SNARKs के साथ, हम verifiable computation को सक्षम करते हैं, जिससे हमें किसी गणना की सटीकता को प्रमाणित (prove) करने की अनुमति मिलती है। वेरीफाई करते समय, संबंधित पक्ष को सटीकता की पुष्टि करने के लिए उस गणना को स्वयं करने की तुलना में कम कम्प्यूटेशनल प्रयास की आवश्यकता होती है। अंतर्निहित डेटा को उजागर किए बिना प्रूफ जनरेट करना भी संभव है, और इस मामले में, हम इसे zkSNARKs कहते हैं।
हमारी ZK बुक का पहला भाग किसी दिए गए R1CS के लिए witness की वैधता साबित करने पर केंद्रित था। यह रिसोर्स इस बात पर केंद्रित है कि प्रोग्रामेटिक रूप से R1CS कैसे जनरेट किया जाए और उन्हें वर्चुअल मशीन या क्रिप्टोग्राफिक हैश फ़ंक्शंस जैसे यथार्थवादी एल्गोरिदम को मॉडल करने के लिए कैसे डिज़ाइन किया जाए।
Prerequisites
हम उम्मीद करते हैं कि पाठक हमारी ZK Book के निम्नलिखित अध्यायों से पहले से ही परिचित होंगे:
- https://rareskills.io/post/p-vs-np
- https://rareskills.io/post/arithmetic-circuit
- https://rareskills.io/post/finite-fields
- https://rareskills.io/post/rank-1-constraint-system
हम यह मानकर चलेंगे कि पाठक को पता है कि R1CS क्या है और यह क्या दर्शाता है। इसे ऊपर दिए गए चार अध्यायों में पूरी तरह से समझाया गया है।
Circom का उपयोग करने के लिए ZK के पीछे के गणित को पूरी तरह से समझना आवश्यक नहीं है, लेकिन कुछ सिद्धांत हैं जिन्हें पूरी तरह से समझना चाहिए, अन्यथा Circom समझ में नहीं आएगा।
फिर भी, यदि पाठक ZK में करियर बनाने को लेकर गंभीर है, तो ZK की नींव सीखना आवश्यक है। इसके लिए, हम दृढ़ता से अनुशंसा करते हैं कि सीखने को मजबूत करने के लिए ZK book के पहले दो खंडों को पढ़ें और शुरुआत से Groth16 प्रूफ सिस्टम बनाएं।
हालाँकि, यदि पाठक का उद्देश्य ZK एप्लिकेशन्स को जल्दी से समझना है, तो हम ऊपर सूचीबद्ध चार अध्यायों को पढ़ने और फिर इस रिसोर्स का उपयोग करने की सलाह देते हैं।
Circom क्यों मौजूद है
Circom को SNARKs के लिए कंस्ट्रेंट सिस्टम (constraint systems) विकसित करने में आने वाली दो प्रमुख समस्याओं के समाधान के लिए बनाया गया था।
- मैन्युअल रूप से कंस्ट्रेंट सिस्टम को डिज़ाइन करना थकाऊ और त्रुटि-प्रवण (error-prone) है, विशेष रूप से जब बड़े पैमाने पर या बार-बार आने वाले कंस्ट्रेंट्स से निपटना हो।
- witness को पॉप्युलेट करना भी समान रूप से चुनौतीपूर्ण है और इसके लिए मध्यवर्ती (intermediary) मानों की मैन्युअल गणना की आवश्यकता होती है जिन्हें अन्यथा प्रोग्रामेटिक रूप से प्राप्त किया जा सकता है।
इस प्रकार, Circom 1) कंस्ट्रेंट डिज़ाइन को सरल बनाता है और 2) witness पॉप्युलेशन को स्वचालित (automate) करता है।
1. कंस्ट्रेंट सिस्टम को डिज़ाइन करना थकाऊ है
(सही) कंस्ट्रेंट्स का एक सेट मैन्युअल रूप से डिज़ाइन करने और फिर उन्हें R1CS में अनुवाद करने का कार्य थकाऊ और त्रुटि-प्रवण होता है। Circom को प्रोग्रामेटिक रूप से कंस्ट्रेंट्स जनरेट करके इस कार्य को कम चुनौतीपूर्ण और थकाऊ बनाने के लिए बनाया गया था।
उदाहरण के लिए, यह कहने के लिए कि वैल्यू x में केवल मान हो सकते हैं, हम इसे इस कंस्ट्रेंट के साथ व्यक्त कर सकते हैं:
हालाँकि, एक R1CS में प्रति कंस्ट्रेंट केवल एक नॉन-कॉन्स्टेंट गुणा (non-constant multiplication) हो सकता है, इसलिए हमें उपरोक्त कंस्ट्रेंट को दो कंस्ट्रेंट्स में तोड़ना होगा:
छोटे सिस्टम के लिए, यह मैन्युअल अनुवाद प्रबंधनीय (manageable) है। हालाँकि, यदि हमें 100 या 1000 वेरिएबल्स के लिए यह कंस्ट्रेंट बनाने की आवश्यकता हो, तो इसे हाथ से करना बेहद कष्टप्रद होगा। यदि हमारे पास हजारों बहुत समान कंस्ट्रेंट्स हैं, तो कंस्ट्रेंट्स के लिए एक “template” बनाना और for loop में कंस्ट्रेंट्स जनरेट करना बेहतर होगा। Circom हमें इन कंस्ट्रेंट्स को प्रोग्रामेटिक रूप से बनाने की अनुमति देता है।
उदाहरण के लिए, मान लें कि हम 1,000 वेरिएबल्स को मान रखने के लिए बाध्य (constrain) करना चाहते हैं। Circom एक लूप में इन मानों को इस प्रकार जनरेट कर सकता है:
template Constrain1000Example() {
signal input in[1000];
for (var i = 0; i < 1000; i++) {
0 === in[i] * (in[i] - 1);
}
}
component main = Constrain1000Example();
हम आगे के अध्यायों में सिंटैक्स को और स्पष्ट करेंगे, लेकिन मुख्य विचार यह है कि हमने एक कंस्ट्रेंट 0 === in[i] * (in[i] - 1) को परिभाषित किया और इसे 1000 बार दोहराया।
2. witness को पॉप्युलेट करना थकाऊ है
ZK के संदर्भ में witness वेरिएबल्स का एक असाइनमेंट है जो एक एरिथमेटिक सर्किट (arithmetic circuit) में सभी कंस्ट्रेंट्स को संतुष्ट करता है।
जैसा कि हमने arithmetic circuits पर लेख में देखा, यह साबित करने के लिए कि एक संख्या दूसरी संख्या से कम है, दोनों संख्याओं को बाइनरी में बदलना आवश्यक है, क्योंकि एक finite field में “greater than” का कोई अर्थ नहीं होता क्योंकि संख्याएँ रैप अराउंड (wrap around) हो जाती हैं।
संख्या को बाइनरी में व्यक्त करने के लिए, यह मानते हुए कि यह चार बिट्स में फिट बैठता है, को निम्नलिखित कंस्ट्रेंट्स को संतुष्ट करना आवश्यक है:
यहाँ, सबसे कम महत्वपूर्ण बिट (least significant bit) है, और सबसे महत्वपूर्ण बिट (most significant bit) है। प्रूवर (prover) को की आपूर्ति करनी चाहिए, जो के बाइनरी बिट्स हैं, और साथ ही को भी देना होगा।
इस मामले में, यह साबित करना कि एक चार-बिट संख्या है, पांच गुना अधिक थकाऊ हो गया है क्योंकि, के अलावा, हमें के बाइनरी मान भी प्रदान करने होंगे, भले ही उन्हें नियतिवादी (deterministically) और सीधे तौर पर प्राप्त किया जा सकता है। Circom इस प्रक्रिया को स्वचालित करता है और हमें अन्य वेरिएबल्स के आधार पर witness में वेरिएबल्स को पॉप्युलेट करने के लिए कोड लिखने की अनुमति देता है। उदाहरण के लिए, बाइनरी वेरिएबल्स को पॉप्युलेट करने के लिए, हम निम्नलिखित Circom कोड लिख सकते हैं (निम्नलिखित कोड में कुछ आवश्यक सुरक्षा सुविधाओं का अभाव है — कृपया इसे आंख मूंदकर कॉपी न करें):
b_0 <-- x & 1; // get the first bit of x via bitmask
b_1 <-- (x >> 1) & 1; // get the second bit of x
b_2 <-- (x >> 2) & 1; // get the third bit of x
b_3 <-- (x >> 3) & 1; // get the fourth bit of x
उपरोक्त कोड witness जनरेट करता है लेकिन हमारे फॉर्मूले में कंस्ट्रेंट्स नहीं बनाता है:
उपरोक्त सर्किट को Circom में अनुवादित करने पर यह होगा (सिंटैक्स को बाद में और समझाया जाएगा):
template BinaryConstraint() {
// assign the values to b_0,...,b_3
x === b_0 + 2*b_1 + 4*b_2 + 8*b_3;
0 === b_0*(b_0 - 1);
0 === b_1*(b_1 - 1);
0 === b_2*(b_2 - 1);
0 === b_3*(b_3 - 1);
}
Circom की एक बड़ी सुविधा यह है कि इसका कोड एरिथमेटिक सर्किट्स में गणित के समान होता है, इसलिए समीकरणों की एक प्रणाली (system of equations) को Circom में अनुवाद करना आसान है।
विचार यह है कि सर्किट में की आपूर्ति करने के बजाय, हम केवल की आपूर्ति करते हैं। Circom हमारे लिए बाइनरी मानों की गणना करेगा और फिर गणना किए गए मानों के साथ कंस्ट्रेंट्स को भर देगा।
कंस्ट्रेंट जनरेशन को स्वचालित करने के अलावा, Circom अपने “assign and constrain” ऑपरेटर, <== के माध्यम से witness को पॉप्युलेट करने की प्रक्रिया में सुधार करता है।
Circom में <== assign and constrain का लाभ
Circom अपने “assign and constrain” ऑपरेटर <== के माध्यम से witness पॉप्युलेशन को और भी सरल बनाता है। मान लीजिए कि हमारे पास यह कंस्ट्रेंट है:
z === x * y
यदि हम x और y के मानों की आपूर्ति करते हैं, तो z के लिए भी मान की आपूर्ति करना थोड़ा कष्टप्रद होगा क्योंकि z का केवल एक ही संभावित समाधान है।
Circom के साथ, हम <== का उपयोग इस प्रकार करते हैं:
z <== x * y
इसके साथ, वेरिएबल z को अब इनपुट के रूप में प्रदान करने की आवश्यकता नहीं है क्योंकि Circom इसे हमारे लिए पॉप्युलेट करता है, और इसका मान शेष सर्किट के लिए में लॉक हो जाएगा।
इसलिए, Circom एक उपयोगकर्ता को witness में हर तत्व के लिए स्पष्ट रूप से एक मान प्रदान करने की परेशानी से बचाता है, जो कि Circom की सुविधा के लिए एक प्रमुख आकर्षण (selling point) है।
Circom एक DSL और एक प्रोग्रामिंग भाषा दोनों है
Circom में प्रोग्रामिंग करते समय भ्रम का सबसे बड़ा कारण यह है कि यह एक प्रोग्रामिंग भाषा (Javascript के समान) और एक DSL दोनों है जो R1CS में कंपाइल होता है। उस दृष्टि से, यह थोड़ा Solidity जैसा है। Solidity ईथर (Ether) को ट्रांसफर करके अंतर्निहित ब्लॉकचेन स्टेट को प्रभावित कर सकता है, लेकिन यह एक नियमित प्रोग्रामिंग भाषा की तरह भी व्यवहार कर सकता है। Circom का “प्रोग्रामिंग भाषा” भाग स्वचालित witness पॉप्युलेशन में सहायता करने के लिए है जैसा कि पहले बताया गया है। हालाँकि, एक नवागंतुक के लिए, यह हमेशा स्पष्ट नहीं होता है कि Circom के कौन से हिस्से अंतर्निहित R1CS को प्रभावित करते हैं।
उदाहरण के लिए, निम्नलिखित एक मान्य Circom कोड है जो किसी संख्या के पॉवर (power) की गणना करता है:
function power(base, exp) {
return base ** exp;
}
template Power() {
signal input base;
signal input exp;
signal output out;
out <-- power(base, exp);
}
component main = Power();
/* INPUT = {
"base": "3",
"exp": "2"
} */
हालाँकि, उपरोक्त कोड कोई कंस्ट्रेंट्स जनरेट नहीं करता है (इसलिए यह कुछ भी साबित करने के लिए उपयोगी नहीं होगा)। जैसा कि हम बाद में जानेंगे, <-- ऑपरेटर का एकमात्र उद्देश्य witness जनरेट करना है, कंस्ट्रेंट्स जनरेट करना नहीं।
Circom क्यों सीखें
ZK के लिए सबसे पुराने Domain-Specific Languages (DSLs) में से एक के रूप में, Circom के पास सबसे अधिक उपलब्ध libraries और projects हैं जिनसे आप सीख सकते हैं और यह battle-tested है।
हमें लगता है कि अधिक आधुनिक ZK DSLs, जैसे Halo2 और Plonky3 को सीखना बहुत आसान होगा यदि हम पहले Circom सिखाएं, इसलिए हम ऐसा कर रहे हैं।
यह देखने के लिए कि ऐसा क्यों है, यहाँ Halo2 में Fibonacci अनुक्रम की गणना करने के लिए कोड और Plonky3 में Fibonacci की गणना करने के लिए कोड दिया गया है। उदाहरणों पर एक सरसरी नज़र डालने से पाठक को यह विश्वास हो जाना चाहिए कि एक शुरुआती के लिए ये DSLs शुरू करने के लिए सबसे अच्छी जगह नहीं हो सकते हैं। यह साबित करने के लिए Circom कोड यहाँ दिया गया है कि out सही n-वां Fibonnaci नंबर है। इसकी तुलना में इसे समझना बहुत आसान है:
pragma circom 2.1.6;
// proves `out` is the nth
// fibonnaci number
template Fibonacci(n) {
var offset = n + 1;
assert(n > 2);
signal fib[offset];
signal output out;
fib[0] <== 0;
fib[1] <== 1;
for (var i = 2; i < offset; i++) {
fib[i] <== fib[i-1] + fib[i - 2];
}
out <== fib[n];
}
// 5th fibonnaci number is 5
// 0 1 1 2 3 5
component main = Fibonacci(5);
इसके विपरीत, ZK डेवलपमेंट में प्रवेश करने वाले शुरुआती लोगों के लिए Circom का लर्निंग कर्व अपेक्षाकृत सरल है।
क्या Noir, Cairo, और Leo कंस्ट्रेंट राइटिंग सीखने की आवश्यकता को दूर (abstract away) नहीं करते हैं?
आप Rust जैसी भाषाओं, जैसे Noir, Cairo और Leo का उपयोग करके ZK ब्लॉकचेन या layer 2s पर स्मार्ट कॉन्ट्रैक्ट्स लिख सकते हैं, जो प्रोग्रामर से कंस्ट्रेंट जनरेशन को “छिपाने” के लिए डिज़ाइन किए गए हैं। यदि आपका लक्ष्य केवल इन ब्लॉकचेन के लिए एप्लिकेशन्स लिखना है, तो यह सीखना सख्त आवश्यक नहीं है कि ZK कंस्ट्रेंट्स पर्दे के पीछे (under the hood) कैसे काम करते हैं।
हालाँकि, इस बात पर विचार करें कि प्रत्येक गंभीर Solidity प्रोग्रामर को इस बात की अच्छी समझ होती है कि Ethereum Virtual Machine (EVM) कैसे काम करता है और वह बेसिक असेंबली लिख सकता है। यह जानना कि पर्दे के पीछे क्या हो रहा है, आपको अधिक कुशल (efficient) कोड लिखने में मदद करेगा, और यह रिसोर्स उस लक्ष्य को पूरा करता है।
इसके अतिरिक्त, अंतर्निहित ZK निष्पादन मॉडल (execution model) के कारण इन निष्पादन वातावरणों में कई बग्स उत्पन्न होते हैं। वास्तव में क्या प्राइवेट है, कंट्रोल फ्लो पर क्या सीमाएँ हो सकती हैं, फील्ड्स का उपयोग करते समय सामान्य त्रुटियाँ, या Noir में अनकंस्ट्रेंड फ़ंक्शंस (unconstrained functions) या o1js में कस्टम कंस्ट्रेंट्स का सुरक्षित रूप से उपयोग करने की क्षमता हासिल करने के लिए एक लो-लेवल (low-level) समझ की आवश्यकता होती है।
इस सीरीज़ का लक्ष्य
फिर भी, हाई-लेवल ZK भाषाएं कंस्ट्रेंट राइटिंग को अप्रचलित (obsolete) नहीं बनाती हैं — वास्तव में — वे उन विशेषज्ञों की मांग बढ़ाती हैं जो वास्तव में समझते हैं कि वे कैसे काम करते हैं। इस रिसोर्स का उद्देश्य अधिक उन्नत डेवलपर्स और सुरक्षा ऑडिटर्स (security auditors) को ऑनबोर्ड करना है ताकि वे अंतर्निहित ब्लॉकचेन, वर्चुअल मशीन और कंपाइलर वातावरण को विकसित और सुरक्षित कर सकें जिनका उपयोग ये हाई-लेवल ZK भाषाएँ करती हैं।
यह रिसोर्स कैसे संरचित (structured) है
यह रिसोर्स दो मुख्य भागों में विभाजित है:
-
पहला भाग Circom का सिंटैक्स सिखाता है। विशेष रूप से, हम सिखाते हैं कि कंस्ट्रेंट्स कैसे लिखें और हमारे लिए अधिकांश witness मानों को पॉप्युलेट करने के लिए Circom को कैसे प्रोग्राम करें।
-
इस रिसोर्स का दूसरा भाग सिखाता है कि सामान्य रूप से ZK एप्लिकेशन्स के लिए कंस्ट्रेंट्स को कैसे डिज़ाइन किया जाए। हम उदाहरणों के लिए Circom का उपयोग करेंगे, लेकिन सामग्री अन्य ZK DSLs, जैसे Halo2 या Plonky3 पर भी लागू होती है।
हम पूरी सामग्री के दौरान ZK एप्लिकेशन्स में सुरक्षा समस्याओं (security issues) पर भी चर्चा करेंगे।
सीखना केवल अध्ययन से नहीं बल्कि अभ्यास से भी आता है
कई अध्यायों में स्पष्ट अभ्यास या कुछ अधूरा कोड शामिल है जिसे “पाठक के लिए एक अभ्यास के रूप में छोड़ दिया गया है”। यदि आप उन समस्याओं को हल करते हैं तो आपकी सीखने की यात्रा कहीं अधिक प्रभावी होगी। हमने उन समस्याओं को उस चीज़ की समीक्षा के रूप में काम करने के लिए डिज़ाइन किया है जो आपने अभी पढ़ी है ताकि सीखने को मजबूत किया जा सके। यदि आप लिखित रिसोर्स को सही ढंग से समझते हैं तो उन्हें हल करने के लिए किसी विशेष “अंतर्दृष्टि (insight)” या “चतुराई (cleverness)” की आवश्यकता नहीं है। हमारी आशा है कि सामग्री को पढ़ने के बाद अंत में दिए गए अभ्यास कुछ हद तक “स्पष्ट (obvious)” लगेंगे (यदि नहीं, तो कृपया एक इश्यू (issue) दर्ज करें या अभ्यास की रिपॉजिटरी में एक पुल रिक्वेस्ट (pull request) ओपन करें!)
Circom इंस्टॉल करना
Circom को इंस्टॉल करने के निर्देश यहाँ दिए गए हैं: https://docs.circom.io/getting-started/installation/#installing-dependencies
Circom के लिए एक ऑनलाइन IDE भी यहाँ मौजूद है: https://zkrepl.dev/
परिशिष्ट (Addendum): Circom के लिए Plonk बनाम Groth16
Plonk प्रूविंग सिस्टम से परिचित पाठकों के लिए, यह ध्यान देने योग्य है कि हम Plonk प्रूवर सिस्टम और Groth16 प्रूवर सिस्टम दोनों के लिए एक ही सर्किट लिखते हैं।
Groth16 प्रति कंस्ट्रेंट असीमित संख्या में जोड़ (addition) ऑपरेशन्स की अनुमति देता है लेकिन केवल एक नॉन-कॉन्स्टेंट गुणा (non-constant multiplication) (इस पर विचार करें कि एक Rank 1 Constraint System में प्रति पंक्ति एक गुणा होता है)। इसके विपरीत, Plonk प्रति कंस्ट्रेंट केवल एक गुणा या एक जोड़ की अनुमति देता है, दोनों की नहीं। जैसे-जैसे हम Circom का पता लगाएंगे, प्रति-कंस्ट्रेंट-एक-गुणा (one-multiplication-per-constraint) की सीमा स्पष्ट हो जाएगी।
हालाँकि, Groth16 के साथ संगत (compatible) Circom सर्किट्स Plonk के साथ भी काम करेंगे। snarkjs लाइब्रेरी जो इनपुट के रूप में Rank 1 Constraints Systems का उपयोग करती है, यदि डेवलपर चाहे तो इसे Plonk कंस्ट्रेंट सिस्टम में अनुवादित कर देती है।
इसलिए, Circom इस बात से अज्ञेय (agnostic) है कि इच्छित अंतर्निहित प्रूफ सिस्टम Groth16 है या Plonk। जब तक सर्किट Groth16 के साथ संगत है, तब तक यह डेवलपर की ओर से किसी अतिरिक्त परिवर्तन के बिना Plonk के साथ भी संगत हो सकता है।
लेखकत्व और श्रेय (Authorship and Credits)
Calnix ने इस पुस्तक का पहला भाग लिखा और इसकी समग्र संरचना को महत्वपूर्ण रूप से प्रभावित किया। कृपया X पर Calnix को फॉलो करें और शायद उन्हें धन्यवाद दें।
हम इस कार्य की उपयोगी समीक्षाओं के लिए Veridise, Privacy Scaling Explorations, zkSecurity के Marco Besier, और Chainlight के आभारी हैं।